summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-10-19 23:26:29 +0200
committerFlorian Dold <florian@dold.me>2021-10-19 23:26:29 +0200
commit6c5d32be7458a6423b8a2b0ab8c3002394620f14 (patch)
tree773d618f52e32f2478e280a5953f2b832d52065d
parent5dc008939237c29fdfd146ddb1adc09054950459 (diff)
downloadwallet-core-6c5d32be7458a6423b8a2b0ab8c3002394620f14.tar.gz
wallet-core-6c5d32be7458a6423b8a2b0ab8c3002394620f14.tar.bz2
wallet-core-6c5d32be7458a6423b8a2b0ab8c3002394620f14.zip
anastasis-core: compatible secret upload
-rw-r--r--packages/anastasis-core/package.json1
-rw-r--r--packages/anastasis-core/src/crypto.test.ts1
-rw-r--r--packages/anastasis-core/src/crypto.ts17
-rw-r--r--packages/anastasis-core/src/index.ts59
-rw-r--r--packages/anastasis-core/src/reducer-types.ts10
-rw-r--r--packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts2
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx23
-rw-r--r--pnpm-lock.yaml2
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: