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: discovery


From: gnunet
Subject: [taler-wallet-core] branch master updated: anastasis: discovery
Date: Tue, 12 Apr 2022 12:55:39 +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 1e92093a anastasis: discovery
1e92093a is described below

commit 1e92093a50962f4702339e872caa4f82af90af70
Author: Florian Dold <florian@dold.me>
AuthorDate: Tue Apr 12 12:54:57 2022 +0200

    anastasis: discovery
---
 packages/anastasis-core/src/crypto.ts              |  39 +++++-
 packages/anastasis-core/src/index.ts               | 152 +++++++++++++++------
 packages/anastasis-core/src/provider-types.ts      |  24 +++-
 packages/anastasis-core/src/reducer-types.ts       |  75 ++++++++--
 .../src/components/menu/NavigationBar.tsx          |   2 +-
 packages/anastasis-webui/src/context/anastasis.ts  |   7 +-
 .../src/hooks/use-anastasis-reducer.ts             |  77 ++++++++++-
 .../src/pages/home/SecretSelectionScreen.tsx       | 100 +++++++++++++-
 8 files changed, 400 insertions(+), 76 deletions(-)

diff --git a/packages/anastasis-core/src/crypto.ts 
b/packages/anastasis-core/src/crypto.ts
index 75bd4b32..37e8c4f5 100644
--- a/packages/anastasis-core/src/crypto.ts
+++ b/packages/anastasis-core/src/crypto.ts
@@ -1,16 +1,15 @@
 import {
-  bytesToString,
   canonicalJson,
   decodeCrock,
   encodeCrock,
   getRandomBytes,
-  kdf,
   kdfKw,
   secretbox,
   crypto_sign_keyPair_fromSeed,
   stringToBytes,
   secretbox_open,
   hash,
+  bytesToString,
 } from "@gnu-taler/taler-util";
 import { argon2id } from "hash-wasm";
 
@@ -111,6 +110,42 @@ export async function decryptRecoveryDocument(
   return anastasisDecrypt(asOpaque(userId), recoveryDocData, "erd");
 }
 
