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

feat: batch issuance #221

Merged
merged 3 commits into from
Nov 22, 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
4 changes: 3 additions & 1 deletion apps/easypid/src/agent/initialize.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { setFallbackSecureEnvironment } from '@animo-id/expo-secure-environment'
import { setFallbackSecureEnvironment, shouldUseFallbackSecureEnvironment } from '@animo-id/expo-secure-environment'
import { trustedX509Certificates } from '@easypid/constants'
import { WalletServiceProviderClient } from '@easypid/crypto/WalletServiceProviderClient'
import { initializeEasyPIDAgent } from '@package/agent'
import { getShouldUseCloudHsm } from '../features/onboarding/useShouldUseCloudHsm'

export async function initializeAppAgent({
walletKey,
Expand Down Expand Up @@ -29,6 +30,7 @@ export async function initializeAppAgent({
await wsp.createSalt()
await wsp.register()
}
if (getShouldUseCloudHsm()) shouldUseFallbackSecureEnvironment(true)
setFallbackSecureEnvironment(wsp)

return agent
Expand Down
75 changes: 39 additions & 36 deletions apps/easypid/src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { type CredentialDataHandlerOptions, DeeplinkHandler, useHaptics } from '
import { HeroIcons, IconContainer } from '@package/ui'
import { useEffect, useState } from 'react'
import { useTheme } from 'tamagui'
import { WithBackgroundPidRefresh } from '../../features/pid/WithBackPidRefresh'

const jsonRecordIds = [activityStorage.recordId]

Expand Down Expand Up @@ -65,42 +66,44 @@ export default function AppLayout() {
return (
<AgentProvider agent={secureUnlock.context.agent}>
<WalletJsonStoreProvider agent={secureUnlock.context.agent} recordIds={jsonRecordIds}>
<DeeplinkHandler credentialDataHandlerOptions={credentialDataHandlerOptions}>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen
options={{
presentation: 'modal',
}}
name="(home)/scan"
/>
<Stack.Screen
name="notifications/openIdPresentation"
options={{
gestureEnabled: false,
}}
/>
<Stack.Screen
name="notifications/openIdCredential"
options={{
gestureEnabled: false,
}}
/>
<Stack.Screen name="credentials/index" options={headerNormalOptions} />
<Stack.Screen name="credentials/[id]/index" options={headerNormalOptions} />
<Stack.Screen name="credentials/[id]/attributes" options={headerNormalOptions} />
<Stack.Screen name="credentials/requestedAttributes" options={headerNormalOptions} />
<Stack.Screen name="menu/index" options={headerNormalOptions} />
<Stack.Screen name="menu/feedback" options={headerNormalOptions} />
<Stack.Screen name="menu/settings" options={headerNormalOptions} />
<Stack.Screen name="menu/about" options={headerNormalOptions} />
<Stack.Screen name="activity/index" options={headerNormalOptions} />
<Stack.Screen name="activity/[id]" options={headerNormalOptions} />
<Stack.Screen name="pinConfirmation" options={headerNormalOptions} />
<Stack.Screen name="pinLocked" options={headerNormalOptions} />
<Stack.Screen name="issuer" options={headerNormalOptions} />
<Stack.Screen name="pidSetup" />
</Stack>
</DeeplinkHandler>
<WithBackgroundPidRefresh>
<DeeplinkHandler credentialDataHandlerOptions={credentialDataHandlerOptions}>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen
options={{
presentation: 'modal',
}}
name="(home)/scan"
/>
<Stack.Screen
name="notifications/openIdPresentation"
options={{
gestureEnabled: false,
}}
/>
<Stack.Screen
name="notifications/openIdCredential"
options={{
gestureEnabled: false,
}}
/>
<Stack.Screen name="credentials/index" options={headerNormalOptions} />
<Stack.Screen name="credentials/[id]/index" options={headerNormalOptions} />
<Stack.Screen name="credentials/[id]/attributes" options={headerNormalOptions} />
<Stack.Screen name="credentials/requestedAttributes" options={headerNormalOptions} />
<Stack.Screen name="menu/index" options={headerNormalOptions} />
<Stack.Screen name="menu/feedback" options={headerNormalOptions} />
<Stack.Screen name="menu/settings" options={headerNormalOptions} />
<Stack.Screen name="menu/about" options={headerNormalOptions} />
<Stack.Screen name="activity/index" options={headerNormalOptions} />
<Stack.Screen name="activity/[id]" options={headerNormalOptions} />
<Stack.Screen name="pinConfirmation" options={headerNormalOptions} />
<Stack.Screen name="pinLocked" options={headerNormalOptions} />
<Stack.Screen name="issuer" options={headerNormalOptions} />
<Stack.Screen name="pidSetup" />
</Stack>
</DeeplinkHandler>
</WithBackgroundPidRefresh>
</WalletJsonStoreProvider>
</AgentProvider>
)
Expand Down
5 changes: 2 additions & 3 deletions apps/easypid/src/features/onboarding/hasFinishedOnboarding.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { MMKV, useMMKVBoolean } from 'react-native-mmkv'

const mmkv = new MMKV()
import { useMMKVBoolean } from 'react-native-mmkv'
import { mmkv } from '../../storage/mmkv'

export function useHasFinishedOnboarding() {
return useMMKVBoolean('hasFinishedOnboarding', mmkv)
Expand Down
13 changes: 11 additions & 2 deletions apps/easypid/src/features/onboarding/onboardingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
type CardScanningState,
type OnboardingPage,
type OnboardingStep,
type PidFlowTypes,
SIMULATOR_PIN,
pidSetupSteps,
} from '@easypid/utils/sharedPidSetup'
Expand All @@ -36,6 +35,7 @@ import { OnboardingBiometrics } from './screens/biometrics'
import { OnboardingIntroductionSteps } from './screens/introduction-steps'
import OnboardingPinEnter from './screens/pin'
import OnboardingWelcome from './screens/welcome'
import { useShouldUseCloudHsm } from './useShouldUseCloudHsm'

export const onboardingSteps = [
{
Expand Down Expand Up @@ -132,6 +132,7 @@ export function OnboardingContextProvider({
const [currentStepName, setCurrentStepName] = useState<OnboardingStep['step']>(initialStep ?? 'welcome')
const router = useRouter()
const [, setHasFinishedOnboarding] = useHasFinishedOnboarding()
const [shouldUseCloudHsm, setShouldUseCloudHsm] = useShouldUseCloudHsm()
const pidDisplay = usePidDisplay()

const [receivePidUseCase, setReceivePidUseCase] = useState<ReceivePidUseCaseCFlow>()
Expand Down Expand Up @@ -627,7 +628,15 @@ export function OnboardingContextProvider({

let screen: React.JSX.Element
if (currentStep.step === 'welcome') {
screen = <currentStep.Screen goToNextStep={goToNextStep} />
screen = (
<currentStep.Screen
goToNextStep={() => {
// TODO: make configurable
// setShouldUseCloudHsm(true)
goToNextStep()
}}
/>
)
} else if (currentStep.step === 'pin' || currentStep.step === 'pin-reenter') {
screen = (
<currentStep.Screen
Expand Down
13 changes: 13 additions & 0 deletions apps/easypid/src/features/onboarding/useShouldUseCloudHsm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useMMKVBoolean } from 'react-native-mmkv'
import { mmkv } from '../../storage/mmkv'

export function getShouldUseCloudHsm() {
return mmkv.getBoolean('shouldUseCloudHsm')
}

export function useShouldUseCloudHsm() {
return useMMKVBoolean('shouldUseCloudHsm', mmkv)
}
export function removeShouldUseCloudHsm() {
mmkv.delete('shouldUseCloudHsm')
}
9 changes: 9 additions & 0 deletions apps/easypid/src/features/pid/WithBackPidRefresh.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { PropsWithChildren } from 'react'
import { useBackgroundPidRefresh } from '../../hooks/useBackgroundPidRefresh'

export function WithBackgroundPidRefresh({ children }: PropsWithChildren) {
// Refresh PID once it reaches 1
useBackgroundPidRefresh(1)

return children
}
67 changes: 67 additions & 0 deletions apps/easypid/src/hooks/useBackgroundPidRefresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { MdocRecord, SdJwtVcRecord } from '@credo-ts/core'
import { getBatchCredentialMetadata } from '@package/agent/src/openid4vc/batchMetadata'
import { useHasInternetConnection } from '@package/app'
import { useEffect, useMemo, useState } from 'react'
import { type AppAgent, useAppAgent } from '../agent'
import { useShouldUseCloudHsm } from '../features/onboarding/useShouldUseCloudHsm'
import { RefreshPidUseCase } from '../use-cases/RefreshPidUseCase.ts'
import { usePidCredential } from './usePidCredential'

async function refreshPid({ agent, sdJwt, mdoc }: { agent: AppAgent; sdJwt?: SdJwtVcRecord; mdoc?: MdocRecord }) {
console.log('refreshing PID')
const useCase = await RefreshPidUseCase.initialize({
agent,
})

await useCase.retrieveCredentialsUsingExistingRecords({
sdJwt,
mdoc,
})
}

export function useBackgroundPidRefresh(batchThreshold: number) {
const { sdJwt, mdoc } = usePidCredential()
const [isRefreshing, setIsRefreshing] = useState(false)
const hasInternet = useHasInternetConnection()
const shouldUseCloudHsm = useShouldUseCloudHsm()
const { agent } = useAppAgent()

const { shouldRefreshMdoc, shouldRefreshSdJwt } = useMemo(() => {
if (!shouldUseCloudHsm) return {}

let shouldRefreshSdJwt = false
if (sdJwt) {
const sdJwtBatch = getBatchCredentialMetadata(sdJwt)
if (sdJwtBatch) {
shouldRefreshSdJwt = sdJwtBatch.additionalCredentials.length <= batchThreshold
}
}

let shouldRefreshMdoc = false
if (mdoc) {
const mdocBatch = getBatchCredentialMetadata(mdoc)
if (mdocBatch) {
shouldRefreshMdoc = mdocBatch.additionalCredentials.length <= batchThreshold
}
}

return {
shouldRefreshSdJwt,
shouldRefreshMdoc,
}
}, [sdJwt, mdoc, batchThreshold, shouldUseCloudHsm])

useEffect(() => {
if (isRefreshing || !hasInternet || !shouldUseCloudHsm) return

if (shouldRefreshMdoc || shouldRefreshSdJwt) {
setIsRefreshing(true)

refreshPid({
agent,
sdJwt: shouldRefreshSdJwt ? sdJwt : undefined,
mdoc: shouldRefreshMdoc ? mdoc : undefined,
}).finally(() => setIsRefreshing(false))
}
}, [shouldRefreshMdoc, shouldRefreshSdJwt, hasInternet, agent, isRefreshing, mdoc, sdJwt, shouldUseCloudHsm])
}
8 changes: 6 additions & 2 deletions apps/easypid/src/hooks/usePidCredential.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ClaimFormat } from '@credo-ts/core'
import { ClaimFormat, MdocRecord, SdJwtVcRecord } from '@credo-ts/core'
import { type CredentialForDisplayId, type CredentialMetadata, useCredentialsForDisplay } from '@package/agent'
import { capitalizeFirstLetter, sanitizeString } from '@package/utils'
import { useMemo } from 'react'
Expand Down Expand Up @@ -386,13 +386,14 @@ export function usePidCredential() {
attributesForDisplay: getPidAttributesForDisplay(attributes, claimFormat),
metadata: pidCredential.metadata,
metadataForDisplay: getPidMetadataAttributesForDisplay(attributes, pidCredential.metadata, ClaimFormat.SdJwtVc),
record: pidCredential.record,
}
})
}, [credentials])

if (isLoading) {
return {
credential: undefined,
credentials: undefined,
isLoading: true,
} as const
}
Expand All @@ -402,6 +403,9 @@ export function usePidCredential() {
pidCredentialForDisplay: pidCredentials[0] as (typeof pidCredentials)[number] | undefined,
credentialIds: pidCredentials.map((p) => p.id),
credentials: pidCredentials,
mdoc: pidCredentials.find((c): c is typeof c & { record: MdocRecord } => c.record instanceof MdocRecord)?.record,
sdJwt: pidCredentials.find((c): c is typeof c & { record: SdJwtVcRecord } => c.record instanceof SdJwtVcRecord)
?.record,
} as const
}

Expand Down
3 changes: 3 additions & 0 deletions apps/easypid/src/storage/mmkv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { MMKV } from 'react-native-mmkv'

export const mmkv = new MMKV()
27 changes: 24 additions & 3 deletions apps/easypid/src/use-cases/ReceivePidUseCaseCFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,32 @@ import {
type SdJwtVcRecord,
receiveCredentialFromOpenId4VciOffer,
resolveOpenId4VciOffer,
setRefreshCredentialMetadata,
storeCredential,
} from '@package/agent'
import { getShouldUseCloudHsm } from '../features/onboarding/useShouldUseCloudHsm'
import { ReceivePidUseCaseFlow, type ReceivePidUseCaseFlowOptions } from './ReceivePidUseCaseFlow'
import { C_SD_JWT_MDOC_OFFER } from './bdrPidIssuerOffers'
import { C_PRIME_SD_JWT_MDOC_OFFER } from './bdrPidIssuerOffers'

export class ReceivePidUseCaseCFlow extends ReceivePidUseCaseFlow {
public static async initialize(options: ReceivePidUseCaseFlowOptions) {
const resolved = await resolveOpenId4VciOffer({
agent: options.agent,
offer: { uri: C_SD_JWT_MDOC_OFFER },
offer: { uri: C_PRIME_SD_JWT_MDOC_OFFER },
authorization: {
clientId: ReceivePidUseCaseCFlow.CLIENT_ID,
redirectUri: ReceivePidUseCaseCFlow.REDIRECT_URI,
},
})

// NOTE: the bdr pid issuer does not include in their metadata that they support batch while they do support is
// and Credo checks for this. We modify the metadata so we can still use batch issuance
if (!resolved.resolvedCredentialOffer.metadata.credentialIssuer.batch_credential_issuance) {
resolved.resolvedCredentialOffer.metadata.credentialIssuer.batch_credential_issuance = {
batch_size: 10,
}
}

if (
!resolved.resolvedAuthorizationRequest ||
resolved.resolvedAuthorizationRequest.authorizationFlow === OpenId4VciAuthorizationFlow.PresentationDuringIssuance
Expand Down Expand Up @@ -57,17 +67,28 @@ export class ReceivePidUseCaseCFlow extends ReceivePidUseCaseFlow {
resolvedCredentialOffer: this.resolvedCredentialOffer,
credentialConfigurationIdsToRequest,
clientId: ReceivePidUseCaseCFlow.CLIENT_ID,
requestBatch: getShouldUseCloudHsm() ? 10 : false,
pidSchemes,
})

const credentialRecords: Array<SdJwtVcRecord | MdocRecord> = []
for (const credentialResponse of credentialResponses) {
const credentialRecord = credentialResponse.credential
if (typeof credentialRecord === 'string') throw new Error('No string expected for c flow')

if (credentialRecord.type !== 'SdJwtVcRecord' && credentialRecord.type !== 'MdocRecord') {
throw new Error(`Unexpected record type ${credentialRecord.type}`)
}

// It seems the refresh token can be re-used, so we store it on all the records
if (this.accessToken.accessTokenResponse.refresh_token) {
setRefreshCredentialMetadata(credentialRecord, {
refreshToken: this.accessToken.accessTokenResponse.refresh_token,
dpop: this.accessToken.dpop
? { alg: this.accessToken.dpop.alg, jwk: this.accessToken.dpop.jwk.toJson() }
: undefined,
})
}

credentialRecords.push(credentialRecord)
await storeCredential(this.options.agent, credentialRecord)
}
Expand Down
Loading