From 1e92093a50962f4702339e872caa4f82af90af70 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 12 Apr 2022 12:54:57 +0200 Subject: [PATCH] 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 +- .../anastasis-webui/src/context/anastasis.ts | 7 +- .../src/hooks/use-anastasis-reducer.ts | 77 ++++++++- .../src/pages/home/SecretSelectionScreen.tsx | 102 +++++++++++- 8 files changed, 401 insertions(+), 77 deletions(-) diff --git a/packages/anastasis-core/src/crypto.ts b/packages/anastasis-core/src/crypto.ts index 75bd4b323..37e8c4f54 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 { + 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 { + 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 a355eaa54..5a9199e02 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 ?? "", + }); 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 { - 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 ?? "", 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 { 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 = {}; @@ -1574,6 +1585,59 @@ const recoveryTransitions: Record< }, }; +export async function discoverPolicies( + state: ReducerState, + cursor?: DiscoveryCursor, +): Promise { + 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 f4d998e0a..fe6292b02 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 4682eddb7..47238cd3f 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() - .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 8d5a0473b..bc6d923d7 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 Report a bug diff --git a/packages/anastasis-webui/src/context/anastasis.ts b/packages/anastasis-webui/src/context/anastasis.ts index c2e7b2a47..40d25d144 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(initial); +const Context = createContext(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 b18610427..321cf3f0a 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 { @@ -98,9 +103,21 @@ export interface ReducerTransactionHandle { transition(action: string, args: any): Promise; } +/** + * 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; exportState: () => string; importState: (s: string) => void; + discoverStart(): Promise; + discoverMore(): Promise; /** * 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 { + const res = await discoverPolicies(this.currentReducerState!, undefined); + const aggregatedPolicies: AggregatedPolicyMetaInfo[] = []; + const polHashToIndex: Record = {}; + 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 {}, 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 { 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 076d205b6..84f0303fe 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(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
no reducer in context
; + } + + if ( + !reducer.currentReducerState || + reducer.currentReducerState.recovery_state === undefined + ) { + return
invalid state
; + } + + const provs = reducer.currentReducerState.authentication_providers ?? {}; + const recoveryDocument = reducer.currentReducerState.recovery_document; + + if (manageProvider) { + return setManageProvider(false)} />; + } + + if (reducer.discoveryState.state === "none") { + // Can this even happen? + return ( + +
waiting to start discovery
+
+ ); + } + + if (reducer.discoveryState.state === "active") { + return ( + +
loading secret versions
+
+ ); + } + + const policies = reducer.discoveryState.aggregatedPolicies ?? []; + + if (policies.length === 0) { + return ( + () => {}} + > + ); + } + + return ( + +

Found versions:

+ {policies.map((x) => ( +
+ {x.policy_hash} / {x.secret_name} + +
+ ))} + +
+ ); +} + +export function OldSecretSelectionScreen(): VNode { + const [selectingVersion, setSelectingVersion] = useState(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 setManageProvider(false)} />; } - const provierInfo = provs[ + const providerInfo = provs[ recoveryDocument.provider_url ] as AuthenticationProviderStatusOk; + return (
-

{provierInfo.business_name}

+

{providerInfo.business_name}

{currentVersion === 0 ? (

Set to recover the latest version