+export interface PolicyMetadata {
+  secret_name: string;
+  policy_hash: string;
+}
+
+export async function encryptPolicyMetadata(
+  userId: UserIdentifier,
+  metadata: PolicyMetadata,
+): Promise<OpaqueData> {
+  const metadataBytes = typedArrayConcat([
+    decodeCrock(metadata.policy_hash),
+    stringToBytes(metadata.secret_name),
+  ]);
+  const nonce = encodeCrock(getRandomBytes(nonceSize));
+  return anastasisEncrypt(
+    nonce,
+    asOpaque(userId),
+    encodeCrock(metadataBytes),
+    "rmd",
+  );
+}
+
+export async function decryptPolicyMetadata(
+  userId: UserIdentifier,
+  metadataEnc: OpaqueData,
+): Promise<PolicyMetadata> {
+  const plain = await anastasisDecrypt(asOpaque(userId), metadataEnc, "rmd");
+  const metadataBytes = decodeCrock(plain);
+  const policyHash = encodeCrock(metadataBytes.slice(0, 64));
+  const secretName = bytesToString(metadataBytes.slice(64));
+  return {
+    policy_hash: policyHash,
+    secret_name: secretName,
+  };
+}
+
 export function typedArrayConcat(chunks: Uint8Array[]): Uint8Array {
   let payloadLen = 0;
   for (const c of chunks) {
diff --git a/packages/anastasis-core/src/index.ts 
b/packages/anastasis-core/src/index.ts
index a355eaa5..5a9199e0 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -22,11 +22,13 @@ import {
   TalerProtocolTimestamp,
   TalerSignaturePurpose,
   AbsoluteTime,
+  URL,
 } from "@gnu-taler/taler-util";
 import { anastasisData } from "./anastasis-data.js";
 import {
   EscrowConfigurationResponse,
   IbanExternalAuthResponse,
+  RecoveryMetaResponse as RecoveryMetaResponse,
   TruthUploadRequest,
 } from "./provider-types.js";
 import {
@@ -68,6 +70,10 @@ import {
   ActionArgsUpdatePolicy,
   ActionArgsAddProvider,
   ActionArgsDeleteProvider,
+  DiscoveryCursor,
+  DiscoveryResult,
+  PolicyMetaInfo,
+  ChallengeInfo,
 } from "./reducer-types.js";
 import fetchPonyfill from "fetch-ponyfill";
 import {
@@ -91,6 +97,8 @@ import {
   KeyShare,
   coreSecretRecover,
   pinAnswerHash,
+  decryptPolicyMetadata,
+  encryptPolicyMetadata,
 } from "./crypto.js";
 import { unzlibSync, zlibSync } from "fflate";
 import {
@@ -112,6 +120,8 @@ export * from "./challenge-feedback-types.js";
 
 const logger = new Logger("anastasis-core:index.ts");
 
+const ANASTASIS_HTTP_HEADER_POLICY_META_DATA = "Anastasis-Policy-Meta-Data";
+
 function getContinents(
   opts: { requireProvider?: boolean } = {},
 ): ContinentInfo[] {
@@ -224,10 +234,12 @@ async function selectCountry(
     });
   }
 
-  const providers: { [x: string]: {} } = {};
+  const providers: { [x: string]: AuthenticationProviderStatus } = {};
   for (const prov of anastasisData.providersList.anastasis_provider) {
     if (currencies.includes(prov.currency)) {
-      providers[prov.url] = {};
+      providers[prov.url] = {
+        status: "not-contacted",
+      };
     }
   }
 
@@ -273,12 +285,14 @@ async function getProviderInfo(
     resp = await fetch(new URL("config", providerBaseUrl).href);
   } catch (e) {
     return {
+      status: "error",
       code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
       hint: "request to provider failed",
     };
   }
   if (resp.status !== 200) {
     return {
+      status: "error",
       code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
       hint: "unexpected status",
       http_status: resp.status,
@@ -287,6 +301,7 @@ async function getProviderInfo(
   try {
     const jsonResp: EscrowConfigurationResponse = await resp.json();
     return {
+      status: "ok",
       http_status: 200,
       annual_fee: jsonResp.annual_fee,
       business_name: jsonResp.business_name,
@@ -299,9 +314,10 @@ async function getProviderInfo(
       salt: jsonResp.server_salt,
       storage_limit_in_megabytes: jsonResp.storage_limit_in_megabytes,
       truth_upload_fee: jsonResp.truth_upload_fee,
-    } as AuthenticationProviderStatusOk;
+    };
   } catch (e) {
     return {
+      status: "error",
       code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
       hint: "provider did not return JSON",
     };
@@ -594,6 +610,7 @@ async function uploadSecret(
     const userId = await getUserIdCaching(prov.provider_url);
     const acctKeypair = accountKeypairDerive(userId);
     const zippedDoc = await compressRecoveryDoc(rd);
+    const recoveryDocHash = encodeCrock(hash(zippedDoc));
     const encRecoveryDoc = await encryptRecoveryDocument(
       userId,
       encodeCrock(zippedDoc),
@@ -603,6 +620,10 @@ async function uploadSecret(
       .put(bodyHash)
       .build();
     const sig = eddsaSign(sigPS, decodeCrock(acctKeypair.priv));
+    const metadataEnc = await encryptPolicyMetadata(userId, {
+      policy_hash: recoveryDocHash,
+      secret_name: state.secret_name ?? "<unnamed secret>",
+    });
     const talerPayUri = state.policy_payment_requests?.find(
       (x) => x.provider === prov.provider_url,
     )?.payto;
@@ -621,6 +642,7 @@ async function uploadSecret(
       headers: {
         "Anastasis-Policy-Signature": encodeCrock(sig),
         "If-None-Match": encodeCrock(bodyHash),
+        [ANASTASIS_HTTP_HEADER_POLICY_META_DATA]: metadataEnc,
         ...(paySecret
           ? {
               "Anastasis-Payment-Identifier": paySecret,
@@ -704,37 +726,21 @@ async function uploadSecret(
 async function downloadPolicy(
   state: ReducerStateRecovery,
 ): Promise<ReducerStateRecovery | ReducerStateError> {
-  const providerUrls = Object.keys(state.authentication_providers ?? {});
   let foundRecoveryInfo: RecoveryInternalData | undefined = undefined;
   let recoveryDoc: RecoveryDocument | undefined = undefined;
-  const newProviderStatus: { [url: string]: AuthenticationProviderStatusOk } =
-    {};
   const userAttributes = state.identity_attributes!;
-  const restrictProvider = state.selected_provider_url;
-  // 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)) {
-      // Could not even get /config of the provider
-      continue;
-    }
-    newProviderStatus[url] = pi;
+  if (!state.selected_version) {
+    throw Error("invalid state");
   }
-  for (const url of providerUrls) {
-    const pi = newProviderStatus[url];
-    if (!pi) {
-      continue;
-    }
-    if (restrictProvider && url !== state.selected_provider_url) {
-      // User wants specific provider.
+  for (const prov of state.selected_version.providers) {
+    const pi = state.authentication_providers?.[prov.provider_url];
+    if (!pi || pi.status !== "ok") {
       continue;
     }
     const userId = await userIdentifierDerive(userAttributes, pi.salt);
     const acctKeypair = accountKeypairDerive(userId);
-    const reqUrl = new URL(`policy/${acctKeypair.pub}`, url);
-    if (state.selected_version) {
-      reqUrl.searchParams.set("version", `${state.selected_version}`);
-    }
+    const reqUrl = new URL(`policy/${acctKeypair.pub}`, prov.provider_url);
+    reqUrl.searchParams.set("version", `${prov.version}`);
     const resp = await fetch(reqUrl.href);
     if (resp.status !== 200) {
       continue;
@@ -752,7 +758,7 @@ async function downloadPolicy(
       policyVersion = Number(resp.headers.get("Anastasis-Version") ?? "0");
     } catch (e) {}
     foundRecoveryInfo = {
-      provider_url: url,
+      provider_url: prov.provider_url,
       secret_name: rd.secret_name ?? "<unknown>",
       version: policyVersion,
     };
@@ -765,16 +771,24 @@ async function downloadPolicy(
       hint: "No backups found at any provider for your identity information.",
     };
   }
+
+  const challenges: ChallengeInfo[] = [];
+
+  for (const x of recoveryDoc.escrow_methods) {
+    const pi = state.authentication_providers?.[x.url];
+    if (!pi || pi.status !== "ok") {
+      continue;
+    }
+    challenges.push({
+      cost: pi.methods.find((m) => m.type === x.escrow_type)?.usage_fee!,
+      instructions: x.instructions,
+      type: x.escrow_type,
+      uuid: x.uuid,
+    });
+  }
+
   const recoveryInfo: RecoveryInformation = {
-    challenges: recoveryDoc.escrow_methods.map((x) => {
-      const prov = newProviderStatus[x.url] as AuthenticationProviderStatusOk;
-      return {
-        cost: prov.methods.find((m) => m.type === x.escrow_type)?.usage_fee!,
-        instructions: x.instructions,
-        type: x.escrow_type,
-        uuid: x.uuid,
-      };
-    }),
+    challenges,
     policies: recoveryDoc.policies.map((x) => {
       return x.uuids.map((m) => {
         return {
@@ -785,7 +799,7 @@ async function downloadPolicy(
   };
   return {
     ...state,
-    recovery_state: RecoveryStates.SecretSelecting,
+    recovery_state: RecoveryStates.ChallengeSelecting,
     recovery_document: foundRecoveryInfo,
     recovery_information: recoveryInfo,
     verbatim_recovery_document: recoveryDoc,
@@ -1019,10 +1033,11 @@ async function recoveryEnterUserAttributes(
   }
   const st: ReducerStateRecovery = {
     ...state,
+    recovery_state: RecoveryStates.SecretSelecting,
     identity_attributes: args.identity_attributes,
     authentication_providers: newProviders,
   };
-  return downloadPolicy(st);
+  return st;
 }
 
 async function changeVersion(
@@ -1031,8 +1046,7 @@ async function changeVersion(
 ): Promise<ReducerStateRecovery | ReducerStateError> {
   const st: ReducerStateRecovery = {
     ...state,
-    selected_version: args.version,
-    selected_provider_url: args.provider_url,
+    selected_version: args.selection,
   };
   return downloadPolicy(st);
 }
@@ -1313,10 +1327,7 @@ async function nextFromAuthenticationsEditing(
   const providers: ProviderInfo[] = [];
   for (const provUrl of Object.keys(state.authentication_providers ?? {})) {
     const prov = state.authentication_providers![provUrl];
-    if ("error_code" in prov) {
-      continue;
-    }
-    if (!("http_status" in prov && prov.http_status === 200)) {
+    if (prov.status !== "ok") {
       continue;
     }
     const methodCost: Record<string, AmountString> = {};
@@ -1574,6 +1585,59 @@ const recoveryTransitions: Record<
   },
 };
 
+export async function discoverPolicies(
+  state: ReducerState,
+  cursor?: DiscoveryCursor,
+): Promise<DiscoveryResult> {
+  if (!state.recovery_state) {
+    throw Error("can only discover providers in recovery state");
+  }
+
+  const policies: PolicyMetaInfo[] = [];
+
+  const providerUrls = Object.keys(state.authentication_providers || {});
+  // FIXME: Do we need to re-contact providers here / check if they're 
disabled?
+
+  for (const providerUrl of providerUrls) {
+    const providerInfo = await getProviderInfo(providerUrl);
+    if (providerInfo.status !== "ok") {
+      continue;
+    }
+    const userId = await userIdentifierDerive(
+      state.identity_attributes!,
+      providerInfo.salt,
+    );
+    const acctKeypair = accountKeypairDerive(userId);
+    const reqUrl = new URL(`policy/${acctKeypair.pub}/meta`, providerUrl);
+    const resp = await fetch(reqUrl.href);
+    if (resp.status !== 200) {
+      logger.warn(`Could not fetch policy metadate from ${reqUrl.href}`);
+      continue;
+    }
+    const respJson: RecoveryMetaResponse = await resp.json();
+    const versions = Object.keys(respJson);
+    for (const version of versions) {
+      const item = respJson[version];
+      if (!item.meta) {
+        continue;
+      }
+      const metaData = await decryptPolicyMetadata(userId, item.meta!);
+      policies.push({
+        attribute_mask: 0,
+        provider_url: providerUrl,
+        server_time: item.upload_time,
+        version: Number.parseInt(version, 10),
+        secret_name: metaData.secret_name,
+        policy_hash: metaData.policy_hash,
+      });
+    }
+  }
+  return {
+    policies,
+    cursor: undefined,
+  };
+}
+
 export async function reduceAction(
   state: ReducerState,
   action: string,
diff --git a/packages/anastasis-core/src/provider-types.ts 
b/packages/anastasis-core/src/provider-types.ts
index f4d998e0..fe6292b0 100644
--- a/packages/anastasis-core/src/provider-types.ts
+++ b/packages/anastasis-core/src/provider-types.ts
@@ -1,4 +1,9 @@
-import { Amounts, AmountString } from "@gnu-taler/taler-util";
+import {
+  Amounts,
+  AmountString,
+  TalerProtocolDuration,
+  TalerProtocolTimestamp,
+} from "@gnu-taler/taler-util";
 
 export interface EscrowConfigurationResponse {
   // Protocol identifier, clarifies that this is an Anastasis provider.
@@ -83,3 +88,20 @@ export interface IbanExternalAuthResponse {
     wire_transfer_subject: string;
   };
 }
+
+export interface RecoveryMetaResponse {
+  /**
+   * Version numbers as a string (!) are used as keys.
+   */
+  [version: string]: RecoveryMetaDataItem;
+}
+
+export interface RecoveryMetaDataItem {
+  // The meta value can be NULL if the document
+  // exists but no meta data was provided.
+  meta?: string;
+
+  // Server-time indicative of when the recovery
+  // document was uploaded.
+  upload_time: TalerProtocolTimestamp;
+}
diff --git a/packages/anastasis-core/src/reducer-types.ts 
b/packages/anastasis-core/src/reducer-types.ts
index 4682eddb..47238cd3 100644
--- a/packages/anastasis-core/src/reducer-types.ts
+++ b/packages/anastasis-core/src/reducer-types.ts
@@ -202,14 +202,9 @@ export interface ReducerStateRecovery {
   /**
    * Explicitly selected version by the user.
    * FIXME: In the C reducer this is called "version".
+   * FIXME: rename to selected_secret / selected_policy?
    */
-  selected_version?: number;
-
-  /**
-   * Explicitly selected provider URL by the user.
-   * FIXME: In the C reducer this is called "provider_url".
-   */
-  selected_provider_url?: string;
+  selected_version?: AggregatedPolicyMetaInfo;
 
   challenge_feedback?: { [uuid: string]: ChallengeFeedback };
 
@@ -291,10 +286,12 @@ export interface MethodSpec {
   usage_fee: string;
 }
 
-// FIXME: This should be tagged!
-export type AuthenticationProviderStatusEmpty = {};
+export type AuthenticationProviderStatusEmpty = {
+  status: "not-contacted";
+};
 
 export interface AuthenticationProviderStatusOk {
+  status: "ok";
   annual_fee: string;
   business_name: string;
   currency: string;
@@ -304,11 +301,15 @@ export interface AuthenticationProviderStatusOk {
   storage_limit_in_megabytes: number;
   truth_upload_fee: string;
   methods: MethodSpec[];
+  // FIXME: add timestamp?
 }
 
 export interface AuthenticationProviderStatusError {
-  http_status: number;
-  error_code: number;
+  status: "error";
+  http_status?: number;
+  code: number;
+  hint?: string;
+  // FIXME: add timestamp?
 }
 
 export type AuthenticationProviderStatus =
@@ -441,8 +442,7 @@ export interface ActionArgsUpdateExpiration {
 }
 
 export interface ActionArgsChangeVersion {
-  provider_url: string;
-  version: number;
+  selection: AggregatedPolicyMetaInfo;
 }
 
 export interface ActionArgsUpdatePolicy {
@@ -450,10 +450,55 @@ export interface ActionArgsUpdatePolicy {
   policy: PolicyMember[];
 }
 
+/**
+ * Cursor for a provider discovery process.
+ */
+export interface DiscoveryCursor {
+  position: {
+    provider_url: string;
+    mask: number;
+    max_version?: number;
+  }[];
+}
+
+export interface PolicyMetaInfo {
+  policy_hash: string;
+  provider_url: string;
+  version: number;
+  attribute_mask: number;
+  server_time: TalerProtocolTimestamp;
+  secret_name?: string;
+}
+
+
+/**
+ * Aggregated / de-duplicated policy meta info.
+ */
+export interface AggregatedPolicyMetaInfo {
+  secret_name?: string;
+  policy_hash: string;
+  attribute_mask: number;
+  providers: {
+    provider_url: string;
+    version: number;
+  }[];
+}
+
+export interface DiscoveryResult {
+  /**
+   * Found policies.
+   */
+  policies: PolicyMetaInfo[];
+
+  /**
+   * Cursor that allows getting more results.
+   */
+  cursor?: DiscoveryCursor;
+}
+
 export const codecForActionArgsChangeVersion = () =>
   buildCodecForObject<ActionArgsChangeVersion>()
-    .property("provider_url", codecForString())
-    .property("version", codecForNumber())
+    .property("selection", codecForAny())
     .build("ActionArgsChangeVersion");
 
 export const codecForPolicyMember = () =>
diff --git a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx 
b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
index 8d5a0473..bc6d923d 100644
--- a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
@@ -46,7 +46,7 @@ export function NavigationBar({ onMobileMenu, title }: 
Props): VNode {
           Contact us
         </a>
         <a
-          href="https://bugs.anastasis.li/";
+          href="https://bugs.anastasis.lu/";
           style={{ alignSelf: "center", padding: "0.5em" }}
         >
           Report a bug
diff --git a/packages/anastasis-webui/src/context/anastasis.ts 
b/packages/anastasis-webui/src/context/anastasis.ts
index c2e7b2a4..40d25d14 100644
--- a/packages/anastasis-webui/src/context/anastasis.ts
+++ b/packages/anastasis-webui/src/context/anastasis.ts
@@ -23,11 +23,9 @@ import { createContext, h, VNode } from "preact";
 import { useContext } from "preact/hooks";
 import { AnastasisReducerApi } from "../hooks/use-anastasis-reducer";
 
-type Type = AnastasisReducerApi | undefined;
-
 const initial = undefined;
 
-const Context = createContext<Type>(initial);
+const Context = createContext<AnastasisReducerApi | undefined>(initial);
 
 interface Props {
   value: AnastasisReducerApi;
@@ -38,4 +36,5 @@ export const AnastasisProvider = ({ value, children }: 
Props): VNode => {
   return h(Context.Provider, { value, children });
 };
 
-export const useAnastasisContext = (): Type => useContext(Context);
+export const useAnastasisContext = (): AnastasisReducerApi | undefined =>
+  useContext(Context);
diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts 
b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
index b1861042..321cf3f0 100644
--- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
+++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
@@ -1,8 +1,12 @@
 import { TalerErrorCode } from "@gnu-taler/taler-util";
 import {
+  AggregatedPolicyMetaInfo,
   BackupStates,
+  discoverPolicies,
+  DiscoveryCursor,
   getBackupStartState,
   getRecoveryStartState,
+  PolicyMetaInfo,
   RecoveryStates,
   reduceAction,
   ReducerState,
@@ -15,6 +19,7 @@ const remoteReducer = false;
 interface AnastasisState {
   reducerState: ReducerState | undefined;
   currentError: any;
+  discoveryState: DiscoveryUiState;
 }
 
 async function getBackupStartStateRemote(): Promise<ReducerState> {
@@ -98,9 +103,21 @@ export interface ReducerTransactionHandle {
   transition(action: string, args: any): Promise<ReducerState>;
 }
 
+/**
+ * UI-relevant state of the policy discovery process.
+ */
+export interface DiscoveryUiState {
+  state: "none" | "active" | "finished";
+
+  aggregatedPolicies?: AggregatedPolicyMetaInfo[];
+
+  cursor?: DiscoveryCursor;
+}
+
 export interface AnastasisReducerApi {
   currentReducerState: ReducerState | undefined;
   currentError: any;
+  discoveryState: DiscoveryUiState;
   dismissError: () => void;
   startBackup: () => void;
   startRecover: () => void;
@@ -109,6 +126,8 @@ export interface AnastasisReducerApi {
   transition(action: string, args: any): Promise<void>;
   exportState: () => string;
   importState: (s: string) => void;
+  discoverStart(): Promise<void>;
+  discoverMore(): Promise<void>;
   /**
    * Run multiple reducer steps in a transaction without
    * affecting the UI-visible transition state in-between.
@@ -152,6 +171,9 @@ export function useAnastasisReducer(): AnastasisReducerApi {
     () => ({
       reducerState: getStateFromStorage(),
       currentError: undefined,
+      discoveryState: {
+        state: "none",
+      },
     }),
   );
 
@@ -192,6 +214,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
   return {
     currentReducerState: anastasisState.reducerState,
     currentError: anastasisState.currentError,
+    discoveryState: anastasisState.discoveryState,
     async startBackup() {
       let s: ReducerState;
       if (remoteReducer) {
@@ -213,17 +236,59 @@ export function useAnastasisReducer(): 
AnastasisReducerApi {
       }
     },
     exportState() {
-      const state = getStateFromStorage()
-      return JSON.stringify(state)
+      const state = getStateFromStorage();
+      return JSON.stringify(state);
     },
     importState(s: string) {
       try {
-        const state = JSON.parse(s)
-        setAnastasisState({ reducerState: state, currentError: undefined })
+        const state = JSON.parse(s);
+        setAnastasisState({
+          reducerState: state,
+          currentError: undefined,
+          discoveryState: {
+            state: "none",
+          },
+        });
       } catch (e) {
-        throw Error('could not restore the state')
+        throw Error("could not restore the state");
+      }
+    },
+    async discoverStart(): Promise<void> {
+      const res = await discoverPolicies(this.currentReducerState!, undefined);
+      const aggregatedPolicies: AggregatedPolicyMetaInfo[] = [];
+      const polHashToIndex: Record<string, number> = {};
+      for (const pol of res.policies) {
+        const oldIndex = polHashToIndex[pol.policy_hash];
+        if (oldIndex != null) {
+          aggregatedPolicies[oldIndex].providers.push({
+            provider_url: pol.provider_url,
+            version: pol.version,
+          });
+        } else {
+          aggregatedPolicies.push({
+            attribute_mask: pol.attribute_mask,
+            policy_hash: pol.policy_hash,
+            providers: [
+              {
+                provider_url: pol.provider_url,
+                version: pol.version,
+              },
+            ],
+            secret_name: pol.secret_name,
+          });
+          polHashToIndex[pol.policy_hash] = aggregatedPolicies.length - 1;
+        }
       }
+      setAnastasisState({
+        ...anastasisState,
+        discoveryState: {
+          state: "finished",
+          aggregatedPolicies,
+          cursor: res.cursor,
+        },
+      });
     },
+    async discoverMore(): Promise<void> {},
     async startRecover() {
       let s: ReducerState;
       if (remoteReducer) {
@@ -301,7 +366,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
 }
 
 class ReducerTxImpl implements ReducerTransactionHandle {
-  constructor(public transactionState: ReducerState) { }
+  constructor(public transactionState: ReducerState) {}
   async transition(action: string, args: any): Promise<ReducerState> {
     let s: ReducerState;
     if (remoteReducer) {
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx 
b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
index 076d205b..84f0303f 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
@@ -1,9 +1,10 @@
 import {
   AuthenticationProviderStatus,
   AuthenticationProviderStatusOk,
+  PolicyMetaInfo,
 } from "@gnu-taler/anastasis-core";
 import { h, VNode } from "preact";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
 import { AsyncButton } from "../../components/AsyncButton";
 import { PhoneNumberInput } from "../../components/fields/NumberInput";
 import { useAnastasisContext } from "../../context/anastasis";
@@ -13,8 +14,100 @@ import { AnastasisClientFrame } from "./index";
 export function SecretSelectionScreen(): VNode {
   const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
   const reducer = useAnastasisContext();
+  const [manageProvider, setManageProvider] = useState(false);
+
+  useEffect(() => {
+    async function f() {
+      if (reducer) {
+        await reducer.discoverStart();
+      }
+    }
+    f().catch((e) => console.log(e));
+  }, []);
+
+  if (!reducer) {
+    return <div>no reducer in context</div>;
+  }
+
+  if (
+    !reducer.currentReducerState ||
+    reducer.currentReducerState.recovery_state === undefined
+  ) {
+    return <div>invalid state</div>;
+  }
 
+  const provs = reducer.currentReducerState.authentication_providers ?? {};
+  const recoveryDocument = reducer.currentReducerState.recovery_document;
+
+  if (manageProvider) {
+    return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
+  }
+
+  if (reducer.discoveryState.state === "none") {
+    // Can this even happen?
+    return (
+      <AnastasisClientFrame title="Recovery: Select secret">
+        <div>waiting to start discovery</div>
+      </AnastasisClientFrame>
+    );
+  }
+
+  if (reducer.discoveryState.state === "active") {
+    return (
+      <AnastasisClientFrame title="Recovery: Select secret">
+        <div>loading secret versions</div>
+      </AnastasisClientFrame>
+    );
+  }
+
+  const policies = reducer.discoveryState.aggregatedPolicies ?? [];
+
+  if (policies.length === 0) {
+    return (
+      <ChooseAnotherProviderScreen
+        providers={provs}
+        selected=""
+        onChange={(newProv) => () => {}}
+      ></ChooseAnotherProviderScreen>
+    );
+  }
+
+  return (
+    <AnastasisClientFrame title="Recovery: Select secret" hideNext="Please 
select version to recover">
+      <p>Found versions:</p>
+      {policies.map((x) => (
+        <div>
+          {x.policy_hash} / {x.secret_name}
+          <button
+            onClick={async () => {
+              await reducer.transition("change_version", {
+                selection: x,
+              });
+            }}
+          >
+            Recover
+          </button>
+        </div>
+      ))}
+      <button>Load older versions</button>
+    </AnastasisClientFrame>
+  );
+}
+
+export function OldSecretSelectionScreen(): VNode {
+  const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
+  const reducer = useAnastasisContext();
   const [manageProvider, setManageProvider] = useState(false);
+
+  useEffect(() => {
+    async function f() {
+      if (reducer) {
+        await reducer.discoverStart();
+      }
+    }
+    f().catch((e) => console.log(e));
+  }, []);
+
   const currentVersion =
     (reducer?.currentReducerState &&
       "recovery_document" in reducer.currentReducerState &&
@@ -71,15 +164,16 @@ export function SecretSelectionScreen(): VNode {
     return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
   }
 
-  const provierInfo = provs[
+  const providerInfo = provs[
     recoveryDocument.provider_url
   ] as AuthenticationProviderStatusOk;
+
   return (
     <AnastasisClientFrame title="Recovery: Select secret">
       <div class="columns">
         <div class="column">
           <div class="box" style={{ border: "2px solid green" }}>
-            <h1 class="subtitle">{provierInfo.business_name}</h1>
+            <h1 class="subtitle">{providerInfo.business_name}</h1>
             <div class="block">
               {currentVersion === 0 ? (
                 <p>Set to recover the latest version</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]