Skip to content

Commit

Permalink
feat: toggle C and B' flow
Browse files Browse the repository at this point in the history
Signed-off-by: Berend Sliedrecht <[email protected]>
  • Loading branch information
Berend Sliedrecht committed Aug 15, 2024
1 parent 36189c5 commit df23bb4
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 49 deletions.
2 changes: 1 addition & 1 deletion apps/ausweis/src/crypto/bPrime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down
80 changes: 55 additions & 25 deletions apps/ausweis/src/features/onboarding/onboardingContext.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { sendCommand } from '@animo-id/expo-ausweis-sdk'
import { type AppAgent, initializeAppAgent, useSecureUnlock } from '@ausweis/agent'
import { ReceivePidUseCaseBPrimeFlow } from '@ausweis/use-cases/ReceivePidUseCaseBPrimeFlow'
import {
ReceivePidUseCaseCFlow,
type ReceivePidUseCaseCFlowOptions,
type ReceivePidUseCaseOptions,
type ReceivePidUseCaseState,
} from '@ausweis/use-cases/ReceivePidUseCaseCFlow'
import type { SdJwtVcHeader } from '@credo-ts/core'
Expand All @@ -20,7 +21,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' }
Expand All @@ -33,7 +33,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,
Expand Down Expand Up @@ -142,7 +142,7 @@ const onboardingStepsCFlow = [
Screen: React.FunctionComponent<any>
}>

export type OnboardingSteps = typeof onboardingStepsCFlow
export type OnboardingSteps = typeof onboardingSteps
export type OnboardingStep = OnboardingSteps[number]

