Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feat/auth-code-flow
Browse files Browse the repository at this point in the history
  • Loading branch information
TimoGlastra committed Nov 20, 2024
2 parents 397bfa1 + d51922e commit b1b4e66
Show file tree
Hide file tree
Showing 13 changed files with 303 additions and 65 deletions.
1 change: 1 addition & 0 deletions apps/easypid/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
EXPO_PUBLIC_WALLET_SERVICE_PROVIDER_URL=
24 changes: 22 additions & 2 deletions apps/easypid/src/agent/initialize.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import { setFallbackSecureEnvironment } from '@animo-id/expo-secure-environment'
import { trustedX509Certificates } from '@easypid/constants'
import { WalletServiceProviderClient } from '@easypid/crypto/WalletServiceProviderClient'
import { initializeEasyPIDAgent } from '@package/agent'

export function initializeAppAgent({ walletKey, walletKeyVersion }: { walletKey: string; walletKeyVersion: number }) {
return initializeEasyPIDAgent({
export async function initializeAppAgent({
walletKey,
walletKeyVersion,
registerWallet,
}: { walletKey: string; walletKeyVersion: number; registerWallet?: boolean }) {
const agent = await initializeEasyPIDAgent({
keyDerivation: 'raw',
walletId: `easypid-wallet-${walletKeyVersion}`,
walletKey,
walletLabel: 'EasyPID Wallet',
trustedX509Certificates,
})

/**
*
* Setup specific for the Wallet Service provider
*
*/
const wsp = new WalletServiceProviderClient(process.env.EXPO_PUBLIC_WALLET_SERVICE_PROVIDER_URL as string, agent)
if (registerWallet) {
await wsp.createSalt()
await wsp.register()
}
setFallbackSecureEnvironment(wsp)

return agent
}
4 changes: 1 addition & 3 deletions apps/easypid/src/app/authenticate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ export default function Authenticate() {
// After resetting the wallet, we want to avoid prompting for face id immediately
// So we add an artificial delay
useEffect(() => {
const timer = setTimeout(() => {
setIsAllowedToUnlockWithFaceId(true)
}, 500)
const timer = setTimeout(() => setIsAllowedToUnlockWithFaceId(true), 500)

return () => clearTimeout(timer)
}, [])
Expand Down
124 changes: 124 additions & 0 deletions apps/easypid/src/crypto/WalletServiceProviderClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type { SecureEnvironment } from '@animo-id/expo-secure-environment'
import {
CredoWebCrypto,
type JwsProtectedHeaderOptions,
JwsService,
JwtPayload,
TypedArrayEncoder,
getJwkFromKey,
} from '@credo-ts/core'
import type { EasyPIDAppAgent } from 'packages/agent/src'
import { deriveKeypairFromPin } from './pin'

let __pin: Array<number> | undefined
export const setWalletServiceProviderPin = (pin?: Array<number>) => {
__pin = pin
}
export const getWalletServiceProviderPin = () => __pin

const GENERIC_RECORD_WALLET_SERVICE_PROVIDER_SALT_ID = 'GENERIC_RECORD_WALLET_SERVICE_PROVIDER_SALT_ID'

export class WalletServiceProviderClient implements SecureEnvironment {
private headers: Headers = new Headers({
'Content-Type': 'application/json',
})

public constructor(
private hsmUrl: string,
private agent: EasyPIDAppAgent
) {}

public async register() {
await this.post('register-wallet', {})
}

private async post<T>(path: string, claims: Record<string, unknown>): Promise<T> {
const pin = getWalletServiceProviderPin()
if (!pin)
throw new Error(
'Pin not set! call `setWalletServiceProviderPin(pin)` before calling a method on the WalletServiceProvider'
)
const jwsService = this.agent.context.dependencyManager.resolve(JwsService)
const salt = await this.getOrCreateSalt()
const key = await deriveKeypairFromPin(this.agent.context, pin, salt)

const payload = new JwtPayload({
additionalClaims: claims,
})

const protectedHeaderOptions: JwsProtectedHeaderOptions = {
alg: 'ES256',
jwk: getJwkFromKey(key),
}

const compactJws = await jwsService.createJwsCompact(this.agent.context, {
key,
payload,
protectedHeaderOptions,
})

const body = {
jwt: compactJws,
}

const response = await fetch(`${this.hsmUrl}/${path}`, {
method: 'POST',
headers: this.headers,
body: JSON.stringify(body),
})

const parsedData = await response.json()
return parsedData
}

public async sign(keyId: string, message: Uint8Array): Promise<Uint8Array> {
const { signature } = await this.post<{ signature: Array<number> }>('sign', {
data: new Array(...message),
keyId,
})

if (!signature) {
throw new Error('No signature property found on the response of the wallet service provider')
}

return new Uint8Array(signature)
}

public async generateKeypair(id: string): Promise<void> {
await this.post('create-key', { keyType: 'P256', keyId: id })
}

public async getPublicBytesForKeyId(keyId: string): Promise<Uint8Array> {
const { publicKey } = await this.post<{ publicKey: Array<number> }>('get-publickey', { keyId })
if (!publicKey) {
throw new Error('No publicKey property found on the response of the wallet service provider')
}

return new Uint8Array(publicKey)
}

public async createSalt() {
const maybeSalt = await this.getSalt()
if (maybeSalt) return maybeSalt

const crypto = new CredoWebCrypto(this.agent.context)

const saltBytes = crypto.getRandomValues(new Uint8Array(12))
const saltString = TypedArrayEncoder.toBase64URL(saltBytes)
await this.agent.genericRecords.save({
content: { salt: saltString },
id: GENERIC_RECORD_WALLET_SERVICE_PROVIDER_SALT_ID,
})
return saltString
}

private async getSalt(): Promise<string | null> {
return (await this.agent.genericRecords.findById(GENERIC_RECORD_WALLET_SERVICE_PROVIDER_SALT_ID))?.content
.salt as string
}

private async getOrCreateSalt() {
const maybeSalt = await this.getSalt()
return maybeSalt ?? (await this.createSalt())
}
}
6 changes: 3 additions & 3 deletions apps/easypid/src/crypto/pin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ import { easyPidAes256Gcm } from './aes'
* @todo Might be good later to add methods like `signWithPidPin`
*
*/
export const deriveKeypairFromPin = async (agentContext: AgentContext, pin: Array<number>) => {
export const deriveKeypairFromPin = async (agentContext: AgentContext, pin: Array<number>, salt?: string) => {
const pinSecret = await easyPidAes256Gcm.aes256GcmEncrypt({
agentContext,
data: new Uint8Array(pin),
})

const pinSeed = await kdf.derive(
TypedArrayEncoder.toUtf8String(new Uint8Array(pin)),
TypedArrayEncoder.toUtf8String(pinSecret)
salt ?? TypedArrayEncoder.toUtf8String(pinSecret)
)

const askarKey = AskarKey.fromSecretBytes({
Expand Down Expand Up @@ -111,7 +111,7 @@ export const signPinNonceAndDeviceKeyWithPinDerivedEphPriv = async (

const payload = Buffer.from([
...TypedArrayEncoder.fromString(pinNonce),
...TypedArrayEncoder.fromString(TypedArrayEncoder.toBase64URL(deviceKeyPair.asJwkInBytes())),
...TypedArrayEncoder.fromString(TypedArrayEncoder.toBase64URL(await deviceKeyPair.asJwkInBytes())),
])

const toBeSigned = `${TypedArrayEncoder.toBase64URL(
Expand Down
7 changes: 6 additions & 1 deletion apps/easypid/src/features/onboarding/onboardingContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sendCommand } from '@animo-id/expo-ausweis-sdk'
import type { SdJwtVcHeader } from '@credo-ts/core'
import { type AppAgent, initializeAppAgent, useSecureUnlock } from '@easypid/agent'
import { setWalletServiceProviderPin } from '@easypid/crypto/WalletServiceProviderClient'
import { ReceivePidUseCaseCFlow } from '@easypid/use-cases/ReceivePidUseCaseCFlow'
import type {
CardScanningErrorDetails,
Expand Down Expand Up @@ -198,6 +199,7 @@ export function OnboardingContextProvider({
const agent = await initializeAppAgent({
walletKey,
walletKeyVersion: secureWalletKey.getWalletKeyVersion(),
registerWallet: true,
})
setAgent(agent)
}, [])
Expand Down Expand Up @@ -225,7 +227,10 @@ export function OnboardingContextProvider({

return secureUnlock
.setup(walletPin as string)
.then(({ walletKey }) => initializeAgent(walletKey))
.then(({ walletKey }) => {
setWalletServiceProviderPin((walletPin as string).split('').map(Number))
return initializeAgent(walletKey)
})
.then(goToNextStep)
.catch((e) => {
reset({ error: e, resetToStep: 'welcome' })
Expand Down
35 changes: 1 addition & 34 deletions apps/easypid/src/features/onboarding/screens/welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,18 @@ import {
XStack,
YStack,
} from '@package/ui'
import { useToastController } from '@package/ui'
import { Image } from '@tamagui/image'
import type React from 'react'
import { useEffect, useState } from 'react'
import { Alert } from 'react-native'
import Animated, { FadingTransition } from 'react-native-reanimated'

import { generateKeypair } from '@animo-id/expo-secure-environment'
import inAppLogo from '../../../../assets/icon.png'

export interface OnboardingWelcomeProps {
goToNextStep: () => void
}

export default function OnboardingWelcome({ goToNextStep }: OnboardingWelcomeProps) {
const toast = useToastController()
const [isBlockedByHsm, setIsBlockedByHsm] = useState(false)

useEffect(() => {
try {
generateKeypair('123', false)
} catch (error) {
setIsBlockedByHsm(true)
Alert.alert(
'Your device is not supported',
'This device does not have a secure enclave. This is required as an additional layer of security for your digital identity. Unfortunately, this means you are unable to use the EasyPID wallet with this device.'
)
}
}, [])

return (
<Animated.View style={{ flexGrow: 1 }} layout={FadingTransition}>
<Stack
Expand Down Expand Up @@ -87,22 +69,7 @@ export default function OnboardingWelcome({ goToNextStep }: OnboardingWelcomePro
</Paragraph>
</YStack>
<XStack gap="$2">
<Button.Solid
opacity={isBlockedByHsm ? 0.8 : 1}
flexGrow={1}
scaleOnPress={!isBlockedByHsm}
onPress={() => {
if (isBlockedByHsm) {
toast.show('Your device is not supported', {
type: 'error',
message:
'Your device does not have a secure enclave. This is required as an additional layer of security for your digital identity.',
})
} else {
goToNextStep()
}
}}
>
<Button.Solid flexGrow={1} onPress={goToNextStep}>
Get Started
</Button.Solid>
</XStack>
Expand Down
8 changes: 4 additions & 4 deletions apps/easypid/src/storage/pidPin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ export const deviceKeyPair = {
generate: () => generateKeypair(EASYPID_WALLET_PID_PIN_KEY_ID, false),
sign: async (message: Uint8Array) => sign(EASYPID_WALLET_PID_PIN_KEY_ID, message, false),
publicKey: () => getPublicBytesForKeyId(EASYPID_WALLET_PID_PIN_KEY_ID),
asJwkInBytes: () =>
asJwkInBytes: async () =>
Key.fromPublicBytes({
publicKey: deviceKeyPair.publicKey(),
publicKey: await deviceKeyPair.publicKey(),
algorithm: KeyAlgs.EcSecp256r1,
}).jwkPublic.toUint8Array(),
asJwk: () =>
asJwk: async () =>
Key.fromPublicBytes({
publicKey: deviceKeyPair.publicKey(),
publicKey: await deviceKeyPair.publicKey(),
algorithm: KeyAlgs.EcSecp256r1,
}).jwkPublic,
}
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,17 @@
"@credo-ts/react-native": "0.6.0-alpha-20241119125554",
"@credo-ts/react-hooks": "0.6.1",
"@animo-id/expo-ausweis-sdk": "0.0.1-alpha.14",
"@animo-id/expo-secure-environment": "0.1.0-alpha.5",
"@animo-id/expo-secure-environment": "0.1.0-alpha.9",
"@animo-id/expo-mdoc-data-transfer": "0.0.3-alpha.7",
"@types/react": "~18.2.79",
"react-docgen-typescript": "2.2.2",
"react": "18.3.1",
"react-native": "~0.74.5"
},
"patchedDependencies": {
"@hyperledger/[email protected]": "patches/@[email protected]"
"@hyperledger/[email protected]": "patches/@[email protected]",
"@credo-ts/[email protected]": "patches/@[email protected]",
"@animo-id/[email protected]": "patches/@[email protected]"
}
}
}
Loading

0 comments on commit b1b4e66

Please sign in to comment.