nelkor / passcryptum

Cryptographic password manager
https://passcryptum.com
MIT License
70 stars 4 forks source link

Services on The Server #187

Open nelkor opened 6 months ago

nelkor commented 6 months ago

I propose a new feature to manage service configurations directly from the server, with a focus on encryption and user consent. Here's an overview of the concept:

Implementation Outline

The process involves generating cryptographic keys from the user's password using the PBKDF2 algorithm. Specifically:

I welcome any suggestions, enhancements, or concerns regarding this proposal.

nelkor commented 3 months ago

I will continue this topic in Russian, because I'm running it alone anyway and it's more convenient for me :)

Исходная энтропия — это буфер байтов, полученный из секретного знания пользователя (исходный пароль, seed-фраза или что угодно ещё).

Исходная энтропия включает:

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

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

Приватный ключ Фиксированный размер 32 байта. Применяется для электронной подписи запросов. Электронная подпись приватным ключом позволяет авторизовать операцию над сущностью, обладающую публичным ключом. То есть сервер сможет быть уверен в том, что операцию выполняет тот, кто имеет на это право.

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

Синхронный ключ Фиксированный размер 32 байта. Используется для зашифровывания и для расшифровывания конфигурации.

Вектор инициализации Фиксированный размер 24 байта. Используется вместе с синхронным ключом по алгоритму Salsa20.

ИТОГО N + 8 + 32 + 32 + 24 = N + 96 байтов, где N — размер зерна генерации паролей для аккаунтов. При N, равном 160, общее значение получается 256 байтов.

nelkor commented 3 months ago

Оказывается, Web Crypto API не умеет выводить асимметричные приватные ключи из произвольной энтропии. Я поисследовал существующие возможности и пришёл к выводу, что лучшим вариантом будет использование библиотеки tweetnacl. Она мало весит и её код мне нравится.

Помимо асимметричного ключа, который нужен для электронной подписи (tweetnacl использует алгоритм электронной подписи Ed25519) в библиотеке есть ещё пару крутых реализаций, которых нет в Web Crypto API и которые можно взять на вооружение в Passcryptum. Во-первых, это хеш-функция BLAKE2b, и во-вторых, алгоритм симметричного шифрования Salsa20.

nelkor commented 3 months ago

Примеры запросов сервера

Сохранение конфигурации POST-запрос. Публичный ключ в заголовке. Тело запроса:

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

Получение конфигурации GET-запрос. В заголовках:

Почему не отдаём любой конфиг кому угодно (он ведь зашифрованный) — чтобы нельзя было следить за изменениями.

nelkor commented 3 months ago

Примеры кода

Хеширование

import { hash } from 'tweetnacl'
import { decodeUTF8 } from 'tweetnacl-util'

const seed = '123'
const seedHash = hash(decodeUTF8(seed)) // Uint8Array 64 bytes

Электронная подпись

import { sign } from 'tweetnacl'

// Обе эти сущности — Uint8Array
import { keyPairSeed } from './entropy' // 32 bytes
import { bufferData } from './text-data'

const { secretKey, publicKey } = sign.keyPair.fromSeed(keyPairSeed)

const createSignarute = (data: Uint8Array) =>
  sign.detached(data, secretKey)

const verifySignature = (data: Uint8Array, signature: Uint8Array) =>
  sign.detached.verify(data, signature, publicKey)

const signarute = createSignarute(bufferData)
const isValid = verifySignature(bufferData, signarute)

console.log(signarute)
console.log(isValid ? 'VERIFIED' : 'NOT verified')

Шифрование

import { secretbox } from 'tweetnacl'

import { secretBoxIv, secretBoxKey } from './entropy' // Uint8Array 24 and 32 bytes
import { bufferData } from './text-data' // Uint8Array

export const encrypt = (data: Uint8Array) =>
  secretbox(data, secretBoxIv, secretBoxKey)

export const decrypt = (data: Uint8Array) =>
  secretbox.open(data, secretBoxIv, secretBoxKey)

const encrypted = encrypt(bufferData)
const decrypted = decrypt(encrypted)

console.log('source', bufferData)
console.log('encrypted', encrypted)
console.log('decrypted', decrypted)
nelkor commented 1 month ago

Онлайн PIN

Мотивация: хранить исходную энтропию на сервере (под защитой PIN). Так сложнее перебирать PIN, чем если бы он хранился на устройстве (как в Passcryptum 2). Исходная энтропия на сервере должна храниться в зашифрованном виде. Ключ расшифровки в это время хранится на устройстве (например, localStorage). Таким образом, для каждого нового устройства необходимо новое хранилище.

Во избежание перебора PIN зашифрованная исходная энтропия сохраняется под уникальным ключом, который также хранится на устройстве (далее — "протектор"). Чтобы воспользоваться своим онлайн PIN, необходимо:

Если забыл PIN — ничего страшного, просто выводишь свою исходную энтропию из источника (например, исходный пароль) и устанавливаешь новый онлайн PIN.

Установка PIN

Для сохранения передаём серверу:

В ответ сервер отправляет протектор.

Запрос энтропии с PIN