/
raiden
/
obsidian-git-encrypt
Обзор
Документация
Войти
/
raiden
/
obsidian-git-encrypt
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/crypto/keyManager.ts
182 строки
5 KB
Robert Kuzhin
feat: implement encrypted vault crypto core
09 авг 2026, 09:55
09 авг 2026, 09:55
54bad50
Код
Авторство
О чём код?
import { DEFAULT_KDF_PARAMETERS, FORMAT_VERSION, KDF_LIMITS, KDF_SALT_BYTES, KEK_BYTES, NONCE_BYTES, VAULT_KEY_BYTES, } from "./constants.js"; import { validateAndDecodeConfig } from "./configValidation.js"; import type { CreateKeyResult, VaultKeyConfig } from "./cryptoTypes.js"; import { buildAssociatedData, randomUuid } from "./encoding.js"; import { AuthenticationError, ConfigValidationError } from "./errors.js"; import { copyKeyRecord, createKeyHandle, destroyKeyHandle, type KeyHandle, } from "./keyHandle.js"; import { getSodium } from "./sodium.js"; export interface KeyManagerOptions { readonly memoryKiB?: number; readonly iterations?: number; readonly parallelism?: 1; } export class KeyManager { private readonly parameters: Readonly<Required<KeyManagerOptions>>; public constructor(options: KeyManagerOptions = {}) { this.parameters = { memoryKiB: options.memoryKiB ?? DEFAULT_KDF_PARAMETERS.memoryKiB, iterations: options.iterations ?? DEFAULT_KDF_PARAMETERS.iterations, parallelism: options.parallelism ?? DEFAULT_KDF_PARAMETERS.parallelism, }; validateLocalParameters(this.parameters); } /** Consumes and zeroes `password` before resolving or rejecting. */ public async create(password: Uint8Array): Promise<CreateKeyResult> { try { const sodium = await getSodium(); const vaultId = randomUuid(sodium.randombytes_buf(16)); const vaultKey = sodium.randombytes_buf(VAULT_KEY_BYTES); try { const config = await this.wrapVaultKey(vaultId, vaultKey, password); return { key: createKeyHandle(vaultId, vaultKey), config }; } finally { sodium.memzero(vaultKey); } } finally { password.fill(0); } } /** Consumes and zeroes `password` before resolving or rejecting. */ public async unlock(password: Uint8Array, configInput: unknown): Promise<KeyHandle> { try { const sodium = await getSodium(); const decoded = validateAndDecodeConfig(configInput, (value) => sodium.from_base64(value, sodium.base64_variants.ORIGINAL), ); const kek = deriveKek(sodium, password, decoded.salt, decoded.config.kdf); try { const aad = buildAssociatedData(decoded.config.vaultId, "vault-key", "vault-key"); try { const vaultKey = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt( null, decoded.ciphertext, aad, decoded.nonce, kek, ); try { return createKeyHandle(decoded.config.vaultId, vaultKey); } finally { sodium.memzero(vaultKey); } } catch (error) { throw new AuthenticationError({ cause: error }); } } finally { sodium.memzero(kek); sodium.memzero(decoded.salt); sodium.memzero(decoded.nonce); sodium.memzero(decoded.ciphertext); } } finally { password.fill(0); } } /** Rewraps the existing Vault Key and consumes/zeroes `newPassword`. */ public async rewrap(handle: KeyHandle, newPassword: Uint8Array): Promise<VaultKeyConfig> { try { const sodium = await getSodium(); const record = copyKeyRecord(handle); try { return await this.wrapVaultKey(record.vaultId, record.key, newPassword); } finally { sodium.memzero(record.key); } } finally { newPassword.fill(0); } } public destroy(handle: KeyHandle): void { destroyKeyHandle(handle); } private async wrapVaultKey( vaultId: string, vaultKey: Uint8Array, password: Uint8Array, ): Promise<VaultKeyConfig> { const sodium = await getSodium(); const salt = sodium.randombytes_buf(KDF_SALT_BYTES); const nonce = sodium.randombytes_buf(NONCE_BYTES); const kdf = { algorithm: "argon2id" as const, salt: sodium.to_base64(salt, sodium.base64_variants.ORIGINAL), ...this.parameters, }; const kek = deriveKek(sodium, password, salt, kdf); try { const aad = buildAssociatedData(vaultId, "vault-key", "vault-key"); const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt( vaultKey, aad, null, nonce, kek, ); return { formatVersion: FORMAT_VERSION, vaultId, kdf, keyEncryption: { algorithm: "xchacha20-poly1305", nonce: sodium.to_base64(nonce, sodium.base64_variants.ORIGINAL), ciphertext: sodium.to_base64(ciphertext, sodium.base64_variants.ORIGINAL), }, }; } finally { sodium.memzero(kek); sodium.memzero(salt); sodium.memzero(nonce); } } } function deriveKek( sodium: Awaited<ReturnType<typeof getSodium>>, password: Uint8Array, salt: Uint8Array, parameters: { readonly memoryKiB: number; readonly iterations: number }, ): Uint8Array { try { return sodium.crypto_pwhash( KEK_BYTES, password, salt, parameters.iterations, parameters.memoryKiB * 1024, sodium.crypto_pwhash_ALG_ARGON2ID13, ); } finally { sodium.memzero(password); } } function validateLocalParameters(parameters: Readonly<Required<KeyManagerOptions>>): void { for (const [name, value] of Object.entries(parameters)) { const range = KDF_LIMITS[name as keyof typeof KDF_LIMITS]; if (!Number.isSafeInteger(value) || value < range.min || value > range.max) { throw new ConfigValidationError(`${name} is outside the supported KDF range`); } } }