Skip to content

Commit

Permalink
fix: handle invalid pin when sharing or after onboarding (#241)
Browse files Browse the repository at this point in the history
Signed-off-by: Berend Sliedrecht <[email protected]>
  • Loading branch information
berendsliedrecht authored Nov 26, 2024
1 parent b2aacf6 commit ef7ed4b
Show file tree
Hide file tree
Showing 15 changed files with 139 additions and 50 deletions.
3 changes: 1 addition & 2 deletions apps/easypid/src/app/authenticate.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Redirect, useLocalSearchParams, useNavigation } from 'expo-router'
import { Redirect, useLocalSearchParams } from 'expo-router'

import { TypedArrayEncoder, WalletInvalidKeyError } from '@credo-ts/core'
import { initializeAppAgent, useSecureUnlock } from '@easypid/agent'
Expand All @@ -25,7 +25,6 @@ export default function Authenticate() {
const pinInputRef = useRef<PinDotsInputRef>(null)
const [isInitializingAgent, setIsInitializingAgent] = useState(false)
const [isAllowedToUnlockWithFaceId, setIsAllowedToUnlockWithFaceId] = useState(false)
const navigation = useNavigation()
const isLoading =
secureUnlock.state === 'acquired-wallet-key' || (secureUnlock.state === 'locked' && secureUnlock.isUnlocking)

Expand Down
62 changes: 54 additions & 8 deletions apps/easypid/src/crypto/WalletServiceProviderClient.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,51 @@
import type { SecureEnvironment } from '@animo-id/expo-secure-environment'
import { AskarModule } from '@credo-ts/askar'
import {
Agent,
CredoWebCrypto,
type JwsProtectedHeaderOptions,
JwsService,
JwtPayload,
KeyDerivationMethod,
TypedArrayEncoder,
WalletInvalidKeyError,
getJwkFromKey,
} from '@credo-ts/core'
import type { EasyPIDAppAgent } from 'packages/agent/src'
import { agentDependencies } from '@credo-ts/react-native'
import { ariesAskar } from '@hyperledger/aries-askar-react-native'
import type { EasyPIDAppAgent } from '@package/agent'
import { secureWalletKey } from 'packages/secure-store/secureUnlock'
import { InvalidPinError } from './error'
import { deriveKeypairFromPin } from './pin'

// TODO: should auto reset after X seconds
let __pin: Array<number> | undefined
export const setWalletServiceProviderPin = (pin?: Array<number>) => {
export const setWalletServiceProviderPin = async (pin: Array<number>) => {
const pinString = pin.join('')
const walletKeyVersion = secureWalletKey.getWalletKeyVersion()
const walletKey = await secureWalletKey.getWalletKeyUsingPin(pinString, walletKeyVersion)
const walletId = `easypid-wallet-${walletKeyVersion}`
const agent = new Agent({
config: {
label: 'pin_test_agent',
walletConfig: { id: walletId, key: walletKey, keyDerivationMethod: KeyDerivationMethod.Raw },
},
modules: {
askar: new AskarModule({ ariesAskar }),
},
dependencies: agentDependencies,
})

try {
await agent.initialize()
} catch (e) {
if (e instanceof WalletInvalidKeyError) {
throw new InvalidPinError()
}
throw e
}

await agent.shutdown()
__pin = pin
}

Expand All @@ -30,10 +63,6 @@ export class WalletServiceProviderClient implements SecureEnvironment {
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)
Expand Down Expand Up @@ -73,6 +102,22 @@ export class WalletServiceProviderClient implements SecureEnvironment {
return parsedData
}

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

public async batchGenerateKeyPair(keyIds: string[]): Promise<Record<string, Uint8Array>> {
const { publicKeys } = await this.post<{ publicKeys: Record<string, Array<number>> }>('batch-create-key', {
keyIds,
keyType: 'P256',
})

return Object.entries(publicKeys).reduce(
(prev, [keyId, publicKey]) => ({ ...prev, [keyId]: new Uint8Array(publicKey) }),
{}
)
}

public async sign(keyId: string, message: Uint8Array): Promise<Uint8Array> {
const { signature } = await this.post<{ signature: Array<number> }>('sign', {
data: new Array(...message),
Expand All @@ -86,8 +131,9 @@ export class WalletServiceProviderClient implements SecureEnvironment {
return new Uint8Array(signature)
}

public async generateKeypair(id: string): Promise<void> {
await this.post('create-key', { keyType: 'P256', keyId: id })
public async generateKeypair(id: string): Promise<Uint8Array> {
const { publicKey } = await this.post<{ publicKey: Array<number> }>('create-key', { keyType: 'P256', keyId: id })
return new Uint8Array(publicKey)
}

public async getPublicBytesForKeyId(keyId: string): Promise<Uint8Array> {
Expand Down
3 changes: 3 additions & 0 deletions apps/easypid/src/crypto/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class InvalidPinError extends Error {
public message = 'Invalid PIN entered'
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCanUseSecureEnclave } from '@easypid/hooks/useCanUseSecureEnclave'
import { Button, HeroIcons, Paragraph, Spinner, XStack, YStack, useToastController } from '@package/ui'
import { isLocalSecureEnvironmentSupported } from '@animo-id/expo-secure-environment'
import { Button, HeroIcons, Spinner, XStack, YStack, useToastController } from '@package/ui'
import { useImageScaler } from 'packages/app/src/hooks'
import React, { useState } from 'react'
import { Linking, Platform } from 'react-native'
Expand All @@ -11,7 +11,6 @@ interface OnboardingDataProtectionProps {

export function OnboardingDataProtection({ goToNextStep }: OnboardingDataProtectionProps) {
const toast = useToastController()
const canUseSecureEnclave = useCanUseSecureEnclave()
const [shouldUseCloudHsm, setShouldUseCloudHsm] = useState(true)

const { height, onLayout } = useImageScaler()
Expand All @@ -27,7 +26,7 @@ export function OnboardingDataProtection({ goToNextStep }: OnboardingDataProtect
const onToggleCloudHsm = () => {
const newShouldUseCloudHsm = !shouldUseCloudHsm

if (newShouldUseCloudHsm === false && !canUseSecureEnclave) {
if (newShouldUseCloudHsm === false && !isLocalSecureEnvironmentSupported()) {
toast.show(`You device does not support on-device ${Platform.OS === 'ios' ? 'Secure Enclave' : 'Strongbox'}.`, {
message: 'Only Cloud HSM supported for PID cryptogrpahic keys.',
customData: {
Expand Down
16 changes: 15 additions & 1 deletion apps/easypid/src/features/pid/FunkePidSetupScreen.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, SdJwtVcRecord } from '@credo-ts/core'
import { useSecureUnlock } from '@easypid/agent'
import { InvalidPinError } from '@easypid/crypto/error'
import type { PidSdJwtVcAttributes } from '@easypid/hooks'
import { ReceivePidUseCaseCFlow } from '@easypid/use-cases/ReceivePidUseCaseCFlow'
import type {
Expand Down Expand Up @@ -190,7 +191,20 @@ export function FunkePidSetupScreen() {
throw new Error('Retry')
}

if (shouldUseCloudHsm) setWalletServiceProviderPin(pin.split('').map(Number))
if (shouldUseCloudHsm) {
try {
await setWalletServiceProviderPin(pin.split('').map(Number))
} catch (e) {
if (e instanceof InvalidPinError) {
toast.show(e.message, {
customData: {
preset: 'danger',
},
})
}
throw e
}
}
await onIdCardStart({ walletPin: pin, allowSimulatorCard: allowSimulatorCard })
}

Expand Down
1 change: 0 additions & 1 deletion apps/easypid/src/features/pid/PidWalletPinSlide.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Heading, Paragraph, YStack } from '@package/ui'
import { PinDotsInput, type PinDotsInputRef, useWizard } from 'packages/app/src'
import { useRef, useState } from 'react'
import { useSafeAreaInsets } from 'react-native-safe-area-context'

interface PidWalletPinSlideProps {
title: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ import {

import { useAppAgent } from '@easypid/agent'

import { InvalidPinError } from '@easypid/crypto/error'
import { SlideWizard, usePushToWallet } from '@package/app'
import { useToastController } from '@package/ui'
import { useCallback, useEffect, useState } from 'react'
import { createParam } from 'solito'
import { setWalletServiceProviderPin } from '../../crypto/WalletServiceProviderClient'
import { useShouldUsePinForSubmission } from '../../hooks/useShouldUsePinForPresentation'
import { addReceivedActivity, useActivities } from '../activity/activityRecord'
import type { PresentationRequestResult } from '../share/components/utils'
import { PinSlide } from '../share/slides/PinSlide'
import { ShareCredentialsSlide } from '../share/slides/ShareCredentialsSlide'
import { AuthCodeFlowSlide } from './slides/AuthCodeFlowSlide'
Expand Down Expand Up @@ -252,7 +254,7 @@ export function FunkeCredentialNotificationScreen() {
[acquireCredentialsPreAuth]
)

const onPresentationAccept = useCallback(
const onPresentationAccept: (pin?: string) => Promise<undefined | PresentationRequestResult> = useCallback(
async (pin?: string) => {
if (
!credentialsForRequest ||
Expand All @@ -273,7 +275,18 @@ export function FunkeCredentialNotificationScreen() {
return
}
// TODO: maybe provide to shareProof method?
setWalletServiceProviderPin(pin.split('').map(Number))
try {
await setWalletServiceProviderPin(pin.split('').map(Number))
} catch (e) {
if (e instanceof InvalidPinError) {
toast.show(e.message, { customData: { preset: 'danger' } })
setIsSharingPresentation(false)
return { status: 'error', result: { title: e.message }, redirectToWallet: false }
}

setErrorReason('Presentation information could not be extracted.')
return
}
}

try {
Expand Down Expand Up @@ -313,6 +326,7 @@ export function FunkeCredentialNotificationScreen() {
resolvedAuthorizationRequest,
resolvedCredentialOffer,
shouldUsePinForPresentation,
toast.show,
]
)

Expand Down
13 changes: 11 additions & 2 deletions apps/easypid/src/features/scan/FunkeQrScannerScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ import { FadeIn, FadeOut, LinearTransition, useAnimatedStyle, withTiming } from
import { useSafeAreaInsets } from 'react-native-safe-area-context'

import easypidLogo from '../../../assets/icon-rounded.png'
import { checkMdocPermissions, getMdocQrCode, requestMdocPermissions, waitForDeviceRequest } from '../proximity'
import {
checkMdocPermissions,
getMdocQrCode,
requestMdocPermissions,
shutdownDataTransfer,
waitForDeviceRequest,
} from '../proximity'

const unsupportedUrlPrefixes = ['_oob=']

Expand Down Expand Up @@ -58,7 +64,10 @@ export function FunkeQrScannerScreen({ credentialDataHandlerOptions }: QrScanner
}
}, [showMyQrCode])

const onCancel = () => back()
const onCancel = () => {
back()
shutdownDataTransfer()
}

const onScan = async (scannedData: string) => {
if (isProcessing || !isFocused) return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useLocalSearchParams } from 'expo-router'
import React, { useEffect, useState, useCallback } from 'react'

import { useAppAgent } from '@easypid/agent'
import { InvalidPinError } from '@easypid/crypto/error'
import { analyzeVerification } from '@easypid/use-cases/ValidateVerification'
import type { VerificationAnalysisResponse } from '@easypid/use-cases/ValidateVerification'
import { usePushToWallet } from '@package/app/src/hooks/usePushToWallet'
Expand Down Expand Up @@ -114,7 +115,25 @@ export function FunkeOpenIdPresentationNotificationScreen() {
}
}
// TODO: maybe provide to shareProof method?
setWalletServiceProviderPin(pin.split('').map(Number))
try {
await setWalletServiceProviderPin(pin.split('').map(Number))
} catch (e) {
if (e instanceof InvalidPinError) {
return {
status: 'error',
result: {
title: 'Authentication Failed',
},
}
}

return {
status: 'error',
result: {
title: 'Authentication failed',
},
}
}
}

try {
Expand Down
2 changes: 1 addition & 1 deletion apps/easypid/src/features/share/slides/PinSlide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useRef, useState } from 'react'
import type { PresentationRequestResult } from '../components/utils'

interface PinSlideProps {
onPinComplete: (pin: string) => Promise<PresentationRequestResult> | Promise<void>
onPinComplete: (pin: string) => Promise<PresentationRequestResult | undefined>
isLoading: boolean
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { PresentationRequestResult } from '../components/utils'

interface ShareCredentialsSlideProps {
logo?: DisplayImage
onAccept?: () => Promise<PresentationRequestResult> | Promise<void>
onAccept?: () => Promise<PresentationRequestResult | undefined>
onDecline?: () => void
submission: FormattedSubmission
isAccepting: boolean
Expand Down
16 changes: 2 additions & 14 deletions apps/easypid/src/hooks/useCanUseSecureEnclave.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
import { generateKeypair } from '@animo-id/expo-secure-environment'
import { useEffect, useState } from 'react'
import { Platform } from 'react-native'
import { isLocalSecureEnvironmentSupported } from '@animo-id/expo-secure-environment'

export function useCanUseSecureEnclave() {
if (Platform.OS === 'ios') return true

const [canUseSecureEnclave, setCanUseSecureEnclave] = useState<boolean>()

useEffect(() => {
generateKeypair('123', false)
.then(() => setCanUseSecureEnclave(true))
.catch(() => setCanUseSecureEnclave(false))
}, [])

return canUseSecureEnclave
return isLocalSecureEnvironmentSupported()
}
1 change: 0 additions & 1 deletion packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ import { agentDependencies } from '@credo-ts/react-native'
import { anoncreds } from '@hyperledger/anoncreds-react-native'
import { ariesAskar } from '@hyperledger/aries-askar-react-native'
import { indyVdr } from '@hyperledger/indy-vdr-react-native'
import { DidWebAnonCredsRegistry } from 'credo-ts-didweb-anoncreds'

import { bdrPidIssuerCertificate, pidSchemes } from '../../../apps/easypid/src/constants'
import { indyNetworks } from './indyNetworks'
Expand Down
Loading

0 comments on commit ef7ed4b

Please sign in to comment.