diff options
-rw-r--r-- | packages/anastasis-core/package.json | 1 | ||||
-rw-r--r-- | packages/anastasis-core/src/crypto.test.ts | 1 | ||||
-rw-r--r-- | packages/anastasis-core/src/crypto.ts | 17 | ||||
-rw-r--r-- | packages/anastasis-core/src/index.ts | 59 | ||||
-rw-r--r-- | packages/anastasis-core/src/reducer-types.ts | 10 | ||||
-rw-r--r-- | packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts | 2 | ||||
-rw-r--r-- | packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx | 23 | ||||
-rw-r--r-- | pnpm-lock.yaml | 2 |
8 files changed, 80 insertions, 35 deletions
diff --git a/packages/anastasis-core/package.json b/packages/anastasis-core/package.json index f4b611ed1..8dbef2d45 100644 --- a/packages/anastasis-core/package.json +++ b/packages/anastasis-core/package.json @@ -23,6 +23,7 @@ "dependencies": { "@gnu-taler/taler-util": "workspace:^0.8.3", "fetch-ponyfill": "^7.1.0", + "fflate": "^0.6.0", "hash-wasm": "^4.9.0", "node-fetch": "^3.0.0" }, diff --git a/packages/anastasis-core/src/crypto.test.ts b/packages/anastasis-core/src/crypto.test.ts index e535b7b2b..1c255014a 100644 --- a/packages/anastasis-core/src/crypto.test.ts +++ b/packages/anastasis-core/src/crypto.test.ts @@ -1,6 +1,7 @@ import test from "ava"; import { accountKeypairDerive, + encryptKeyshare, encryptTruth, policyKeyDerive, secureAnswerHash, diff --git a/packages/anastasis-core/src/crypto.ts b/packages/anastasis-core/src/crypto.ts index f7cfa9654..63de795b0 100644 --- a/packages/anastasis-core/src/crypto.ts +++ b/packages/anastasis-core/src/crypto.ts @@ -10,6 +10,7 @@ import { crypto_sign_keyPair_fromSeed, stringToBytes, } from "@gnu-taler/taler-util"; +import { gzipSync } from "fflate"; import { argon2id } from "hash-wasm"; export type Flavor<T, FlavorT extends string> = T & { @@ -84,21 +85,25 @@ export function accountKeypairDerive(userId: UserIdentifier): AccountKeyPair { }; } +/** + * Encrypt the recovery document. + * + * The caller should first compress the recovery doc. + */ export async function encryptRecoveryDocument( userId: UserIdentifier, - recoveryDoc: any, + recoveryDocData: OpaqueData, ): Promise<OpaqueData> { - const plaintext = stringToBytes(JSON.stringify(recoveryDoc)); const nonce = encodeCrock(getRandomBytes(nonceSize)); return anastasisEncrypt( nonce, asOpaque(userId), - encodeCrock(plaintext), + recoveryDocData, "erd", ); } -function taConcat(chunks: Uint8Array[]): Uint8Array { +export function typedArrayConcat(chunks: Uint8Array[]): Uint8Array { let payloadLen = 0; for (const c of chunks) { payloadLen += c.byteLength; @@ -120,7 +125,7 @@ export async function policyKeyDerive( const chunks = keyShares.map((x) => decodeCrock(x)); const polKey = kdfKw({ outputLength: 64, - ikm: taConcat(chunks), + ikm: typedArrayConcat(chunks), salt: decodeCrock(policySalt), info: stringToBytes("anastasis-policy-key-derive"), }); @@ -150,7 +155,7 @@ async function anastasisEncrypt( const key = await deriveKey(keySeed, nonce, salt); const nonceBuf = decodeCrock(nonce); const cipherText = secretbox(decodeCrock(plaintext), decodeCrock(nonce), key); - return encodeCrock(taConcat([nonceBuf, cipherText])); + return encodeCrock(typedArrayConcat([nonceBuf, cipherText])); } export const asOpaque = (x: string): OpaqueData => x; diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts index d8071e996..2909cf619 100644 --- a/packages/anastasis-core/src/index.ts +++ b/packages/anastasis-core/src/index.ts @@ -1,6 +1,7 @@ import { AmountString, buildSigPS, + bytesToString, decodeCrock, eddsaSign, encodeCrock, @@ -58,7 +59,9 @@ import { TruthUuid, UserIdentifier, userIdentifierDerive, + typedArrayConcat, } from "./crypto.js"; +import { zlibSync } from "fflate"; const { fetch, Request, Response, Headers } = fetchPonyfill({}); @@ -93,7 +96,7 @@ interface DecryptionPolicy { /** * List of escrow methods identified by their UUID. */ - uuid: string[]; + uuids: string[]; } interface EscrowMethod { @@ -115,8 +118,10 @@ interface EscrowMethod { // Client has to provide this key to the server when using /truth/. truth_key: TruthKey; - // Salt used to encrypt the truth on the Anastasis server. - salt: string; + /** + * Salt to hash the security question answer if applicable. + */ + truth_salt: TruthSalt; // Salt from the provider to derive the user ID // at this provider. @@ -401,7 +406,11 @@ async function getTruthValue( switch (authMethod.type) { case "question": { return asOpaque( - await secureAnswerHash(authMethod.challenge, truthUuid, questionSalt), + await secureAnswerHash( + bytesToString(decodeCrock(authMethod.challenge)), + truthUuid, + questionSalt, + ), ); } case "sms": @@ -414,12 +423,28 @@ async function getTruthValue( } } +/** + * Compress the recovery document and add a size header. + */ +async function compressRecoveryDoc(rd: any): Promise<Uint8Array> { + console.log("recovery document", rd); + const docBytes = stringToBytes(JSON.stringify(rd)); + console.log("plain doc length", docBytes.length); + const sizeHeaderBuf = new ArrayBuffer(4); + const dvbuf = new DataView(sizeHeaderBuf); + dvbuf.setUint32(0, docBytes.length, false); + const zippedDoc = zlibSync(docBytes); + return typedArrayConcat([new Uint8Array(sizeHeaderBuf), zippedDoc]); +} + async function uploadSecret( state: ReducerStateBackup, ): Promise<ReducerStateBackup | ReducerStateError> { const policies = state.policies!; const secretName = state.secret_name!; - const coreSecret = state.core_secret?.value!; + const coreSecret: OpaqueData = encodeCrock( + stringToBytes(JSON.stringify(state.core_secret!)), + ); // Truth key is `${methodIndex}/${providerUrl}` const truthMetadataMap: Record<string, TruthMetaData> = {}; @@ -435,8 +460,8 @@ async function uploadSecret( const methUuids: string[] = []; for (let methIndex = 0; methIndex < pol.methods.length; methIndex++) { const meth = pol.methods[methIndex]; - const truthKey = `${meth.authentication_method}:${meth.provider}`; - if (truthMetadataMap[truthKey]) { + const truthReference = `${meth.authentication_method}:${meth.provider}`; + if (truthMetadataMap[truthReference]) { continue; } const keyShare = encodeCrock(getRandomBytes(32)); @@ -445,15 +470,16 @@ async function uploadSecret( key_share: keyShare, nonce: encodeCrock(getRandomBytes(24)), truth_salt: encodeCrock(getRandomBytes(16)), - truth_key: encodeCrock(getRandomBytes(32)), + truth_key: encodeCrock(getRandomBytes(64)), uuid: encodeCrock(getRandomBytes(32)), pol_method_index: methIndex, policy_index: policyIndex, }; methUuids.push(tm.uuid); - truthMetadataMap[truthKey] = tm; + truthMetadataMap[truthReference] = tm; } const policyKey = await policyKeyDerive(keyShares, policySalt); + policyUuids.push(methUuids); policyKeys.push(policyKey); policySalts.push(policySalt); } @@ -492,7 +518,9 @@ async function uploadSecret( const encryptedKeyShare = await encryptKeyshare( tm.key_share, uid, - tm.truth_salt, + authMethod.type === "question" + ? bytesToString(decodeCrock(authMethod.challenge)) + : undefined, ); console.log( "encrypted key share len", @@ -524,7 +552,7 @@ async function uploadSecret( escrow_type: authMethod.type, instructions: authMethod.instructions, provider_salt: provider.salt, - salt: tm.truth_salt, + truth_salt: tm.truth_salt, truth_key: tm.truth_key, url: meth.provider, uuid: tm.uuid, @@ -542,7 +570,7 @@ async function uploadSecret( policies: policies.map((x, i) => { return { master_key: csr.encMasterKeys[i], - uuid: policyUuids[i], + uuids: policyUuids[i], salt: policySalts[i], }; }), @@ -553,7 +581,12 @@ async function uploadSecret( for (const prov of state.policy_providers!) { const uid = uidMap[prov.provider_url]; const acctKeypair = accountKeypairDerive(uid); - const encRecoveryDoc = await encryptRecoveryDocument(uid, rd); + const zippedDoc = await compressRecoveryDoc(rd); + console.log("zipped doc", zippedDoc); + const encRecoveryDoc = await encryptRecoveryDocument( + uid, + encodeCrock(zippedDoc), + ); const bodyHash = hash(decodeCrock(encRecoveryDoc)); const sigPS = buildSigPS(TalerSignaturePurpose.ANASTASIS_POLICY_UPLOAD) .put(bodyHash) diff --git a/packages/anastasis-core/src/reducer-types.ts b/packages/anastasis-core/src/reducer-types.ts index 92b1c532d..44761ea0a 100644 --- a/packages/anastasis-core/src/reducer-types.ts +++ b/packages/anastasis-core/src/reducer-types.ts @@ -34,6 +34,11 @@ export interface SuccessDetails { }; } +export interface CoreSecret { + mime: string; + value: string; +} + export interface ReducerStateBackup { recovery_state?: undefined; backup_state: BackupStates; @@ -61,10 +66,7 @@ export interface ReducerStateBackup { provider: string; }[]; - core_secret?: { - mime: string; - value: string; - }; + core_secret?: CoreSecret; expiration?: Duration; } diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts index 72424e82a..4a242a2e5 100644 --- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts +++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts @@ -3,7 +3,7 @@ import { BackupStates, getBackupStartState, getRecoveryStartState, RecoveryState import { useState } from "preact/hooks"; const reducerBaseUrl = "http://localhost:5000/"; -const remoteReducer = true; +const remoteReducer = false; interface AnastasisState { reducerState: ReducerState | undefined; diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx index 2963930fd..086d4921d 100644 --- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx @@ -1,20 +1,19 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { - encodeCrock, - stringToBytes -} from "@gnu-taler/taler-util"; +import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { BackupReducerProps, AnastasisClientFrame, LabeledInput } from "./index"; +import { + BackupReducerProps, + AnastasisClientFrame, + LabeledInput, +} from "./index"; export function SecretEditorScreen(props: BackupReducerProps): VNode { const { reducer } = props; const [secretName, setSecretName] = useState( - props.backupState.secret_name ?? "" - ); - const [secretValue, setSecretValue] = useState( - props.backupState.core_secret?.value ?? "" ?? "" + props.backupState.secret_name ?? "", ); + const [secretValue, setSecretValue] = useState(""); const secretNext = (): void => { reducer.runTransaction(async (tx) => { await tx.transition("enter_secret_name", { @@ -41,12 +40,14 @@ export function SecretEditorScreen(props: BackupReducerProps): VNode { <LabeledInput label="Secret Name:" grabFocus - bind={[secretName, setSecretName]} /> + bind={[secretName, setSecretName]} + /> </div> <div> <LabeledInput label="Secret Value:" - bind={[secretValue, setSecretValue]} /> + bind={[secretValue, setSecretValue]} + /> </div> </AnastasisClientFrame> ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30b9e8d02..f5f77e575 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,12 +17,14 @@ importers: '@gnu-taler/taler-util': workspace:^0.8.3 ava: ^3.15.0 fetch-ponyfill: ^7.1.0 + fflate: ^0.6.0 hash-wasm: ^4.9.0 node-fetch: ^3.0.0 typescript: ^4.4.3 dependencies: '@gnu-taler/taler-util': link:../taler-util fetch-ponyfill: 7.1.0 + fflate: 0.6.0 hash-wasm: 4.9.0 node-fetch: 3.0.0 devDependencies: |