Photo Credit: Eavesdropping (Hello Hello anyone there) by Julie anne Johnson (2014)
Disclaimer: здесь лежит английская версия статьи. В русской некоторые моменты разобраны подробнее
Личные и конфиденциальные данные пользователей необходимо защищать тщательнее. К такой мысле приходишь, читая бесконечные отчеты об утечках данных и об их последствиях: в исследовании Gemalto, к примеру, утверждается, что с 2013 года более 9 миллиардов "единиц" личных данных утекли или были украдены.
Использование оконечного шифрования (End-to-End Encryption или E2EE) — один из наиболее надежных вариантов защиты конфиденциальных данных. Оно позволяет зашифровывать и расшифровывать данные непосредственно на устройствах отправителя и получателя, данные никогда не передаются по сети и не хранятся на серверах (в частности облачных, т.е. "на чьем-то компьютере") в открытом виде. Таким образом E2EE предотвращает несанкционированный доступ к данным со стороны любых третьих лиц: администраторов серверов данных, интернет-провайдеров, хакеров, спецслужб и т.д. Кроме того использование E2EE снижает риск атак на ЦОДы: зачем красть данные, которые не получится прочитать?
К сожалению, большинство сервисов, работающих с пользовательскими данными, не используют E2EE. Это прежде всего связано с высокой сложностью внедрения. К тому же малейшая ошибка при проектировании или внедрении криптографического функционала с большой долей вероятности сводит на нет защищенность всего продукта. Недавнее исследовние, опубликованное Veracode, показало, что в более чем половине изученных приложений присутствуют уязвимости, связанные именно с криптографическими механизмами. Также, сложность разработки E2EE решения резко возрастает вместе с количеством предлагаемых функций: пользователи хотят не просто безопсно хранить данные, но и, например, делиться ими с другими пользователями, или анализировать их неосредственно в зашифрованном виде (экзотично, но возможно при использовании гомоморфного шифрования).
DataPeps — облачный сервис, упрощающий встраивание E2EE в существующие приложения. С его помощью приложение может шифровать пользовательские данные, позволяет пользователям обмениваться ими друг с другом и получать полную информацию по доступу к ним.
В этом туториале я расскажу о том, как, используя DataPeps, внедрить E2EE в MyPrivateNote, простенькое приложение для создания и хранения заметок.
Что такое MyPrivateNote
MyPrivateNote — минималистичное браузерное приложение, позволяющее создавать, хранить, экспортировать/импортировать и удалять заметки.
Интерфейс MyPrivateNote
MyPrivateNote написано на JS и использует React, Bulma и FontAwesome. Исходники лежат здесь.
Для простоты MyPrivateNote хранит заметки в открытом виде в браузерной IndexedDB. При входе в приложение пользователи вводят логин для доступа к своим заметкам.
Здесь мы и сталкиваемся с основной проблемой MyPrivateNote: заметки никак не защищены, а значит любой пользователь может получить к ним доступ, используя чужой логин.
Можно было бы добавить механизм парольной аутентификации, но, к сожалению, проблемы это бы не решило. Так как заметки хранятся в IndexedDB в открытом виде, злоумышленник может просматривать их используя средства бразуера. Хранение заметок в удаленной БД также не явлется решением: администратор БД всегда имел бы к ним доступ, а хакер мог бы украсть их и слить.
Злоумышленник (или любопытный пользователь) видит содержимое чужих заметок
Эту проблему можно решить, используя E2EE: в IndexedDB будут храниться только зашифрованные заметки, владельцы смогут расшифровать их и прочитать в рамках своей сессии. Давайте внедрим E2EE, используя DataPeps.
Подготовительный этап
Перед добавлением E2EE в приложение разработчик регистрирует MyPrivateNote в DataPeps.
Создаем приложение
При регистрации приложения генерируется секретный токен, который в дальнейшем используется для аутентификации приложения в DataPeps.
Так разработчик может увидеть секретный токен приложения
Пользователи приложения должны быть зарегистрированы в DataPeps: это позволит им шифровать и расшифровывать данные через приложение.
Вход в MyPrivateNote
До внедрения E2EE логин пользователя использовался только для создания новых или открытия существующих пользовательских хранилищ (user's store) в IndexedDB. Приложение использует два хранилища: в note-content хранятся тексты заметок, в note-metadata — метаданные заметок. Для простоты мы не используем второе хранилище, но на практике, мы могли бы записывать в него, к примеру, даты создания заметок и т.п.
// имя хранилища метаданных заметок
export const STORENAME_METADATA = "note-metadata";
// имя хранилища содержимого заметок
export const STORENAME_CONTENT = "note-content";
...
// Инициализация хранилищ
init() {
return new Promise((resolve, reject) => {
// Открываем соединение
let databaseConnection = indexedDB.open(this.login);
// Создаем хранилища
databaseConnection.onupgradeneeded = (e) => {
let db = e.target.result;
if (!db.objectStoreNames.contains(STORENAME_METADATA)) {
db.createObjectStore(STORENAME_METADATA);
};
if (!db.objectStoreNames.contains(STORENAME_CONTENT)) {
db.createObjectStore(STORENAME_CONTENT);
};
};
...
})
}
Вот код входа пользователя в MyPrivateNote с E2EE:
async _initDataPepsSession() {
// запрос приложением делегированного доступа
let accessRequest = await DataPeps.requestDelegatedAccess(this.login, sign);
// октрытие всплывающего окна для разрешения доступа
accessRequest.openResolver();
// ждем пользовательского разрешения
this.session = await accessRequest.waitSession();
}
Для шифрования/расшифрования заметок пользователь разрешает приложению доступ к своему DataPeps аккаунту.
Пользователь разрешает доступ нажатием кнопки «Authorize»
Когда пользователь нажимает на кнопку Log in, MyPrivateNote запрашивает делегированный доступ (Delegated access) к DataPeps аккаунту пользователя. Это позволяет приложению зашифровывать и расшифровывать данные от лица пользователя; подробнее (на английском) это описано здесь.
Когда пользователь разрешает приложению делегированный доступ, DataPeps создает делегат (Delegate) приложения. Делегат — это изолированный DataPeps аккаунт, используемый приложением для доступа к E2EE функционалу. Пользователь имеет полный доступ (в том числе может расшифровывать) данные, созданные делегатом, в то время как делегаты (т.е. фактически приложения) имеют доступ только к данным, созданным ими самими.
Функция sign
, которая передается в качестве параметра в requestDelegatedAccess()
, подписывает с помощью секретного токена запрос приложения на создание DataPeps сессии. Такми образом приложение проходит аутентификацию. В реальности, клиентская часть приложения должна запрашивать подпись запроса у серверной части:
// этой функции нет в MyPrivateNote, она здесь для примера
async sign(signParameters) {
return await requestSignatureFromServer(signParameters);
}
В MyPrivateNote нет серверной части, потому sign
(как и секретный токен) "вшиты" в клиентский код:
// Идентификатор приложения
let applicationID = 'myprivatenote@datapeps.com';
// Это секретный токен, и его НЕЛЬЗЯ публиковать
const secretToken = 'BUSuhYu5TGTyxyxNqwqRuRc1AkVW0hMoOwTIm55fip1RawGYe6x77ChzbU/o3r8rTMea3mWmp2uFTbfE0yo9Gw==';
// Кастуем B64-строку в Uint8Array
let uSecretToken = Uint8Array.from(atob(secretToken), c => c.charCodeAt(0))
// Эта функция должна выполняться на сервере приложения
export function sign({ login, publicKey }) {
let ulogin = new TextEncoder().encode(login);
let msg = new Uint8Array(ulogin.byteLength + publicKey.byteLength);
msg.set(ulogin, 0);
msg.set(publicKey, ulogin.byteLength);
// Подписываем запрос
let sign = tweetnacl.sign.detached(msg, uSecretToken);
return Promise.resolve({ requester: applicationID, sign })
}
Работа с заметками
MyPrivateNote без E2EE сохраняет заметки в IndexedDB в открытом виде.
Для защиты их содержимого:
- MyPrivateNote должна сохранять только зашифрованные заметки и расшифровывать их по запросу пользователя;
- пользователь должен экспортировать (т.е. сохранять заметки локально) только в зашифрованном виде.
Также было бы удобно позволить пользователям делиться заметками друг с другом.
Приступим!
Сохранение заметок
MyPrivateNote без E2EE сохраняет заметки в IndexedDB в открытом виде:
async save(note) {
if (note.id === undefined) {
note.id = Date.now().toString();
}
let tx = this.database.transaction([
STORENAME_METADATA,
STORENAME_CONTENT
], 'readwrite');
let storeMetadata = tx.objectStore(STORENAME_METADATA);
let storeContent = tx.objectStore(STORENAME_CONTENT);
let content = note.content;
// удаляем содержимое заметки, чтобы сохранить объект в хранилище метаданных
delete note.content;
storeMetadata.put(note, note.id);
// сохраняем текст заметки
storeContent.put(content, note.id);
return new Promise((resolve, reject) => {
tx.oncomplete = () => {
resolve();
};
tx.onerror = (e) => {
reject(e);
};
});
}
Для того чтобы зашифровать заметку, необходимо:
- создать новый ресурс DataPeps (DataPeps resource)
- зашифровать содержимое заметки с помощью этого ресурса:
// Сохранить заметку
async save(note) {
if (note.id === undefined) {
note.id = Date.now().toString();
}
note = await this._encryptNote(note);
this.saveEncrypted(note)
}
...
// Зашифровать содержимое заметки и добавить метаданные для DataPeps
async _encryptNote(note) {
// Создаем ресурс DataPeps для шифрования заметки
let resource = await this.session.Resource.create("myprivatenote/note", {
description: note.description ? note.description : ("note " + note.id),
MIMEType: "text/plain",
URI: window.location.origin + "#" + note.id
}, [this.session.login]);
// Удаляем поле "описание" -- оно не должно храниться в открытом виде
delete note.description
// Шифруем содержимое заметки
let encryptedContent = resource.encrypt(new TextEncoder().encode(note.content));
return { ...note, dataPepsId: resource.id.toString(), encryptedContent };
}
По сути (и достаточно грубо), ресурс DataPeps — обертка ключей шифрования данных.
Массив, передаваемый последним аргументом в Resource.create()
, — список пользователей DataPeps (точнее спсиок идентифицируемых сущностей, DataPeps Identities), которые имеют доступ к ресурсу и могут зашифровывать/расщифровывать данные с его помощью. Эти пользователи составляют группу доступа к ресурсу (resource sharing group): ресурсы хранятся на сервере DataPeps в зашифрованном виде, только пользователи из группы доступа могут расшифровывать их.
Пользователь может видеть ресурсы, к которым он имеет доступ, в своем аккаунте DataPeps. Оттуда же он может управлять ими (например, удалять их). В частности, пользователь видит поле description
ресурса. Хорошей практикой явялется хранение в этом поле информации, которая позволяет ассоциировать ресурс с данными, зашифрованными с его использованием. MyPrivateNote, например, хранит в поле description
идентификатор заметки.
Список ресурсов, созданных MyPrivateNote
Таким образом мы устранили риск утечки содержимого пользовательских заметок:
Содержимое заметок зашифровано
Когда пользователь разворачивает заметку, приложение расшифровывает и показывает ее содержимое. Технически это процесс обратный процессу шифрования:
- приложение запрашивает у DataPeps ресурс, с помощью которого заметка была зашифрована;
- приложение расшифровывает заметку, используя ресурс.
// Расшифровать содержимое заметки
async getContent(note) {
note.encryptedContent = await this._get(note.id, STORENAME_CONTENT);
await this._decryptNote(note);
}
async _decryptNote(note) {
// Получаем ресурс
let resource = await this.session.Resource.get(note.dataPepsId);
// Расшифровываем содержимое заметки
note.content = new TextDecoder().decode(resource.decrypt(note.encryptedContent));
// Добавляем описание заметки, зашифрованное вместе с ресурсом
note.description = resource.payload.description
}
Экспорт и импорт заметок
Пользователи MyPrivateNote без E2EE экспортируют заметку, просто копируя ее из IndexedDB:
// Скачать копию заметки
async onDownloadNote(note) {
let reader = new FileReader()
reader.onloadend = () => {
let url = reader.result;
// создаем загрузочную ссылку
var link = document.createElement("a");
link.download = note.id + fileNameExtension;
link.href = url;
document.body.appendChild(link);
// "кликаем" на ссылку
link.click();
document.body.removeChild(link);
};
await this.props.store.getContent(note);
reader.readAsDataURL(new Blob([note.content]));
delete note.content;
}
Для того чтобы импортировать заметку обратно, достаточно просто добавить ее в хранилище:
// Сохранить экспортированную заметку в хранилище MyPrivateNote
async onImportNote(note) {
// сохраняем заметку в хранилище
await this.props.store.save(note);
// обновляем список заметок в хранилище
await this.loadNotes();
// разворачиваем добавленную заметку
await this.onExpandNote(note);
}
Т.к. после добавления E2EE заметки хранятся в IndexedDB и экспортируются/импортируются в зашифрованном виде, мы лишь немного изменяем код. Это экспорт:
// Скачать копию заметки
async onDownloadNote(note) {
let reader = new FileReader()
reader.onloadend = () => {
let url = reader.result;
// создаем загрузочную ссылку
var link = document.createElement("a");
link.download = note.dataPepsId + fileNameExtension;
link.href = url;
document.body.appendChild(link);
// "кликаем" на ссылку
link.click();
document.body.removeChild(link);
};
await this.props.store.getContent(note);
reader.readAsDataURL(new Blob([note.encryptedContent]));
delete note.content;
}
...
// Получить содержимое заметки
async getContent(note) {
// Извлекаем содержимое из хранилища
note.encryptedContent = await this._get(note.id, STORENAME_CONTENT);
// Расшифровываем заметку
await this._decryptNote(note);
}
А это импорт:
// Сохранить экспортированную заметку в хранилище MyPrivateNote
async onImportNote(note) {
note.id = Date.now().toString();
// Сохраняем заметку в хранилище
await this.props.store.saveEncrypted(note);
// Обновляем список заметок в хранилище
await this.loadNotes();
// Разворачиваем заметку
await this.onExpandNote(note);
}
Делимся заметками
Т.к. пользователи экспортируют заметки в зашифрованном виде, они не могут обмениваться ими напрямую. DataPeps позволяет с легкостью добавить функцию "поделиться".
Чтобы пользователь смог расшифровать переданную ему заметку, владелец заметки добавляет его в группу доступа к ресурсу DataPeps, использованному для шифрования заметки:
// Поделиться заметкой (вызывается при нажатии кнопки "Поделиться")
async onShare() {
if (this.state.login === '') {
return;
}
// Добавить пользователя с логином, записанным в this.state.login, в группу доступа
await this.props.session.Resource.extendSharingGroup(
this.props.note.dataPepsId,
[this.state.login]
);
this.props.onClose();
}
В пользовательском интерфейсе это делается так: Алиса нажимает на иконку Поделиться, вводит в открывшемся поле идентификатор делегата Боба (что такое делегат мы обсудили чуть раньше), и нажимает на кнопку Поделиться заметкой (Share the note). После чего Алиса экспортирует заметку (заметка зашифрована!) и отправляет ее Бобу. Бобу достаточно импортировать полученную заметку, чтобы прочесть ее.
Логин делегата Боба
Алиса добавляет Боба в группу доступа к ресурсу заметки
Заключение
На примере MyPrivateNote мы увидели, что использование E2EE является эффективным решением для обеспечения безопасности конфиденциальных данных, предающихся во время клиент-серверного взаимодействия,
DataPeps, в свою очередь, упрощает встраивание E2EE в новые или уже существующие приложения. Сервис предоставляет весь необходимый криптографический функционал и инфраструктуру для его эффективного использования.
В репозитории приложения для удобства создано три ветви: no-E2EE-integration (без E2EE), E2EE-integration (с внедренным E2EE) и E2EE-with-sharing (с E2EE и возможностьбю поделиться заметкой). Так вы можете посмотреть, какие именно 50 линий кода (помимо приведенных в статье) были добавлены в MyPrivateNote. Вы также можете развернуть приложение локально.
Попробуйте добавить E2EE в свое приложение: зарегистрируйте аккаунт, установите SDK и начинайте с легкостью защищать данные пользователей!
MAXHO
Как я понимаю DataPeps = внешний ресурс.
1. Что произойдет с записями если доступ к ресурсу перекроют?
2. Кто нибудь осуществлял аудит безопасности самого DataPeps?
Пожалуй уже этих двух вопросов достаточно для того, чтобы задуматься о целесообразности использования подобных решений. Причем я не криптолог, а человек с улицы. Интересно было услышать мнение профессионалов об атаках на подобные решения.
buddha_pinmask Автор
По поводу профессионалов — согласен с вами.