export type OnboardingContext = {
Expand All @@ -159,26 +159,27 @@ export function OnboardingContextProvider({
children,
}: PropsWithChildren<{
initialStep?: OnboardingStep['step']
flow?: 'c' | 'bprime'
}>) {
const toast = useToastController()
const secureUnlock = useSecureUnlock()
const [currentStepName, setCurrentStepName] = useState<OnboardingStep['step']>(initialStep ?? 'welcome')
const router = useRouter()

const [selectedFlow, setSelectedFlow] = useState<'c' | 'bprime'>('c')
const [receivePidUseCase, setReceivePidUseCase] = useState<ReceivePidUseCaseCFlow | ReceivePidUseCaseBPrimeFlow>()
const [receivePidUseCaseState, setReceivePidUseCaseState] = useState<ReceivePidUseCaseState | 'initializing'>()

const [walletPin, setWalletPin] = useState<string>()
const [idCardPin, setIdCardPin] = useState<string>()
const [userName, setUserName] = useState<string>()
const [agent, setAgent] = useState<AppAgent>()

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)
Expand All @@ -189,8 +190,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)
Expand All @@ -202,6 +203,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
Expand Down Expand Up @@ -245,7 +252,7 @@ export function OnboardingContextProvider({

const [onIdCardPinReEnter, setOnIdCardPinReEnter] = useState<(idCardPin: string) => Promise<void>>()

const onEnterPin: ReceivePidUseCaseCFlowOptions['onEnterPin'] = useCallback(
const onEnterPin: ReceivePidUseCaseOptions['onEnterPin'] = useCallback(
(options) => {
console.log('options', options, idCardPin)
if (!idCardPin) {
Expand Down Expand Up @@ -297,19 +304,40 @@ export function OnboardingContextProvider({
}

if (!receivePidUseCase && receivePidUseCaseState !== 'initializing') {
return ReceivePidUseCaseCFlow.initialize({
agent: secureUnlock.context.agent,
onStateChange: setReceivePidUseCaseState,
onEnterPin: (options) => onEnterPinRef.current.onEnterPin(options),
})
.then((receivePidUseCase) => {
setReceivePidUseCase(receivePidUseCase)
goToNextStep()
if (selectedFlow === 'c') {
return ReceivePidUseCaseCFlow.initialize({
agent: secureUnlock.context.agent,
onStateChange: setReceivePidUseCaseState,
onEnterPin: (options) => onEnterPinRef.current.onEnterPin(options),
})
.catch((e) => {
reset({ error: e, resetToStep: 'id-card-pin' })
throw e
.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()
Expand All @@ -325,8 +353,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')) {
Expand Down Expand Up @@ -416,7 +444,9 @@ export function OnboardingContextProvider({
}

let screen: React.JSX.Element
if (currentStep.step === 'pin' || currentStep.step === 'pin-reenter') {
if (currentStep.step === 'welcome') {
screen = <currentStep.Screen goToNextStep={selectFlow} />
} else if (currentStep.step === 'pin' || currentStep.step === 'pin-reenter') {
screen = <currentStep.Screen key="pin-now" goToNextStep={currentStep.step === 'pin' ? onPinEnter : onPinReEnter} />
} else if (currentStep.step === 'id-card-pin') {
screen = <currentStep.Screen goToNextStep={onIdCardPinReEnter ?? onIdCardPinEnter} />
Expand Down
2 changes: 1 addition & 1 deletion apps/ausweis/src/features/onboarding/screens/pin.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
20 changes: 14 additions & 6 deletions apps/ausweis/src/features/onboarding/screens/welcome.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Animated.View style={{ flex: 1 }} layout={FadingTransition}>
<FlexPage p={0} safeArea={false}>
Expand All @@ -32,10 +34,16 @@ export default function OnboardingWelcome({ goToNextStep }: OnboardingWelcomePro
</YStack>
<YStack flex-1 />
<XStack gap="$2" my="$6">
<Button.Outline p="$0" width="$buttonHeight">
<HeroIcons.GlobeAlt size={24} />
<Button.Outline
p="$0"
width="$buttonHeight"
onPress={() => setSelectedFlow(selectedFlow === 'c' ? 'bprime' : 'c')}
>
<Heading variant="h2" color={'$primary-500'} fontWeight={'bold'}>
{selectedFlow === 'c' ? 'C' : "B'"}
</Heading>
</Button.Outline>
<Button.Solid flexGrow={1} onPress={goToNextStep}>
<Button.Solid flexGrow={1} onPress={() => goToNextStep(selectedFlow)}>
Get Started
</Button.Solid>
</XStack>
Expand Down
32 changes: 28 additions & 4 deletions apps/ausweis/src/use-cases/ReceivePidUseCaseBPrimeFlow.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
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 {
type FullAppAgent,
type OpenId4VciRequestTokenResponse,
type OpenId4VciResolvedAuthorizationRequest,
type OpenId4VciResolvedCredentialOffer,
acquireAccessToken,
receiveCredentialFromOpenId4VciOffer,
resolveOpenId4VciOffer,
} from '@package/agent'
import { receiveCredentialFromOpenId4VciOfferAuthenticatedChannel } from '@package/agent'

export interface ReceivePidUseCaseBPrimeOptions extends Pick<AusweisAuthFlowOptions, 'onInsertCard'> {
agent: AppAgent
Expand All @@ -18,6 +21,7 @@ export interface ReceivePidUseCaseBPrimeOptions extends Pick<AusweisAuthFlowOpti
currentSessionPinAttempts: number
}
) => string | Promise<string>
pidPin: Array<number>
}

export type ReceivePidUseCaseState = 'id-card-auth' | 'acquire-access-token' | 'retrieve-credential' | 'error'
Expand All @@ -38,7 +42,7 @@ export class ReceivePidUseCaseBPrimeFlow {
}

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%3D%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'
Expand Down Expand Up @@ -102,7 +106,11 @@ export class ReceivePidUseCaseBPrimeFlow {
throw new Error('Expected authorization_code grant, but not found')
}

return new ReceivePidUseCaseBPrimeFlow(options, resolved.resolvedAuthorizationRequest, resolved.resolvedCredentialOffer)
return new ReceivePidUseCaseBPrimeFlow(
options,
resolved.resolvedAuthorizationRequest,
resolved.resolvedCredentialOffer
)
}

public authenticateUsingIdCard() {
Expand Down Expand Up @@ -149,8 +157,24 @@ export class ReceivePidUseCaseBPrimeFlow {
throw new Error('Expected accessToken be defined in state retrieve-credential')
}

// 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 receiveCredentialFromOpenId4VciOffer({
const credentialRecord = await receiveCredentialFromOpenId4VciOfferAuthenticatedChannel({
pinDerivedEphKeyPop,
pinDerivedEph,
deviceKey,
agent: this.options.agent,
accessToken: this.accessToken,
resolvedCredentialOffer: this.resolvedCredentialOffer,
Expand Down
8 changes: 4 additions & 4 deletions apps/ausweis/src/use-cases/ReceivePidUseCaseCFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
resolveOpenId4VciOffer,
} from '@package/agent'

export interface ReceivePidUseCaseCFlowOptions extends Pick<AusweisAuthFlowOptions, 'onInsertCard'> {
export interface ReceivePidUseCaseOptions extends Pick<AusweisAuthFlowOptions, 'onInsertCard'> {
agent: AppAgent
onStateChange?: (newState: ReceivePidUseCaseState) => void
onEnterPin: (
Expand All @@ -23,7 +23,7 @@ export interface ReceivePidUseCaseCFlowOptions extends Pick<AusweisAuthFlowOptio
export type ReceivePidUseCaseState = 'id-card-auth' | 'acquire-access-token' | 'retrieve-credential' | 'error'

export class ReceivePidUseCaseCFlow {
private options: ReceivePidUseCaseCFlowOptions
private options: ReceivePidUseCaseOptions

private resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer
private resolvedAuthorizationRequest: OpenId4VciResolvedAuthorizationRequest
Expand Down Expand Up @@ -56,7 +56,7 @@ export class ReceivePidUseCaseCFlow {
]

private constructor(
options: ReceivePidUseCaseCFlowOptions,
options: ReceivePidUseCaseOptions,
resolvedAuthorizationRequest: OpenId4VciResolvedAuthorizationRequest,
resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer
) {
Expand Down Expand Up @@ -88,7 +88,7 @@ export class ReceivePidUseCaseCFlow {
this.options.onStateChange?.('id-card-auth')
}

public static async initialize(options: ReceivePidUseCaseCFlowOptions) {
public static async initialize(options: ReceivePidUseCaseOptions) {
const resolved = await resolveOpenId4VciOffer({
agent: options.agent,
offer: { uri: ReceivePidUseCaseCFlow.SD_JWT_VC_OFFER },
Expand Down
4 changes: 0 additions & 4 deletions apps/funke/crypto/aes.ts

This file was deleted.

6 changes: 3 additions & 3 deletions apps/storybook/.storybook/withThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import { TamaguiProvider } from '@package/ui'
import type { Decorator } from '@storybook/react'
import React from 'react'

import funkeConfig from '../../funke/tamagui.config'
import ausweisConfig from '../../ausweis/tamagui.config'
import paradymConfig from '../../paradym/tamagui.config'

const configs = {
funke: funkeConfig,
ausweis: ausweisConfig,
paradym: paradymConfig,
}

const withThemeProvider: Decorator = (Story, context) => {
const configName = context.parameters.theme ?? 'funke'
const configName = context.parameters.theme ?? 'ausweis'

const config = configs[configName]
if (!config)
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const initializeAusweisAgent = async ({
keyDerivationMethod: keyDerivation === 'raw' ? KeyDerivationMethod.Raw : KeyDerivationMethod.Argon2IMod,
},
autoUpdateStorageOnStartup: true,
// logger: appLogger(LogLevel.debug),
logger: appLogger(LogLevel.debug),
},
modules: {
ariesAskar: askarModule,
Expand Down
Loading

0 comments on commit df23bb4

Please sign in to comment.