image
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 — минималистичное браузерное приложение, позволяющее создавать, хранить, экспортировать/импортировать и удалять заметки.


image
Интерфейс MyPrivateNote

MyPrivateNote написано на JS и использует React, Bulma и FontAwesome. Исходники лежат здесь.


Для простоты MyPrivateNote хранит заметки в открытом виде в браузерной IndexedDB. При входе в приложение пользователи вводят логин для доступа к своим заметкам.


Здесь мы и сталкиваемся с основной проблемой MyPrivateNote: заметки никак не защищены, а значит любой пользователь может получить к ним доступ, используя чужой логин.


Можно было бы добавить механизм парольной аутентификации, но, к сожалению, проблемы это бы не решило. Так как заметки хранятся в IndexedDB в открытом виде, злоумышленник может просматривать их используя средства бразуера. Хранение заметок в удаленной БД также не явлется решением: администратор БД всегда имел бы к ним доступ, а хакер мог бы украсть их и слить.


image
Злоумышленник (или любопытный пользователь) видит содержимое чужих заметок

Эту проблему можно решить, используя E2EE: в IndexedDB будут храниться только зашифрованные заметки, владельцы смогут расшифровать их и прочитать в рамках своей сессии. Давайте внедрим E2EE, используя DataPeps.


Подготовительный этап


Перед добавлением E2EE в приложение разработчик регистрирует MyPrivateNote в DataPeps.


image
Создаем приложение

При регистрации приложения генерируется секретный токен, который в дальнейшем используется для аутентификации приложения в DataPeps.


image
Так разработчик может увидеть секретный токен приложения

Пользователи приложения должны быть зарегистрированы в 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 аккаунту.


image
Пользователь разрешает доступ нажатием кнопки «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 в открытом виде.


Для защиты их содержимого:


  1. MyPrivateNote должна сохранять только зашифрованные заметки и расшифровывать их по запросу пользователя;
  2. пользователь должен экспортировать (т.е. сохранять заметки локально) только в зашифрованном виде.

Также было бы удобно позволить пользователям делиться заметками друг с другом.


Приступим!


Сохранение заметок


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);
      };
    });
}

Для того чтобы зашифровать заметку, необходимо:


  1. создать новый ресурс DataPeps (DataPeps resource)
  2. зашифровать содержимое заметки с помощью этого ресурса:

// Сохранить заметку
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 идентификатор заметки.


image
Список ресурсов, созданных MyPrivateNote

Таким образом мы устранили риск утечки содержимого пользовательских заметок:


image
Содержимое заметок зашифровано

Когда пользователь разворачивает заметку, приложение расшифровывает и показывает ее содержимое. Технически это процесс обратный процессу шифрования:


  1. приложение запрашивает у DataPeps ресурс, с помощью которого заметка была зашифрована;
  2. приложение расшифровывает заметку, используя ресурс.

// Расшифровать содержимое заметки
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). После чего Алиса экспортирует заметку (заметка зашифрована!) и отправляет ее Бобу. Бобу достаточно импортировать полученную заметку, чтобы прочесть ее.


image
Логин делегата Боба

image
Алиса добавляет Боба в группу доступа к ресурсу заметки

Заключение


На примере MyPrivateNote мы увидели, что использование E2EE является эффективным решением для обеспечения безопасности конфиденциальных данных, предающихся во время клиент-серверного взаимодействия,


DataPeps, в свою очередь, упрощает встраивание E2EE в новые или уже существующие приложения. Сервис предоставляет весь необходимый криптографический функционал и инфраструктуру для его эффективного использования.


В репозитории приложения для удобства создано три ветви: no-E2EE-integration (без E2EE), E2EE-integration (с внедренным E2EE) и E2EE-with-sharing (с E2EE и возможностьбю поделиться заметкой). Так вы можете посмотреть, какие именно 50 линий кода (помимо приведенных в статье) были добавлены в MyPrivateNote. Вы также можете развернуть приложение локально.


Попробуйте добавить E2EE в свое приложение: зарегистрируйте аккаунт, установите SDK и начинайте с легкостью защищать данные пользователей!

Комментарии (2)


  1. MAXHO
    30.05.2018 08:58

    Как я понимаю DataPeps = внешний ресурс.
    1. Что произойдет с записями если доступ к ресурсу перекроют?
    2. Кто нибудь осуществлял аудит безопасности самого DataPeps?

    Пожалуй уже этих двух вопросов достаточно для того, чтобы задуматься о целесообразности использования подобных решений. Причем я не криптолог, а человек с улицы. Интересно было услышать мнение профессионалов об атаках на подобные решения.


    1. buddha_pinmask Автор
      30.05.2018 09:21

      1. В данный момент DataPeps существует как облачный сервис, потому, если допустить его гипотетическую полную блокировку, зашифрованные данные будут недоступны пользователями. С БД ресурсов при этом ничего не произойдет, при разблокировке ее можно будет продолжать использовать. Также ничто не мешает разместить DataPeps "on-premise", на сервере компании, или, например, коммьюнити, использующего DataPeps.
      2. DataPeps — новое решение, не успевшее пройти эти процедуры. Сейчас оно готовится к сертификации и аттестации ANSSI.

      По поводу профессионалов — согласен с вами.