gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: anastasis: make recovery work


From: gnunet
Subject: [taler-wallet-core] branch master updated: anastasis: make recovery work, at least for security questions
Date: Thu, 21 Oct 2021 18:51:25 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 37400101 anastasis: make recovery work, at least for security questions
37400101 is described below

commit 3740010117df56c0ab8cfa97c983d9cf0143daf1
Author: Florian Dold <florian@dold.me>
AuthorDate: Thu Oct 21 18:51:19 2021 +0200

    anastasis: make recovery work, at least for security questions
---
 packages/anastasis-core/src/crypto.ts              |  26 +++
 packages/anastasis-core/src/index.ts               | 201 ++++++++++++++++++++-
 .../anastasis-core/src/recovery-document-types.ts  |  47 +++--
 packages/anastasis-core/src/reducer-types.ts       |  28 ++-
 .../src/pages/home/RecoveryFinishedScreen.tsx      |   1 -
 5 files changed, 277 insertions(+), 26 deletions(-)

diff --git a/packages/anastasis-core/src/crypto.ts 
b/packages/anastasis-core/src/crypto.ts
index 8df893f4..da833863 100644
--- a/packages/anastasis-core/src/crypto.ts
+++ b/packages/anastasis-core/src/crypto.ts
@@ -185,6 +185,7 @@ async function anastasisDecrypt(
 export const asOpaque = (x: string): OpaqueData => x;
 const asEncryptedKeyShare = (x: OpaqueData): EncryptedKeyShare => x as string;
 const asEncryptedTruth = (x: OpaqueData): EncryptedTruth => x as string;
+const asKeyShare = (x: OpaqueData): KeyShare => x as string;
 
 export async function encryptKeyshare(
   keyShare: KeyShare,
@@ -198,6 +199,17 @@ export async function encryptKeyshare(
   );
 }
 
+export async function decryptKeyShare(
+  encKeyShare: EncryptedKeyShare,
+  userId: UserIdentifier,
+  answerSalt?: string,
+): Promise<KeyShare> {
+  const s = answerSalt ?? "eks";
+  return asKeyShare(
+    await anastasisDecrypt(asOpaque(userId), asOpaque(encKeyShare), s),
+  );
+}
+
 export async function encryptTruth(
   nonce: EncryptionNonce,
   truthEncKey: TruthKey,
@@ -226,6 +238,20 @@ export interface CoreSecretEncResult {
   encMasterKeys: EncryptedMasterKey[];
 }
 
+export async function coreSecretRecover(args: {
+  encryptedMasterKey: OpaqueData;
+  policyKey: PolicyKey;
+  encryptedCoreSecret: OpaqueData;
+}): Promise<OpaqueData> {
+  const masterKey = await anastasisDecrypt(
+    asOpaque(args.policyKey),
+    args.encryptedMasterKey,
+    "emk",
+  );
+  console.log("recovered master key", masterKey);
+  return await anastasisDecrypt(masterKey, args.encryptedCoreSecret, "cse");
+}
+
 export async function coreSecretEncrypt(
   policyKeys: PolicyKey[],
   coreSecret: OpaqueData,
diff --git a/packages/anastasis-core/src/index.ts 
b/packages/anastasis-core/src/index.ts
index b8fedf00..b4e911ff 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -26,7 +26,8 @@ import {
   ActionArgEnterSecret,
   ActionArgEnterSecretName,
   ActionArgEnterUserAttributes,
-  ActionArgSelectChallenge,
+  ActionArgsSelectChallenge,
+  ActionArgsSolveChallengeRequest,
   AuthenticationProviderStatus,
   AuthenticationProviderStatusOk,
   AuthMethod,
@@ -66,6 +67,9 @@ import {
   userIdentifierDerive,
   typedArrayConcat,
   decryptRecoveryDocument,
+  decryptKeyShare,
+  KeyShare,
+  coreSecretRecover,
 } from "./crypto.js";
 import { unzlibSync, zlibSync } from "fflate";
 import { EscrowMethod, RecoveryDocument } from "./recovery-document-types.js";
@@ -626,8 +630,10 @@ async function downloadPolicy(
   const providerUrls = Object.keys(state.authentication_providers ?? {});
   let foundRecoveryInfo: RecoveryInternalData | undefined = undefined;
   let recoveryDoc: RecoveryDocument | undefined = undefined;
-  const newProviderStatus: { [url: string]: AuthenticationProviderStatus } = 
{};
+  const newProviderStatus: { [url: string]: AuthenticationProviderStatusOk } =
+    {};
   const userAttributes = state.identity_attributes!;
+  // FIXME:  Shouldn't we also store the status of bad providers?
   for (const url of providerUrls) {
     const pi = await getProviderInfo(url);
     if ("error_code" in pi || !("http_status" in pi)) {
@@ -635,6 +641,12 @@ async function downloadPolicy(
       continue;
     }
     newProviderStatus[url] = pi;
+  }
+  for (const url of providerUrls) {
+    const pi = newProviderStatus[url];
+    if (!pi) {
+      continue;
+    }
     const userId = await userIdentifierDerive(userAttributes, pi.salt);
     const acctKeypair = accountKeypairDerive(userId);
     const resp = await fetch(new URL(`policy/${acctKeypair.pub}`, url).href);
@@ -670,7 +682,7 @@ async function downloadPolicy(
   }
   const recoveryInfo: RecoveryInformation = {
     challenges: recoveryDoc.escrow_methods.map((x) => {
-      console.log("providers", state.authentication_providers);
+      console.log("providers", newProviderStatus);
       const prov = newProviderStatus[x.url] as AuthenticationProviderStatusOk;
       return {
         cost: prov.methods.find((m) => m.type === x.escrow_type)?.usage_fee!,
@@ -692,9 +704,124 @@ async function downloadPolicy(
     recovery_state: RecoveryStates.SecretSelecting,
     recovery_document: foundRecoveryInfo,
     recovery_information: recoveryInfo,
+    verbatim_recovery_document: recoveryDoc,
   };
 }
 
+/**
+ * Try to reconstruct the secret from the available shares.
+ *
+ * Returns the state unmodified if not enough key shares are available yet.
+ */
+async function tryRecoverSecret(
+  state: ReducerStateRecovery,
+): Promise<ReducerStateRecovery | ReducerStateError> {
+  const rd = state.verbatim_recovery_document!;
+  for (const p of rd.policies) {
+    const keyShares: KeyShare[] = [];
+    let missing = false;
+    for (const truthUuid of p.uuids) {
+      const ks = (state.recovered_key_shares ?? {})[truthUuid];
+      if (!ks) {
+        missing = true;
+        break;
+      }
+      keyShares.push(ks);
+    }
+
+    if (missing) {
+      continue;
+    }
+
+    const policyKey = await policyKeyDerive(keyShares, p.salt);
+    const coreSecretBytes = await coreSecretRecover({
+      encryptedCoreSecret: rd.encrypted_core_secret,
+      encryptedMasterKey: p.master_key,
+      policyKey,
+    });
+
+    return {
+      ...state,
+      recovery_state: RecoveryStates.RecoveryFinished,
+      selected_challenge_uuid: undefined,
+      core_secret: JSON.parse(bytesToString(decodeCrock(coreSecretBytes))),
+    };
+  }
+  return { ...state };
+}
+
+async function solveChallenge(
+  state: ReducerStateRecovery,
+  ta: ActionArgsSolveChallengeRequest,
+): Promise<ReducerStateRecovery | ReducerStateError> {
+  const recDoc: RecoveryDocument = state.verbatim_recovery_document!;
+  const truth = recDoc.escrow_methods.find(
+    (x) => x.uuid === state.selected_challenge_uuid,
+  );
+  if (!truth) {
+    throw "truth for challenge not found";
+  }
+
+  const url = new URL(`/truth/${truth.uuid}`, truth.url);
+
+  // FIXME: This isn't correct for non-question truth responses.
+  url.searchParams.set(
+    "response",
+    await secureAnswerHash(ta.answer, truth.uuid, truth.truth_salt),
+  );
+
+  const resp = await fetch(url.href, {
+    headers: {
+      "Anastasis-Truth-Decryption-Key": truth.truth_key,
+    },
+  });
+
+  console.log(resp);
+
+  if (resp.status !== 200) {
+    return {
+      code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
+      hint: "got non-200 response",
+      http_status: resp.status,
+    } as ReducerStateError;
+  }
+
+  const answerSalt = truth.escrow_type === "question" ? ta.answer : undefined;
+
+  const userId = await userIdentifierDerive(
+    state.identity_attributes,
+    truth.provider_salt,
+  );
+
+  const respBody = new Uint8Array(await resp.arrayBuffer());
+  const keyShare = await decryptKeyShare(
+    encodeCrock(respBody),
+    userId,
+    answerSalt,
+  );
+
+  const recoveredKeyShares = {
+    ...(state.recovered_key_shares ?? {}),
+    [truth.uuid]: keyShare,
+  };
+
+  const challengeFeedback = {
+    ...state.challenge_feedback,
+    [truth.uuid]: {
+      state: "solved",
+    },
+  };
+
+  const newState: ReducerStateRecovery = {
+    ...state,
+    recovery_state: RecoveryStates.ChallengeSelecting,
+    challenge_feedback: challengeFeedback,
+    recovered_key_shares: recoveredKeyShares,
+  };
+
+  return tryRecoverSecret(newState);
+}
+
 async function recoveryEnterUserAttributes(
   state: ReducerStateRecovery,
   attributes: Record<string, string>,
@@ -707,6 +834,33 @@ async function recoveryEnterUserAttributes(
   return downloadPolicy(st);
 }
 
+async function selectChallenge(
+  state: ReducerStateRecovery,
+  ta: ActionArgsSelectChallenge,
+): Promise<ReducerStateRecovery | ReducerStateError> {
+  const recDoc: RecoveryDocument = state.verbatim_recovery_document!;
+  const truth = recDoc.escrow_methods.find((x) => x.uuid === ta.uuid);
+  if (!truth) {
+    throw "truth for challenge not found";
+  }
+
+  const url = new URL(`/truth/${truth.uuid}`, truth.url);
+
+  const resp = await fetch(url.href, {
+    headers: {
+      "Anastasis-Truth-Decryption-Key": truth.truth_key,
+    },
+  });
+
+  console.log(resp);
+
+  return {
+    ...state,
+    recovery_state: RecoveryStates.ChallengeSolving,
+    selected_challenge_uuid: ta.uuid,
+  };
+}
+
 export async function reduceAction(
   state: ReducerState,
   action: string,
@@ -989,17 +1143,22 @@ export async function reduceAction(
 
   if (state.recovery_state === RecoveryStates.ChallengeSelecting) {
     if (action === "select_challenge") {
-      const ta: ActionArgSelectChallenge = args;
-      return {
-        ...state,
-        recovery_state: RecoveryStates.ChallengeSolving,
-        selected_challenge_uuid: ta.uuid,
-      };
+      const ta: ActionArgsSelectChallenge = args;
+      return selectChallenge(state, ta);
     } else if (action === "back") {
       return {
         ...state,
         recovery_state: RecoveryStates.SecretSelecting,
       };
+    } else if (action === "next") {
+      const s2 = await tryRecoverSecret(state);
+      if (s2.recovery_state === RecoveryStates.RecoveryFinished) {
+        return s2;
+      }
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: "Not enough challenges solved",
+      };
     } else {
       return {
         code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
@@ -1010,12 +1169,34 @@ export async function reduceAction(
 
   if (state.recovery_state === RecoveryStates.ChallengeSolving) {
     if (action === "back") {
-      const ta: ActionArgSelectChallenge = args;
+      const ta: ActionArgsSelectChallenge = args;
+      return {
+        ...state,
+        selected_challenge_uuid: undefined,
+        recovery_state: RecoveryStates.ChallengeSelecting,
+      };
+    } else if (action === "solve_challenge") {
+      const ta: ActionArgsSolveChallengeRequest = args;
+      return solveChallenge(state, ta);
+    } else {
+      return {
+        code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
+        hint: `Unsupported action '${action}'`,
+      };
+    }
+  }
+
+  if (state.recovery_state === RecoveryStates.RecoveryFinished) {
+    if (action === "back") {
+      const ta: ActionArgsSelectChallenge = args;
       return {
         ...state,
         selected_challenge_uuid: undefined,
         recovery_state: RecoveryStates.ChallengeSelecting,
       };
+    } else if (action === "solve_challenge") {
+      const ta: ActionArgsSolveChallengeRequest = args;
+      return solveChallenge(state, ta);
     } else {
       return {
         code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
diff --git a/packages/anastasis-core/src/recovery-document-types.ts 
b/packages/anastasis-core/src/recovery-document-types.ts
index a1d9a55f..74003ccb 100644
--- a/packages/anastasis-core/src/recovery-document-types.ts
+++ b/packages/anastasis-core/src/recovery-document-types.ts
@@ -1,22 +1,37 @@
 import { TruthKey, TruthSalt, TruthUuid } from "./crypto.js";
 
 export interface RecoveryDocument {
-  // Human-readable name of the secret
+  /**
+   * Human-readable name of the secret
+   * FIXME: Why is this optional?
+   */
   secret_name?: string;
 
-  // Encrypted core secret.
-  encrypted_core_secret: string; // bytearray of undefined length
+  /**
+   * Encrypted core secret.
+   * 
+   * Variable-size length, base32-crock encoded.
+   */
+  encrypted_core_secret: string;
 
-  // List of escrow providers and selected authentication method.
+  /**
+   * List of escrow providers and selected authentication method.
+   */
   escrow_methods: EscrowMethod[];
 
-  // List of possible decryption policies.
+  /**
+   * List of possible decryption policies.
+   */
   policies: DecryptionPolicy[];
 }
 
 export interface DecryptionPolicy {
-  // Salt included to encrypt master key share when
-  // using this decryption policy.
+  /**
+   * Salt included to encrypt master key share when
+   * using this decryption policy.
+   *
+   * FIXME: Rename to policy_salt
+   */
   salt: string;
 
   /**
@@ -43,12 +58,16 @@ export interface EscrowMethod {
    */
   escrow_type: string;
 
-  // UUID of the escrow method.
-  // 16 bytes base32-crock encoded.
+  /**
+   * UUID of the escrow method.
+   * 16 bytes base32-crock encoded.
+   */
   uuid: TruthUuid;
 
-  // Key used to encrypt the Truth this EscrowMethod is related to.
-  // Client has to provide this key to the server when using /truth/.
+  /**
+   * Key used to encrypt the Truth this EscrowMethod is related to.
+   * Client has to provide this key to the server when using /truth/.
+   */
   truth_key: TruthKey;
 
   /**
@@ -60,7 +79,9 @@ export interface EscrowMethod {
   // at this provider.
   provider_salt: string;
 
-  // The instructions to give to the user (i.e. the security question
-  // if this is challenge-response).
+  /**
+   * The instructions to give to the user (i.e. the security question
+   * if this is challenge-response).
+   */
   instructions: string;
 }
diff --git a/packages/anastasis-core/src/reducer-types.ts 
b/packages/anastasis-core/src/reducer-types.ts
index 4c73dfa6..f7ba9e0f 100644
--- a/packages/anastasis-core/src/reducer-types.ts
+++ b/packages/anastasis-core/src/reducer-types.ts
@@ -1,4 +1,6 @@
 import { Duration, Timestamp } from "@gnu-taler/taler-util";
+import { KeyShare } from "./crypto.js";
+import { RecoveryDocument } from "./recovery-document-types.js";
 
 export type ReducerState =
   | ReducerStateBackup
@@ -110,8 +112,16 @@ export interface RecoveryInformation {
 }
 
 export interface ReducerStateRecovery {
-  backup_state?: undefined;
   recovery_state: RecoveryStates;
+
+  /**
+   * Unused in the recovery states.
+   */
+  backup_state?: undefined;
+
+  /**
+   * Unused in the recovery states.
+   */
   code?: undefined;
 
   identity_attributes?: { [n: string]: string };
@@ -133,10 +143,18 @@ export interface ReducerStateRecovery {
   // FIXME: This should really be renamed to recovery_internal_data
   recovery_document?: RecoveryInternalData;
 
+  // FIXME: The C reducer should also use this!
+  verbatim_recovery_document?: RecoveryDocument;
+
   selected_challenge_uuid?: string;
 
   challenge_feedback?: { [uuid: string]: ChallengeFeedback };
 
+  /**
+   * Key shares that we managed to recover so far.
+   */
+  recovered_key_shares?: { [truth_uuid: string]: KeyShare };
+
   core_secret?: {
     mime: string;
     value: string;
@@ -254,6 +272,12 @@ export interface ActionArgEnterSecret {
   expiration: Duration;
 }
 
-export interface ActionArgSelectChallenge {
+export interface ActionArgsSelectChallenge {
   uuid: string;
 }
+
+export type ActionArgsSolveChallengeRequest = SolveChallengeAnswerRequest;
+
+export interface SolveChallengeAnswerRequest {
+  answer: string;
+}
diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx 
b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
index 7ef9f345..7ccc511f 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
@@ -8,7 +8,6 @@ import { RecoveryReducerProps, AnastasisClientFrame } from 
"./index";
 export function RecoveryFinishedScreen(props: RecoveryReducerProps): VNode {
   return (
     <AnastasisClientFrame title="Recovery Finished" hideNext>
-      <h1>Recovery Finished</h1>
       <p>
         Secret: 
{bytesToString(decodeCrock(props.recoveryState.core_secret?.value!))}
       </p>

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]