import { bytesToString, canonicalJson, decodeCrock, encodeCrock, getRandomBytes, kdf, kdfKw, secretbox, crypto_sign_keyPair_fromSeed, stringToBytes, } from "@gnu-taler/taler-util"; import { gzipSync } from "fflate"; import { argon2id } from "hash-wasm"; export type Flavor = T & { _flavor?: `anastasis.${FlavorT}`; }; export type FlavorP = T & { _flavor?: `anastasis.${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; export type TruthUuid = Flavor; export type SecureAnswerHash = Flavor; /** * Truth-specific randomness, also called question salt sometimes. */ export type TruthSalt = 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: decodeCrock(userId), info: stringToBytes("ver"), }); const pair = crypto_sign_keyPair_fromSeed(d); return { priv: encodeCrock(d), pub: encodeCrock(pair.publicKey), }; } /** * Encrypt the recovery document. * * The caller should first compress the recovery doc. */ export async function encryptRecoveryDocument( userId: UserIdentifier, recoveryDocData: OpaqueData, ): Promise { const nonce = encodeCrock(getRandomBytes(nonceSize)); return anastasisEncrypt( nonce, asOpaque(userId), recoveryDocData, "erd", ); } export function typedArrayConcat(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 = kdfKw({ outputLength: 64, ikm: typedArrayConcat(chunks), salt: decodeCrock(policySalt), info: stringToBytes("anastasis-policy-key-derive"), }); return encodeCrock(polKey); } async function deriveKey( keySeed: OpaqueData, nonce: EncryptionNonce, salt: string, ): Promise { return kdfKw({ outputLength: 32, salt: decodeCrock(nonce), ikm: decodeCrock(keySeed), info: stringToBytes(salt), }); } 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(typedArrayConcat([nonceBuf, cipherText])); } export 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, }; } export async function secureAnswerHash( answer: string, truthUuid: TruthUuid, questionSalt: TruthSalt, ): Promise { const powResult = await argon2id({ hashLength: 64, iterations: 3, memorySize: 1024 /* kibibytes */, parallelism: 1, password: stringToBytes(answer), salt: decodeCrock(questionSalt), outputType: "binary", }); const kdfResult = kdfKw({ outputLength: 64, salt: decodeCrock(truthUuid), ikm: powResult, info: stringToBytes("anastasis-secure-question-hashing"), }); return encodeCrock(kdfResult); }