import { bytesToString, canonicalJson, decodeCrock, encodeCrock, getRandomBytes, kdf, kdfKw, secretbox, crypto_sign_keyPair_fromSeed, stringToBytes, } from "@gnu-taler/taler-util"; import { argon2id } from "hash-wasm"; export type Flavor = T & { _flavor?: FlavorT }; export type FlavorP = T & { _flavor?: FlavorT; _size?: S; }; export type UserIdentifier = Flavor; export type ServerSalt = Flavor; export type PolicySalt = Flavor; export type PolicyKey = FlavorP; export type KeyShare = Flavor; export type EncryptedKeyShare = Flavor; export type EncryptedTruth = Flavor; export type EncryptedCoreSecret = Flavor; export type EncryptedMasterKey = Flavor; export type EddsaPublicKey = Flavor; export type EddsaPrivateKey = Flavor; /** * Truth key, found in the recovery document. */ export type TruthKey = Flavor; export type EncryptionNonce = Flavor; export type OpaqueData = Flavor; const nonceSize = 24; const masterKeySize = 64; export async function userIdentifierDerive( idData: any, serverSalt: ServerSalt, ): Promise { const canonIdData = canonicalJson(idData); const hashInput = stringToBytes(canonIdData); const result = await argon2id({ hashLength: 64, iterations: 3, memorySize: 1024 /* kibibytes */, parallelism: 1, password: hashInput, salt: decodeCrock(serverSalt), outputType: "binary", }); return encodeCrock(result); } export interface AccountKeyPair { priv: EddsaPrivateKey; pub: EddsaPublicKey; } export function accountKeypairDerive(userId: UserIdentifier): AccountKeyPair { // FIXME: the KDF invocation looks fishy, but that's what the C code presently does. const d = kdfKw({ outputLength: 32, ikm: stringToBytes("ver"), salt: decodeCrock(userId), }); // FIXME: This bit twiddling seems wrong/unnecessary. d[0] &= 248; d[31] &= 127; d[31] |= 64; const pair = crypto_sign_keyPair_fromSeed(d); return { priv: encodeCrock(pair.secretKey), pub: encodeCrock(pair.publicKey), }; } export async function encryptRecoveryDocument( userId: UserIdentifier, recoveryDoc: any, ): Promise { const plaintext = stringToBytes(JSON.stringify(recoveryDoc)); const nonce = encodeCrock(getRandomBytes(nonceSize)); return anastasisEncrypt( nonce, asOpaque(userId), encodeCrock(plaintext), "erd", ); } function taConcat(chunks: Uint8Array[]): Uint8Array { let payloadLen = 0; for (const c of chunks) { payloadLen += c.byteLength; } const buf = new ArrayBuffer(payloadLen); const u8buf = new Uint8Array(buf); let p = 0; for (const c of chunks) { u8buf.set(c, p); p += c.byteLength; } return u8buf; } export async function policyKeyDerive( keyShares: KeyShare[], policySalt: PolicySalt, ): Promise { const chunks = keyShares.map((x) => decodeCrock(x)); const polKey = kdf( 64, taConcat(chunks), decodeCrock(policySalt), new Uint8Array(0), ); return encodeCrock(polKey); } async function deriveKey( keySeed: OpaqueData, nonce: EncryptionNonce, salt: string, ): Promise { return kdf(32, decodeCrock(keySeed), stringToBytes(salt), decodeCrock(nonce)); } async function anastasisEncrypt( nonce: EncryptionNonce, keySeed: OpaqueData, plaintext: OpaqueData, salt: string, ): Promise { const key = await deriveKey(keySeed, nonce, salt); const nonceBuf = decodeCrock(nonce); const cipherText = secretbox(decodeCrock(plaintext), decodeCrock(nonce), key); return encodeCrock(taConcat([nonceBuf, cipherText])); } const asOpaque = (x: string): OpaqueData => x; const asEncryptedKeyShare = (x: OpaqueData): EncryptedKeyShare => x as string; const asEncryptedTruth = (x: OpaqueData): EncryptedTruth => x as string; export async function encryptKeyshare( keyShare: KeyShare, userId: UserIdentifier, answerSalt?: string, ): Promise { const s = answerSalt ?? "eks"; const nonce = encodeCrock(getRandomBytes(24)); return asEncryptedKeyShare( await anastasisEncrypt(nonce, asOpaque(userId), asOpaque(keyShare), s), ); } export async function encryptTruth( nonce: EncryptionNonce, truthEncKey: TruthKey, truth: OpaqueData, ): Promise { const salt = "ect"; return asEncryptedTruth( await anastasisEncrypt(nonce, asOpaque(truthEncKey), truth, salt), ); } export interface CoreSecretEncResult { encCoreSecret: EncryptedCoreSecret; encMasterKeys: EncryptedMasterKey[]; } export async function coreSecretEncrypt( policyKeys: PolicyKey[], coreSecret: OpaqueData, ): Promise { const masterKey = getRandomBytes(masterKeySize); const nonce = encodeCrock(getRandomBytes(nonceSize)); const coreSecretEncSalt = "cse"; const masterKeyEncSalt = "emk"; const encCoreSecret = (await anastasisEncrypt( nonce, encodeCrock(masterKey), coreSecret, coreSecretEncSalt, )) as string; const encMasterKeys: EncryptedMasterKey[] = []; for (let i = 0; i < policyKeys.length; i++) { const polNonce = encodeCrock(getRandomBytes(nonceSize)); const encMasterKey = await anastasisEncrypt( polNonce, asOpaque(policyKeys[i]), encodeCrock(masterKey), masterKeyEncSalt, ); encMasterKeys.push(encMasterKey as string); } return { encCoreSecret, encMasterKeys, }; }