Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle invalid pin when sharing or after onboarding #241

Merged
merged 1 commit into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also close

} 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 @@ -253,7 +255,7 @@ export function FunkeCredentialNotificationScreen() {
[acquireCredentialsPreAuth]
)

const onPresentationAccept = useCallback(
const onPresentationAccept: (pin?: string) => Promise<undefined | PresentationRequestResult> = useCallback(
async (pin?: string) => {
if (
!credentialsForRequest ||
Expand All @@ -274,7 +276,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 @@ -314,6 +327,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