diff --git a/apps/funke/authenticated-channel/requestPid.ts b/apps/ausweis/src/authenticated-channel/requestPid.ts similarity index 100% rename from apps/funke/authenticated-channel/requestPid.ts rename to apps/ausweis/src/authenticated-channel/requestPid.ts diff --git a/apps/ausweis/src/crypto/bPrime.ts b/apps/ausweis/src/crypto/bPrime.ts index dda2ea35..29d7bb6f 100644 --- a/apps/ausweis/src/crypto/bPrime.ts +++ b/apps/ausweis/src/crypto/bPrime.ts @@ -8,8 +8,8 @@ import { TypedArrayEncoder, getJwkFromKey, } from '@credo-ts/core' -import type { FullAppAgent } from '@package/agent/src' import { kdf } from '@package/secure-store/kdf' +import type { FullAppAgent } from 'packages/agent/src' import { ausweisAes256Gcm } from './aes' /** diff --git a/apps/ausweis/src/features/onboarding/onboardingContext.tsx b/apps/ausweis/src/features/onboarding/onboardingContext.tsx index 678e9725..f243f7b3 100644 --- a/apps/ausweis/src/features/onboarding/onboardingContext.tsx +++ b/apps/ausweis/src/features/onboarding/onboardingContext.tsx @@ -1,5 +1,6 @@ import { sendCommand } from '@animo-id/expo-ausweis-sdk' import { type AppAgent, initializeAppAgent, useSecureUnlock } from '@ausweis/agent' +import { ReceivePidUseCaseBPrimeFlow } from '@ausweis/use-cases/ReceivePidUseCaseBPrimeFlow' import { type CardScanningErrorDetails, ReceivePidUseCaseCFlow, @@ -22,7 +23,6 @@ import { OnboardingIdCardStartScan } from './screens/id-card-start-scan' import { OnboardingIntroductionSteps } from './screens/introduction-steps' import OnboardingPinEnter from './screens/pin' import OnboardingWelcome from './screens/welcome' -import { ReceivePidUseCaseBPrimeFlow } from '@ausweis/use-cases/ReceivePidUseCaseBPrimeFlow' type Page = | { type: 'fullscreen' } @@ -35,7 +35,7 @@ type Page = // Same animation key means the content won't fade out and then in again. So if the two screens have most content in common // this looks nicer. -const onboardingStepsCFlow = [ +const onboardingSteps = [ { step: 'welcome', progress: 0, @@ -147,7 +147,7 @@ const onboardingStepsCFlow = [ Screen: React.FunctionComponent }> -export type OnboardingSteps = typeof onboardingStepsCFlow +export type OnboardingSteps = typeof onboardingSteps export type OnboardingStep = OnboardingSteps[number] export type OnboardingContext = { @@ -164,15 +164,16 @@ export function OnboardingContextProvider({ children, }: PropsWithChildren<{ initialStep?: OnboardingStep['step'] - flow?: 'c' | 'bprime' }>) { const toast = useToastController() const secureUnlock = useSecureUnlock() const [currentStepName, setCurrentStepName] = useState(initialStep ?? 'welcome') const router = useRouter() + const [selectedFlow, setSelectedFlow] = useState<'c' | 'bprime'>('c') const [receivePidUseCase, setReceivePidUseCase] = useState() const [receivePidUseCaseState, setReceivePidUseCaseState] = useState() + const [walletPin, setWalletPin] = useState() const [idCardPin, setIdCardPin] = useState() const [userName, setUserName] = useState() @@ -189,12 +190,12 @@ export function OnboardingContextProvider({ showScanModal: true, }) - const currentStep = onboardingStepsCFlow.find((step) => step.step === currentStepName) + const currentStep = onboardingSteps.find((step) => step.step === currentStepName) if (!currentStep) throw new Error(`Invalid step ${currentStepName}`) const goToNextStep = useCallback(() => { - const currentStepIndex = onboardingStepsCFlow.findIndex((step) => step.step === currentStepName) - const nextStep = onboardingStepsCFlow[currentStepIndex + 1] + const currentStepIndex = onboardingSteps.findIndex((step) => step.step === currentStepName) + const nextStep = onboardingSteps[currentStepIndex + 1] if (nextStep) { setCurrentStepName(nextStep.step) @@ -205,8 +206,8 @@ export function OnboardingContextProvider({ }, [currentStepName, router]) const goToPreviousStep = useCallback(() => { - const currentStepIndex = onboardingStepsCFlow.findIndex((step) => step.step === currentStepName) - const previousStep = onboardingStepsCFlow[currentStepIndex - 1] + const currentStepIndex = onboardingSteps.findIndex((step) => step.step === currentStepName) + const previousStep = onboardingSteps[currentStepIndex - 1] if (previousStep) { setCurrentStepName(previousStep.step) @@ -218,6 +219,12 @@ export function OnboardingContextProvider({ goToNextStep() } + const selectFlow = (flow: 'c' | 'bprime') => { + setSelectedFlow(flow) + + goToNextStep() + } + // Bit sad but if we try to call this in the initializeAgent callback sometimes the state hasn't updated // in the secure unlock yet, which means that it will throw an error, so we use an effect. Probably need // to do a refactor on this and move more logic outside of the react world, as it's a bit weird with state @@ -343,26 +350,42 @@ export function OnboardingContextProvider({ } if (!receivePidUseCase && receivePidUseCaseState !== 'initializing') { - return ReceivePidUseCaseCFlow.initialize({ - agent: secureUnlock.context.agent, - onStateChange: setReceivePidUseCaseState, - onCardAttachedChanged: ({ isCardAttached }) => - setIdCardScanningState((state) => ({ - ...state, - isCardAttached, - state: state.state === 'readyToScan' && isCardAttached ? 'scanning' : state.state, - })), - onStatusProgress: ({ progress }) => setIdCardScanningState((state) => ({ ...state, progress })), - onEnterPin: (options) => onEnterPinRef.current.onEnterPin(options), - }) - .then((receivePidUseCase) => { + if (selectedFlow === 'c') { + return ReceivePidUseCaseCFlow.initialize({ + agent: secureUnlock.context.agent, + onStateChange: setReceivePidUseCaseState, + onCardAttachedChanged: ({ isCardAttached }) => + setIdCardScanningState((state) => ({ + ...state, + isCardAttached, + state: state.state === 'readyToScan' && isCardAttached ? 'scanning' : state.state, + })), + onStatusProgress: ({ progress }) => setIdCardScanningState((state) => ({ ...state, progress })), + onEnterPin: (options) => onEnterPinRef.current.onEnterPin(options), + }).then((receivePidUseCase) => { setReceivePidUseCase(receivePidUseCase) goToNextStep() }) - .catch((e) => { - reset({ error: e, resetToStep: 'id-card-pin' }) - throw e + } + if (selectedFlow === 'bprime') { + if (!walletPin) { + throw new Error('wallet Pin has not been setup!') + } + return ReceivePidUseCaseBPrimeFlow.initialize({ + agent: secureUnlock.context.agent, + onStateChange: setReceivePidUseCaseState, + onEnterPin: (options) => onEnterPinRef.current.onEnterPin(options), + pidPin: walletPin.split('').map(Number), }) + .then((receivePidUseCase) => { + setReceivePidUseCase(receivePidUseCase) + goToNextStep() + }) + .catch((e) => { + reset({ error: e, resetToStep: 'id-card-pin' }) + throw e + }) + } } goToNextStep() @@ -380,8 +403,8 @@ export function OnboardingContextProvider({ }) => { if (error) console.error(error) - const stepsToCompleteAfterReset = onboardingStepsCFlow - .slice(onboardingStepsCFlow.findIndex((step) => step.step === resetToStep)) + const stepsToCompleteAfterReset = onboardingSteps + .slice(onboardingSteps.findIndex((step) => step.step === resetToStep)) .map((step) => step.step) if (stepsToCompleteAfterReset.includes('pin')) { @@ -510,7 +533,9 @@ export function OnboardingContextProvider({ } let screen: React.JSX.Element - if (currentStep.step === 'pin' || currentStep.step === 'pin-reenter') { + if (currentStep.step === 'welcome') { + screen = + } else if (currentStep.step === 'pin' || currentStep.step === 'pin-reenter') { screen = } else if (currentStep.step === 'id-card-pin') { screen = diff --git a/apps/ausweis/src/features/onboarding/screens/pin.tsx b/apps/ausweis/src/features/onboarding/screens/pin.tsx index f587e60b..5b4912f4 100644 --- a/apps/ausweis/src/features/onboarding/screens/pin.tsx +++ b/apps/ausweis/src/features/onboarding/screens/pin.tsx @@ -1,4 +1,4 @@ -import { PinDotsInput, type PinDotsInputRef, YStack } from '@package/ui' +import { PinDotsInput, type PinDotsInputRef } from '@package/ui' import React, { useRef } from 'react' export interface OnboardingPinEnterProps { diff --git a/apps/ausweis/src/features/onboarding/screens/welcome.tsx b/apps/ausweis/src/features/onboarding/screens/welcome.tsx index fe6698b9..45b2f8d0 100644 --- a/apps/ausweis/src/features/onboarding/screens/welcome.tsx +++ b/apps/ausweis/src/features/onboarding/screens/welcome.tsx @@ -1,13 +1,15 @@ -import { Button, FlexPage, Heading, HeroIcons, Separator, XStack, YStack } from '@package/ui' -import React from 'react' +import { Button, FlexPage, Heading, Separator, XStack, YStack } from '@package/ui' +import React, { useState } from 'react' import Animated, { FadingTransition } from 'react-native-reanimated' import { LinearGradient } from 'tamagui/linear-gradient' export interface OnboardingWelcomeProps { - goToNextStep: () => void + goToNextStep: (selectedFlow: 'c' | 'bprime') => void } export default function OnboardingWelcome({ goToNextStep }: OnboardingWelcomeProps) { + const [selectedFlow, setSelectedFlow] = useState<'c' | 'bprime'>('c') + return ( @@ -32,10 +34,16 @@ export default function OnboardingWelcome({ goToNextStep }: OnboardingWelcomePro - - + setSelectedFlow((selectedFlow) => (selectedFlow === 'c' ? 'bprime' : 'c'))} + > + + {selectedFlow === 'c' ? 'C' : "B'"} + - + goToNextStep(selectedFlow)}> Get Started diff --git a/apps/ausweis/src/use-cases/ReceivePidUseCaseBPrimeFlow.ts b/apps/ausweis/src/use-cases/ReceivePidUseCaseBPrimeFlow.ts index bbbf9d69..3c651f77 100644 --- a/apps/ausweis/src/use-cases/ReceivePidUseCaseBPrimeFlow.ts +++ b/apps/ausweis/src/use-cases/ReceivePidUseCaseBPrimeFlow.ts @@ -1,107 +1,99 @@ +import { AusweisAuthFlow, type AusweisAuthFlowOptions } from '@animo-id/expo-ausweis-sdk' +import type { AppAgent } from '@ausweis/agent' +import { pidSchemes } from '@ausweis/constants' +import { createPinDerivedEphKeyPop, deriveKeypairFromPin } from '@ausweis/crypto/bPrime' +import { Key, KeyType } from '@credo-ts/core' import { - AusweisAuthFlow, - type AusweisAuthFlowOptions, -} from "@animo-id/expo-ausweis-sdk"; -import type { AppAgent } from "@ausweis/agent"; -import { pidSchemes } from "@ausweis/constants"; -import { + type FullAppAgent, type OpenId4VciRequestTokenResponse, type OpenId4VciResolvedAuthorizationRequest, type OpenId4VciResolvedCredentialOffer, acquireAccessToken, - receiveCredentialFromOpenId4VciOffer, resolveOpenId4VciOffer, -} from "@package/agent"; +} from '@package/agent' +import { receiveCredentialFromOpenId4VciOfferAuthenticatedChannel } from '@package/agent' export interface ReceivePidUseCaseBPrimeOptions extends Pick< AusweisAuthFlowOptions, - | "onAttachCard" - | "onRequestAccessRights" - | "onStatusProgress" - | "onCardAttachedChanged" + 'onAttachCard' | 'onRequestAccessRights' | 'onStatusProgress' | 'onCardAttachedChanged' > { - agent: AppAgent; - onStateChange?: (newState: ReceivePidUseCaseState) => void; + agent: AppAgent + onStateChange?: (newState: ReceivePidUseCaseState) => void onEnterPin: ( - options: Parameters[0] & { - currentSessionPinAttempts: number; + options: Parameters[0] & { + currentSessionPinAttempts: number } - ) => string | Promise; + ) => string | Promise + pidPin: Array } -export type ReceivePidUseCaseState = - | "id-card-auth" - | "acquire-access-token" - | "retrieve-credential" - | "error"; +export type ReceivePidUseCaseState = 'id-card-auth' | 'acquire-access-token' | 'retrieve-credential' | 'error' export class ReceivePidUseCaseBPrimeFlow { - private options: ReceivePidUseCaseBPrimeOptions; + private options: ReceivePidUseCaseBPrimeOptions - private resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer; - private resolvedAuthorizationRequest: OpenId4VciResolvedAuthorizationRequest; - private idCardAuthFlow: AusweisAuthFlow; - private accessToken?: OpenId4VciRequestTokenResponse; - private refreshUrl?: string; - private currentSessionPinAttempts = 0; + private resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer + private resolvedAuthorizationRequest: OpenId4VciResolvedAuthorizationRequest + private idCardAuthFlow: AusweisAuthFlow + private accessToken?: OpenId4VciRequestTokenResponse + private refreshUrl?: string + private currentSessionPinAttempts = 0 - private currentState: ReceivePidUseCaseState = "id-card-auth"; + private currentState: ReceivePidUseCaseState = 'id-card-auth' public get state() { - return this.currentState; + return this.currentState } private static SD_JWT_VC_OFFER = - "openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fdemo.pid-issuer.bundesdruckerei.de%2Fc%22%2C%22credential_configuration_ids%22%3A%5B%22pid-sd-jwt%22%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%7D%7D%7D"; + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fdemo.pid-issuer.bundesdruckerei.de%2Fb1%22%2C%22credential_configuration_ids%22%3A%5B%22pid-sd-jwt%22%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%7D%7D%7D' private static MDL_OFFER = - "openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fdemo.pid-issuer.bundesdruckerei.de%2Fc%22%2C%22credential_configuration_ids%22%3A%5B%22pid-mso-mdoc%22%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%7D%7D%7D"; - private static CLIENT_ID = "7598ca4c-cc2e-4ff1-a4b4-ed58f249e274"; - private static REDIRECT_URI = "https://funke.animo.id/redirect"; - - private errorCallbacks: AusweisAuthFlowOptions["onError"][] = [ - this.handleError, - ]; - private successCallbacks: AusweisAuthFlowOptions["onSuccess"][] = [ + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fdemo.pid-issuer.bundesdruckerei.de%2Fc%22%2C%22credential_configuration_ids%22%3A%5B%22pid-mso-mdoc%22%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%7D%7D%7D' + private static CLIENT_ID = '7598ca4c-cc2e-4ff1-a4b4-ed58f249e274' + private static REDIRECT_URI = 'https://funke.animo.id/redirect' + + private errorCallbacks: AusweisAuthFlowOptions['onError'][] = [this.handleError] + private successCallbacks: AusweisAuthFlowOptions['onSuccess'][] = [ ({ refreshUrl }) => { - this.refreshUrl = refreshUrl; + this.refreshUrl = refreshUrl this.assertState({ - expectedState: "id-card-auth", - newState: "acquire-access-token", - }); + expectedState: 'id-card-auth', + newState: 'acquire-access-token', + }) }, - ]; + ] private constructor( options: ReceivePidUseCaseBPrimeOptions, resolvedAuthorizationRequest: OpenId4VciResolvedAuthorizationRequest, resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer ) { - this.resolvedAuthorizationRequest = resolvedAuthorizationRequest; - this.resolvedCredentialOffer = resolvedCredentialOffer; - this.options = options; + this.resolvedAuthorizationRequest = resolvedAuthorizationRequest + this.resolvedCredentialOffer = resolvedCredentialOffer + this.options = options this.idCardAuthFlow = new AusweisAuthFlow({ onEnterPin: async (options) => { const pin = await this.options.onEnterPin({ ...options, currentSessionPinAttempts: this.currentSessionPinAttempts, - }); - this.currentSessionPinAttempts += 1; - return pin; + }) + this.currentSessionPinAttempts += 1 + return pin }, onError: (error) => { for (const errorCallback of this.errorCallbacks) { - errorCallback(error); + errorCallback(error) } }, onSuccess: ({ refreshUrl }) => { for (const successCallback of this.successCallbacks) { - successCallback({ refreshUrl }); + successCallback({ refreshUrl }) } }, onAttachCard: () => this.options.onAttachCard?.(), - }); - this.options.onStateChange?.("id-card-auth"); + }) + this.options.onStateChange?.('id-card-auth') } public static async initialize(options: ReceivePidUseCaseBPrimeOptions) { @@ -112,123 +104,120 @@ export class ReceivePidUseCaseBPrimeFlow { clientId: ReceivePidUseCaseBPrimeFlow.CLIENT_ID, redirectUri: ReceivePidUseCaseBPrimeFlow.REDIRECT_URI, }, - }); + }) if (!resolved.resolvedAuthorizationRequest) { - throw new Error("Expected authorization_code grant, but not found"); + throw new Error('Expected authorization_code grant, but not found') } return new ReceivePidUseCaseBPrimeFlow( options, resolved.resolvedAuthorizationRequest, resolved.resolvedCredentialOffer - ); + ) } public authenticateUsingIdCard() { if (this.idCardAuthFlow.isActive) { - throw new Error("authentication flow already active"); + throw new Error('authentication flow already active') } - if (this.currentState !== "id-card-auth") { - throw new Error( - `Current state is ${this.currentState}. Expected id-card-auth` - ); + if (this.currentState !== 'id-card-auth') { + throw new Error(`Current state is ${this.currentState}. Expected id-card-auth`) } - this.currentSessionPinAttempts = 0; + this.currentSessionPinAttempts = 0 // We return an authentication promise to make it easier to track the state // We remove the callbacks once the error or success is triggered. const authenticationPromise = new Promise((resolve, reject) => { - const successCallback: AusweisAuthFlowOptions["onSuccess"] = ({ - refreshUrl, - }) => { - this.errorCallbacks = this.errorCallbacks.filter( - (c) => c === errorCallback - ); - this.successCallbacks = this.successCallbacks.filter( - (c) => c === successCallback - ); - resolve(null); - }; - const errorCallback: AusweisAuthFlowOptions["onError"] = (error) => { - this.errorCallbacks = this.errorCallbacks.filter( - (c) => c === errorCallback - ); - this.successCallbacks = this.successCallbacks.filter( - (c) => c === successCallback - ); - reject(error); - }; - - this.successCallbacks.push(successCallback); - this.errorCallbacks.push(errorCallback); + const successCallback: AusweisAuthFlowOptions['onSuccess'] = ({ refreshUrl }) => { + this.errorCallbacks = this.errorCallbacks.filter((c) => c === errorCallback) + this.successCallbacks = this.successCallbacks.filter((c) => c === successCallback) + resolve(null) + } + const errorCallback: AusweisAuthFlowOptions['onError'] = (error) => { + this.errorCallbacks = this.errorCallbacks.filter((c) => c === errorCallback) + this.successCallbacks = this.successCallbacks.filter((c) => c === successCallback) + reject(error) + } + + this.successCallbacks.push(successCallback) + this.errorCallbacks.push(errorCallback) this.idCardAuthFlow.start({ tcTokenUrl: this.resolvedAuthorizationRequest.authorizationRequestUri, - }); - }); + }) + }) - return authenticationPromise; + return authenticationPromise } public async retrieveCredential() { try { - this.assertState({ expectedState: "retrieve-credential" }); + this.assertState({ expectedState: 'retrieve-credential' }) if (!this.accessToken) { - throw new Error( - "Expected accessToken be defined in state retrieve-credential" - ); + throw new Error('Expected accessToken be defined in state retrieve-credential') } - const credentialConfigurationIdToRequest = - this.resolvedCredentialOffer.offeredCredentials[0].id; - const credentialRecord = await receiveCredentialFromOpenId4VciOffer({ + // TODO: get the device key + const deviceKey = Key.fromPublicKey(new Uint8Array(), KeyType.P256) + + const pinDerivedEph = await deriveKeypairFromPin(this.options.agent.context, this.options.pidPin) + + // TODO: how do we get the audience? + const pinDerivedEphKeyPop = await createPinDerivedEphKeyPop(this.options.agent as unknown as FullAppAgent, { + aud: 'a', + pinDerivedEph, + deviceKey, + cNonce: 'a', + }) + + const credentialConfigurationIdToRequest = this.resolvedCredentialOffer.offeredCredentials[0].id + const credentialRecord = await receiveCredentialFromOpenId4VciOfferAuthenticatedChannel({ + pinDerivedEphKeyPop, + pinDerivedEph, + deviceKey, agent: this.options.agent, accessToken: this.accessToken, resolvedCredentialOffer: this.resolvedCredentialOffer, credentialConfigurationIdToRequest, clientId: ReceivePidUseCaseBPrimeFlow.CLIENT_ID, pidSchemes, - }); + }) // TODO: add error handling everywhere to set state to error - if (credentialRecord.type !== "SdJwtVcRecord") { - throw new Error("Unexpected record type"); + if (credentialRecord.type !== 'SdJwtVcRecord') { + throw new Error('Unexpected record type') } - return credentialRecord; + return credentialRecord } catch (error) { - this.handleError(); - throw error; + this.handleError() + throw error } } public async acquireAccessToken() { - this.assertState({ expectedState: "acquire-access-token" }); + this.assertState({ expectedState: 'acquire-access-token' }) try { if (!this.refreshUrl) { - throw new Error( - "Expected refreshUrl be defined in state acquire-access-token" - ); + throw new Error('Expected refreshUrl be defined in state acquire-access-token') } - const authorizationCodeResponse = await fetch(this.refreshUrl); + const authorizationCodeResponse = await fetch(this.refreshUrl) if (!authorizationCodeResponse.ok) { - this.handleError(); - return; + this.handleError() + return } - const authorizationCode = new URL( - authorizationCodeResponse.url - ).searchParams.get("code"); + const authorizationCode = new URL(authorizationCodeResponse.url).searchParams.get('code') if (!authorizationCode) { - this.handleError(); - return; + this.handleError() + return } this.accessToken = await acquireAccessToken({ @@ -238,48 +227,46 @@ export class ReceivePidUseCaseBPrimeFlow { code: authorizationCode, }, agent: this.options.agent, - }); + }) this.assertState({ - expectedState: "acquire-access-token", - newState: "retrieve-credential", - }); + expectedState: 'acquire-access-token', + newState: 'retrieve-credential', + }) } catch (error) { - this.handleError(); - throw error; + this.handleError() + throw error } } public async cancelIdCardScanning() { - if (this.currentState !== "id-card-auth" || !this.idCardAuthFlow.isActive) { - return; + if (this.currentState !== 'id-card-auth' || !this.idCardAuthFlow.isActive) { + return } - await this.idCardAuthFlow.cancel(); + await this.idCardAuthFlow.cancel() } private assertState({ expectedState, newState, }: { - expectedState: ReceivePidUseCaseBPrimeFlow["currentState"]; - newState?: ReceivePidUseCaseBPrimeFlow["currentState"]; + expectedState: ReceivePidUseCaseBPrimeFlow['currentState'] + newState?: ReceivePidUseCaseBPrimeFlow['currentState'] }) { if (this.currentState !== expectedState) { - throw new Error( - `Expected state to be ${expectedState}. Found ${this.currentState}` - ); + throw new Error(`Expected state to be ${expectedState}. Found ${this.currentState}`) } if (newState) { - this.currentState = newState; - this.options.onStateChange?.(newState); + this.currentState = newState + this.options.onStateChange?.(newState) } } private handleError() { - this.currentState = "error"; + this.currentState = 'error' - this.options.onStateChange?.("error"); + this.options.onStateChange?.('error') } } diff --git a/apps/funke/app/(app)/index.tsx b/apps/funke/app/(app)/index.tsx deleted file mode 100644 index f7c4e5d5..00000000 --- a/apps/funke/app/(app)/index.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useAppAgent } from '@/agent' -import { ReceivePidUseCaseBPrimeFlow } from '@/use-cases/ReceivePidUseCaseBPrimeFlow' -import { Button, Paragraph, XStack, YStack } from '@package/ui' -import { Stack } from 'expo-router' -import { useEffect, useState } from 'react' -import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { ReceivePidUseCaseCFlow, type ReceivePidUseCaseState } from '../../use-cases/ReceivePidUseCaseCFlow' - -export default function Screen() { - const [flow, setFlow] = useState('c') - const { top } = useSafeAreaInsets() - // FIXME: should be useReceivePidUseCase as the state is now not updated.... - const [receivePidUseCaseCFlow, setReceivePidUseCaseCFlow] = useState() - const [receivePidUseCaseBPrimeFlow, setReceivePidUseCaseBPrimeFlow] = useState() - const [state, setState] = useState('not-initialized') - const [credential, setCredential] = useState() - const { agent } = useAppAgent() - - const startFlow = async () => { - if (flow === 'c') { - ReceivePidUseCaseCFlow.initialize({ - agent, - onStateChange: setState, - }).then((pidUseCase) => { - setReceivePidUseCaseCFlow(pidUseCase) - }) - } else if (flow === "b'") { - ReceivePidUseCaseBPrimeFlow.initialize({ - agent, - onStateChange: setState, - }).then((pidUseCase) => { - setReceivePidUseCaseBPrimeFlow(pidUseCase) - }) - } else { - throw Error(`invalid flow value: ${flow}`) - } - } - - const nextStep = async () => { - if (state === 'error') return - if (state === 'acquire-access-token') return - - if (flow === 'c') { - if (!receivePidUseCaseCFlow) return - if (state === 'id-card-auth') { - await receivePidUseCaseCFlow.authenticateUsingIdCard() - return - } - if (state === 'retrieve-credential') { - const credential = await receivePidUseCaseCFlow.retrieveCredential() - setCredential(credential.compactSdJwtVc) - } - } - - if (flow === "b'") { - if (!receivePidUseCaseBPrimeFlow) return - if (state === 'id-card-auth') { - await receivePidUseCaseBPrimeFlow.authenticateUsingIdCard() - return - } - if (state === 'retrieve-credential') { - const credential = await receivePidUseCaseBPrimeFlow.retrieveCredential() - setCredential(credential.compactSdJwtVc) - } - } - } - - return ( - <> - { - return - }, - }} - /> - - flow: {flow} - (flow === 'c' ? setFlow("b'") : setFlow('c'))}>toggle flow - start - Start flow:{flow} - State: {state} - - ID Card Auth - - - Retrieve credential - - credential: {credential} - - - ) -} diff --git a/apps/funke/crypto/aes.ts b/apps/funke/crypto/aes.ts deleted file mode 100644 index d6e4021a..00000000 --- a/apps/funke/crypto/aes.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { aes256Gcm } from '@package/agent' -import { FUNKE_WALLET_INSTANCE_LONG_TERM_AES_KEY_ID } from '../constants' - -export const funkeAes256Gcm = aes256Gcm(FUNKE_WALLET_INSTANCE_LONG_TERM_AES_KEY_ID) diff --git a/packages/agent/src/invitation/handler.ts b/packages/agent/src/invitation/handler.ts index ec7e7a95..352e34b9 100644 --- a/packages/agent/src/invitation/handler.ts +++ b/packages/agent/src/invitation/handler.ts @@ -3,6 +3,7 @@ import type { CredentialStateChangedEvent, DifPexCredentialsForRequest, JwkDidCreateOptions, + Key, KeyDidCreateOptions, OutOfBandInvitation, OutOfBandRecord, @@ -28,6 +29,7 @@ import { DidKey, JwaSignatureAlgorithm, KeyBackend, + KeyType, OutOfBandRepository, ProofEventTypes, ProofState, @@ -45,6 +47,7 @@ import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' import { getHostNameFromUrl } from '@package/utils' import { filter, first, firstValueFrom, merge, timeout } from 'rxjs' +import type { AppAgent } from '@ausweis/agent' import { extractOpenId4VcCredentialMetadata, setOpenId4VcCredentialMetadata } from '../openid4vc/metadata' export async function resolveOpenId4VciOffer({ @@ -135,6 +138,84 @@ export async function acquireAccessToken({ return await agent.modules.openId4VcHolder.requestToken(tokenOptions) } +export const receiveCredentialFromOpenId4VciOfferAuthenticatedChannel = async ({ + agent, + resolvedCredentialOffer, + credentialConfigurationIdToRequest, + accessToken, + clientId, + pidSchemes, + deviceKey, + pinDerivedEphKeyPop, + pinDerivedEph, +}: { + agent: AppAgent + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer + credentialConfigurationIdToRequest?: string + clientId?: string + pidSchemes?: { sdJwtVcVcts: Array; msoMdocNamespaces: Array } + deviceKey: Key + pinDerivedEphKeyPop: string + pinDerivedEph: Key + + // TODO: cNonce should maybe be provided separately (multiple calls can have different c_nonce values) + accessToken: OpenId4VciRequestTokenResponse +}) => { + // By default request the first offered credential + // TODO: extract the first supported offered credential + const offeredCredentialToRequest = credentialConfigurationIdToRequest + ? resolvedCredentialOffer.offeredCredentials.find((offered) => offered.id === credentialConfigurationIdToRequest) + : resolvedCredentialOffer.offeredCredentials[0] + if (!offeredCredentialToRequest) { + throw new Error( + `Parameter 'credentialConfigurationIdToRequest' with value ${credentialConfigurationIdToRequest} is not a credential_configuration_id in the credential offer.` + ) + } + + // FIXME: return credential_supported entry for credential so it's easy to store metadata + const credentials = await agent.modules.openId4VcHolder.requestCredentials({ + resolvedCredentialOffer, + ...accessToken, + customFormat: 'seed_credential', + additionalCredentialRequestPayloadClaims: { + pin_derived_eph_key_pop: pinDerivedEphKeyPop, + }, + additionalProofOfPossessionPayloadClaims: { + pin_derived_eph_pub: getJwkFromKey(pinDerivedEph).toJson(), + }, + + clientId, + credentialsToRequest: [offeredCredentialToRequest.id], + verifyCredentialStatus: false, + allowedProofOfPossessionSignatureAlgorithms: [ + // NOTE: MATTR launchpad for JFF MUST use EdDSA. So it is important that the default (first allowed one) + // is EdDSA. The list is ordered by preference, so if no suites are defined by the issuer, the first one + // will be used + JwaSignatureAlgorithm.EdDSA, + JwaSignatureAlgorithm.ES256, + ], + credentialBindingResolver: async ({ keyType, supportsJwk }) => { + if (!supportsJwk) { + throw Error('Issuer does not support JWK') + } + + if (keyType !== KeyType.P256) { + throw new Error(`invalid key type used '${keyType}' and only ${KeyType.P256} is allowed.`) + } + return { + method: 'jwk', + jwk: getJwkFromKey(deviceKey), + } + }, + }) + + console.log('TODO: store seed credential') + console.log(JSON.stringify(credentials, null, 2)) + + const record: SdJwtVcRecord = new SdJwtVcRecord({ compactSdJwtVc: 'todo' }) + return record +} + export const receiveCredentialFromOpenId4VciOffer = async ({ agent, resolvedCredentialOffer, diff --git a/packages/agent/src/invitation/index.ts b/packages/agent/src/invitation/index.ts index 6e7e6f77..e61d949e 100644 --- a/packages/agent/src/invitation/index.ts +++ b/packages/agent/src/invitation/index.ts @@ -8,6 +8,7 @@ export { parseInvitationUrl, parseDidCommInvitation, InvitationQrTypes } from '. export { receiveOutOfBandInvitation, receiveCredentialFromOpenId4VciOffer, + receiveCredentialFromOpenId4VciOfferAuthenticatedChannel, acquireAccessToken, resolveOpenId4VciOffer, storeCredential,