mirror of
https://github.com/tompro/sattle.git
synced 2026-10-26 01:49:58 +00:00
64 lines
1.9 KiB
TypeScript
64 lines
1.9 KiB
TypeScript
// Password wraps protect serialized wallet secrets with authenticated AES-GCM.
|
|
// PBKDF2 parameters and wire fields stay centralized so legacy linking-key and
|
|
// v2 wallet-material records remain interoperable with their own readers.
|
|
|
|
import {bytesToHex, hexToBytes, utf8ToBytes} from '@noble/hashes/utils.js'
|
|
|
|
const PBKDF2_ITERATIONS = 210_000
|
|
|
|
const deriveAesKeyFromPassword = (password: string, salt: Uint8Array): Promise<CryptoKey> =>
|
|
crypto.subtle
|
|
.importKey('raw', utf8ToBytes(password), 'PBKDF2', false, ['deriveKey'])
|
|
.then((baseKey) =>
|
|
crypto.subtle.deriveKey(
|
|
{
|
|
name: 'PBKDF2',
|
|
salt: new Uint8Array(salt),
|
|
iterations: PBKDF2_ITERATIONS,
|
|
hash: 'SHA-256',
|
|
},
|
|
baseKey,
|
|
{name: 'AES-GCM', length: 256},
|
|
false,
|
|
['encrypt', 'decrypt'],
|
|
),
|
|
)
|
|
|
|
export type EncryptedSecretParts = {
|
|
readonly salt: string
|
|
readonly iv: string
|
|
readonly ciphertext: string
|
|
}
|
|
|
|
export const encryptSecretParts = async (
|
|
value: string,
|
|
password: string,
|
|
): Promise<EncryptedSecretParts> => {
|
|
const salt = crypto.getRandomValues(new Uint8Array(16))
|
|
const iv = crypto.getRandomValues(new Uint8Array(12))
|
|
const aesKey = await deriveAesKeyFromPassword(password, salt)
|
|
const ciphertext = new Uint8Array(
|
|
await crypto.subtle.encrypt({name: 'AES-GCM', iv}, aesKey, utf8ToBytes(value)),
|
|
)
|
|
return {
|
|
salt: bytesToHex(salt),
|
|
iv: bytesToHex(iv),
|
|
ciphertext: bytesToHex(ciphertext),
|
|
}
|
|
}
|
|
|
|
export const decryptSecretParts = async (
|
|
parts: EncryptedSecretParts,
|
|
password: string,
|
|
): Promise<string> => {
|
|
const salt = hexToBytes(parts.salt)
|
|
const iv = hexToBytes(parts.iv)
|
|
const aesKey = await deriveAesKeyFromPassword(password, salt)
|
|
const plaintext = await crypto.subtle.decrypt(
|
|
{name: 'AES-GCM', iv},
|
|
aesKey,
|
|
hexToBytes(parts.ciphertext),
|
|
)
|
|
return new TextDecoder().decode(plaintext)
|
|
}
|