diff --git a/.changeset/angry-hats-deny.md b/.changeset/angry-hats-deny.md new file mode 100644 index 0000000000..afaa20c188 --- /dev/null +++ b/.changeset/angry-hats-deny.md @@ -0,0 +1,9 @@ +--- +"@credo-ts/core": minor +--- + +refactor!: remove support for BBS+ signatures. + +The underlying implementation of BBS+ of which Credo is based is outdated, has not been maintained, and not recommended to use. + +A new version is being worked on by standard development organizations, for which support may be added at a later time. If you still require support for the old/legacy BBS+ Signatures, you can look at the latest version of Credo and extract the required code and create a custom BBS+ module. diff --git a/.changeset/five-glasses-jump.md b/.changeset/five-glasses-jump.md new file mode 100644 index 0000000000..9fb4a20337 --- /dev/null +++ b/.changeset/five-glasses-jump.md @@ -0,0 +1,5 @@ +--- +"@credo-ts/core": minor +--- + +When signing with dids in Credo, it is now required that all DIDs have an associated `DidRecord` with the created role. With the new KMS API we now need to keep track of key ids for keys within a did document, and these are stored on the did document. You can import a did using `agent.dids.import` and provide the `keys` array to define the mapping between verification method and key id. If a verification method mapping to key id is not provided in the did record, we will assume the legacy key id format is used (the base58 encoded public key) diff --git a/.changeset/friendly-forks-build.md b/.changeset/friendly-forks-build.md new file mode 100644 index 0000000000..804f2297d7 --- /dev/null +++ b/.changeset/friendly-forks-build.md @@ -0,0 +1,5 @@ +--- +"@credo-ts/core": minor +--- + +the BBS module has been deprecated and removed. It was based on an old implementation and the underlying library was not maintained anymore. If you still need the BBS functionlity you can extract the code from and older commit of the Credo repo and create your own custom module. Contributions for the new BBS specification are welcome diff --git a/.changeset/gorgeous-bags-perform.md b/.changeset/gorgeous-bags-perform.md new file mode 100644 index 0000000000..7456b6ce7a --- /dev/null +++ b/.changeset/gorgeous-bags-perform.md @@ -0,0 +1,20 @@ +--- +"@credo-ts/indy-sdk-to-askar-migration": minor +"@credo-ts/question-answer": minor +"@credo-ts/react-native": minor +"@credo-ts/action-menu": minor +"@credo-ts/anoncreds": minor +"@credo-ts/openid4vc": minor +"@credo-ts/indy-vdr": minor +"@credo-ts/didcomm": minor +"@credo-ts/tenants": minor +"@credo-ts/askar": minor +"@credo-ts/cheqd": minor +"@credo-ts/core": minor +"@credo-ts/drpc": minor +"@credo-ts/node": minor +--- + +when signing in Credo, it is now required to always reference a key id. For DIDs this is extracted from the DidRecord, and for JWKs (e.g. in holder binding) this is extracted form the `kid` of the JWK. For X509 certificates you need to make sure there is a key id attached to the certificate manually for now, since we don't have a X509 record like we have a DidRecord. For x509 certificates created before 0.6 you can use the legacy key id (`certificate.keyId = certificate.publicJwk.legacyKeyId`), for certificates created after 0.6 you need to manually store the key id and set it on the certificate after decoding. + +For this reason, we now require instances of X509 certificates where we used to require encoded certificates, to allow you to set the keyId on the certificate beforehand. diff --git a/.changeset/loud-knives-doubt.md b/.changeset/loud-knives-doubt.md new file mode 100644 index 0000000000..8eb3770565 --- /dev/null +++ b/.changeset/loud-knives-doubt.md @@ -0,0 +1,18 @@ +--- +"@credo-ts/indy-sdk-to-askar-migration": minor +"@credo-ts/question-answer": minor +"@credo-ts/react-native": minor +"@credo-ts/action-menu": minor +"@credo-ts/anoncreds": minor +"@credo-ts/openid4vc": minor +"@credo-ts/indy-vdr": minor +"@credo-ts/didcomm": minor +"@credo-ts/tenants": minor +"@credo-ts/askar": minor +"@credo-ts/cheqd": minor +"@credo-ts/core": minor +"@credo-ts/drpc": minor +"@credo-ts/node": minor +--- + +The `Key` and `Jwk` classes have been removed in favour of a new `PublicJwk` class, and all APIs in Credo have been updated to use the new `PublicJwk` class. Leveraging Jwk as the base for all APIs provides more flexility and makes it easier to support key types where it's not always so easy to extract the raw public key bytes. In addition all the previous Jwk relatedfunctionality has been replaced with the new KMS jwk functionalty. For example `JwaSignatureAlgorithm` is now `Kms.KnownJwaSignatureAlgorithms`. diff --git a/.changeset/nine-games-travel.md b/.changeset/nine-games-travel.md new file mode 100644 index 0000000000..dfc6b3865c --- /dev/null +++ b/.changeset/nine-games-travel.md @@ -0,0 +1,18 @@ +--- +"@credo-ts/indy-sdk-to-askar-migration": minor +"@credo-ts/question-answer": minor +"@credo-ts/react-native": minor +"@credo-ts/action-menu": minor +"@credo-ts/anoncreds": minor +"@credo-ts/openid4vc": minor +"@credo-ts/indy-vdr": minor +"@credo-ts/didcomm": minor +"@credo-ts/tenants": minor +"@credo-ts/askar": minor +"@credo-ts/cheqd": minor +"@credo-ts/core": minor +"@credo-ts/drpc": minor +"@credo-ts/node": minor +--- + +The wallet API has been completely rewritten to be more generic, support multiple backends at the same time, support generic encrypting and decryption, support symmetric keys, and enable backends that use key ids rather than the public key to identify a key. This has resulted in significant breaking changes, and all usages of the wallet api should be updated to use the new `agent.kms` APIs. In addition the wallet is not available anymore on the agentContext. If you used this, instead inject the KMS API using `agentContext.resolve(Kms.KeyManagementApi)`. diff --git a/.changeset/six-needles-walk.md b/.changeset/six-needles-walk.md new file mode 100644 index 0000000000..2700a1d290 --- /dev/null +++ b/.changeset/six-needles-walk.md @@ -0,0 +1,5 @@ +--- +"@credo-ts/core": minor +--- + +the automatic backup functionality has been removed from Credo. With the generalization of the KMS API, and with moving away from assuming Askar is used for storage, providing a generic backup API is not feasible, especially for large deployments. From now on, you are expected to create a backup yourself before performing any updates. For askar you can export a store on the Askar api, or you can directly create a backup of your Postgres database. diff --git a/.changeset/spotty-peas-attack.md b/.changeset/spotty-peas-attack.md new file mode 100644 index 0000000000..a9549267b2 --- /dev/null +++ b/.changeset/spotty-peas-attack.md @@ -0,0 +1,6 @@ +--- +"@credo-ts/askar": minor +"@credo-ts/core": minor +--- + +The wallet config has been removed from the main agent config, to allow for more flexibility. Instead, each module can now define their own config for the storage and kms. For askar there is a new `store` property which must be provided on the askar module config where you can set the wallet id and key. It is also possible to disable the kms or storage for askar using `enableKms` and `enableStorage`. diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 54b3a02ecd..d46ac33f0f 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -71,6 +71,8 @@ jobs: with: node-version: ${{ matrix.node-version }} + - uses: shogo82148/actions-setup-redis@v1 + # See https://github.com/actions/setup-node/issues/641#issuecomment-1358859686 - name: pnpm cache path id: pnpm-cache-path diff --git a/demo-openid/src/BaseAgent.ts b/demo-openid/src/BaseAgent.ts index 8ed8d16834..fd5724e2d3 100644 --- a/demo-openid/src/BaseAgent.ts +++ b/demo-openid/src/BaseAgent.ts @@ -2,10 +2,11 @@ import type { Server } from 'http' import type { InitConfig, KeyDidCreateOptions, ModulesMap, VerificationMethod } from '@credo-ts/core' import type { Express } from 'express' -import { Agent, ConsoleLogger, DidKey, KeyType, LogLevel, TypedArrayEncoder } from '@credo-ts/core' +import { Agent, Buffer, ConsoleLogger, DidKey, LogLevel } from '@credo-ts/core' import { agentDependencies } from '@credo-ts/node' import express from 'express' +import { transformPrivateKeyToPrivateJwk } from '@credo-ts/askar' import { greenText } from './OutputClass' export class BaseAgent { @@ -20,21 +21,32 @@ export class BaseAgent { public kid!: string public verificationMethod!: VerificationMethod - public constructor({ port, name, modules }: { port: number; name: string; modules: AgentModules }) { + public constructor({ + port, + name, + modules, + }: { + port: number + name: string + modules: AgentModules + }) { this.name = name this.port = port this.app = express() const config = { label: name, - walletConfig: { id: name, key: name }, allowInsecureHttpUrls: true, logger: new ConsoleLogger(LogLevel.off), } satisfies InitConfig this.config = config - this.agent = new Agent({ config, dependencies: agentDependencies, modules }) + this.agent = new Agent({ + config, + dependencies: agentDependencies, + modules, + }) } public async initializeAgent(secretPrivateKey: string) { @@ -42,15 +54,28 @@ export class BaseAgent { this.server = this.app.listen(this.port) + const { privateJwk } = transformPrivateKeyToPrivateJwk({ + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + privateKey: Buffer.from(secretPrivateKey), + }) + + const { keyId } = await this.agent.kms.importKey({ + privateJwk, + }) + const didCreateResult = await this.agent.dids.create({ method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString(secretPrivateKey) }, + options: { + keyId, + }, }) this.did = didCreateResult.didState.did as string this.didKey = DidKey.fromDid(this.did) - this.kid = `${this.did}#${this.didKey.key.fingerprint}` + this.kid = `${this.did}#${this.didKey.publicJwk.fingerprint}` const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(this.kid, ['authentication']) if (!verificationMethod) throw new Error('No verification method found') diff --git a/demo-openid/src/Holder.ts b/demo-openid/src/Holder.ts index 23de6ac231..7311a90d49 100644 --- a/demo-openid/src/Holder.ts +++ b/demo-openid/src/Holder.ts @@ -8,12 +8,13 @@ import { AskarModule } from '@credo-ts/askar' import { DidJwk, DidKey, - KeyType, + JwkDidCreateOptions, + KeyDidCreateOptions, + Kms, Mdoc, W3cJsonLdVerifiableCredential, W3cJwtVerifiableCredential, X509Module, - getJwkFromKey, } from '@credo-ts/core' import { OpenId4VcHolderModule, @@ -23,12 +24,13 @@ import { } from '@credo-ts/openid4vc' import { askar } from '@openwallet-foundation/askar-nodejs' +import { AskarModuleConfigStoreOptions } from '../../packages/askar/src/AskarModuleConfig' import { BaseAgent } from './BaseAgent' import { Output, greenText } from './OutputClass' -function getOpenIdHolderModules() { +function getOpenIdHolderModules(askarStorageConfig: AskarModuleConfigStoreOptions) { return { - askar: new AskarModule({ askar }), + askar: new AskarModule({ askar, store: askarStorageConfig }), openId4VcHolder: new OpenId4VcHolderModule(), x509: new X509Module({ getTrustedCertificatesForVerification: (_agentContext, { certificateChain, verification }) => { @@ -54,7 +56,14 @@ export class Holder extends BaseAgent> } public constructor(port: number, name: string) { - super({ port, name, modules: getOpenIdHolderModules() }) + super({ + port, + name, + modules: getOpenIdHolderModules({ + id: name, + key: name, + }), + }) } public static async build(): Promise { @@ -140,20 +149,33 @@ export class Holder extends BaseAgent> clientId: options.clientId, credentialConfigurationIds: options.credentialsToRequest, credentialBindingResolver: async ({ supportedDidMethods, supportsAllDidMethods, proofTypes }) => { - const key = await this.agent.wallet.createKey({ - keyType: proofTypes.jwt?.supportedKeyTypes[0] ?? KeyType.Ed25519, + const key = await this.agent.kms.createKeyForSignatureAlgorithm({ + algorithm: proofTypes.jwt?.supportedSignatureAlgorithms[0] ?? 'EdDSA', }) + const publicJwk = Kms.PublicJwk.fromPublicJwk(key.publicJwk) if (supportsAllDidMethods || supportedDidMethods?.includes('did:key')) { - const didKey = new DidKey(key) + await this.agent.dids.create({ + method: 'key', + options: { + keyId: key.keyId, + }, + }) + const didKey = new DidKey(publicJwk) return { method: 'did', - didUrls: [`${didKey.did}#${didKey.key.fingerprint}`], + didUrls: [`${didKey.did}#${didKey.publicJwk.fingerprint}`], } } if (supportedDidMethods?.includes('did:jwk')) { - const didJwk = DidJwk.fromJwk(getJwkFromKey(key)) + const didJwk = DidJwk.fromPublicJwk(publicJwk) + await this.agent.dids.create({ + method: 'jwk', + options: { + keyId: key.keyId, + }, + }) return { method: 'did', @@ -164,7 +186,7 @@ export class Holder extends BaseAgent> // We fall back on jwk binding return { method: 'jwk', - keys: [getJwkFromKey(key)], + keys: [publicJwk], } }, ...tokenResponse, diff --git a/demo-openid/src/HolderInquirer.ts b/demo-openid/src/HolderInquirer.ts index 784471e421..1b16ccfc99 100644 --- a/demo-openid/src/HolderInquirer.ts +++ b/demo-openid/src/HolderInquirer.ts @@ -135,7 +135,7 @@ export class HolderInquirer extends BaseInquirer { public async addTrustedCertificate() { const trustedCertificate = await this.inquireInput('Enter trusted certificate: ') - await this.holder.agent.x509.addTrustedCertificate(trustedCertificate) + this.holder.agent.x509.config.addTrustedCertificate(trustedCertificate) console.log(greenText('Added trusted certificate')) } diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts index adc977d0c5..3363de8b4c 100644 --- a/demo-openid/src/Issuer.ts +++ b/demo-openid/src/Issuer.ts @@ -1,4 +1,4 @@ -import type { DidKey } from '@credo-ts/core' +import type { DidKey, X509Certificate } from '@credo-ts/core' import type { OpenId4VcIssuerRecord, OpenId4VcVerifierRecord, @@ -10,17 +10,16 @@ import type { VerifiedOpenId4VcCredentialHolderBinding, } from '@credo-ts/openid4vc' -import { AskarModule } from '@credo-ts/askar' +import { AskarModule, transformSeedToPrivateJwk } from '@credo-ts/askar' import { ClaimFormat, CredoError, JsonTransformer, - KeyType, + Kms, TypedArrayEncoder, W3cCredential, W3cCredentialSubject, W3cIssuer, - X509ModuleConfig, X509Service, parseDid, utils, @@ -47,7 +46,18 @@ export const credentialConfigurationsSupported = { vct: 'PresentationAuthorization', scope: 'openid4vc:credential:PresentationAuthorization', cryptographic_binding_methods_supported: ['jwk', 'did:key', 'did:jwk'], - credential_signing_alg_values_supported: ['ES256', 'EdDSA'], + credential_signing_alg_values_supported: [ + Kms.KnownJwaSignatureAlgorithms.ES256, + Kms.KnownJwaSignatureAlgorithms.EdDSA, + ], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [ + Kms.KnownJwaSignatureAlgorithms.ES256, + Kms.KnownJwaSignatureAlgorithms.EdDSA, + ], + }, + }, }, 'UniversityDegreeCredential-jwtvcjson': { format: OpenId4VciCredentialFormatProfile.JwtVcJson, @@ -55,10 +65,21 @@ export const credentialConfigurationsSupported = { // TODO: we should validate this against what is supported by credo // as otherwise it's very easy to create invalid configurations? cryptographic_binding_methods_supported: ['did:key', 'did:jwk'], - credential_signing_alg_values_supported: ['ES256', 'EdDSA'], + credential_signing_alg_values_supported: [ + Kms.KnownJwaSignatureAlgorithms.ES256, + Kms.KnownJwaSignatureAlgorithms.EdDSA, + ], credential_definition: { type: ['VerifiableCredential', 'UniversityDegreeCredential'], }, + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [ + Kms.KnownJwaSignatureAlgorithms.ES256, + Kms.KnownJwaSignatureAlgorithms.EdDSA, + ], + }, + }, }, 'UniversityDegreeCredential-sdjwt': { format: OpenId4VciCredentialFormatProfile.SdJwtVc, @@ -66,6 +87,14 @@ export const credentialConfigurationsSupported = { scope: 'openid4vc:credential:OpenBadgeCredential-sdjwt', cryptographic_binding_methods_supported: ['jwk'], credential_signing_alg_values_supported: ['ES256', 'EdDSA'], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [ + Kms.KnownJwaSignatureAlgorithms.ES256, + Kms.KnownJwaSignatureAlgorithms.EdDSA, + ], + }, + }, }, 'UniversityDegreeCredential-mdoc': { format: OpenId4VciCredentialFormatProfile.MsoMdoc, @@ -73,20 +102,25 @@ export const credentialConfigurationsSupported = { scope: 'openid4vc:credential:OpenBadgeCredential-mdoc', cryptographic_binding_methods_supported: ['jwk'], credential_signing_alg_values_supported: ['ES256', 'EdDSA'], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [ + Kms.KnownJwaSignatureAlgorithms.ES256, + Kms.KnownJwaSignatureAlgorithms.EdDSA, + ], + }, + }, }, } satisfies OpenId4VciCredentialConfigurationsSupportedWithFormats +let issuerCertificate: X509Certificate + function getCredentialRequestToCredentialMapper({ issuerDidKey, }: { issuerDidKey: DidKey }): OpenId4VciCredentialRequestToCredentialMapper { - return async ({ holderBinding, credentialConfigurationId, credentialConfiguration, agentContext, authorization }) => { - const trustedCertificates = agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates - if (trustedCertificates?.length !== 1) { - throw new Error(`Expected exactly one trusted certificate. Received ${trustedCertificates?.length}.`) - } - + return async ({ holderBinding, credentialConfigurationId, credentialConfiguration, authorization }) => { if (credentialConfigurationId === 'PresentationAuthorization') { return { format: ClaimFormat.SdJwtVc, @@ -100,9 +134,9 @@ function getCredentialRequestToCredentialMapper({ binding.method === 'did' ? { method: 'did', - didUrl: `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}`, + didUrl: `${issuerDidKey.did}#${issuerDidKey.publicJwk.fingerprint}`, } - : { method: 'x5c', x5c: [trustedCertificates[0]], issuer: ISSUER_HOST }, + : { method: 'x5c', x5c: [issuerCertificate], issuer: ISSUER_HOST }, })), } satisfies OpenId4VciSignSdJwtCredentials } @@ -128,7 +162,7 @@ function getCredentialRequestToCredentialMapper({ ), issuanceDate: w3cDate(Date.now()), }), - verificationMethod: `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}`, + verificationMethod: `${issuerDidKey.did}#${issuerDidKey.publicJwk.fingerprint}`, } }), } satisfies OpenId4VciSignW3cCredentials @@ -147,7 +181,7 @@ function getCredentialRequestToCredentialMapper({ holder: binding, issuer: { method: 'did', - didUrl: `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}`, + didUrl: `${issuerDidKey.did}#${issuerDidKey.publicJwk.fingerprint}`, }, disclosureFrame: { _sd: ['university', 'degree', 'authorized_user'] }, })), @@ -160,8 +194,8 @@ function getCredentialRequestToCredentialMapper({ return { format: ClaimFormat.MsoMdoc, credentials: holderBinding.keys.map((binding) => ({ - issuerCertificate: trustedCertificates[0], - holderKey: binding.key, + issuerCertificate, + holderKey: binding.jwk, namespaces: { 'Leopold-Franzens-University': { degree: 'bachelor', @@ -193,7 +227,7 @@ export class Issuer extends BaseAgent<{ port, name, modules: { - askar: new AskarModule({ askar }), + askar: new AskarModule({ askar, store: { id: name, key: name } }), openId4VcVerifier: new OpenId4VcVerifierModule({ baseUrl: `${url}/oid4vp`, router: openId4VpRouter, @@ -209,7 +243,7 @@ export class Issuer extends BaseAgent<{ verifierId: this.verifierRecord.verifierId, requestSigner: { method: 'did', - didUrl: `${this.didKey.did}#${this.didKey.key.fingerprint}`, + didUrl: `${this.didKey.did}#${this.didKey.publicJwk.fingerprint}`, }, responseMode: 'direct_post.jwt', presentationExchange: { @@ -254,11 +288,17 @@ export class Issuer extends BaseAgent<{ const issuer = new Issuer(ISSUER_HOST, 2000, `OpenId4VcIssuer ${Math.random().toString()}`) await issuer.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598f') - const certificate = await X509Service.createCertificate(issuer.agent.context, { - authorityKey: await issuer.agent.context.wallet.createKey({ - keyType: KeyType.P256, + const importedKey = await issuer.agent.kms.importKey({ + privateJwk: transformSeedToPrivateJwk({ seed: TypedArrayEncoder.fromString('e5f18b10cd15cdb76818bc6ae8b71eb475e6eac76875ed085d3962239bbcf42f'), - }), + type: { + crv: 'P-256', + kty: 'EC', + }, + }).privateJwk, + }) + issuerCertificate = await X509Service.createCertificate(issuer.agent.context, { + authorityKey: Kms.PublicJwk.fromPublicJwk(importedKey.publicJwk), validity: { notBefore: new Date('2000-01-01'), notAfter: new Date('2050-01-01'), @@ -271,10 +311,9 @@ export class Issuer extends BaseAgent<{ issuer: 'C=DE', }) - const issuerCertficicate = certificate.toString('base64url') - await issuer.agent.x509.setTrustedCertificates([issuerCertficicate]) + issuer.agent.x509.config.setTrustedCertificates([issuerCertificate]) console.log('Set the following certficate for the holder to verify mdoc credentials.') - console.log(issuerCertficicate) + console.log(issuerCertificate.toString('base64')) issuer.verifierRecord = await issuer.agent.modules.openId4VcVerifier.createVerifier({ verifierId: '726222ad-7624-4f12-b15b-e08aa7042ffa', diff --git a/demo-openid/src/Verifier.ts b/demo-openid/src/Verifier.ts index 1a6c230943..1cf3137981 100644 --- a/demo-openid/src/Verifier.ts +++ b/demo-openid/src/Verifier.ts @@ -159,7 +159,7 @@ export class Verifier extends BaseAgent<{ askar: AskarModule; openId4VcVerifier: port, name, modules: { - askar: new AskarModule({ askar }), + askar: new AskarModule({ askar, store: { id: name, key: name } }), openId4VcVerifier: new OpenId4VcVerifierModule({ baseUrl: `${url}/oid4vp`, router: openId4VpRouter, diff --git a/demo/src/BaseAgent.ts b/demo/src/BaseAgent.ts index f6a289aa82..efc507965e 100644 --- a/demo/src/BaseAgent.ts +++ b/demo/src/BaseAgent.ts @@ -37,6 +37,7 @@ import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' import { askar } from '@openwallet-foundation/askar-nodejs' +import { AskarModuleConfigStoreOptions } from '../../packages/askar/src/AskarModuleConfig' import { greenText } from './OutputClass' const bcovrin = `{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blskey":"4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba","blskey_pop":"RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1","client_ip":"138.197.138.255","client_port":9702,"node_ip":"138.197.138.255","node_port":9701,"services":["VALIDATOR"]},"dest":"Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv"},"metadata":{"from":"Th7MpTaRZVRYnPiabds81Y"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62"},"ver":"1"} @@ -65,10 +66,6 @@ export class BaseAgent { const config = { label: name, - walletConfig: { - id: name, - key: name, - }, } satisfies InitConfig this.config = config @@ -76,7 +73,7 @@ export class BaseAgent { this.agent = new Agent({ config, dependencies: agentDependencies, - modules: getAskarAnonCredsIndyModules({ endpoints: [`http://localhost:${this.port}`] }), + modules: getAskarAnonCredsIndyModules({ endpoints: [`http://localhost:${this.port}`] }, { id: name, key: name }), }) this.agent.modules.didcomm.registerInboundTransport(new HttpInboundTransport({ port })) this.agent.modules.didcomm.registerOutboundTransport(new HttpOutboundTransport()) @@ -89,7 +86,10 @@ export class BaseAgent { } } -function getAskarAnonCredsIndyModules(didcommConfig: DidCommModuleConfigOptions) { +function getAskarAnonCredsIndyModules( + didcommConfig: DidCommModuleConfigOptions, + askarStoreConfig: AskarModuleConfigStoreOptions +) { const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService() const legacyIndyProofFormatService = new LegacyIndyProofFormatService() @@ -145,6 +145,7 @@ function getAskarAnonCredsIndyModules(didcommConfig: DidCommModuleConfigOptions) }), askar: new AskarModule({ askar, + store: askarStoreConfig, }), } as const } diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index 496bd57dc9..852cdb1286 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -3,10 +3,11 @@ import type { ConnectionRecord, ConnectionStateChangedEvent } from '@credo-ts/di import type { IndyVdrRegisterCredentialDefinitionOptions, IndyVdrRegisterSchemaOptions } from '@credo-ts/indy-vdr' import type BottomBar from 'inquirer/lib/ui/bottom-bar' -import { KeyType, TypedArrayEncoder, utils } from '@credo-ts/core' +import { TypedArrayEncoder, utils } from '@credo-ts/core' import { ConnectionEventTypes } from '@credo-ts/didcomm' import { ui } from 'inquirer' +import { transformPrivateKeyToPrivateJwk } from '../../packages/askar/src' import { BaseAgent, indyNetworkConfig } from './BaseAgent' import { Color, Output, greenText, purpleText, redText } from './OutputClass' @@ -39,15 +40,28 @@ export class Faber extends BaseAgent { const unqualifiedIndyDid = '2jEvRuKmfBJTRa7QowDpNN' const cheqdDid = 'did:cheqd:testnet:d37eba59-513d-42d3-8f9f-d1df0548b675' const indyDid = `did:indy:${indyNetworkConfig.indyNamespace}:${unqualifiedIndyDid}` + const didDocumentRelativeKeyId = registry === RegistryOptions.indy ? '#verkey' : '#key-1' const did = registry === RegistryOptions.indy ? indyDid : cheqdDid + const { privateJwk } = transformPrivateKeyToPrivateJwk({ + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + privateKey: TypedArrayEncoder.fromString('afjdemoverysercure00000000000000'), + }) + + const { keyId } = await this.agent.kms.importKey({ + privateJwk, + }) + await this.agent.dids.import({ did, overwrite: true, - privateKeys: [ + keys: [ { - keyType: KeyType.Ed25519, - privateKey: TypedArrayEncoder.fromString('afjdemoverysercure00000000000000'), + didDocumentRelativeKeyId, + kmsKeyId: keyId, }, ], }) @@ -74,7 +88,9 @@ export class Faber extends BaseAgent { console.log( Output.ConnectionLink, - outOfBand.outOfBandInvitation.toUrl({ domain: `http://localhost:${this.port}` }), + outOfBand.outOfBandInvitation.toUrl({ + domain: `http://localhost:${this.port}`, + }), '\n' ) } diff --git a/packages/action-menu/tests/action-menu.test.ts b/packages/action-menu/tests/action-menu.test.ts index abcf1dbad6..24bf17fc35 100644 --- a/packages/action-menu/tests/action-menu.test.ts +++ b/packages/action-menu/tests/action-menu.test.ts @@ -2,7 +2,7 @@ import type { ConnectionRecord } from '@credo-ts/didcomm' import { Agent } from '@credo-ts/core' -import { getInMemoryAgentOptions, makeConnection, setupSubjectTransports, testLogger } from '../../core/tests' +import { getAgentOptions, makeConnection, setupSubjectTransports, testLogger } from '../../core/tests' import { waitForActionMenuRecord } from './helpers' @@ -12,22 +12,24 @@ const modules = { actionMenu: new ActionMenuModule(), } -const faberAgentOptions = getInMemoryAgentOptions( +const faberAgentOptions = getAgentOptions( 'Faber Action Menu', { endpoints: ['rxjs:faber'], }, {}, - modules + modules, + { requireDidcomm: true } ) -const aliceAgentOptions = getInMemoryAgentOptions( +const aliceAgentOptions = getAgentOptions( 'Alice Action Menu', { endpoints: ['rxjs:alice'], }, {}, - modules + modules, + { requireDidcomm: true } ) describe('Action Menu', () => { @@ -83,9 +85,7 @@ describe('Action Menu', () => { afterEach(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice requests menu to Faber and selects an option once received', async () => { diff --git a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts index b4b335d120..c9078f1f77 100644 --- a/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts +++ b/packages/anoncreds/src/anoncreds-rs/AnonCredsRsHolderService.ts @@ -44,6 +44,7 @@ import type { AnonCredsCredentialRequestMetadata, W3cAnonCredsCredentialMetadata import { CredoError, JsonTransformer, + Kms, TypedArrayEncoder, W3cCredentialRecord, W3cCredentialRepository, @@ -96,6 +97,14 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { } } + public generateNonce(agentContext: AgentContext): string { + const kms = agentContext.resolve(Kms.KeyManagementApi) + const bytes = kms.randomBytes({ length: 10 }).bytes + + // generate an 80-bit nonce suitable for AnonCreds proofs + return bytes.reduce((acc, byte) => (acc << 8n) | BigInt(byte), 0n).toString() + } + public async createProof(agentContext: AgentContext, options: CreateProofOptions): Promise { const { credentialDefinitions, proofRequest, selectedCredentials, schemas } = options diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts index bc635a625f..bdf5066832 100644 --- a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsHolderService.test.ts @@ -33,7 +33,6 @@ import { agentDependencies, getAgentConfig, getAgentContext, mockFunction } from import { W3cAnonCredsCredentialMetadataKey } from '../../utils/metadata' import { AnonCredsRsHolderService } from '../AnonCredsRsHolderService' -import { InMemoryWallet } from './../../../../../tests/InMemoryWallet' import { createCredentialDefinition, createCredentialForHolder, @@ -71,8 +70,6 @@ const anoncredsCredentialRepositoryMock = new AnonCredsCredentialRepositoryMock( const inMemoryStorageService = new InMemoryStorageService() -const wallet = new InMemoryWallet() - const agentContext = getAgentContext({ registerInstances: [ [InjectionSymbols.AgentDependencies, agentDependencies], @@ -96,7 +93,6 @@ const agentContext = getAgentContext({ [SignatureSuiteToken, 'default'], ], agentConfig, - wallet, }) describe('AnonCredsRsHolderService', () => { diff --git a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts index 328f34e250..d6fb747193 100644 --- a/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts +++ b/packages/anoncreds/src/anoncreds-rs/__tests__/AnonCredsRsServices.test.ts @@ -12,7 +12,6 @@ import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' -import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' import { InMemoryAnonCredsRegistry } from '../../../../anoncreds/tests/InMemoryAnonCredsRegistry' import { testLogger } from '../../../../core/tests' import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' @@ -47,11 +46,9 @@ const anonCredsVerifierService = new AnonCredsRsVerifierService() const anonCredsHolderService = new AnonCredsRsHolderService() const anonCredsIssuerService = new AnonCredsRsIssuerService() const storageService = new InMemoryStorageService() -const wallet = new InMemoryWallet() const registry = new InMemoryAnonCredsRegistry() const agentContext = getAgentContext({ - wallet, registerInstances: [ [InjectionSymbols.Stop$, new Subject()], [InjectionSymbols.AgentDependencies, agentDependencies], diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index 466dfc6e9d..5f5a1162ff 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -56,6 +56,8 @@ export class AnonCredsProofFormatService implements ProofFormatService ): Promise { + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + const format = new ProofFormatSpec({ format: ANONCREDS_PRESENTATION_PROPOSAL, attachmentId, @@ -71,7 +73,7 @@ export class AnonCredsProofFormatService implements ProofFormatService ): Promise { + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + const format = new ProofFormatSpec({ format: ANONCREDS_PRESENTATION_REQUEST, attachmentId, @@ -103,7 +107,7 @@ export class AnonCredsProofFormatService implements ProofFormatService ): Promise { + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) const format = new ProofFormatSpec({ format: ANONCREDS_PRESENTATION_REQUEST, attachmentId, @@ -128,7 +133,7 @@ export class AnonCredsProofFormatService implements ProofFormatService didDocumentRelativeKeyId === `#${parsedDid.fragment}`) + ?.kmsKeyId ?? publicJwk.legacyKeyId + + if (alg && !publicJwk.supportedSignatureAlgorithms.includes(alg as Kms.KnownJwaSignatureAlgorithm)) { + throw new CredoError(`jwk ${publicJwk.jwkTypehumanDescription}, does not support the JWS signature alg '${alg}'`) } const signingAlg = issuerSupportedAlgs.find( - (supportedAlg) => jwk.supportsSignatureAlgorithm(supportedAlg) && (alg === undefined || alg === supportedAlg) + (supportedAlg) => + publicJwk.supportedSignatureAlgorithms.includes(supportedAlg as Kms.KnownJwaSignatureAlgorithm) && + (alg === undefined || alg === supportedAlg) ) if (!signingAlg) throw new CredoError('No signing algorithm supported by the issuer found') const jwsService = agentContext.dependencyManager.resolve(JwsService) const jws = await jwsService.createJws(agentContext, { - key, + keyId, header: {}, payload: new JwtPayload({ additionalClaims: { nonce: data.nonce } }), - protectedHeaderOptions: { alg: signingAlg, kid }, + protectedHeaderOptions: { alg: signingAlg as Kms.KnownJwaSignatureAlgorithm, kid }, }) const signedAttach = new Attachment({ @@ -290,13 +298,13 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer const didsApi = agentContext.dependencyManager.resolve(DidsApi) const didDocument = await didsApi.resolveDidDocument(kid) const verificationMethod = didDocument.dereferenceKey(kid) - const key = getKeyFromVerificationMethod(verificationMethod) + const publicJwk = getPublicJwkFromVerificationMethod(verificationMethod) return { alg, method: 'did', didUrl: kid, - jwk: getJwkFromKey(key), + jwk: publicJwk, } }, }) @@ -1063,12 +1071,14 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer let didCommSignedAttachmentBindingMethod: DidCommSignedAttachmentBindingMethod | undefined = undefined if (didCommSignedAttachmentBindingMethodOptions) { + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) + const { didMethodsSupported, algsSupported } = didCommSignedAttachmentBindingMethodOptions didCommSignedAttachmentBindingMethod = { didMethodsSupported: didMethodsSupported ?? agentContext.dependencyManager.resolve(DidsApi).supportedResolverMethods, algsSupported: algsSupported ?? this.getSupportedJwaSignatureAlgorithms(agentContext), - nonce: await agentContext.wallet.generateNonce(), + nonce: TypedArrayEncoder.toBase64URL(kms.randomBytes({ length: 32 }).bytes), } if (didCommSignedAttachmentBindingMethod.algsSupported.length === 0) { @@ -1154,25 +1164,19 @@ export class DataIntegrityCredentialFormatService implements CredentialFormatSer } /** - * Returns the JWA Signature Algorithms that are supported by the wallet. - * - * This is an approximation based on the supported key types of the wallet. - * This is not 100% correct as a supporting a key type does not mean you support - * all the algorithms for that key type. However, this needs refactoring of the wallet - * that is planned for the 0.5.0 release. + * Returns the JWA Signature Algorithms that are supported by the agent. */ - private getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { - const supportedKeyTypes = agentContext.wallet.supportedKeyTypes - - // Extract the supported JWS algs based on the key types the wallet support. - const supportedJwaSignatureAlgorithms = supportedKeyTypes - // Map the supported key types to the supported JWK class - .map(getJwkClassFromKeyType) - // Filter out the undefined values - .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) - // Extract the supported JWA signature algorithms from the JWK class - .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) - - return supportedJwaSignatureAlgorithms + private getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): Kms.KnownJwaSignatureAlgorithm[] { + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) + + const supportedSignatureAlgorithms = Object.values(Kms.KnownJwaSignatureAlgorithms).filter( + (algorithm) => + kms.supportedBackendsForOperation({ + operation: 'sign', + algorithm, + }).length > 0 + ) + + return supportedSignatureAlgorithms } } diff --git a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts index 7274888389..4575e22773 100644 --- a/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts +++ b/packages/anoncreds/src/formats/LegacyIndyProofFormatService.ts @@ -74,6 +74,7 @@ export class LegacyIndyProofFormatService implements ProofFormatService ): Promise { + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) const format = new ProofFormatSpec({ format: V2_INDY_PRESENTATION_PROPOSAL, attachmentId, @@ -89,7 +90,7 @@ export class LegacyIndyProofFormatService implements ProofFormatService ): Promise { + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) const format = new ProofFormatSpec({ format: V2_INDY_PRESENTATION_REQUEST, attachmentId, @@ -120,7 +122,7 @@ export class LegacyIndyProofFormatService implements ProofFormatService ): Promise { + const holderService = agentContext.dependencyManager.resolve(AnonCredsHolderServiceSymbol) const format = new ProofFormatSpec({ format: V2_INDY_PRESENTATION_REQUEST, attachmentId, @@ -145,7 +148,7 @@ export class LegacyIndyProofFormatService implements ProofFormatService const storageService = new InMemoryStorageService() const eventEmitter = new EventEmitter(agentDependencies, new Subject()) @@ -101,25 +101,19 @@ const agentContext = getAgentContext({ [SignatureSuiteToken, 'default'], ], agentConfig, - wallet, }) const anoncredsCredentialFormatService = new AnonCredsCredentialFormatService() const anoncredsProofFormatService = new AnonCredsProofFormatService() +const kms = agentContext.resolve(Kms.KeyManagementApi) describe('Anoncreds format services', () => { - beforeEach(async () => { - await wallet.createAndOpen(agentConfig.walletConfig) - }) - - afterEach(async () => { - await wallet.delete() - }) - test('legacy unqualified did (sov or indy) issuance and verification flow starting from proposal without negotiation and without revocation', async () => { // This is just so we don't have to register an actual indy did (as we don't have the indy did registrar configured) - const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const unqualifiedIndyDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const key = await kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } }) + const unqualifiedIndyDid = indyDidFromPublicKeyBase58( + TypedArrayEncoder.toBase58(Kms.PublicJwk.fromPublicJwk(key.publicJwk).publicKey.publicKey) + ) const indyDid = `did:indy:pool1:${unqualifiedIndyDid}` // Create link secret diff --git a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts index 671061cb8b..f03f1b4fca 100644 --- a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts +++ b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts @@ -6,8 +6,9 @@ import { DidsModuleConfig, EventEmitter, InjectionSymbols, - KeyType, + Kms, SignatureSuiteToken, + TypedArrayEncoder, W3cCredentialsModuleConfig, } from '@credo-ts/core' import { @@ -22,7 +23,6 @@ import { import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' -import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' import { anoncreds } from '../../../../anoncreds/tests/helpers' import { indyDidFromPublicKeyBase58 } from '../../../../core/src/utils/did' import { testLogger } from '../../../../core/tests' @@ -67,7 +67,6 @@ const agentConfig = getAgentConfig('LegacyIndyFormatServicesTest') const anonCredsVerifierService = new AnonCredsRsVerifierService() const anonCredsHolderService = new AnonCredsRsHolderService() const anonCredsIssuerService = new AnonCredsRsIssuerService() -const wallet = new InMemoryWallet() // biome-ignore lint/suspicious/noExplicitAny: const storageService = new InMemoryStorageService() const eventEmitter = new EventEmitter(agentDependencies, new Subject()) @@ -107,27 +106,21 @@ const agentContext = getAgentContext({ [SignatureSuiteToken, 'default'], ], agentConfig, - wallet, }) const indyCredentialFormatService = new LegacyIndyCredentialFormatService() const indyProofFormatService = new LegacyIndyProofFormatService() +const kms = agentContext.resolve(Kms.KeyManagementApi) // We can split up these tests when we can use AnonCredsRS as a backend, but currently // we need to have the link secrets etc in the wallet which is not so easy to do with Indy describe('Legacy indy format services', () => { - beforeEach(async () => { - await wallet.createAndOpen(agentConfig.walletConfig) - }) - - afterEach(async () => { - await wallet.delete() - }) - test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { // This is just so we don't have to register an actual indy did (as we don't have the indy did registrar configured) - const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const unqualifiedIndyDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const key = await kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } }) + const unqualifiedIndyDid = indyDidFromPublicKeyBase58( + TypedArrayEncoder.toBase58(Kms.PublicJwk.fromPublicJwk(key.publicJwk).publicKey.publicKey) + ) const indyDid = `did:indy:pool1:${unqualifiedIndyDid}` // Create link secret diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts index 5794eff3eb..67a6da29c3 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts @@ -2,7 +2,7 @@ import type { AcceptCredentialOfferOptions, AcceptCredentialRequestOptions } fro import type { EventReplaySubject } from '../../../../../../core/tests' import type { AnonCredsTestsAgent } from '../../../../../tests/legacyAnonCredsSetup' -import { AutoAcceptCredential, CredentialExchangeRecord, CredentialState, MessageReceiver } from '@credo-ts/didcomm' +import { AutoAcceptCredential, CredentialExchangeRecord, CredentialState } from '@credo-ts/didcomm' import { testLogger, waitForCredentialRecordSubject } from '../../../../../../core/tests' import { setupAnonCredsTests } from '../../../../../tests/legacyAnonCredsSetup' @@ -39,9 +39,7 @@ describe('V1 Connectionless Credentials', () => { afterEach(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Faber starts with connection-less credential offer to Alice', async () => { @@ -159,14 +157,13 @@ describe('V1 Connectionless Credentials', () => { autoAcceptCredential: AutoAcceptCredential.ContentApproved, }) - const { message: offerMessage } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ + const { invitationUrl } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ message, domain: 'https://a-domain.com', }) // Receive Message - const messageReceiver = aliceAgent.context.dependencyManager.resolve(MessageReceiver) - await messageReceiver.receiveMessage(offerMessage.toJSON()) + await aliceAgent.modules.oob.receiveInvitationFromUrl(invitationUrl) // Wait for it to be processed let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials-auto-accept.e2e.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials-auto-accept.e2e.test.ts index 42c46e2c99..268a577e60 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials-auto-accept.e2e.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials-auto-accept.e2e.test.ts @@ -52,9 +52,7 @@ describe('V1 Credentials Auto Accept', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test("Alice starts with V1 credential proposal to Faber, both with autoAcceptCredential on 'always'", async () => { @@ -174,9 +172,7 @@ describe('V1 Credentials Auto Accept', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) // ============================== diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials.e2e.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials.e2e.test.ts index f730785b7a..ce178eae72 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials.e2e.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-credentials.e2e.test.ts @@ -35,9 +35,7 @@ describe('V1 Credentials', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice starts with V1 credential proposal to Faber', async () => { diff --git a/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts b/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts index f0b7eb7706..f851928434 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts @@ -32,6 +32,7 @@ import { import { composeProofAutoAccept, createRequestFromPreview } from '../../../utils' +import { AnonCredsHolderService, AnonCredsHolderServiceSymbol } from '../../../services' import { V1PresentationProblemReportError } from './errors' import { V1PresentationAckHandler, @@ -257,6 +258,8 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< const indyFormat = proofFormats?.indy + const anonCredsHolderService = agentContext.resolve(AnonCredsHolderServiceSymbol) + // Create a proof request from the preview, so we can let the messages // be handled using the indy proof format which supports RFC0592 const requestFromPreview = createRequestFromPreview({ @@ -264,7 +267,7 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< predicates: proposalMessage.presentationProposal.predicates, name: indyFormat?.name ?? 'Proof Request', version: indyFormat?.version ?? '1.0', - nonce: await agentContext.wallet.generateNonce(), + nonce: anonCredsHolderService.generateNonce(agentContext), }) const proposalAttachment = new Attachment({ @@ -973,10 +976,12 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< const requestAttachment = requestMessage?.getRequestAttachmentById(INDY_PROOF_REQUEST_ATTACHMENT_ID) if (!requestAttachment) return false + const anonCredsHolderService = agentContext.resolve(AnonCredsHolderServiceSymbol) + const rfc0592Proposal = JsonTransformer.toJSON( createRequestFromPreview({ name: 'Proof Request', - nonce: await agentContext.wallet.generateNonce(), + nonce: anonCredsHolderService.generateNonce(agentContext), version: '1.0', attributes: proposalMessage.presentationProposal.attributes, predicates: proposalMessage.presentationProposal.predicates, @@ -1018,9 +1023,11 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< const proposalMessage = await this.findProposalMessage(agentContext, proofRecord.id) if (!proposalMessage) return false + const anonCredsHolderService = agentContext.resolve(AnonCredsHolderServiceSymbol) + const rfc0592Proposal = createRequestFromPreview({ name: 'Proof Request', - nonce: await agentContext.wallet.generateNonce(), + nonce: anonCredsHolderService.generateNonce(agentContext), version: '1.0', attributes: proposalMessage.presentationProposal.attributes, predicates: proposalMessage.presentationProposal.predicates, @@ -1065,11 +1072,13 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< // We are in the ContentApproved case. We need to make sure we've sent a proposal, and it matches the request const proposalMessage = await this.findProposalMessage(agentContext, proofRecord.id) + const anonCredsHolderService = agentContext.resolve(AnonCredsHolderServiceSymbol) + const rfc0592Proposal = proposalMessage ? JsonTransformer.toJSON( createRequestFromPreview({ name: 'Proof Request', - nonce: await agentContext.wallet.generateNonce(), + nonce: await anonCredsHolderService.generateNonce(agentContext), version: '1.0', attributes: proposalMessage.presentationProposal.attributes, predicates: proposalMessage.presentationProposal.predicates, @@ -1126,6 +1135,8 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< agentContext: AgentContext, proofRecordId: string ): Promise> { + const anonCredsHolderService = agentContext.resolve(AnonCredsHolderServiceSymbol) + // TODO: we could looking at fetching all record using a single query and then filtering based on the type of the message. const [proposalMessage, requestMessage, presentationMessage] = await Promise.all([ this.findProposalMessage(agentContext, proofRecordId), @@ -1149,7 +1160,7 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< indyProposeProof = createRequestFromPreview({ name: 'Proof Request', version: '1.0', - nonce: await agentContext.wallet.generateNonce(), + nonce: anonCredsHolderService.generateNonce(agentContext), attributes: proposalMessage.presentationProposal.attributes, predicates: proposalMessage.presentationProposal.predicates, }) diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts index 8251fde4a2..d1a2fdc542 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts @@ -8,7 +8,7 @@ import { SubjectOutboundTransport } from '../../../../../../../tests/transport/S import { Agent } from '../../../../../../core/src' import { uuid } from '../../../../../../core/src/utils/uuid' import { - getInMemoryAgentOptions, + getAgentOptions, makeConnection, setupEventReplaySubjects, testLogger, @@ -24,7 +24,6 @@ import { MediationRecipientModule, MediatorModule, MediatorPickupStrategy, - MessageReceiver, ProofEventTypes, ProofState, } from '../../../../../../didcomm/src' @@ -42,7 +41,6 @@ describe('V1 Proofs - Connectionless - Indy', () => { afterEach(async () => { for (const agent of agents) { await agent.shutdown() - await agent.wallet.delete() } }) @@ -237,13 +235,13 @@ describe('V1 Proofs - Connectionless - Indy', () => { autoAcceptProof: AutoAcceptProof.ContentApproved, }) - const { message: requestMessage } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ + const { invitationUrl } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ recordId: faberProofExchangeRecord.id, message, domain: 'https://a-domain.com', }) - await aliceAgent.context.dependencyManager.resolve(MessageReceiver).receiveMessage(requestMessage.toJSON()) + await aliceAgent.modules.oob.receiveInvitationFromUrl(invitationUrl) await waitForProofExchangeRecordSubject(aliceReplay, { state: ProofState.Done, @@ -360,7 +358,7 @@ describe('V1 Proofs - Connectionless - Indy', () => { const unique = uuid().substring(0, 4) - const mediatorAgentOptions = getInMemoryAgentOptions( + const mediatorAgentOptions = getAgentOptions( `Connectionless proofs with mediator Mediator-${unique}`, { endpoints: ['rxjs:mediator'], @@ -370,7 +368,8 @@ describe('V1 Proofs - Connectionless - Indy', () => { mediator: new MediatorModule({ autoAcceptMediationRequests: true, }), - } + }, + { requireDidcomm: true } ) const mediatorMessages = new Subject() @@ -392,7 +391,7 @@ describe('V1 Proofs - Connectionless - Indy', () => { handshakeProtocols: [HandshakeProtocol.Connections], }) - const faberAgentOptions = getInMemoryAgentOptions( + const faberAgentOptions = getAgentOptions( `Connectionless proofs with mediator Faber-${unique}`, {}, {}, @@ -406,10 +405,11 @@ describe('V1 Proofs - Connectionless - Indy', () => { }), mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - } + }, + { requireDidcomm: true } ) - const aliceAgentOptions = getInMemoryAgentOptions( + const aliceAgentOptions = getAgentOptions( `Connectionless proofs with mediator Alice-${unique}`, {}, {}, @@ -423,20 +423,17 @@ describe('V1 Proofs - Connectionless - Indy', () => { }), mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - } + }, + { requireDidcomm: true } ) const faberAgent = new Agent(faberAgentOptions) faberAgent.modules.didcomm.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) - // FIXME: This should be done automatically when agent initializes await faberAgent.initialize() - await faberAgent.modules.mediationRecipient.initialize() const aliceAgent = new Agent(aliceAgentOptions) aliceAgent.modules.didcomm.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() - // FIXME: This should be done automatically when agent initializes - await aliceAgent.modules.mediationRecipient.initialize() const [faberReplay, aliceReplay] = setupEventReplaySubjects( [faberAgent, aliceAgent], @@ -514,11 +511,12 @@ describe('V1 Proofs - Connectionless - Indy', () => { autoAcceptProof: AutoAcceptProof.ContentApproved, }) - const { message: requestMessage } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ - recordId: faberProofExchangeRecord.id, - message, - domain: 'https://a-domain.com', - }) + const { message: requestMessage, invitationUrl } = + await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'https://a-domain.com', + }) const mediationRecord = await faberAgent.modules.mediationRecipient.findDefaultMediator() if (!mediationRecord) { @@ -533,7 +531,7 @@ describe('V1 Proofs - Connectionless - Indy', () => { }, }) - await aliceAgent.context.dependencyManager.resolve(MessageReceiver).receiveMessage(requestMessage.toJSON()) + await aliceAgent.modules.oob.receiveInvitationFromUrl(invitationUrl) await waitForProofExchangeRecordSubject(aliceReplay, { state: ProofState.Done, diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-negotiation.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-negotiation.e2e.test.ts index 36b0bfebb1..eeb67c6726 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-negotiation.e2e.test.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-negotiation.e2e.test.ts @@ -29,9 +29,7 @@ describe('Present Proof', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Proof negotiation between Alice and Faber', async () => { diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-presentation.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-presentation.e2e.test.ts index 9be4898025..e82f66548d 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-presentation.e2e.test.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-presentation.e2e.test.ts @@ -55,9 +55,7 @@ describe('Present Proof', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice Creates and sends Proof Proposal to Faber', async () => { diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-proposal.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-proposal.e2e.test.ts index 4e37aadf6e..0317e738cb 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-proposal.e2e.test.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-proposal.e2e.test.ts @@ -27,9 +27,7 @@ describe('Present Proof', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice Creates and sends Proof Proposal to Faber', async () => { diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-request.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-request.e2e.test.ts index ffdc34b349..ff7c2c143b 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-request.e2e.test.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proof-request.e2e.test.ts @@ -27,9 +27,7 @@ describe('Present Proof | V1ProofProtocol', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice Creates and sends Proof Proposal to Faber and Faber accepts the proposal', async () => { diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proofs.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proofs.e2e.test.ts index ca828dc509..22e5444ee3 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proofs.e2e.test.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-indy-proofs.e2e.test.ts @@ -50,9 +50,7 @@ describe('Present Proof', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice starts with proof proposal to Faber', async () => { diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-proofs-auto-accept.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-proofs-auto-accept.e2e.test.ts index dcb4025b31..0a3cd93878 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-proofs-auto-accept.e2e.test.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-proofs-auto-accept.e2e.test.ts @@ -48,9 +48,7 @@ describe('Auto accept present proof', () => { }) afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test("Alice starts with proof proposal to Faber, both with autoAcceptProof on 'always'", async () => { @@ -170,9 +168,7 @@ describe('Auto accept present proof', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test("Alice starts with proof proposal to Faber, both with autoAcceptProof on 'contentApproved'", async () => { diff --git a/packages/anoncreds/src/services/AnonCredsHolderService.ts b/packages/anoncreds/src/services/AnonCredsHolderService.ts index 05ea6d75b2..810290375e 100644 --- a/packages/anoncreds/src/services/AnonCredsHolderService.ts +++ b/packages/anoncreds/src/services/AnonCredsHolderService.ts @@ -57,4 +57,9 @@ export interface AnonCredsHolderService { agentContext: AgentContext, options: LegacyToW3cCredentialOptions ): Promise + + /** + * Generate an AnonCreds compatible nonce + */ + generateNonce(agentContext: AgentContext): string } diff --git a/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/linkSecret.test.ts b/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/linkSecret.test.ts index 200e81914b..5eb980f900 100644 --- a/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/linkSecret.test.ts +++ b/packages/anoncreds/src/updates/0.3.1-0.4/__tests__/linkSecret.test.ts @@ -16,11 +16,6 @@ jest.mock('../../../../../core/src/agent/Agent', () => { Agent: jest.fn(() => ({ config: agentConfig, context: agentContext, - wallet: { - walletConfig: { - id: 'wallet-id', - }, - }, dependencyManager: { resolve: jest.fn(() => linkSecretRepository), }, @@ -49,15 +44,6 @@ describe('0.3.1-0.4.0 | AnonCreds Migration | Link Secret', () => { await testModule.migrateLinkSecretToV0_4(agent) expect(linkSecretRepository.findDefault).toHaveBeenCalledTimes(1) - expect(linkSecretRepository.save).toHaveBeenCalledTimes(1) - - const [, linkSecretRecord] = mockFunction(linkSecretRepository.save).mock.calls[0] - expect(linkSecretRecord.toJSON()).toMatchObject({ - linkSecretId: 'wallet-id', - }) - expect(linkSecretRecord.getTags()).toMatchObject({ - isDefault: true, - }) }) test('does not create default link secret record if default link secret record already exists', async () => { diff --git a/packages/anoncreds/src/updates/0.3.1-0.4/linkSecret.ts b/packages/anoncreds/src/updates/0.3.1-0.4/linkSecret.ts index 828d68edb9..498d401a91 100644 --- a/packages/anoncreds/src/updates/0.3.1-0.4/linkSecret.ts +++ b/packages/anoncreds/src/updates/0.3.1-0.4/linkSecret.ts @@ -1,6 +1,6 @@ import type { BaseAgent } from '@credo-ts/core' -import { AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository } from '../../repository' +import { AnonCredsLinkSecretRepository } from '../../repository' /** * Creates an {@link AnonCredsLinkSecretRecord} based on the wallet id. If an {@link AnonCredsLinkSecretRecord} @@ -15,24 +15,16 @@ export async function migrateLinkSecretToV0_4(agent: Ag const defaultLinkSecret = await linkSecretRepository.findDefault(agent.context) if (!defaultLinkSecret) { - // If no default link secret record exists, we create one based on the wallet id and set is as default - agent.config.logger.debug('No default link secret record found. Creating one based on wallet id.') - - if (!agent.wallet.walletConfig?.id) { - agent.config.logger.error('Wallet id not found. Cannot create default link secret record. Skipping...') - return - } - - // We can't store the link secret value. This is not exposed by indy-sdk. - const linkSecret = new AnonCredsLinkSecretRecord({ - linkSecretId: agent.wallet.walletConfig?.id, - }) - linkSecret.setTag('isDefault', true) + // NOTE: this migration is not relevant here, but kept for documentation purposes. + // This migration was relevant if you were upgrading from 0.3 to 0.4 and kept using + // the indy-sdk over askar. However since 0.5 there is no indy-sdk anymore, and thus + // you MUST use Askar now, and the Askar migration already handles the link secret migration + // and also actually sets the value. So there is no flow in which we would need this code. + // If it would, the previous code would not store the value (only the link secret id) as we couldn't + // access the value with Indy. So it wouldn't have been usable anyway - agent.config.logger.debug( - `Saving default link secret record with record id ${linkSecret.id} and link secret id ${linkSecret.linkSecretId} to storage` - ) - await linkSecretRepository.save(agent.context, linkSecret) + // If no default link secret record exists, we create one based on the wallet id and set is as default + agent.config.logger.error('No default link secret record found. This should not happen') } else { agent.config.logger.debug( `Default link secret record with record id ${defaultLinkSecret.id} and link secret id ${defaultLinkSecret.linkSecretId} found. Skipping...` diff --git a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts index 661e1ff529..d09acbdfcf 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/__tests__/w3cCredentialRecordMigration.test.ts @@ -1,4 +1,4 @@ -import type { DidRepository, Wallet } from '@credo-ts/core' +import type { DidRepository } from '@credo-ts/core' import { Agent, @@ -34,8 +34,6 @@ const anonCredsModuleConfig = new AnonCredsModuleConfig({ registries: [registry], }) -const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet - const stop = new Subject() const eventEmitter = new EventEmitter(agentDependencies, stop) @@ -81,7 +79,6 @@ const agentContext = getAgentContext({ [SignatureSuiteToken, 'default'], ], agentConfig, - wallet, }) const anonCredsRepo = { diff --git a/packages/anoncreds/src/updates/__tests__/0.3.test.ts b/packages/anoncreds/src/updates/__tests__/0.3.test.ts index 8edc36b086..8d34736fd9 100644 --- a/packages/anoncreds/src/updates/__tests__/0.3.test.ts +++ b/packages/anoncreds/src/updates/__tests__/0.3.test.ts @@ -3,8 +3,7 @@ import path from 'path' import { Agent, DependencyManager, InjectionSymbols, UpdateAssistant, utils } from '@credo-ts/core' import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' -import { RegisteredAskarTestWallet } from '../../../../askar/tests/helpers' -import { agentDependencies, getAskarWalletConfig } from '../../../../core/tests' +import { agentDependencies } from '../../../../core/tests' import { InMemoryAnonCredsRegistry } from '../../../tests/InMemoryAnonCredsRegistry' import { anoncreds } from '../../../tests/helpers' import { AnonCredsModule } from '../../AnonCredsModule' @@ -32,8 +31,6 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) dependencyManager.registerInstance(AnonCredsIssuerServiceSymbol, {}) dependencyManager.registerInstance(AnonCredsHolderServiceSymbol, {}) dependencyManager.registerInstance(AnonCredsVerifierServiceSymbol, {}) @@ -42,7 +39,6 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { { config: { label: 'Test Agent', - walletConfig: getAskarWalletConfig('0.3 Update AnonCreds - Holder', { inMemory: false, random: 'static' }), }, dependencies: agentDependencies, modules: { @@ -90,7 +86,6 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) @@ -108,8 +103,6 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) dependencyManager.registerInstance(AnonCredsIssuerServiceSymbol, {}) dependencyManager.registerInstance(AnonCredsHolderServiceSymbol, {}) dependencyManager.registerInstance(AnonCredsVerifierServiceSymbol, {}) @@ -118,7 +111,6 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { { config: { label: 'Test Agent', - walletConfig: getAskarWalletConfig('0.3 Update AnonCreds - Issuer', { inMemory: false, random: 'static' }), }, dependencies: agentDependencies, modules: { @@ -232,7 +224,6 @@ describe('UpdateAssistant | AnonCreds | v0.3.1 - v0.4', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) diff --git a/packages/anoncreds/src/updates/__tests__/0.4.test.ts b/packages/anoncreds/src/updates/__tests__/0.4.test.ts index 95cb23771e..796f7720cb 100644 --- a/packages/anoncreds/src/updates/__tests__/0.4.test.ts +++ b/packages/anoncreds/src/updates/__tests__/0.4.test.ts @@ -11,8 +11,7 @@ import { } from '@credo-ts/core' import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' -import { RegisteredAskarTestWallet } from '../../../../askar/tests/helpers' -import { agentDependencies, getAskarWalletConfig } from '../../../../core/tests' +import { agentDependencies } from '../../../../core/tests' import { InMemoryAnonCredsRegistry } from '../../../tests/InMemoryAnonCredsRegistry' import { anoncreds } from '../../../tests/helpers' import { AnonCredsModule } from '../../AnonCredsModule' @@ -44,8 +43,6 @@ describe('UpdateAssistant | AnonCreds | v0.4 - v0.5', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) dependencyManager.registerInstance(AnonCredsIssuerServiceSymbol, {}) dependencyManager.registerInstance(AnonCredsHolderServiceSymbol, {}) dependencyManager.registerInstance(AnonCredsVerifierServiceSymbol, {}) @@ -54,7 +51,6 @@ describe('UpdateAssistant | AnonCreds | v0.4 - v0.5', () => { { config: { label: 'Test Agent', - walletConfig: getAskarWalletConfig('0.4 Update AnonCreds - Holder', { inMemory: false, random: 'static' }), }, dependencies: agentDependencies, modules: { @@ -117,6 +113,5 @@ describe('UpdateAssistant | AnonCreds | v0.4 - v0.5', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() }) }) diff --git a/packages/anoncreds/src/updates/__tests__/__snapshots__/0.3.test.ts.snap b/packages/anoncreds/src/updates/__tests__/__snapshots__/0.3.test.ts.snap index 0bb9565761..d176bfe6df 100644 --- a/packages/anoncreds/src/updates/__tests__/__snapshots__/0.3.test.ts.snap +++ b/packages/anoncreds/src/updates/__tests__/__snapshots__/0.3.test.ts.snap @@ -2,24 +2,6 @@ exports[`UpdateAssistant | AnonCreds | v0.3.1 - v0.4 should correctly update the credential exchange records for holders 1`] = ` { - "1-4e4f-41d9-94c4-f49351b811f1": { - "id": "1-4e4f-41d9-94c4-f49351b811f1", - "tags": { - "isDefault": true, - "linkSecretId": "Wallet: 0.3 Update AnonCreds - Holder - static", - }, - "type": "AnonCredsLinkSecretRecord", - "value": { - "_tags": { - "isDefault": true, - }, - "id": "1-4e4f-41d9-94c4-f49351b811f1", - "linkSecretId": "Wallet: 0.3 Update AnonCreds - Holder - static", - "metadata": {}, - "updatedAt": "2023-03-19T22:50:20.522Z", - "value": undefined, - }, - }, "2c250bf3-da8b-46ac-999d-509e4e6daafa": { "id": "2c250bf3-da8b-46ac-999d-509e4e6daafa", "tags": { @@ -343,24 +325,6 @@ exports[`UpdateAssistant | AnonCreds | v0.3.1 - v0.4 should correctly update the exports[`UpdateAssistant | AnonCreds | v0.3.1 - v0.4 should correctly update the schema and credential definition, and create link secret records for issuers 1`] = ` { - "1-4e4f-41d9-94c4-f49351b811f1": { - "id": "1-4e4f-41d9-94c4-f49351b811f1", - "tags": { - "isDefault": true, - "linkSecretId": "Wallet: 0.3 Update AnonCreds - Issuer - static", - }, - "type": "AnonCredsLinkSecretRecord", - "value": { - "_tags": { - "isDefault": true, - }, - "id": "1-4e4f-41d9-94c4-f49351b811f1", - "linkSecretId": "Wallet: 0.3 Update AnonCreds - Issuer - static", - "metadata": {}, - "updatedAt": "2023-03-19T22:50:20.522Z", - "value": undefined, - }, - }, "1545e17d-fc88-4020-a1f7-e6dbcf1e5266": { "id": "1545e17d-fc88-4020-a1f7-e6dbcf1e5266", "tags": { diff --git a/packages/anoncreds/tests/anoncreds-flow.test.ts b/packages/anoncreds/tests/anoncreds-flow.test.ts index dcb70eb1cf..3e51c5fd14 100644 --- a/packages/anoncreds/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds/tests/anoncreds-flow.test.ts @@ -1,5 +1,5 @@ import type { AnonCredsCredentialRequest } from '@credo-ts/anoncreds' -import type { DidRepository, Wallet } from '@credo-ts/core' +import type { DidRepository } from '@credo-ts/core' import { DidResolverService, @@ -66,8 +66,6 @@ const anonCredsVerifierService = new AnonCredsRsVerifierService() const anonCredsHolderService = new AnonCredsRsHolderService() const anonCredsIssuerService = new AnonCredsRsIssuerService() -const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet - const inMemoryStorageService = new InMemoryStorageService() const agentContext = getAgentContext({ @@ -87,7 +85,6 @@ const agentContext = getAgentContext({ [SignatureSuiteToken, 'default'], ], agentConfig, - wallet, }) const anoncredsCredentialFormatService = new AnonCredsCredentialFormatService() diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index 81f5305d3e..682fac29b2 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -1,6 +1,6 @@ -import { Agent, KeyType, TypedArrayEncoder } from '@credo-ts/core' +import { Agent } from '@credo-ts/core' -import { getInMemoryAgentOptions } from '../../core/tests' +import { getAgentOptions } from '../../core/tests' import { AnonCredsModule } from '../src' import { InMemoryAnonCredsRegistry } from './InMemoryAnonCredsRegistry' @@ -71,7 +71,7 @@ const existingRevocationStatusLists = { } const agent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'credo-anoncreds-package', {}, {}, @@ -88,6 +88,9 @@ const agent = new Agent( }), ], }), + }, + { + requireDidcomm: true, } ) ) @@ -98,7 +101,6 @@ describe('AnonCreds API', () => { }) afterEach(async () => { - await agent.wallet.delete() await agent.shutdown() }) @@ -181,9 +183,13 @@ describe('AnonCreds API', () => { test('register a credential definition', async () => { // Create key - await agent.wallet.createKey({ - privateKey: TypedArrayEncoder.fromString('00000000000000000000000000000My1'), - keyType: KeyType.Ed25519, + await agent.kms.importKey({ + privateJwk: { + kty: 'OKP', + crv: 'Ed25519', + x: '6cZ2bZKmKiUiF9MLKCV8IIYIEsOLHsJG5qBJ9SrQYBk', + d: 'MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBNeTE', + }, }) const issuerId = 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX' diff --git a/packages/anoncreds/tests/anoncredsSetup.ts b/packages/anoncreds/tests/anoncredsSetup.ts index 742da726c7..4d5692ec9e 100644 --- a/packages/anoncreds/tests/anoncredsSetup.ts +++ b/packages/anoncreds/tests/anoncredsSetup.ts @@ -45,7 +45,7 @@ import { getCheqdModuleConfig } from '../../cheqd/tests/setupCheqdModule' import { sleep } from '../../core/src/utils/sleep' import { setupEventReplaySubjects, setupSubjectTransports } from '../../core/tests' import { - getInMemoryAgentOptions, + getAgentOptions, makeConnection, waitForCredentialRecordSubject, waitForProofExchangeRecordSubject, @@ -55,6 +55,7 @@ import { AnonCredsCredentialFormatService, AnonCredsModule, AnonCredsProofFormat import { DataIntegrityCredentialFormatService } from '../src/formats/DataIntegrityCredentialFormatService' import { InMemoryAnonCredsRegistry } from '../tests/InMemoryAnonCredsRegistry' +import { transformPrivateKeyToPrivateJwk } from '../../askar/src/utils' import { InMemoryTailsFileService } from './InMemoryTailsFileService' import { LocalDidResolver } from './LocalDidResolver' import { anoncreds } from './helpers' @@ -332,7 +333,7 @@ export async function setupAnonCredsTests< registries?: [AnonCredsRegistry, ...AnonCredsRegistry[]] }): Promise> { const issuerAgent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( issuerName, { endpoints: ['rxjs:issuer'], @@ -343,12 +344,13 @@ export async function setupAnonCredsTests< autoAcceptProofs, registries, cheqd, - }) + }), + { requireDidcomm: true } ) ) const holderAgent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( holderName, { endpoints: ['rxjs:holder'], @@ -359,13 +361,14 @@ export async function setupAnonCredsTests< autoAcceptProofs, registries, cheqd, - }) + }), + { requireDidcomm: true } ) ) const verifierAgent = verifierName ? new Agent( - getInMemoryAgentOptions( + getAgentOptions( verifierName, { endpoints: ['rxjs:verifier'], @@ -402,18 +405,23 @@ export async function setupAnonCredsTests< await issuerAgent.dids.import({ did: issuerId, didDocument }) } else if (cheqd) { const privateKey = TypedArrayEncoder.fromString('000000000000000000000000001cheqd') + const { privateJwk } = transformPrivateKeyToPrivateJwk({ + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + privateKey, + }) + const didDocumentKey = await issuerAgent.kms.importKey({ + privateJwk, + }) + const did = await issuerAgent.dids.create({ method: 'cheqd', - secret: { - verificationMethod: { - id: 'key-10', - type: 'Ed25519VerificationKey2020', - privateKey, - }, - }, options: { network: 'testnet', methodSpecificIdAlgo: 'uuid', + keyId: didDocumentKey.keyId, }, }) issuerId = did.didState.did as string diff --git a/packages/anoncreds/tests/data-integrity-flow-anoncreds-pex.test.ts b/packages/anoncreds/tests/data-integrity-flow-anoncreds-pex.test.ts index e679ca0866..0d394f545b 100644 --- a/packages/anoncreds/tests/data-integrity-flow-anoncreds-pex.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-anoncreds-pex.test.ts @@ -31,9 +31,7 @@ describe('anoncreds w3c data integrity tests', () => { afterEach(async () => { await issuerAgent.shutdown() - await issuerAgent.wallet.delete() await holderAgent.shutdown() - await holderAgent.wallet.delete() }) test('issuance and verification flow starting from offer with revocation', async () => { diff --git a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts index 88ae92f3de..86997b6b1d 100644 --- a/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-anoncreds.test.ts @@ -26,7 +26,6 @@ import { import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' -import { InMemoryWallet } from '../../../tests/InMemoryWallet' import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' import { dateToTimestamp } from '../../anoncreds/src/utils/timestamp' @@ -81,8 +80,6 @@ const didsModuleConfig = new DidsModuleConfig({ }) const fileSystem = new agentDependencies.FileSystem() -const wallet = new InMemoryWallet() - const agentContext = getAgentContext({ registerInstances: [ [InjectionSymbols.Stop$, new Subject()], @@ -101,7 +98,6 @@ const agentContext = getAgentContext({ [SignatureSuiteToken, 'default'], ], agentConfig, - wallet, }) agentContext.dependencyManager.registerInstance(AgentContext, agentContext) @@ -112,10 +108,6 @@ const anoncredsProofFormatService = new AnonCredsProofFormatService() const indyDid = 'did:indy:local:LjgpST2rjsoxYegQDRm7EL' describe('data integrity format service (anoncreds)', () => { - beforeAll(async () => { - await wallet.createAndOpen(agentConfig.walletConfig) - }) - afterEach(async () => { inMemoryStorageService.contextCorrelationIdToRecords = {} }) diff --git a/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts b/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts index ed8976c9c4..cc07c96308 100644 --- a/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow-w3c.test.ts @@ -1,4 +1,4 @@ -import type { DidRepository } from '@credo-ts/core' +import type { DidRepository, SuiteInfo } from '@credo-ts/core' import type { CreateDidKidVerificationMethodReturn } from '../../core/tests' import { @@ -9,7 +9,7 @@ import { InjectionSymbols, KeyDidRegistrar, KeyDidResolver, - KeyType, + Kms, SignatureSuiteToken, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, @@ -27,7 +27,6 @@ import { import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' -import { InMemoryWallet } from '../../../tests/InMemoryWallet' import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' @@ -36,6 +35,7 @@ import { createDidKidVerificationMethod, getAgentConfig, getAgentContext, + getAskarStoreConfig, testLogger, } from '../../core/tests' import { @@ -46,6 +46,9 @@ import { } from '../src' import { AnonCredsRsHolderService, AnonCredsRsIssuerService, AnonCredsRsVerifierService } from '../src/anoncreds-rs' +import { askar } from '@openwallet-foundation/askar-nodejs' +import { AskarModuleConfig } from '../../askar/src/AskarModuleConfig' +import { AksarKeyManagementService } from '../../askar/src/kms/AskarKeyManagementService' import { InMemoryTailsFileService } from './InMemoryTailsFileService' import { anoncreds } from './helpers' @@ -70,8 +73,6 @@ const didsModuleConfig = new DidsModuleConfig({ }) const fileSystem = new agentDependencies.FileSystem() -const wallet = new InMemoryWallet() - const agentContext = getAgentContext({ registerInstances: [ [InjectionSymbols.Stop$, new Subject()], @@ -96,12 +97,19 @@ const agentContext = getAgentContext({ VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, ], - keyTypes: [KeyType.Ed25519], - }, + supportedPublicJwkType: [Kms.Ed25519PublicJwk], + } satisfies SuiteInfo, + ], + [ + AskarModuleConfig, + new AskarModuleConfig({ + askar, + store: getAskarStoreConfig('data-integrity-flow-w3c'), + }), ], ], agentConfig, - wallet, + kmsBackends: [new AksarKeyManagementService()], }) agentContext.dependencyManager.registerInstance(AgentContext, agentContext) @@ -113,8 +121,6 @@ describe('data integrity format service (w3c)', () => { let holderKdv: CreateDidKidVerificationMethodReturn beforeAll(async () => { - await wallet.createAndOpen(agentConfig.walletConfig) - issuerKdv = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598g') holderKdv = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598f') }) diff --git a/packages/anoncreds/tests/data-integrity-flow.test.ts b/packages/anoncreds/tests/data-integrity-flow.test.ts index 35adbba09c..117a4927b1 100644 --- a/packages/anoncreds/tests/data-integrity-flow.test.ts +++ b/packages/anoncreds/tests/data-integrity-flow.test.ts @@ -1,4 +1,4 @@ -import type { DidRepository } from '@credo-ts/core' +import type { DidRepository, SuiteInfo } from '@credo-ts/core' import type { CreateDidKidVerificationMethodReturn } from '../../core/tests' import { @@ -9,7 +9,7 @@ import { InjectionSymbols, KeyDidRegistrar, KeyDidResolver, - KeyType, + Kms, SignatureSuiteToken, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, @@ -27,7 +27,6 @@ import { import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../tests/InMemoryStorageService' -import { InMemoryWallet } from '../../../tests/InMemoryWallet' import { DataIntegrityCredentialFormatService } from '../../anoncreds/src/formats/DataIntegrityCredentialFormatService' import { AnonCredsRegistryService } from '../../anoncreds/src/services/registry/AnonCredsRegistryService' import { InMemoryAnonCredsRegistry } from '../../anoncreds/tests/InMemoryAnonCredsRegistry' @@ -70,8 +69,6 @@ const didsModuleConfig = new DidsModuleConfig({ }) const fileSystem = new agentDependencies.FileSystem() -const wallet = new InMemoryWallet() - const agentContext = getAgentContext({ registerInstances: [ [InjectionSymbols.Stop$, new Subject()], @@ -96,12 +93,11 @@ const agentContext = getAgentContext({ VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, ], - keyTypes: [KeyType.Ed25519], - }, + supportedPublicJwkType: [Kms.Ed25519PublicJwk], + } satisfies SuiteInfo, ], ], agentConfig, - wallet, }) agentContext.dependencyManager.registerInstance(AgentContext, agentContext) @@ -115,8 +111,6 @@ describe('data integrity format service (w3c)', () => { let holderKdv: CreateDidKidVerificationMethodReturn beforeAll(async () => { - await wallet.createAndOpen(agentConfig.walletConfig) - issuerKdv = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598g') holderKdv = await createDidKidVerificationMethod(agentContext, '96213c3d7fc8d4d6754c7a0fd969598f') }) diff --git a/packages/anoncreds/tests/indy-flow.test.ts b/packages/anoncreds/tests/indy-flow.test.ts index 1354b10d4a..876d0ad769 100644 --- a/packages/anoncreds/tests/indy-flow.test.ts +++ b/packages/anoncreds/tests/indy-flow.test.ts @@ -1,5 +1,5 @@ import type { AnonCredsCredentialRequest } from '@credo-ts/anoncreds' -import type { DidRepository, Wallet } from '@credo-ts/core' +import type { DidRepository } from '@credo-ts/core' import { DidResolverService, @@ -61,8 +61,6 @@ const anonCredsVerifierService = new AnonCredsRsVerifierService() const anonCredsHolderService = new AnonCredsRsHolderService() const anonCredsIssuerService = new AnonCredsRsIssuerService() -const wallet = { generateNonce: () => Promise.resolve('947121108704767252195123') } as Wallet - const inMemoryStorageService = new InMemoryStorageService() const agentContext = getAgentContext({ registerInstances: [ @@ -80,7 +78,6 @@ const agentContext = getAgentContext({ [SignatureSuiteToken, 'default'], ], agentConfig, - wallet, }) const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService() diff --git a/packages/anoncreds/tests/legacyAnonCredsSetup.ts b/packages/anoncreds/tests/legacyAnonCredsSetup.ts index 79c9eca636..f7c3421437 100644 --- a/packages/anoncreds/tests/legacyAnonCredsSetup.ts +++ b/packages/anoncreds/tests/legacyAnonCredsSetup.ts @@ -30,7 +30,7 @@ import { import { sleep } from '../../core/src/utils/sleep' import { setupEventReplaySubjects, setupSubjectTransports } from '../../core/tests' import { - getInMemoryAgentOptions, + getAgentOptions, importExistingIndyDidFromPrivateKey, makeConnection, publicDidSeed, @@ -315,7 +315,7 @@ export async function setupAnonCredsTests< createConnections?: CreateConnections }): Promise> { const issuerAgent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( issuerName, { endpoints: ['rxjs:issuer'], @@ -326,12 +326,13 @@ export async function setupAnonCredsTests< getAnonCredsIndyModules({ autoAcceptCredentials, autoAcceptProofs, - }) + }), + { requireDidcomm: true } ) ) const holderAgent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( holderName, { endpoints: ['rxjs:holder'], @@ -340,13 +341,14 @@ export async function setupAnonCredsTests< getAnonCredsIndyModules({ autoAcceptCredentials, autoAcceptProofs, - }) + }), + { requireDidcomm: true } ) ) const verifierAgent = verifierName ? new Agent( - getInMemoryAgentOptions( + getAgentOptions( verifierName, { endpoints: ['rxjs:verifier'], @@ -355,7 +357,8 @@ export async function setupAnonCredsTests< getAnonCredsIndyModules({ autoAcceptCredentials, autoAcceptProofs, - }) + }), + { requireDidcomm: true } ) ) : undefined diff --git a/packages/anoncreds/tests/v2-credential-revocation.test.ts b/packages/anoncreds/tests/v2-credential-revocation.test.ts index a1e7d119b4..e4b4083abb 100644 --- a/packages/anoncreds/tests/v2-credential-revocation.test.ts +++ b/packages/anoncreds/tests/v2-credential-revocation.test.ts @@ -60,9 +60,7 @@ describe('IC v2 credential revocation', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice starts with V2 credential proposal to Faber', async () => { diff --git a/packages/anoncreds/tests/v2-credentials.test.ts b/packages/anoncreds/tests/v2-credentials.test.ts index 838cefacf9..97fd210ec1 100644 --- a/packages/anoncreds/tests/v2-credentials.test.ts +++ b/packages/anoncreds/tests/v2-credentials.test.ts @@ -82,9 +82,7 @@ describe('IC V2 AnonCreds credentials', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice starts with V2 credential proposal to Faber', async () => { diff --git a/packages/anoncreds/tests/v2-proofs.test.ts b/packages/anoncreds/tests/v2-proofs.test.ts index 6f698f45fd..fb5a8cf709 100644 --- a/packages/anoncreds/tests/v2-proofs.test.ts +++ b/packages/anoncreds/tests/v2-proofs.test.ts @@ -102,9 +102,7 @@ describe('PP V2 AnonCreds Proofs', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice starts with proof proposal to Faber', async () => { diff --git a/packages/askar/package.json b/packages/askar/package.json index 310c1a8d6b..5615d82318 100644 --- a/packages/askar/package.json +++ b/packages/askar/package.json @@ -31,7 +31,7 @@ "tsyringe": "^4.8.0" }, "devDependencies": { - "@animo-id/expo-secure-environment": "^0.1.0", + "@credo-ts/tenants": "workspace:*", "@openwallet-foundation/askar-nodejs": "^0.3.1", "@openwallet-foundation/askar-shared": "^0.3.1", "@types/ref-array-di": "^1.2.6", @@ -41,12 +41,6 @@ "typescript": "~5.5.2" }, "peerDependencies": { - "@openwallet-foundation/askar-shared": "^0.3.1", - "@animo-id/expo-secure-environment": "^0.1.0" - }, - "peerDependenciesMeta": { - "@animo-id/expo-secure-environment": { - "optional": true - } + "@openwallet-foundation/askar-shared": "^0.3.1" } } diff --git a/packages/askar/src/AskarApi.ts b/packages/askar/src/AskarApi.ts new file mode 100644 index 0000000000..77ce6a7462 --- /dev/null +++ b/packages/askar/src/AskarApi.ts @@ -0,0 +1,92 @@ +import { AgentContext } from '@credo-ts/core' +import { injectable } from 'tsyringe' + +import { AskarStoreExportOptions, AskarStoreImportOptions, AskarStoreRotateKeyOptions } from './AskarApiOptions' +import { AskarModuleConfig } from './AskarModuleConfig' +import { AskarStoreManager } from './AskarStoreManager' + +@injectable() +export class AskarApi { + public constructor( + private agentContext: AgentContext, + private askarStoreManager: AskarStoreManager, + public readonly config: AskarModuleConfig + ) {} + + public get isStoreOpen() { + return this.askarStoreManager.isStoreOpen(this.agentContext) + } + + /** + * @throws {AskarStoreDuplicateError} if the wallet already exists + * @throws {AskarStoreError} if another error occurs + */ + public async provisionStore(): Promise { + await this.askarStoreManager.provisionStore(this.agentContext) + } + + /** + * @throws {AskarStoreNotFoundError} if the wallet does not exist + * @throws {AskarStoreError} if another error occurs + */ + public async openStore(): Promise { + await this.askarStoreManager.openStore(this.agentContext) + } + + /** + * Rotate the key of the current askar store. + * + * NOTE: multiple agent contexts (tenants) can use the same store. This method rotates the key for the whole store, + * it is advised to only run this method on the root tenant agent when using profile per wallet database strategy. + * After running this method you should change the store configuration in the Askar module. + * + * @throws {AskarStoreNotFoundError} if the wallet does not exist + * @throws {AskarStoreError} if another error occurs + */ + public async rotateStoreKey(options: AskarStoreRotateKeyOptions): Promise { + await this.askarStoreManager.rotateStoreKey(this.agentContext, options) + } + + /** + * Exports the current askar store. + * + * NOTE: a store can contain profiles for multiple tenants. When you export a store + * all profiles will be exported with it. + * + * NOTE: store must be open before store can be expored + */ + public async exportStore(options: AskarStoreExportOptions) { + await this.askarStoreManager.exportStore(this.agentContext, options) + } + + /** + * Imports from an external store config into the current askar store config. + * + * NOTE: store must be closed first (using `closeStore`) before store can be imported + */ + public async importStore(options: AskarStoreImportOptions) { + await this.askarStoreManager.importStore(this.agentContext, options) + } + + /** + * Delete the current askar store. + * + * NOTE: multiple agent contexts (tenants) can use the same store. This method deletes the whole store. + * + * + * @throws {AskarStoreNotFoundError} if the wallet does not exist + * @throws {AskarStoreError} if another error occurs + */ + public async deleteStore(): Promise { + await this.askarStoreManager.deleteStore(this.agentContext) + } + + /** + * Close the current askar store. + * + * This will close all sessions (also for tenants) in this store. + */ + public async closeStore() { + await this.askarStoreManager.closeStore(this.agentContext) + } +} diff --git a/packages/askar/src/AskarApiOptions.ts b/packages/askar/src/AskarApiOptions.ts new file mode 100644 index 0000000000..8861c6a5ab --- /dev/null +++ b/packages/askar/src/AskarApiOptions.ts @@ -0,0 +1,28 @@ +import type { AskarModuleConfigStoreOptions } from './AskarModuleConfig' + +export interface AskarStoreExportOptions { + /** + * The store config to export the current store to. + */ + exportToStore: AskarModuleConfigStoreOptions +} + +export interface AskarStoreImportOptions { + /** + * The store config to import the current store from. + */ + importFromStore: AskarModuleConfigStoreOptions +} + +export interface AskarStoreRotateKeyOptions { + /** + * The new key to use for the store. + */ + newKey: string + + /** + * The new key derivation method to use for the store. If not provided the + * key derivation method from the current store config will be used. + */ + newKeyDerivationMethod?: AskarModuleConfigStoreOptions['keyDerivationMethod'] +} diff --git a/packages/askar/src/AskarModule.ts b/packages/askar/src/AskarModule.ts index b88a7d2f59..53150fee95 100644 --- a/packages/askar/src/AskarModule.ts +++ b/packages/askar/src/AskarModule.ts @@ -1,13 +1,14 @@ import type { AgentContext, DependencyManager, Module } from '@credo-ts/core' import type { AskarModuleConfigOptions } from './AskarModuleConfig' -import { CredoError, InjectionSymbols } from '@credo-ts/core' -import { Store } from '@openwallet-foundation/askar-shared' +import { AgentConfig, CredoError, InjectionSymbols, Kms } from '@credo-ts/core' +import { AskarApi } from './AskarApi' import { AskarModuleConfig, AskarMultiWalletDatabaseScheme } from './AskarModuleConfig' +import { AskarStoreManager } from './AskarStoreManager' +import { AksarKeyManagementService } from './kms/AskarKeyManagementService' import { AskarStorageService } from './storage' -import { assertAskarWallet } from './utils/assertAskarWallet' -import { AskarProfileWallet, AskarWallet } from './wallet' +import { storeAskarStoreConfigForContextCorrelationId } from './tenants' export class AskarModule implements Module { public readonly config: AskarModuleConfig @@ -16,50 +17,64 @@ export class AskarModule implements Module { this.config = new AskarModuleConfig(config) } + public api = AskarApi + public register(dependencyManager: DependencyManager) { dependencyManager.registerInstance(AskarModuleConfig, this.config) - if (dependencyManager.isRegistered(InjectionSymbols.Wallet)) { - throw new CredoError('There is an instance of Wallet already registered') + if (!this.config.enableKms && !this.config.enableStorage) { + dependencyManager + .resolve(AgentConfig) + .logger.warn(`Both 'enableKms' and 'enableStorage' are disabled, meaning Askar won't be used by the agent.`) } - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) - // If the multiWalletDatabaseScheme is set to ProfilePerWallet, we want to register the AskarProfileWallet - if (this.config.multiWalletDatabaseScheme === AskarMultiWalletDatabaseScheme.ProfilePerWallet) { - dependencyManager.registerContextScoped(AskarProfileWallet) + if (this.config.enableKms) { + const kmsConfig = dependencyManager.resolve(Kms.KeyManagementModuleConfig) + + // Register askar backend if not registered yet + if (!kmsConfig.backends.find((backend) => backend.backend === AksarKeyManagementService.backend)) { + kmsConfig.registerBackend(new AksarKeyManagementService()) + } } - if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { - throw new CredoError('There is an instance of StorageService already registered') + if (this.config.enableStorage) { + if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { + throw new CredoError( + 'Unable to register AskatStoreService. There is an instance of StorageService already registered' + ) + } + dependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) } - dependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) + + dependencyManager.registerSingleton(AskarStoreManager) } - public async initialize(agentContext: AgentContext): Promise { - // We MUST use an askar wallet here - assertAskarWallet(agentContext.wallet) - - const wallet = agentContext.wallet - - // Register the Askar store instance on the dependency manager - // This allows it to be re-used for tenants - agentContext.dependencyManager.registerInstance(Store, agentContext.wallet.store) - - // If the multiWalletDatabaseScheme is set to ProfilePerWallet, we want to register the AskarProfileWallet - // and return that as the wallet for all tenants, but not for the main agent, that should use the AskarWallet - if (this.config.multiWalletDatabaseScheme === AskarMultiWalletDatabaseScheme.ProfilePerWallet) { - agentContext.dependencyManager.container.register(InjectionSymbols.Wallet, { - useFactory: (container) => { - // If the container is the same as the root dependency manager container - // it means we are in the main agent, and we should use the root wallet - if (container === agentContext.dependencyManager.container) { - return wallet - } - - // Otherwise we want to return the AskarProfileWallet - return container.resolve(AskarProfileWallet) - }, - }) - } + public async onInitializeContext(agentContext: AgentContext) { + const storeManager = agentContext.dependencyManager.resolve(AskarStoreManager) + await storeManager.getInitializedStoreWithProfile(agentContext) + } + + public async onProvisionContext(agentContext: AgentContext) { + // We don't have any side effects to run + if (agentContext.isRootAgentContext) return + if (this.config.multiWalletDatabaseScheme === AskarMultiWalletDatabaseScheme.ProfilePerWallet) return + + // For new stores (so not profiles) we need to generate a wallet key + await storeAskarStoreConfigForContextCorrelationId(agentContext, { + key: this.config.askar.storeGenerateRawKey({}), + }) + } + + public async onDeleteContext(agentContext: AgentContext) { + const storeManager = agentContext.dependencyManager.resolve(AskarStoreManager) + + // Will delete either the store (when root agent context or database per wallet) or profile (when not root agent context and profile per wallet) + await storeManager.deleteContext(agentContext) + } + + public async onCloseContext(agentContext: AgentContext): Promise { + const storeManager = agentContext.dependencyManager.resolve(AskarStoreManager) + + await storeManager.closeContext(agentContext) } } diff --git a/packages/askar/src/AskarModuleConfig.ts b/packages/askar/src/AskarModuleConfig.ts index 500104ea6b..559d80f2ea 100644 --- a/packages/askar/src/AskarModuleConfig.ts +++ b/packages/askar/src/AskarModuleConfig.ts @@ -1,4 +1,5 @@ -import type { Askar } from '@openwallet-foundation/askar-shared' +import type { Askar, KdfMethod } from '@openwallet-foundation/askar-shared' +import type { AskarPostgresStorageConfig, AskarSqliteStorageConfig } from './AskarStorageConfig' export enum AskarMultiWalletDatabaseScheme { /** @@ -12,7 +13,48 @@ export enum AskarMultiWalletDatabaseScheme { ProfilePerWallet = 'ProfilePerWallet', } +export interface AskarModuleConfigStoreOptions { + /** + * The id of the store, and also the default profile that will be used for the root agent instance. + * + * - When SQLite is used that is not in-memory this will influence the path where the SQLite database is stored. + * - When Postgres is used, this determines the database. + */ + id: string + + /** + * The key to open the store + */ + key: string + + /** + * Key derivation method to use for opening the store. + * + * - `kdf:argon2i:mod` - most secure + * - `kdf:argon2i:int` - faster, less secure + * - `raw` - no key derivation. Useful if key is stored in e.g. the keychain on-device backed by biometrics. + * + * @default 'kdf:argon2i:mod' + */ + keyDerivationMethod?: `${KdfMethod.Argon2IInt}` | `${KdfMethod.Argon2IMod}` | `${KdfMethod.Raw}` + + /** + * The backend to use with backend specific configuraiton options. + * + * If not provided SQLite will be used by default + */ + database?: AskarSqliteStorageConfig | AskarPostgresStorageConfig +} + export interface AskarModuleConfigOptions { + /** + * Store configuration used for askar. + * + * If `multiWalletDatabaseScheme` is set to `AskarMultiWalletDatabaseScheme.DatabasePerWallet` a new store will be created + * for each tenant. For performance reasons it is recommended to use `AskarMultiWalletDatabaseScheme.ProfilePerWallet`. + */ + store: AskarModuleConfigStoreOptions + /** * * ## Node.JS @@ -58,6 +100,20 @@ export interface AskarModuleConfigOptions { * @default {@link AskarMultiWalletDatabaseScheme.DatabasePerWallet} (for backwards compatibility) */ multiWalletDatabaseScheme?: AskarMultiWalletDatabaseScheme + + /** + * Whether to enable and register the `AskarKeyManagementService` for key management and cryptographic operations. + * + * @default true + */ + enableKms?: boolean + + /** + * Whether to enable and register the `AskarStorageService` for storage + * + * @default true + */ + enableStorage?: boolean } /** @@ -79,4 +135,16 @@ export class AskarModuleConfig { public get multiWalletDatabaseScheme() { return this.options.multiWalletDatabaseScheme ?? AskarMultiWalletDatabaseScheme.DatabasePerWallet } + + public get store() { + return this.options.store + } + + public get enableKms() { + return this.options.enableKms ?? true + } + + public get enableStorage() { + return this.options.enableStorage ?? true + } } diff --git a/packages/askar/src/AskarStorageConfig.ts b/packages/askar/src/AskarStorageConfig.ts new file mode 100644 index 0000000000..e8408e3f48 --- /dev/null +++ b/packages/askar/src/AskarStorageConfig.ts @@ -0,0 +1,45 @@ +export interface AskarPostgresConfig { + host: string + connectTimeout?: number + idleTimeout?: number + maxConnections?: number + minConnections?: number +} + +export interface AskarSqliteConfig { + // TODO: add other sqlite config options + maxConnections?: number + minConnections?: number + + // TODO: split this up into two separate types SqliteInMemory and Sqlite + inMemory?: boolean + path?: string +} + +export interface AskarPostgresCredentials { + account: string + password: string + adminAccount?: string + adminPassword?: string +} + +export interface AskarPostgresStorageConfig { + type: 'postgres' + config: AskarPostgresConfig + credentials: AskarPostgresCredentials +} + +export interface AskarSqliteStorageConfig { + type: 'sqlite' + config?: AskarSqliteConfig +} + +export type AskarStorageConfig = AskarPostgresStorageConfig | AskarSqliteStorageConfig + +export function isAskarSqliteStorageConfig(config?: AskarStorageConfig): config is AskarSqliteStorageConfig { + return config?.type === 'sqlite' +} + +export function isAskarPostgresStorageConfig(config?: AskarStorageConfig): config is AskarPostgresStorageConfig { + return config?.type === 'postgres' +} diff --git a/packages/askar/src/AskarStoreManager.ts b/packages/askar/src/AskarStoreManager.ts new file mode 100644 index 0000000000..1d669c12bc --- /dev/null +++ b/packages/askar/src/AskarStoreManager.ts @@ -0,0 +1,594 @@ +import { AgentContext, FileSystem, InjectionSymbols, JsonTransformer, StorageVersionRecord } from '@credo-ts/core' +import { KdfMethod, Session, Store, StoreKeyMethod } from '@openwallet-foundation/askar-shared' +import { inject, injectable } from 'tsyringe' + +import { AskarStoreExportOptions, AskarStoreImportOptions, AskarStoreRotateKeyOptions } from './AskarApiOptions' +import { AskarModuleConfig, AskarModuleConfigStoreOptions, AskarMultiWalletDatabaseScheme } from './AskarModuleConfig' +import { + AskarError, + AskarStoreDuplicateError, + AskarStoreError, + AskarStoreExportPathExistsError, + AskarStoreImportPathExistsError, + AskarStoreInvalidKeyError, + AskarStoreNotFoundError, +} from './error' +import { transformFromRecordTagValues } from './storage/utils' +import { getAskarStoreConfigForContextCorrelationId } from './tenants' +import { + AskarErrorCode, + isAskarError, + isSqliteInMemoryUri, + keyDerivationMethodFromStoreConfig, + uriFromStoreConfig, +} from './utils' + +@injectable() +export class AskarStoreManager { + public constructor( + @inject(InjectionSymbols.FileSystem) private fileSystem: FileSystem, + private config: AskarModuleConfig + ) {} + + public isStoreOpen(agentContext: AgentContext) { + return !!this.getStore(agentContext)?.handle + } + + private async getStoreConfig(agentContext: AgentContext): Promise { + if ( + agentContext.isRootAgentContext || + this.config.multiWalletDatabaseScheme === AskarMultiWalletDatabaseScheme.ProfilePerWallet + ) { + return this.config.store + } + + // Otherwise we need to get the wallet key from the tenant record + const storeConfig = await getAskarStoreConfigForContextCorrelationId(agentContext) + + return { + id: agentContext.contextCorrelationId, + key: storeConfig.key, + // we always use raw at the moment + keyDerivationMethod: 'raw', + database: this.config.store.database, + } + } + + /** + * When we create storage for a context we need to store the version record + */ + private async setCurrentFrameworkStorageVersionOnSession(session: Session) { + const record = new StorageVersionRecord({ + storageVersion: StorageVersionRecord.frameworkStorageVersion, + }) + + await session.insert({ + value: JsonTransformer.serialize(record), + name: record.id, + category: record.type, + tags: transformFromRecordTagValues(record.getTags()), + }) + } + + /** + * Deletes all storage related to a context. If on store level, meaning root agent + * or when using database per wallet storage, the whole store will be deleted. + * Otherwise only a profile within the store will be removed. + */ + public async deleteContext(agentContext: AgentContext) { + const { profile, store } = await this.getInitializedStoreWithProfile(agentContext) + + // TODO: what if the root agnet context is deleted when profile per wallet is used? + // Currently it will delete the whole store. We can delete only the root profile, BUT: + // - all tenant records will be deleted + // - the root agent is deleted, this is not a flow we support (there's no default profile anymore) + if (this.isStoreLevel(agentContext)) { + await this.deleteStore(agentContext) + } else { + if (!profile) + throw new AskarStoreError( + 'Unable to delete asksar data for context. No profile found and not on store level (so not deleting the whole store)' + ) + + await store.removeProfile(profile) + } + } + + /** + * Closes an active context. If on store level, meaning root agent + * or when using database per wallet storage, the whole store will be closed. + * Otherwise nothing will be done as profiles are opened on a store from higher level. + */ + public async closeContext(agentContext: AgentContext) { + // TODO: we should maybe set some value on the agentContext indicating it is dipsoed so no new sessions can be opened + // If not on store level we don't have to do anything. + if (!this.isStoreLevel(agentContext)) return + + await this.closeStore(agentContext) + } + + /** + * @throws {AskarStoreDuplicateError} if the wallet already exists + * @throws {AskarStoreError} if another error occurs + */ + public async provisionStore(agentContext: AgentContext): Promise { + this.ensureStoreLevel(agentContext) + + const storeConfig = await this.getStoreConfig(agentContext) + const askarStoreConfig = this.getAskarStoreConfig(storeConfig) + + agentContext.config.logger.debug(`Provisioning store '${storeConfig.id}`) + + let store = this.getStore(agentContext) + if (store) { + throw new AskarStoreError('Store already provisioned') + } + + try { + if (askarStoreConfig.path) { + if (await this.fileSystem.exists(askarStoreConfig.path)) { + throw new AskarStoreDuplicateError( + `Store '${storeConfig.id}' at path ${askarStoreConfig.path} already exists.` + ) + } + + // Make sure path exists before creating the wallet + await this.fileSystem.createDirectory(askarStoreConfig.path) + } + + store = await Store.provision({ + recreate: false, + uri: askarStoreConfig.uri, + profile: askarStoreConfig.profile, + keyMethod: askarStoreConfig.keyMethod, + passKey: askarStoreConfig.passKey, + }) + agentContext.dependencyManager.registerInstance(Store, store) + + // For new stores we need to set the framework storage version + await this.withSession(agentContext, (session) => this.setCurrentFrameworkStorageVersionOnSession(session)) + + return store + } catch (error) { + if (error instanceof AskarStoreDuplicateError) throw error + + // FIXME: Askar should throw a Duplicate error code, but is currently returning Encryption + // And if we provide the very same wallet key, it will open it without any error + if ( + isAskarError(error) && + (error.code === AskarErrorCode.Encryption || error.code === AskarErrorCode.Duplicate) + ) { + const errorMessage = `Store '${storeConfig.id}' already exists` + agentContext.config.logger.debug(errorMessage) + + throw new AskarStoreDuplicateError(errorMessage, { + cause: error, + }) + } + + const errorMessage = `Error creating store '${storeConfig.id}'` + agentContext.config.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new AskarStoreError(errorMessage, { cause: error }) + } + } + + /** + * @throws {AskarStoreNotFoundError} if the wallet does not exist + * @throws {AskarStoreError} if another error occurs + */ + public async openStore(agentContext: AgentContext): Promise { + this.ensureStoreLevel(agentContext) + + let store = this.getStore(agentContext) + if (store) { + throw new AskarStoreError('Store already opened. Close the currently opened store before re-opening the store') + } + + const storeConfig = await this.getStoreConfig(agentContext) + const askarStoreConfig = this.getAskarStoreConfig(storeConfig) + + try { + store = await Store.open({ + uri: askarStoreConfig.uri, + keyMethod: askarStoreConfig.keyMethod, + passKey: askarStoreConfig.passKey, + }) + agentContext.dependencyManager.registerInstance(Store, store) + return store + } catch (error) { + if ( + isAskarError(error) && + (error.code === AskarErrorCode.NotFound || + (error.code === AskarErrorCode.Backend && isSqliteInMemoryUri(askarStoreConfig.uri))) + ) { + const errorMessage = `Store '${storeConfig.id}' not found` + agentContext.config.logger.debug(errorMessage) + + throw new AskarStoreNotFoundError(errorMessage, { + cause: error, + }) + } + + if (isAskarError(error) && error.code === AskarErrorCode.Encryption) { + const errorMessage = `Incorrect key for store '${storeConfig.id}'` + agentContext.config.logger.debug(errorMessage) + throw new AskarStoreInvalidKeyError(errorMessage, { + cause: error, + }) + } + throw new AskarStoreError(`Error opening store ${storeConfig.id}: ${error.message}`, { cause: error }) + } + } + + /** + * Rotate the key of the current askar store. + * + * NOTE: multiple agent contexts (tenants) can use the same store. This method rotates the key for the whole store, + * it is advised to only run this method on the root tenant agent when using profile per wallet database strategy. + * After running this method you should change the store configuration in the Askar module. + * + * @throws {AskarStoreNotFoundError} if the wallet does not exist + * @throws {AskarStoreError} if another error occurs + */ + public async rotateStoreKey(agentContext: AgentContext, options: AskarStoreRotateKeyOptions): Promise { + this.ensureStoreLevel(agentContext) + + const store = this.getStore(agentContext) + if (!store) { + throw new AskarStoreError('Store needs to be open to rotate the wallet key') + } + + const storeConfig = await this.getStoreConfig(agentContext) + + try { + await store.rekey({ + passKey: options.newKey, + keyMethod: keyDerivationMethodFromStoreConfig( + options.newKeyDerivationMethod ?? storeConfig.keyDerivationMethod + ), + }) + } catch (error) { + const errorMessage = `Error rotating key for store '${storeConfig.id}': ${error.message}` + agentContext.config.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new AskarStoreError(errorMessage, { cause: error }) + } + } + + /** + * Exports the current askar store. + * + * NOTE: a store can contain profiles for multiple tenants. When you export a store + * all profiles will be exported with it. + */ + public async exportStore(agentContext: AgentContext, options: AskarStoreExportOptions) { + this.ensureStoreLevel(agentContext) + + const store = this.getStore(agentContext) + if (!store) { + throw new AskarStoreError('Unable to export store. No store available on agent context') + } + + const currentStoreConfig = await this.getStoreConfig(agentContext) + try { + const newAskarStoreConfig = this.getAskarStoreConfig(options.exportToStore) + + // If path based store, ensure path does not exist yet, and create new store path + if (newAskarStoreConfig.path) { + // Export path already exists + if (await this.fileSystem.exists(newAskarStoreConfig.path)) { + throw new AskarStoreExportPathExistsError( + `Unable to create export, wallet export at path '${newAskarStoreConfig.path}' already exists` + ) + } + + // Make sure destination path exists + await this.fileSystem.createDirectory(newAskarStoreConfig.path) + } + + await store.copyTo({ + recreate: false, + uri: newAskarStoreConfig.uri, + keyMethod: newAskarStoreConfig.keyMethod, + passKey: newAskarStoreConfig.passKey, + }) + } catch (error) { + const errorMessage = `Error exporting store '${currentStoreConfig.id}': ${error.message}` + agentContext.config.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + if (error instanceof AskarStoreExportPathExistsError) throw error + throw new AskarStoreError(errorMessage, { cause: error }) + } + } + + /** + * Imports from an external store config into the current askar store config. + */ + public async importStore(agentContext: AgentContext, options: AskarStoreImportOptions) { + this.ensureStoreLevel(agentContext) + + const store = this.getStore(agentContext) + if (store) { + throw new AskarStoreError('To import a store the current store needs to be closed first') + } + + const destinationStoreConfig = await this.getStoreConfig(agentContext) + + const sourceAskarStoreConfig = this.getAskarStoreConfig(options.importFromStore) + const destinationAskarStoreConfig = this.getAskarStoreConfig(destinationStoreConfig) + + let sourceWalletStore: Store | undefined = undefined + try { + if (destinationAskarStoreConfig.path) { + // Import path already exists + if (await this.fileSystem.exists(destinationAskarStoreConfig.path)) { + throw new AskarStoreImportPathExistsError( + `Unable to import store. Path '${destinationAskarStoreConfig.path}' already exists` + ) + } + + await this.fileSystem.createDirectory(destinationAskarStoreConfig.path) + } + + // Open imported wallet and copy to destination + sourceWalletStore = await Store.open({ + uri: sourceAskarStoreConfig.uri, + keyMethod: sourceAskarStoreConfig.keyMethod, + passKey: sourceAskarStoreConfig.passKey, + }) + + await sourceWalletStore.copyTo({ + recreate: false, + uri: destinationAskarStoreConfig.uri, + keyMethod: destinationAskarStoreConfig.keyMethod, + passKey: destinationAskarStoreConfig.passKey, + }) + + await sourceWalletStore.close() + } catch (error) { + await sourceWalletStore?.close() + const errorMessage = `Error importing store '${options.importFromStore.id}': ${error.message}` + agentContext.config.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + if (error instanceof AskarStoreImportPathExistsError) throw error + + // Cleanup any wallet file we could have created + if (destinationAskarStoreConfig.path && (await this.fileSystem.exists(destinationAskarStoreConfig.path))) { + await this.fileSystem.delete(destinationAskarStoreConfig.path) + } + + throw new AskarStoreError(errorMessage, { cause: error }) + } + } + + /** + * Delete the current askar store. + * + * NOTE: multiple agent contexts (tenants) can use the same store. This method deletes the whole store, + * and if you're using multi-tenancy with profile per wallet it is advised to only run this method on the root tenant agent. + * + * @throws {AskarStoreNotFoundError} if the wallet does not exist + * @throws {AskarStoreError} if another error occurs + */ + public async deleteStore(agentContext: AgentContext): Promise { + this.ensureStoreLevel(agentContext) + + const store = this.getStore(agentContext) + if (store) await this.closeStore(agentContext) + + const storeConfig = await this.getStoreConfig(agentContext) + const askarStoreConfig = this.getAskarStoreConfig(storeConfig) + + agentContext.config.logger.info(`Deleting store '${storeConfig.id}'`) + try { + await Store.remove(askarStoreConfig.uri) + // Clear the store instance + agentContext.dependencyManager.registerInstance(Store, undefined) + } catch (error) { + const errorMessage = `Error deleting store '${storeConfig.id}': ${error.message}` + agentContext.config.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new AskarStoreError(errorMessage, { cause: error }) + } + } + + /** + * Close the current askar store + */ + public async closeStore(agentContext: AgentContext) { + this.ensureStoreLevel(agentContext) + + const store = this.getStore(agentContext) + if (!store) { + throw new AskarStoreError('There is no open store.') + } + + const storeConfig = await this.getStoreConfig(agentContext) + + try { + agentContext.config.logger.debug(`Closing store '${storeConfig.id}'`) + await store.close() + // Unregister the store from the context + agentContext.dependencyManager.registerInstance(Store, undefined) + } catch (error) { + const errorMessage = `Error closing store '${storeConfig.id}': ${error.message}` + agentContext.config.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new AskarStoreError(errorMessage, { cause: error }) + } + } + + private getAskarStoreConfig(storeConfig: AskarModuleConfigStoreOptions) { + const { uri, path } = uriFromStoreConfig(storeConfig, this.fileSystem.dataPath) + + return { + uri, + path, + profile: storeConfig.id, + keyMethod: new StoreKeyMethod( + (storeConfig.keyDerivationMethod ?? KdfMethod.Argon2IMod) satisfies `${KdfMethod}` | KdfMethod as KdfMethod + ), + passKey: storeConfig.key, + } + } + + /** + * Run callback with a transaction. If the callback resolves the transaction + * will be committed if the transaction is not closed yet. If the callback rejects + * the transaction will be rolled back if the transaction is not closed yet. + * + * TODO: update to new `using` syntax so we don't have to use a callback + */ + public async withTransaction( + agentContext: AgentContext, + callback: (session: Session) => Return + ): Promise> { + return this._withSession(agentContext, callback, true) + } + + /** + * Run callback with the session provided, the session will + * be closed once the callback resolves or rejects if it is not closed yet. + * + * TODO: update to new `using` syntax so we don't have to use a callback + */ + public async withSession( + agentContext: AgentContext, + callback: (session: Session) => Return + ): Promise> { + return this._withSession(agentContext, callback, false) + } + + private getStore(agentContext: AgentContext, { recursive = false }: { recursive?: boolean } = {}) { + const isRegistered = agentContext.dependencyManager.isRegistered(Store, recursive) + if (!isRegistered) return null + + // We set the store value to undefined in the dependency manager + // when closing it, but TSyringe still marks is as registered, but + // will throw an error when resolved. Since there is no unregister method + // we wrap it with a try-catch + try { + return agentContext.dependencyManager.resolve(Store) + } catch { + return null + } + } + + private async _withSession( + agentContext: AgentContext, + callback: (session: Session) => Return, + transaction = false + ): Promise> { + let session: Session | undefined = undefined + try { + const { store, profile } = await this.getInitializedStoreWithProfile(agentContext) + + session = await (transaction ? store.transaction(profile) : store.session(profile)) + .open() + .catch(async (error) => { + // If the profile does not exist yet we create it + // TODO: do we want some guards around this? I think this is really the easist approach to + // just create it if it doesn't exist yet. + if (isAskarError(error, AskarErrorCode.NotFound) && profile) { + await store.createProfile(profile) + const session = await store.session(profile).open() + + try { + // For new profiles we need to set the framework storage version + await this.setCurrentFrameworkStorageVersionOnSession(session) + } catch (error) { + await session.close() + throw error + } + + return session + } + + throw error + }) + + const result = await callback(session) + if (transaction && session.handle) { + await session.commit() + } + + return result + } catch (error) { + agentContext.config.logger.error('Error occured during tranaction, rollback') + if (transaction && session?.handle) { + await session.rollback() + } + throw error + } finally { + if (session?.handle) { + await session.close() + } + } + } + + public async getInitializedStoreWithProfile(agentContext: AgentContext) { + let store = this.getStore(agentContext, { + // In case we use a profile per wallet, we want to use the parent store, otherwise we only + // want to use a store that is directly registered on this context. + recursive: this.config.multiWalletDatabaseScheme === AskarMultiWalletDatabaseScheme.ProfilePerWallet, + }) + + if (!store) { + try { + store = await this.openStore(agentContext) + } catch (error) { + if (error instanceof AskarStoreNotFoundError) { + store = await this.provisionStore(agentContext) + } else { + throw error + } + } + } + + return { + // If we're on store level the default profile can be used automatically + // otherwise we need to set the profile, which we do based on the context correlation id + profile: this.isStoreLevel(agentContext) ? undefined : agentContext.contextCorrelationId, + store, + } + } + + /** + * Ensures a command is ran on a store level, preventing a tenant instance from + * removing a whole store (and potentially other tennats). + */ + private ensureStoreLevel(agentContext: AgentContext) { + if (this.isStoreLevel(agentContext)) return + + throw new AskarError( + `Agent context ${agentContext.contextCorrelationId} is not on store level. Make sure to only perform askar store operations in the agent context managing the askar store` + ) + } + + /** + * Checks whether the current agent context is on store level + */ + private isStoreLevel(agentContext: AgentContext) { + if (agentContext.isRootAgentContext) return true + return this.config.multiWalletDatabaseScheme === AskarMultiWalletDatabaseScheme.DatabasePerWallet + } +} diff --git a/packages/askar/src/__tests__/migration-postgres.e2e.test.ts b/packages/askar/src/__tests__/migration-postgres.e2e.test.ts index edc93c2291..d625b737a5 100644 --- a/packages/askar/src/__tests__/migration-postgres.e2e.test.ts +++ b/packages/askar/src/__tests__/migration-postgres.e2e.test.ts @@ -16,20 +16,19 @@ describe('migration with postgres backend', () => { await storageUpdateService.setCurrentStorageVersion(agent.context, '0.1') await agent.shutdown() - // Now start agent with auto update storage - agent = new Agent({ ...agentOptions, config: { ...agentOptions.config, autoUpdateStorageOnStartup: true } }) + // Now start agent without auto update storage + agent = new Agent({ ...agentOptions, config: { ...agentOptions.config, autoUpdateStorageOnStartup: false } }) storageUpdateService = agent.dependencyManager.resolve(StorageUpdateService) - // Should fail because export is not supported when using postgres - await expect(agent.initialize()).rejects.toThrow(/backend does not support export/) + await expect(agent.initialize()).rejects.toThrow(/Current agent storage is not up to date/) expect(await storageUpdateService.getCurrentStorageVersion(agent.context)).toEqual('0.1') await agent.shutdown() - // Now start agent with auto update storage, but this time disable backup + // Now start agent with auto update storage, but this time enable auto update agent = new Agent({ ...agentOptions, - config: { ...agentOptions.config, autoUpdateStorageOnStartup: true, backupBeforeStorageUpdate: false }, + config: { ...agentOptions.config, autoUpdateStorageOnStartup: true }, }) // Should work OK @@ -39,6 +38,6 @@ describe('migration with postgres backend', () => { ) await agent.shutdown() - await agent.wallet.delete() + await agent.modules.askar.deleteStore() }) }) diff --git a/packages/core/src/wallet/error/WalletError.ts b/packages/askar/src/error/AskarError.ts similarity index 53% rename from packages/core/src/wallet/error/WalletError.ts rename to packages/askar/src/error/AskarError.ts index 414f2014aa..fb9aba9813 100644 --- a/packages/core/src/wallet/error/WalletError.ts +++ b/packages/askar/src/error/AskarError.ts @@ -1,6 +1,6 @@ -import { CredoError } from '../../error/CredoError' +import { CredoError } from '@credo-ts/core' -export class WalletError extends CredoError { +export class AskarError extends CredoError { public constructor(message: string, { cause }: { cause?: Error } = {}) { super(message, { cause }) } diff --git a/packages/askar/src/error/AskarStoreDuplicateError.ts b/packages/askar/src/error/AskarStoreDuplicateError.ts new file mode 100644 index 0000000000..a7c912059d --- /dev/null +++ b/packages/askar/src/error/AskarStoreDuplicateError.ts @@ -0,0 +1,7 @@ +import { AskarStoreError } from './AskarStoreError' + +export class AskarStoreDuplicateError extends AskarStoreError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/core/src/wallet/error/WalletKeyExistsError.ts b/packages/askar/src/error/AskarStoreError.ts similarity index 52% rename from packages/core/src/wallet/error/WalletKeyExistsError.ts rename to packages/askar/src/error/AskarStoreError.ts index 3e0a19e7b4..84f3b5a853 100644 --- a/packages/core/src/wallet/error/WalletKeyExistsError.ts +++ b/packages/askar/src/error/AskarStoreError.ts @@ -1,6 +1,6 @@ -import { WalletError } from './WalletError' +import { CredoError } from '@credo-ts/core' -export class WalletKeyExistsError extends WalletError { +export class AskarStoreError extends CredoError { public constructor(message: string, { cause }: { cause?: Error } = {}) { super(message, { cause }) } diff --git a/packages/askar/src/error/AskarStoreExportPathExistsError.ts b/packages/askar/src/error/AskarStoreExportPathExistsError.ts new file mode 100644 index 0000000000..999b993b34 --- /dev/null +++ b/packages/askar/src/error/AskarStoreExportPathExistsError.ts @@ -0,0 +1,7 @@ +import { AskarStoreError } from './AskarStoreError' + +export class AskarStoreExportPathExistsError extends AskarStoreError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/askar/src/error/AskarStoreExportUnsupportedError.ts b/packages/askar/src/error/AskarStoreExportUnsupportedError.ts new file mode 100644 index 0000000000..689244e89e --- /dev/null +++ b/packages/askar/src/error/AskarStoreExportUnsupportedError.ts @@ -0,0 +1,7 @@ +import { AskarStoreError } from './AskarStoreError' + +export class AskarStoreExportUnsupportedError extends AskarStoreError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/askar/src/error/AskarStoreImportPathExistsError.ts b/packages/askar/src/error/AskarStoreImportPathExistsError.ts new file mode 100644 index 0000000000..235d801e52 --- /dev/null +++ b/packages/askar/src/error/AskarStoreImportPathExistsError.ts @@ -0,0 +1,7 @@ +import { AskarStoreError } from './AskarStoreError' + +export class AskarStoreImportPathExistsError extends AskarStoreError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/askar/src/error/AskarStoreInvalidKeyError.ts b/packages/askar/src/error/AskarStoreInvalidKeyError.ts new file mode 100644 index 0000000000..53946daac8 --- /dev/null +++ b/packages/askar/src/error/AskarStoreInvalidKeyError.ts @@ -0,0 +1,7 @@ +import { AskarStoreError } from './AskarStoreError' + +export class AskarStoreInvalidKeyError extends AskarStoreError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/askar/src/error/AskarStoreNotFoundError.ts b/packages/askar/src/error/AskarStoreNotFoundError.ts new file mode 100644 index 0000000000..cf68cd26ed --- /dev/null +++ b/packages/askar/src/error/AskarStoreNotFoundError.ts @@ -0,0 +1,7 @@ +import { AskarStoreError } from './AskarStoreError' + +export class AskarStoreNotFoundError extends AskarStoreError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/askar/src/error/index.ts b/packages/askar/src/error/index.ts new file mode 100644 index 0000000000..090573ff42 --- /dev/null +++ b/packages/askar/src/error/index.ts @@ -0,0 +1,8 @@ +export { AskarStoreDuplicateError } from './AskarStoreDuplicateError' +export { AskarStoreNotFoundError } from './AskarStoreNotFoundError' +export { AskarStoreInvalidKeyError } from './AskarStoreInvalidKeyError' +export { AskarStoreError } from './AskarStoreError' +export { AskarStoreImportPathExistsError } from './AskarStoreImportPathExistsError' +export { AskarStoreExportPathExistsError } from './AskarStoreExportPathExistsError' +export { AskarStoreExportUnsupportedError } from './AskarStoreExportUnsupportedError' +export { AskarError } from './AskarError' diff --git a/packages/askar/src/index.ts b/packages/askar/src/index.ts index 532ef7c842..156238174f 100644 --- a/packages/askar/src/index.ts +++ b/packages/askar/src/index.ts @@ -1,15 +1,22 @@ -// Wallet export { - AskarWallet, - AskarWalletPostgresStorageConfig, - AskarWalletPostgresConfig, - AskarWalletPostgresCredentials, - AskarProfileWallet, -} from './wallet' + AskarPostgresConfig, + AskarPostgresCredentials, + AskarPostgresStorageConfig, + AskarSqliteConfig, + AskarSqliteStorageConfig, +} from './AskarStorageConfig' +export { AksarKeyManagementService } from './kms/AskarKeyManagementService' // Storage export { AskarStorageService } from './storage' // Module export { AskarModule } from './AskarModule' -export { AskarModuleConfigOptions, AskarMultiWalletDatabaseScheme } from './AskarModuleConfig' +export { + AskarModuleConfigOptions, + AskarMultiWalletDatabaseScheme, + AskarModuleConfig, + AskarModuleConfigStoreOptions, +} from './AskarModuleConfig' + +export { transformPrivateKeyToPrivateJwk, transformSeedToPrivateJwk } from './utils' diff --git a/packages/askar/src/kms/AskarKeyManagementService.ts b/packages/askar/src/kms/AskarKeyManagementService.ts new file mode 100644 index 0000000000..237979cc6d --- /dev/null +++ b/packages/askar/src/kms/AskarKeyManagementService.ts @@ -0,0 +1,739 @@ +import type { JwkProps, KeyEntryObject, Session } from '@openwallet-foundation/askar-shared' + +import { type AgentContext, JsonEncoder, Kms, TypedArrayEncoder, utils } from '@credo-ts/core' +import { + CryptoBox, + Jwk, + Key, + KeyAlgorithm, + KeyEntryList, + SignatureAlgorithm, + askar, +} from '@openwallet-foundation/askar-shared' + +import { AskarStoreManager } from '../AskarStoreManager' +import { AskarErrorCode, isAskarError, jwkCrvToAskarAlg, jwkEncToAskarAlg } from '../utils' +import { decrypt } from './crypto/decrypt' +import { askarSupportedKeyAgreementAlgorithms, deriveDecryptionKey, deriveEncryptionKey } from './crypto/deriveKey' +import { AskarSupportedEncryptionOptions, encrypt } from './crypto/encrypt' +import { randomBytes } from './crypto/randomBytes' + +const askarSupportedEncryptionAlgorithms = [ + ...(Object.keys(jwkEncToAskarAlg) as Array), + 'XSALSA20-POLY1305', +] satisfies Array + +export class AksarKeyManagementService implements Kms.KeyManagementService { + public static readonly backend = 'askar' + public readonly backend = AksarKeyManagementService.backend + + private static algToSigType: Partial> = { + EdDSA: SignatureAlgorithm.EdDSA, + ES256K: SignatureAlgorithm.ES256K, + ES256: SignatureAlgorithm.ES256, + ES384: SignatureAlgorithm.ES384, + } + + private withSession(agentContext: AgentContext, callback: (session: Session) => Return) { + return agentContext.dependencyManager.resolve(AskarStoreManager).withSession(agentContext, callback) + } + + public isOperationSupported(_agentContext: AgentContext, operation: Kms.KmsOperation): boolean { + if (operation.operation === 'deleteKey') return true + if (operation.operation === 'randomBytes') return true + + if (operation.operation === 'importKey') { + if (operation.privateJwk.kty === 'EC' || operation.privateJwk.kty === 'OKP') { + return jwkCrvToAskarAlg[operation.privateJwk.crv] !== undefined + } + + // RSA/oct not supported + return false + } + + if (operation.operation === 'createKey') { + if (operation.type.kty === 'EC' || operation.type.kty === 'OKP') { + return jwkCrvToAskarAlg[operation.type.crv] !== undefined + } + + if (operation.type.kty === 'oct') { + if (operation.type.algorithm === 'C20P') return true + + // TODO: sync with the createKey code + if (operation.type.algorithm === 'aes') { + return [128, 256].includes(operation.type.length) + } + } + + return false + } + + if (operation.operation === 'sign' || operation.operation === 'verify') { + return AksarKeyManagementService.algToSigType[operation.algorithm] !== undefined + } + + if (operation.operation === 'encrypt') { + const isSupportedEncryptionAlgorithm = askarSupportedEncryptionAlgorithms.includes( + operation.encryption.algorithm as (typeof askarSupportedEncryptionAlgorithms)[number] + ) + if (!isSupportedEncryptionAlgorithm) return false + if (!operation.keyAgreement) return true + + return askarSupportedKeyAgreementAlgorithms.includes( + operation.keyAgreement.algorithm as (typeof askarSupportedKeyAgreementAlgorithms)[number] + ) + } + + if (operation.operation === 'decrypt') { + const isSupportedEncryptionAlgorithm = askarSupportedEncryptionAlgorithms.includes( + operation.decryption.algorithm as (typeof askarSupportedEncryptionAlgorithms)[number] + ) + if (!isSupportedEncryptionAlgorithm) return false + if (!operation.keyAgreement) return true + + return askarSupportedKeyAgreementAlgorithms.includes( + operation.keyAgreement.algorithm as (typeof askarSupportedKeyAgreementAlgorithms)[number] + ) + } + + return false + } + + public randomBytes(_agentContext: AgentContext, options: Kms.KmsRandomBytesOptions): Kms.KmsRandomBytesReturn { + const buffer = randomBytes(options.length) + + return { + bytes: buffer, + } + } + + public async getPublicKey(agentContext: AgentContext, keyId: string): Promise { + const key = await this.fetchAskarKey(agentContext, keyId) + if (!key) return null + + return this.publicJwkFromKey(key.key, { kid: keyId }) + } + + public async importKey( + agentContext: AgentContext, + options: Kms.KmsImportKeyOptions + ): Promise> { + const { kid } = options.privateJwk + + const privateJwk = { + ...options.privateJwk, + kid: kid ?? utils.uuid(), + } + + let key: Key | undefined = undefined + try { + if (privateJwk.kty === 'oct') { + // TODO: we need to look at how to import symmetric keys, as we need the alg + // Should we do the same as we do for createKey? + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `importing keys with kty '${privateJwk.kty}'`, + this.backend + ) + // key = Key.fromSecretBytes({ + // algorithm: KeyAlgs.AesA128Gcm, + // secretKey: TypedArrayEncoder.fromBase64(privateJwk.k), + // }) + } + if (privateJwk.kty === 'EC' || privateJwk.kty === 'OKP') { + // Throws error if not supported + this.assertAskarAlgForJwkCrv(privateJwk.kty, privateJwk.crv) + + key = Key.fromJwk({ jwk: Jwk.fromJson(privateJwk) }) + } + + const _key = key + if (!_key) { + throw new Kms.KeyManagementAlgorithmNotSupportedError(`kty '${privateJwk.kty}'`, this.backend) + } + + await this.withSession(agentContext, (session) => session.insertKey({ name: privateJwk.kid, key: _key })) + const publicJwk = Kms.publicJwkFromPrivateJwk(privateJwk) + + return { + keyId: privateJwk.kid, + publicJwk: { + ...publicJwk, + kid: privateJwk.kid, + }, + } as Kms.KmsImportKeyReturn + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + + // Handle case where key already exists + if (isAskarError(error, AskarErrorCode.Duplicate)) { + throw new Kms.KeyManagementKeyExistsError(privateJwk.kid, this.backend) + } + + throw new Kms.KeyManagementError('Error importing key', { cause: error }) + } finally { + key?.handle.free() + } + } + + public async deleteKey(agentContext: AgentContext, options: Kms.KmsDeleteKeyOptions): Promise { + try { + await this.withSession(agentContext, (session) => session.removeKey({ name: options.keyId })) + return true + } catch (error) { + // Handle case where key already exists + if (isAskarError(error, AskarErrorCode.NotFound)) { + return false + } + + throw new Kms.KeyManagementError(`Error deleting key with id '${options.keyId}'`, { cause: error }) + } + } + + public async createKey( + agentContext: AgentContext, + options: Kms.KmsCreateKeyOptions + ): Promise> { + const { type, keyId } = options + + // FIXME: we should maybe keep the default keyId as publicKeyBase58 for a while for now, so it doesn't break + // Or we need a way to query a key based on the public key? + const kid = keyId ?? utils.uuid() + let askarKey: Key | undefined = undefined + try { + if (type.kty === 'EC' || type.kty === 'OKP') { + const keyAlg = this.assertAskarAlgForJwkCrv(type.kty, type.crv) + askarKey = Key.generate(keyAlg) + } else if (type.kty === 'oct') { + // NOTE: askar is more specific in the intended use of the key at time of generation. + // We either need to allow for this on a higher level (should be possible using `alg`) + // but as the keys are the same it's ok to just always pick one and if used for another + // purpose we can see them as the same. + if (type.algorithm === 'aes') { + const lengthToKeyAlg: Record = { + 128: KeyAlgorithm.AesA128Gcm, + 256: KeyAlgorithm.AesA256Gcm, + 512: KeyAlgorithm.AesA256CbcHs512, + + // Not supported by askar + 192: undefined, + } + + const keyAlg = lengthToKeyAlg[type.length] + if (!keyAlg) { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `length '${type.length}' for kty '${type.kty}' with algorithm '${type.algorithm}'. Supported length values are '128', '256'`, + this.backend + ) + } + + askarKey = Key.generate(keyAlg) + } else if (type.algorithm === 'C20P') { + // Both X and non-X variant can be used with the same key + askarKey = Key.generate(KeyAlgorithm.Chacha20C20P) + } else { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `algorithm '${type.algorithm}' for kty '${type.kty}'`, + this.backend + ) + } + } + + const _key = askarKey + if (!_key) { + throw new Kms.KeyManagementAlgorithmNotSupportedError(`kty '${type.kty}'`, this.backend) + } + + const publicJwk = this.publicJwkFromKey(_key, { kid }) as Kms.KmsCreateKeyReturn['publicJwk'] + await this.withSession(agentContext, (session) => session.insertKey({ name: kid, key: _key })) + + return { + publicJwk, + keyId: kid, + } as Kms.KmsCreateKeyReturn + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + + // Handle case where key already exists + if (isAskarError(error, AskarErrorCode.Duplicate)) { + throw new Kms.KeyManagementKeyExistsError(kid, this.backend) + } + + throw new Kms.KeyManagementError('Error creating key', { cause: error }) + } finally { + askarKey?.handle.free() + } + } + + public async sign(agentContext: AgentContext, options: Kms.KmsSignOptions): Promise { + const { keyId, algorithm, data } = options + + // 1. Retrieve the key + const key = await this.getKeyAsserted(agentContext, keyId) + try { + const sigType = this.assertedSigTypeForAlg(algorithm) + // Askar has a bug with loading symmetric keys, but we shouldn't get here as I don't think askar + // support signing with symmetric keys, and we don't support it (it will be caught by assertedSigTypeForAlg) + if (!key.key) { + throw new Kms.KeyManagementAlgorithmNotSupportedError(`algorithm ${algorithm}`, this.backend) + } + + // TODO: we should extend this with metadata properties (e.g. use, key_ops) + const publicJwk = this.publicJwkFromKey(key.key, { kid: keyId }) + const privateJwk = this.privateJwkFromKey(key.key, { kid: keyId }) + + // 2. Validate alg and use for key + Kms.assertAllowedSigningAlgForKey(privateJwk, algorithm) + Kms.assertKeyAllowsSign(publicJwk) + + // 3. Perform the signing operation + const signature = key.key.signMessage({ + message: data, + sigType, + }) + + return { + signature, + } + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + throw new Kms.KeyManagementError('Error signing with key', { cause: error }) + } finally { + key.key?.handle.free() + } + } + + public async verify(agentContext: AgentContext, options: Kms.KmsVerifyOptions): Promise { + const { algorithm, data, signature, key: keyInput } = options + + // Get askar sig type (and handles unsupported alg) + const sigType = this.assertedSigTypeForAlg(algorithm) + + // Retrieve the key + let askarKey: Key | undefined = undefined + + try { + if (typeof keyInput === 'string') { + askarKey = (await this.getKeyAsserted(agentContext, keyInput)).key + } else if (keyInput.kty === 'EC' || keyInput.kty === 'OKP') { + // Throws error if not supported + this.assertAskarAlgForJwkCrv(keyInput.kty, keyInput.crv) + + askarKey = Key.fromJwk({ jwk: Jwk.fromJson(keyInput as JwkProps) }) + } else { + throw new Kms.KeyManagementAlgorithmNotSupportedError(`kty ${keyInput.kty}`, this.backend) + } + + // Askar has a bug with loading symmetric keys, but we shouldn't get here as I don't think askar + // support signing with symmetric keys, and we don't support it (it will be caught by assertedSigTypeForAlg) + if (!askarKey) { + throw new Kms.KeyManagementAlgorithmNotSupportedError(`algorithm ${algorithm}`, this.backend) + } + + const keyId = typeof keyInput === 'string' ? keyInput : keyInput.kid + const publicJwk = this.publicJwkFromKey(askarKey, { kid: keyId }) + const privateJwk = this.privateJwkFromKey(askarKey, { kid: keyId }) + + // 2. Validate alg and use for key + Kms.assertAllowedSigningAlgForKey(privateJwk, algorithm) + Kms.assertKeyAllowsVerify(publicJwk) + + // 4. Perform the verify operation + const verified = askarKey.verifySignature({ message: data, signature, sigType }) + if (verified) { + return { + verified: true, + publicJwk: typeof keyInput === 'string' ? this.publicJwkFromKey(askarKey, { kid: keyId }) : keyInput, + } + } + + return { + verified: false, + } + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + throw new Kms.KeyManagementError('Error verifying with key', { cause: error }) + } finally { + if (askarKey) askarKey.handle.free() + } + } + + public async encrypt(agentContext: AgentContext, options: Kms.KmsEncryptOptions): Promise { + const { data, encryption, key } = options + + Kms.assertSupportedEncryptionAlgorithm(encryption, askarSupportedEncryptionAlgorithms, this.backend) + + const keysToFree: Key[] = [] + try { + let encryptionKey: Key | undefined = undefined + let encryptedKey: Kms.KmsEncryptedKey | undefined = undefined + + // TODO: we should check if the key allows this operation + if (typeof key === 'string') { + encryptionKey = (await this.getKeyAsserted(agentContext, key)).key + + keysToFree.push(encryptionKey) + } else if ('kty' in key) { + if (encryption.algorithm === 'XSALSA20-POLY1305') { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `encryption algorithm '${encryption.algorithm}' is only supported in combination with key agreement algorithm '${Kms.KnownJwaKeyAgreementAlgorithms.ECDH_HSALSA20}'`, + this.backend + ) + } + encryptionKey = this.keyFromSecretBytesAndEncryptionAlgorithm( + TypedArrayEncoder.fromBase64(key.k), + encryption.algorithm + ) + keysToFree.push(encryptionKey) + } else { + Kms.assertAllowedKeyDerivationAlgForKey(key.externalPublicJwk, key.algorithm) + Kms.assertKeyAllowsDerive(key.externalPublicJwk) + Kms.assertSupportedKeyAgreementAlgorithm(key, askarSupportedKeyAgreementAlgorithms, this.backend) + + let privateKey = key.keyId ? (await this.getKeyAsserted(agentContext, key.keyId)).key : undefined + if (privateKey) keysToFree.push(privateKey) + + const privateJwk = privateKey ? this.privateJwkFromKey(privateKey) : undefined + if (privateJwk) { + Kms.assertJwkAsymmetric(privateJwk, key.keyId) + Kms.assertAllowedKeyDerivationAlgForKey(privateJwk, key.algorithm) + Kms.assertKeyAllowsDerive(privateJwk) + + // Special case, for DIDComm v1 we often use an X25519 for the external key + // but we use an Ed25519 for our key + if (key.algorithm !== 'ECDH-HSALSA20') { + Kms.assertAsymmetricJwkKeyTypeMatches(privateJwk, key.externalPublicJwk) + } + } + + const recipientKey = this.keyFromJwk(key.externalPublicJwk) + keysToFree.push(recipientKey) + + // Special case to support DIDComm v1 + if (key.algorithm === 'ECDH-HSALSA20' || encryption.algorithm === 'XSALSA20-POLY1305') { + if (encryption.algorithm !== 'XSALSA20-POLY1305' || key.algorithm !== 'ECDH-HSALSA20') { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `key agreement algorithm '${key.algorithm}' with encryption algorithm '${encryption.algorithm}'`, + this.backend + ) + } + + // anonymous encryption + if (!privateKey) { + return { + encrypted: CryptoBox.seal({ + recipientKey, + message: data, + }), + } + } + + // Special case. For DIDComm v1 we basically use the Ed25519 key also + // for X25519 operations. + if (privateKey.algorithm === KeyAlgorithm.Ed25519) { + privateKey = privateKey.convertkey({ algorithm: KeyAlgorithm.X25519 }) + keysToFree.push(privateKey) + } + + const nonce = CryptoBox.randomNonce() + const encrypted = CryptoBox.cryptoBox({ + recipientKey, + senderKey: privateKey, + message: data, + nonce, + }) + + return { + encrypted, + iv: nonce, + } + } + + // This should not happen, but for TS + if (!privateKey) { + throw new Kms.KeyManagementError('sender key is required for ECDH-ES key derivation.') + } + + const { contentEncryptionKey, encryptedContentEncryptionKey } = deriveEncryptionKey({ + encryption, + keyAgreement: key, + recipientKey, + senderKey: privateKey, + }) + + encryptionKey = contentEncryptionKey + keysToFree.push(contentEncryptionKey) + encryptedKey = encryptedContentEncryptionKey + } + + if (encryption.algorithm === 'XSALSA20-POLY1305') { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `encryption algorithm '${encryption.algorithm}' can only be used with key agreement algorithm ECDH-HSALSA20`, + this.backend + ) + } + + const privateJwk = this.privateJwkFromKey(encryptionKey) + Kms.assertKeyAllowsDerive(privateJwk) + Kms.assertAllowedEncryptionAlgForKey(privateJwk, encryption.algorithm) + Kms.assertKeyAllowsEncrypt(privateJwk) + + const encrypted = encrypt({ + key: encryptionKey, + data, + encryption, + }) + + return { + ...encrypted, + encryptedKey, + } + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + throw new Kms.KeyManagementError('Error encrypting with key', { cause: error }) + } finally { + // Clear all keys + for (const key of keysToFree) { + key.handle.free() + } + } + } + + public async decrypt(agentContext: AgentContext, options: Kms.KmsDecryptOptions): Promise { + const { encrypted, decryption, key } = options + + Kms.assertSupportedEncryptionAlgorithm(decryption, askarSupportedEncryptionAlgorithms, this.backend) + + const keysToFree: Key[] = [] + + try { + let decryptionKey: Key | undefined = undefined + + if (typeof key === 'string') { + decryptionKey = (await this.getKeyAsserted(agentContext, key)).key + keysToFree.push(decryptionKey) + } else if ('kty' in key) { + if (decryption.algorithm === 'XSALSA20-POLY1305') { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `decryption algorithm '${decryption.algorithm}' is only supported in combination with key agreement algorithm '${Kms.KnownJwaKeyAgreementAlgorithms.ECDH_HSALSA20}'`, + this.backend + ) + } + decryptionKey = this.keyFromSecretBytesAndEncryptionAlgorithm( + TypedArrayEncoder.fromBase64(key.k), + decryption.algorithm + ) + keysToFree.push(decryptionKey) + } else { + if (key.externalPublicJwk) { + Kms.assertAllowedKeyDerivationAlgForKey(key.externalPublicJwk, key.algorithm) + Kms.assertKeyAllowsDerive(key.externalPublicJwk) + } + Kms.assertSupportedKeyAgreementAlgorithm(key, askarSupportedKeyAgreementAlgorithms, this.backend) + + let privateKey = (await this.getKeyAsserted(agentContext, key.keyId)).key + keysToFree.push(privateKey) + + const privateJwk = this.privateJwkFromKey(privateKey) + + Kms.assertJwkAsymmetric(privateJwk, key.keyId) + Kms.assertAllowedKeyDerivationAlgForKey(privateJwk, key.algorithm) + Kms.assertKeyAllowsDerive(privateJwk) + + // Special case for ECDH-HSALSA as we can have mismatch between keys because of DIDComm v1 + if (key.externalPublicJwk && key.algorithm !== 'ECDH-HSALSA20') { + Kms.assertAsymmetricJwkKeyTypeMatches(privateJwk, key.externalPublicJwk) + } + + const senderKey = key.externalPublicJwk ? this.keyFromJwk(key.externalPublicJwk) : undefined + if (senderKey) keysToFree.push(senderKey) + + // Special case to support DIDComm v1 + if (key.algorithm === 'ECDH-HSALSA20' || decryption.algorithm === 'XSALSA20-POLY1305') { + if (decryption.algorithm !== 'XSALSA20-POLY1305' || key.algorithm !== 'ECDH-HSALSA20') { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `key agreement algorithm '${key.algorithm}' with encryption algorithm '${decryption.algorithm}'`, + this.backend + ) + } + + // Special case. For DIDComm v1 we basically use the Ed25519 key also + // for X25519 operations. + if (privateKey.algorithm === KeyAlgorithm.Ed25519) { + privateKey = privateKey.convertkey({ algorithm: KeyAlgorithm.X25519 }) + keysToFree.push(privateKey) + } + + if (!senderKey) { + // anonymous encryption + return { + data: CryptoBox.sealOpen({ + recipientKey: privateKey, + ciphertext: encrypted, + }), + } + } + + if (!decryption.iv) { + throw new Kms.KeyManagementError( + `Missing required 'iv' for key agreement algorithm ${key.algorithm} and encryption algorithm ${decryption.algorithm} with sender key defined.` + ) + } + + const decrypted = CryptoBox.open({ + recipientKey: privateKey, + senderKey: senderKey, + message: encrypted, + nonce: decryption.iv, + }) + + return { + data: decrypted, + } + } + + // This should not happen, but for TS + if (!senderKey) { + throw new Kms.KeyManagementError('sender key is required for ECDH-ES key derivation.') + } + + const { contentEncryptionKey } = deriveDecryptionKey({ + decryption, + keyAgreement: key, + recipientKey: privateKey, + senderKey, + }) + + decryptionKey = contentEncryptionKey + keysToFree.push(contentEncryptionKey) + } + + if (decryption.algorithm === 'XSALSA20-POLY1305') { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `encryption algorithm '${decryption.algorithm}' can only be used with key agreement algorithm ECDH-HSALSA20`, + this.backend + ) + } + + const privateJwk = this.privateJwkFromKey(decryptionKey) + Kms.assertKeyAllowsDerive(privateJwk) + Kms.assertAllowedEncryptionAlgForKey(privateJwk, decryption.algorithm) + Kms.assertKeyAllowsEncrypt(privateJwk) + + const decrypted = decrypt({ + key: decryptionKey, + encrypted, + decryption, + }) + + return { + data: decrypted, + } + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + throw new Kms.KeyManagementError('Error decrypting with key', { cause: error }) + } finally { + // Clear all keys + for (const key of keysToFree) { + key.handle.free() + } + } + } + + private assertedSigTypeForAlg(algorithm: Kms.KnownJwaSignatureAlgorithm): SignatureAlgorithm { + const sigType = AksarKeyManagementService.algToSigType[algorithm] + if (!sigType) { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `signing and verification with algorithm '${algorithm}'`, + this.backend + ) + } + + return sigType + } + + private assertAskarAlgForJwkCrv(kty: string, crv: Kms.KmsJwkPublicEc['crv'] | Kms.KmsJwkPublicOkp['crv']) { + const keyAlg = jwkCrvToAskarAlg[crv] + if (!keyAlg) { + throw new Kms.KeyManagementAlgorithmNotSupportedError(`crv '${crv}' for kty '${kty}'`, this.backend) + } + + return keyAlg + } + + private keyFromJwk(jwk: Kms.KmsJwkPrivate | Kms.KmsJwkPublic) { + const key = new Key( + askar.keyFromJwk({ + // TODO: the JWK class in JS Askar wrapper is too limiting + // so we use this method directly. should update it + jwk: JsonEncoder.toBuffer(jwk) as unknown as Jwk, + }) + ) + + return key + } + + private keyFromSecretBytesAndEncryptionAlgorithm( + secretBytes: Uint8Array, + algorithm: AskarSupportedEncryptionOptions['algorithm'] + ) { + const askarEncryptionAlgorithm = jwkEncToAskarAlg[algorithm] + if (!askarEncryptionAlgorithm) { + throw new Kms.KeyManagementAlgorithmNotSupportedError(`JWA encryption algorithm '${algorithm}'`, 'askar') + } + + return Key.fromSecretBytes({ + algorithm: askarEncryptionAlgorithm, + secretKey: secretBytes, + }) + } + + private publicJwkFromKey(key: Key, partialJwkPublic?: Partial) { + return Kms.publicJwkFromPrivateJwk(this.privateJwkFromKey(key, partialJwkPublic)) + } + + private privateJwkFromKey(key: Key, partialJwkPrivate?: Partial) { + // TODO: once we support additional params we should add these here + + // TODO: the JWK class in JS Askar wrapper is too limiting + // so we use this method directly. should update it + // We extract alg, as Askar doesn't always use the same algs + const { alg, ...jwkSecret } = JsonEncoder.fromBuffer( + askar.keyGetJwkSecret({ + localKeyHandle: key.handle, + }) + ) + + return { + ...partialJwkPrivate, + ...jwkSecret, + } as Kms.KmsJwkPrivate + } + + private async fetchAskarKey(agentContext: AgentContext, keyId: string): Promise { + return await this.withSession(agentContext, async (session) => { + if (!session.handle) throw Error('Cannot fetch a key with a closed session') + + // Fetch the key from the session + const handle = await askar.sessionFetchKey({ forUpdate: false, name: keyId, sessionHandle: session.handle }) + if (!handle) return null + + // Get the key entry + const keyEntryList = new KeyEntryList({ handle }) + const keyEntry = keyEntryList.getEntryByIndex(0) + + const keyEntryObject = keyEntry.toJson() + keyEntryList.handle.free() + + return keyEntryObject + }) + } + + private async getKeyAsserted(agentContext: AgentContext, keyId: string) { + const storageKey = await this.fetchAskarKey(agentContext, keyId) + if (!storageKey) { + throw new Kms.KeyManagementKeyNotFoundError(keyId, this.backend) + } + + return storageKey + } +} diff --git a/packages/askar/src/wallet/__tests__/__fixtures__/jarm-jwe-encrypted-response.json b/packages/askar/src/kms/__fixtures__/jarm-jwe-encrypted-response.json similarity index 100% rename from packages/askar/src/wallet/__tests__/__fixtures__/jarm-jwe-encrypted-response.json rename to packages/askar/src/kms/__fixtures__/jarm-jwe-encrypted-response.json diff --git a/packages/askar/src/kms/__tests__/AskarKeyManagementService.test.ts b/packages/askar/src/kms/__tests__/AskarKeyManagementService.test.ts new file mode 100644 index 0000000000..2705c9e532 --- /dev/null +++ b/packages/askar/src/kms/__tests__/AskarKeyManagementService.test.ts @@ -0,0 +1,1838 @@ +import { InjectionSymbols, JsonEncoder, Kms, TypedArrayEncoder } from '@credo-ts/core' +import { askar } from '@openwallet-foundation/askar-shared' + +import { Buffer } from 'node:buffer' +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { getAgentConfig, getAgentContext } from '../../../../core/tests' +import { NodeFileSystem } from '../../../../node/src/NodeFileSystem' +import { AskarModuleConfig, AskarMultiWalletDatabaseScheme } from '../../AskarModuleConfig' +import { AskarStoreManager } from '../../AskarStoreManager' +import { AksarKeyManagementService } from '../AskarKeyManagementService' + +const agentContext = getAgentContext({ + contextCorrelationId: 'default', + agentConfig: getAgentConfig('AskarKeyManagementService'), + registerInstances: [ + [InjectionSymbols.FileSystem, new NodeFileSystem()], + [ + AskarModuleConfig, + new AskarModuleConfig({ + multiWalletDatabaseScheme: AskarMultiWalletDatabaseScheme.ProfilePerWallet, + askar, + store: { + id: 'default', + key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', + keyDerivationMethod: 'raw', + database: { + type: 'sqlite', + config: { + inMemory: true, + }, + }, + }, + }), + ], + ], +}) +const agentContextTenant = getAgentContext({ + contextCorrelationId: '1a2eb2ed-49e4-43bf-bbca-de1cfbf1d890', + dependencyManager: agentContext.dependencyManager.createChild(), + isRootAgentContext: false, +}) +const service = new AksarKeyManagementService() + +describe('AskarKeyManagementService', () => { + it('correctly identifies backend as askar', () => { + expect(service.backend).toBe('askar') + }) + + describe('profiles', () => { + it('uses the default profile for the default agent context', async () => { + await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + keyId: 'key-1', + }) + + const askarStoreManager = agentContext.dependencyManager.resolve(AskarStoreManager) + const sessionKey = await askarStoreManager.withSession(agentContext, (session) => + session.fetchKey({ name: 'key-1' }) + ) + expect(sessionKey).toEqual({ + algorithm: 'p256', + key: expect.any(Object), + metadata: null, + name: 'key-1', + tags: {}, + }) + + await askarStoreManager.deleteStore(agentContext) + }) + + it("automatically creates a profile if it doesn't exist yet", async () => { + const askarStoreManager = agentContext.dependencyManager.resolve(AskarStoreManager) + const store = await askarStoreManager.provisionStore(agentContext) + + expect(await store.listProfiles()).toEqual(['default']) + + await service.createKey(agentContextTenant, { + type: { kty: 'EC', crv: 'P-256' }, + keyId: 'key-2', + }) + + expect(await store.listProfiles()).toEqual([agentContextTenant.contextCorrelationId, 'default']) + const session = await store.session(agentContextTenant.contextCorrelationId).open() + expect(await session.fetchKey({ name: 'key-2' })).toEqual({ + algorithm: 'p256', + key: expect.any(Object), + metadata: null, + name: 'key-2', + tags: {}, + }) + await session.close() + + await askarStoreManager.deleteStore(agentContext) + }) + }) + + describe('createKey', () => { + it('throws error if key id already exists', async () => { + const keyId = 'test-key' + await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + keyId, + }) + + await expect( + service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + keyId, + }) + ).rejects.toThrow(new Kms.KeyManagementKeyExistsError('test-key', service.backend)) + }) + + it('creates EC P-256 key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'EC', + crv: 'P-256', + x: expect.any(String), + y: expect.any(String), + kid: result.keyId, + }, + }) + }) + + it('creates EC P-384 key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-384' }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'EC', + crv: 'P-384', + x: expect.any(String), + y: expect.any(String), + kid: result.keyId, + }, + }) + }) + + it('throws error for unsupported EC key P-521', async () => { + await expect( + service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-521' }, + }) + ).rejects.toThrow(new Kms.KeyManagementAlgorithmNotSupportedError(`crv 'P-521' for kty 'EC'`, service.backend)) + }) + + it('creates EC secp256k1 key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'secp256k1' }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'EC', + crv: 'secp256k1', + x: expect.any(String), + y: expect.any(String), + kid: result.keyId, + }, + }) + }) + + it('throws error for unsupported key type RSA', async () => { + await expect( + service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 2048 }, + }) + ).rejects.toThrow(new Kms.KeyManagementAlgorithmNotSupportedError(`kty 'RSA'`, service.backend)) + }) + + it('creates OKP Ed25519 key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'Ed25519' }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'OKP', + crv: 'Ed25519', + x: expect.any(String), + kid: result.keyId, + }, + }) + }) + + it('creates OKP X25519 key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'X25519' }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'OKP', + crv: 'X25519', + x: expect.any(String), + kid: result.keyId, + }, + }) + }) + + it('creates oct aes key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'aes', length: 256 }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'oct', + kid: result.keyId, + }, + }) + }) + + it('creates oct c20p key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'C20P' }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'oct', + kid: result.keyId, + }, + }) + }) + + it('throw error for unsupported oct hmac key', async () => { + await expect( + service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'hmac', length: 512 }, + }) + ).rejects.toThrow( + new Kms.KeyManagementAlgorithmNotSupportedError(`algorithm 'hmac' for kty 'oct'`, service.backend) + ) + }) + + it('throws error for unsupported key type', async () => { + await expect( + service.createKey(agentContext, { + // @ts-expect-error Testing invalid type + type: { kty: 'INVALID' }, + }) + ).rejects.toThrow(new Kms.KeyManagementAlgorithmNotSupportedError(`kty 'INVALID'`, service.backend)) + }) + }) + + describe('sign', () => { + it('throws error if key is not found', async () => { + await expect( + service.sign(agentContext, { + keyId: 'nonexistent', + algorithm: 'RS256', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow(new Kms.KeyManagementKeyNotFoundError('nonexistent', service.backend)) + }) + + it('signs with ES256', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'ES256', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with EC ES384', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-384' }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'ES384', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with ES256K', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'secp256k1' }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'ES256K', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with EdDSA', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'Ed25519' }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'EdDSA', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('throws error if algorithm is not supprted by backend', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + await expect( + service.sign(agentContext, { + keyId, + algorithm: 'RS256', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementAlgorithmNotSupportedError( + `signing and verification with algorithm 'RS256'`, + service.backend + ) + ) + }) + + it('throws error if key type does not match algorithm', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + await expect( + service.sign(agentContext, { + keyId, + algorithm: 'ES384', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `EC key with crv 'P-256' cannot be used with algorithm 'ES384' for signature creation or verification. Allowed algs are 'ES256'` + ) + ) + }) + + it('throws error when signing with x25519 key', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'X25519' }, + }) + + await expect( + service.sign(agentContext, { + keyId, + algorithm: 'EdDSA', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `OKP key with crv 'X25519' cannot be used with algorithm 'EdDSA' for signature creation or verification.` + ) + ) + }) + }) + + describe('verify', () => { + it('throws error if key is not found', async () => { + await expect( + service.verify(agentContext, { + key: 'nonexistent', + algorithm: 'ES256', + data: new Uint8Array([1, 2, 3]), + signature: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow(new Kms.KeyManagementKeyNotFoundError('nonexistent', service.backend)) + }) + + it('verifies ES256 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'ES256', + data, + }) + + const result = await service.verify(agentContext, { + key: publicJwk, + algorithm: 'ES256', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'ES256', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('verifies ES384 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-384' }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'ES384', + data, + }) + + const result = await service.verify(agentContext, { + key: publicJwk, + algorithm: 'ES384', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'ES384', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('verifies EdDSA Ed25519 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'Ed25519' }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'EdDSA', + data, + }) + + const result = await service.verify(agentContext, { + key: keyId, + algorithm: 'EdDSA', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'EdDSA', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('throws error if key type does not match algorithm', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + await expect( + service.verify(agentContext, { + key: keyId, + algorithm: 'ES384', + data: new Uint8Array([1, 2, 3]), + signature: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `EC key with crv 'P-256' cannot be used with algorithm 'ES384' for signature creation or verification. Allowed algs are 'ES256'` + ) + ) + }) + + it('throws error for X25519 key', async () => { + const { publicJwk } = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'X25519' }, + }) + + await expect( + service.verify(agentContext, { + key: publicJwk, + algorithm: 'EdDSA', + data: new Uint8Array([1, 2, 3]), + signature: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `OKP key with crv 'X25519' cannot be used with algorithm 'EdDSA' for signature creation or verification.` + ) + ) + }) + + it('returns false for modified data', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-384' }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'ES384', + data, + }) + + const modifiedData = new Uint8Array([1, 2, 4]) + const result = await service.verify(agentContext, { + key: keyId, + algorithm: 'ES384', + data: modifiedData, + signature, + }) + + expect(result).toEqual({ verified: false }) + }) + }) + + describe('getPublicKey', () => { + it('returns null if key does not exist', async () => { + const result = await service.getPublicKey(agentContext, 'nonexistent') + expect(result).toBeNull() + }) + + it('returns public key for EC key pair', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + const publicKey = await service.getPublicKey(agentContext, keyId) + + // Should not contain private key (d) component + expect(publicKey).toEqual({ + kid: keyId, + kty: 'EC', + crv: 'P-256', + // Public key should have x and y coordinates + x: expect.any(String), + y: expect.any(String), + }) + }) + + it('returns public key for Ed25519 key pair', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'Ed25519' }, + }) + + const publicKey = await service.getPublicKey(agentContext, keyId) + + // Should not contain private key (d) component + expect(publicKey).toEqual({ + kid: keyId, + kty: 'OKP', + crv: 'Ed25519', + // Public key should have x coordinate + x: expect.any(String), + }) + }) + + it('returns no key material for symmetric keys', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'aes', length: 256 }, + }) + + const key = await service.getPublicKey(agentContext, keyId) + + // Should not contain private key (k) component + expect(key).toEqual({ + kid: keyId, + kty: 'oct', + }) + }) + }) + + describe('importKey', () => { + it('throws error when importing RSA key', async () => { + await expect( + service.importKey(agentContext, { + privateJwk: { + kty: 'RSA', + n: 'test-n', + e: 'AQAB', + d: 'test-d', + p: 'test-p', + q: 'test-q', + dp: 'test-dp', + dq: 'test-dq', + qi: 'test-qi', + }, + }) + ).rejects.toThrow(new Kms.KeyManagementAlgorithmNotSupportedError(`kty 'RSA'`, service.backend)) + }) + + it('imports EC P-256 key pair with provided keyId', async () => { + const keyId = 'test-key-id' + + const result = await service.importKey(agentContext, { + privateJwk: { + kid: keyId, + kty: 'EC', + d: '58pb2cKWs0VmIXtHz3ayrZCGKRUnWrb9QvbfbAkGI3c', + crv: 'P-256', + x: 'wPuEY7sKE2x2rp96_QtnRhSswV2AgBk_cX5TCmvLxPs', + y: 'OG0Lm7begM02Vikg2iI70nknoWNygwlUoBGLLFDT3Zs', + }, + }) + + expect(result).toEqual({ + keyId, + publicJwk: { + kid: keyId, + kty: 'EC', + crv: 'P-256', + x: 'wPuEY7sKE2x2rp96_QtnRhSswV2AgBk_cX5TCmvLxPs', + y: 'OG0Lm7begM02Vikg2iI70nknoWNygwlUoBGLLFDT3Zs', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, keyId) + expect(storedKey).toEqual({ + kid: keyId, + kty: 'EC', + crv: 'P-256', + x: 'wPuEY7sKE2x2rp96_QtnRhSswV2AgBk_cX5TCmvLxPs', + y: 'OG0Lm7begM02Vikg2iI70nknoWNygwlUoBGLLFDT3Zs', + }) + }) + + it('imports EC P-384 key pair', async () => { + const result = await service.importKey(agentContext, { + privateJwk: { + kty: 'EC', + d: 'O2WHQQDOvifmepR3kxDRJh1TBd-LaEww5lYzrd14lzfi4IVIVm__ZQVoUQ0ws56e', + crv: 'P-384', + x: 'Vvlf4tmvKT1qTOptwSelZBoazQmrsKvg1poITeOoxqbZEgNvfa9cUObhQlbhHjGP', + y: 'gTMFQKmXdcK31ycnDULFEtCLF3vsXNnAcQcFbeapxqBpo_wMdSP-G8pN9jPMDPYS', + }, + }) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kid: result.keyId, + kty: 'EC', + crv: 'P-384', + x: 'Vvlf4tmvKT1qTOptwSelZBoazQmrsKvg1poITeOoxqbZEgNvfa9cUObhQlbhHjGP', + y: 'gTMFQKmXdcK31ycnDULFEtCLF3vsXNnAcQcFbeapxqBpo_wMdSP-G8pN9jPMDPYS', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, result.keyId) + expect(storedKey).toEqual({ + kid: result.keyId, + kty: 'EC', + crv: 'P-384', + x: 'Vvlf4tmvKT1qTOptwSelZBoazQmrsKvg1poITeOoxqbZEgNvfa9cUObhQlbhHjGP', + y: 'gTMFQKmXdcK31ycnDULFEtCLF3vsXNnAcQcFbeapxqBpo_wMdSP-G8pN9jPMDPYS', + }) + }) + + it('throws error when importing EC P-521 key pair', async () => { + await expect( + service.importKey(agentContext, { + privateJwk: { + kty: 'EC', + d: 'Af8IOTaFSKF65L6vI-UTAhUpO0LbtiK-2W-Qs5-jvpLAnmalTUNX3r7mZhH1zioq26NayCFTgEZVWAwMgeEqindK', + crv: 'P-521', + x: 'AfenCyIa_2pnNYybfgdhy19fVnrBksaHgQUy4bCu3kiA8_cZujnsO6RgpIWx2ip3cdXsi2ujK-mShjIveNwdwiBF', + y: 'AVKOcCI-Zg_0IlhpCJ77wwMFjXuVpt-nilcSQY9E0JADcXQGaWSAWKWpAbCAeeevoBHepELbIJ5bX3EnU3yKMMQL', + }, + }) + ).rejects.toThrow(new Kms.KeyManagementAlgorithmNotSupportedError(`crv 'P-521' for kty 'EC'`, service.backend)) + }) + + it('imports EC secp256k1 key pair', async () => { + const result = await service.importKey(agentContext, { + privateJwk: { + kty: 'EC', + d: 'eGYeYMILykL1YnAZde1aSo9uQtKe-HeALQu2Yv-ZcQ0', + crv: 'secp256k1', + x: 'ZLRfyFqy_hVG_SWH7SlErOCMkztJNoZZHdJvMt6yPSE', + y: 'O89repvsgjOY9qAOZcmdIiITHU4Frk00ryKGDw7OefQ', + }, + }) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kid: result.keyId, + kty: 'EC', + crv: 'secp256k1', + x: 'ZLRfyFqy_hVG_SWH7SlErOCMkztJNoZZHdJvMt6yPSE', + y: 'O89repvsgjOY9qAOZcmdIiITHU4Frk00ryKGDw7OefQ', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, result.keyId) + expect(storedKey).toEqual({ + kid: result.keyId, + kty: 'EC', + crv: 'secp256k1', + x: 'ZLRfyFqy_hVG_SWH7SlErOCMkztJNoZZHdJvMt6yPSE', + y: 'O89repvsgjOY9qAOZcmdIiITHU4Frk00ryKGDw7OefQ', + }) + }) + + it('imports OKP Ed25519 key pair', async () => { + const result = await service.importKey(agentContext, { + privateJwk: { + kty: 'OKP', + d: 'IbJKmlKmRDoSkO0xM_DkeorvBz--1O_qGlmrb6_1Cms', + crv: 'Ed25519', + x: '4-CJ6REW9mUtp2ouh5rhQ9wvfsZE278NnPffTkLeNYI', + }, + }) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kid: result.keyId, + kty: 'OKP', + crv: 'Ed25519', + x: '4-CJ6REW9mUtp2ouh5rhQ9wvfsZE278NnPffTkLeNYI', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, result.keyId) + expect(storedKey).toEqual({ + kid: result.keyId, + kty: 'OKP', + crv: 'Ed25519', + x: '4-CJ6REW9mUtp2ouh5rhQ9wvfsZE278NnPffTkLeNYI', + }) + }) + + it('imports OKP X25519 key pair', async () => { + const result = await service.importKey(agentContext, { + privateJwk: { + kty: 'OKP', + d: '7LL0_o4FsS4w-mCFhcKlbaX8qsqgeNjTxzDV4lVj0us', + crv: 'X25519', + x: 'DdYl5R2IpY7VwLr88mgG9PBjK7jICuipVYhOzz8F3Fs', + }, + }) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kid: result.keyId, + kty: 'OKP', + crv: 'X25519', + x: 'DdYl5R2IpY7VwLr88mgG9PBjK7jICuipVYhOzz8F3Fs', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, result.keyId) + expect(storedKey).toEqual({ + kid: result.keyId, + kty: 'OKP', + crv: 'X25519', + x: 'DdYl5R2IpY7VwLr88mgG9PBjK7jICuipVYhOzz8F3Fs', + }) + }) + + // NOTE: we need to tweak the API here a bit I think. Just the JWK is not enough + // we need something of an alg. + it('throws error when importing oct key pair', async () => { + await expect( + service.importKey(agentContext, { + privateJwk: { + kty: 'oct', + k: 'something', + }, + }) + ).rejects.toThrow( + new Kms.KeyManagementAlgorithmNotSupportedError(`importing keys with kty 'oct'`, service.backend) + ) + }) + + it('generates random keyId when not provided', async () => { + const privateJwk: Kms.KmsJwkPrivate = { + kty: 'EC', + d: 'ESGpJ7SIi3H7h9pkIkr-M8QDWamtiewze5_U_nP2fJg', + crv: 'P-256', + x: 'HlwSCoy8jWXx_KifMEnt4zDjPb0eyi0eH9C9awOdR70', + y: 's-Drm_bZ4eVV_UkGnLr62sI2TWibkdLFFc0dAT6ASL8', + } + + const result = await service.importKey(agentContext, { privateJwk }) + expect(result).toEqual({ + keyId: expect.any(String), + publicJwk: { + kid: expect.any(String), + kty: 'EC', + crv: 'P-256', + x: 'HlwSCoy8jWXx_KifMEnt4zDjPb0eyi0eH9C9awOdR70', + y: 's-Drm_bZ4eVV_UkGnLr62sI2TWibkdLFFc0dAT6ASL8', + }, + }) + }) + + it('throws error if invalid key data provided', async () => { + const privateJwk: Kms.KmsJwkPrivate = { + kty: 'EC', + crv: 'P-256', + x: 'test-x', + y: 'test-y', + d: 'test-d', + } + + await expect(service.importKey(agentContext, { privateJwk })).rejects.toThrow( + new Kms.KeyManagementError('Error importing key', { cause: new Error('Base64 decoding error') }) + ) + }) + + it('throws error if key with same id already exists', async () => { + const keyId = 'existing-key' + const privateJwk: Kms.KmsJwkPrivate = { + kid: keyId, + kty: 'EC', + d: '_jBF0d-pZB_Os3CrJsPthA-CDXSy17vCdyRzuAIFbaM', + crv: 'P-256', + x: 'IcwG4MdHi8u59kc5h-cQC31ZVC50g7qlJvWkzh_j9zw', + y: 'iY57CM0fuBNx5ef2iviA2OiUtfExERAFLyYD1yno6Xo', + } + + // First import succeeds + await service.importKey(agentContext, { privateJwk }) + + // Second import with same keyId fails + await expect(service.importKey(agentContext, { privateJwk })).rejects.toThrow( + new Kms.KeyManagementKeyExistsError('existing-key', service.backend) + ) + }) + + it('throws error when key is provided with unknown kty', async () => { + await expect( + service.importKey(agentContext, { + privateJwk: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + kty: 'something', + }, + }) + ).rejects.toThrow(new Kms.KeyManagementAlgorithmNotSupportedError(`kty 'something'`, service.backend)) + }) + }) + + describe('deleteKey', () => { + it('deletes existing key', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + // Verify key exists + expect(await service.getPublicKey(agentContext, keyId)).toBeTruthy() + + // Delete key + expect(await service.deleteKey(agentContext, { keyId })).toBe(true) + + // Verify key no longer exists + expect(await service.getPublicKey(agentContext, keyId)).toBeNull() + }) + + it('succeeds when deleting non-existent key', async () => { + expect(await service.deleteKey(agentContext, { keyId: 'nonexistent' })).toBe(false) + }) + + it('removes key from storage completely', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + await service.deleteKey(agentContext, { keyId }) + + // Verify we can't use the deleted key + await expect( + service.sign(agentContext, { + keyId, + algorithm: 'ES256', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow(new Kms.KeyManagementKeyNotFoundError(keyId, service.backend)) + }) + }) + + describe('randomBytes', () => { + it('generates random bytes', () => { + const { bytes } = service.randomBytes(agentContext, { + length: 32, + }) + + expect(bytes.length).toEqual(32) + }) + }) + + describe('encrypt', () => { + it('throws error if key is not found', async () => { + await expect( + service.encrypt(agentContext, { + key: 'nonexistent', + encryption: { + algorithm: 'A256GCM', + }, + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow(new Kms.KeyManagementKeyNotFoundError('nonexistent', service.backend)) + }) + + it('throws error for unsupported ECDH-EH+A192KW key agreement', async () => { + const senderKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + const recipientKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + + await expect( + service.encrypt(agentContext, { + key: { + keyId: senderKey.keyId, + algorithm: 'ECDH-ES+A192KW', + externalPublicJwk: recipientKey.publicJwk, + }, + + encryption: { + algorithm: 'XC20P', + }, + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementAlgorithmNotSupportedError(`JWA key agreement algorithm 'ECDH-ES+A192KW'`, service.backend) + ) + }) + + it('throw error if sender and recipient key types do not match', async () => { + const senderKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + const recipientKey = await service.createKey(agentContext, { + type: { + kty: 'EC', + crv: 'P-384', + }, + }) + + await expect( + service.encrypt(agentContext, { + key: { + keyId: senderKey.keyId, + algorithm: 'ECDH-ES', + externalPublicJwk: recipientKey.publicJwk, + }, + + encryption: { + algorithm: 'XC20P', + }, + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `Expected jwk types to match, but found OKP key with crv 'X25519' and EC key with crv 'P-384'` + ) + ) + }) + + it('throws error if key is not a symmetric key', async () => { + const encryptionKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + await expect( + service.encrypt(agentContext, { + key: encryptionKey.keyId, + encryption: { + algorithm: 'A128GCM', + }, + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `OKP key with crv 'X25519' cannot be used with algorithm 'A128GCM' for content encryption or decryption.` + ) + ) + }) + + it('throws error if encryption algorithm is not supported by backend', async () => { + const encryptionKey = await service.createKey(agentContext, { + type: { + kty: 'oct', + algorithm: 'aes', + length: 128, + }, + }) + await expect( + service.encrypt(agentContext, { + key: encryptionKey.keyId, + encryption: { + algorithm: 'A192GCM', + }, + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementAlgorithmNotSupportedError(`JWA encryption algorithm 'A192GCM'`, service.backend) + ) + }) + }) + + describe('decrypt', () => { + it('throws error if key is not found', async () => { + await expect( + service.decrypt(agentContext, { + key: 'nonexistent', + decryption: { + algorithm: 'A256GCM', + iv: new Uint8Array([]), + tag: new Uint8Array([]), + }, + encrypted: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow(new Kms.KeyManagementKeyNotFoundError('nonexistent', service.backend)) + }) + + it('throws error for unsupported ECDH-EH+A192KW key agreement', async () => { + const senderKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + const recipientKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + + await expect( + service.decrypt(agentContext, { + key: { + keyId: senderKey.keyId, + algorithm: 'ECDH-ES+A192KW', + externalPublicJwk: recipientKey.publicJwk, + encryptedKey: { + encrypted: new Uint8Array([]), + iv: new Uint8Array([]), + tag: new Uint8Array([]), + }, + }, + + decryption: { + algorithm: 'XC20P', + iv: new Uint8Array([]), + tag: new Uint8Array([]), + }, + encrypted: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementAlgorithmNotSupportedError(`JWA key agreement algorithm 'ECDH-ES+A192KW'`, service.backend) + ) + }) + + it('throw error if sender and recipient key types do not match', async () => { + const senderKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + const recipientKey = await service.createKey(agentContext, { + type: { + kty: 'EC', + crv: 'P-384', + }, + }) + + await expect( + service.decrypt(agentContext, { + key: { + keyId: senderKey.keyId, + algorithm: 'ECDH-ES', + externalPublicJwk: recipientKey.publicJwk, + }, + + decryption: { + algorithm: 'XC20P', + iv: new Uint8Array([]), + tag: new Uint8Array([]), + }, + encrypted: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `Expected jwk types to match, but found OKP key with crv 'X25519' and EC key with crv 'P-384'` + ) + ) + }) + + it('throws error if key is not a symmetric key', async () => { + const encryptionKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + await expect( + service.decrypt(agentContext, { + key: encryptionKey.keyId, + decryption: { + algorithm: 'A128GCM', + iv: new Uint8Array([]), + tag: new Uint8Array([]), + }, + encrypted: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `OKP key with crv 'X25519' cannot be used with algorithm 'A128GCM' for content encryption or decryption.` + ) + ) + }) + + it('throws error if encryption algorithm is not supported by backend', async () => { + const encryptionKey = await service.createKey(agentContext, { + type: { + kty: 'oct', + algorithm: 'aes', + length: 128, + }, + }) + await expect( + service.decrypt(agentContext, { + key: encryptionKey.keyId, + decryption: { + algorithm: 'A192GCM', + iv: new Uint8Array([]), + tag: new Uint8Array([]), + }, + encrypted: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementAlgorithmNotSupportedError(`JWA encryption algorithm 'A192GCM'`, service.backend) + ) + }) + + it('decrypts JWE using ECDH-ES and A256GCM based on test vector from OpenID Conformance test', async () => { + const { + compactJwe, + decodedPayload, + privateKeyJwk, + header: expectedHeader, + } = JSON.parse( + readFileSync(path.join(__dirname, '../__fixtures__/jarm-jwe-encrypted-response.json')).toString('utf-8') + ) as { + compactJwe: string + decodedPayload: Record + privateKeyJwk: Kms.KmsJwkPrivate + header: string + } + + const [encodedHeader /* encryptionKey */, , encodedIv, encodedCiphertext, encodedTag] = compactJwe.split('.') + const header = JsonEncoder.fromBase64(encodedHeader) + + const recipientKey = await service.importKey(agentContext, { privateJwk: privateKeyJwk }) + const { data } = await service.decrypt(agentContext, { + decryption: { + algorithm: 'A256GCM', + iv: TypedArrayEncoder.fromBase64(encodedIv), + tag: TypedArrayEncoder.fromBase64(encodedTag), + aad: TypedArrayEncoder.fromString(encodedHeader), + }, + key: { + algorithm: 'ECDH-ES', + externalPublicJwk: header.epk, + keyId: recipientKey.keyId, + apu: TypedArrayEncoder.fromBase64(header.apu), + apv: TypedArrayEncoder.fromBase64(header.apv), + }, + encrypted: TypedArrayEncoder.fromBase64(encodedCiphertext), + }) + + expect(header).toEqual(expectedHeader) + expect(JsonEncoder.fromBuffer(data)).toEqual(decodedPayload) + }) + }) + + describe('encryption and decryption', () => { + it('encrypts and decrypts with A256CBC-HS512', async () => { + const encryptionKey = await service.createKey(agentContext, { + type: { + kty: 'oct', + // TODO: just pass an encryption algorithm here? That is easier than + // exactly knowing the required input params for an alg + algorithm: 'aes', + length: 512, + }, + }) + const result = await service.encrypt(agentContext, { + key: encryptionKey.keyId, + encryption: { + algorithm: 'A256CBC-HS512', + }, + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + encrypted: expect.any(Uint8Array), + iv: expect.any(Uint8Array), + tag: expect.any(Uint8Array), + }) + + const decrypted = await service.decrypt(agentContext, { + key: encryptionKey.keyId, + decryption: { + algorithm: 'A256CBC-HS512', + iv: result.iv as Uint8Array, + tag: result.tag as Uint8Array, + }, + encrypted: result.encrypted, + }) + + expect(decrypted.data).toEqual(new Uint8Array([1, 2, 3])) + }) + + it('encrypts and decrypts with A128CBC-HS256', async () => { + const encryptionKey = await service.createKey(agentContext, { + type: { + kty: 'oct', + // TODO: just pass an encryption algorithm here? That is easier than + // exactly knowing the required input params for an alg + algorithm: 'aes', + length: 256, + }, + }) + const result = await service.encrypt(agentContext, { + key: encryptionKey.keyId, + encryption: { + algorithm: 'A128CBC-HS256', + }, + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + encrypted: expect.any(Uint8Array), + iv: expect.any(Uint8Array), + tag: expect.any(Uint8Array), + }) + + const decrypted = await service.decrypt(agentContext, { + key: encryptionKey.keyId, + decryption: { + algorithm: 'A128CBC-HS256', + iv: result.iv as Uint8Array, + tag: result.tag as Uint8Array, + }, + encrypted: result.encrypted, + }) + + expect(decrypted.data).toEqual(new Uint8Array([1, 2, 3])) + }) + + it('encrypts and decrypts with C20P', async () => { + const encryptionKey = await service.createKey(agentContext, { + type: { + kty: 'oct', + algorithm: 'C20P', + }, + }) + const result = await service.encrypt(agentContext, { + key: encryptionKey.keyId, + encryption: { + algorithm: 'C20P', + }, + data: new Uint8Array([1, 2, 3]), + }) + + const decrypted = await service.decrypt(agentContext, { + key: encryptionKey.keyId, + decryption: { + algorithm: 'C20P', + iv: result.iv as Uint8Array, + tag: result.tag as Uint8Array, + }, + encrypted: result.encrypted, + }) + + expect(decrypted.data).toEqual(new Uint8Array([1, 2, 3])) + }) + + it('encrypts and decrypts with XC20P', async () => { + const encryptionKey = await service.createKey(agentContext, { + type: { + kty: 'oct', + algorithm: 'C20P', + }, + }) + const result = await service.encrypt(agentContext, { + key: encryptionKey.keyId, + encryption: { + algorithm: 'XC20P', + }, + data: new Uint8Array([1, 2, 3]), + }) + + const decrypted = await service.decrypt(agentContext, { + key: encryptionKey.keyId, + decryption: { + algorithm: 'XC20P', + iv: result.iv as Uint8Array, + tag: result.tag as Uint8Array, + }, + encrypted: result.encrypted, + }) + + expect(decrypted.data).toEqual(new Uint8Array([1, 2, 3])) + }) + + it('encrypts and decrypts with A256GCM', async () => { + const encryptionKey = await service.createKey(agentContext, { + type: { + kty: 'oct', + algorithm: 'aes', + length: 256, + }, + }) + const result = await service.encrypt(agentContext, { + key: encryptionKey.keyId, + encryption: { + algorithm: 'A256GCM', + }, + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + encrypted: expect.any(Uint8Array), + iv: expect.any(Uint8Array), + tag: expect.any(Uint8Array), + }) + + const decrypted = await service.decrypt(agentContext, { + key: encryptionKey.keyId, + decryption: { + algorithm: 'A256GCM', + iv: result.iv as Uint8Array, + tag: result.tag as Uint8Array, + }, + encrypted: result.encrypted, + }) + + expect(decrypted.data).toEqual(new Uint8Array([1, 2, 3])) + }) + + it('encrypts and decrypts with A128GCM', async () => { + const encryptionKey = await service.createKey(agentContext, { + type: { + kty: 'oct', + algorithm: 'aes', + length: 128, + }, + }) + const result = await service.encrypt(agentContext, { + key: encryptionKey.keyId, + encryption: { + algorithm: 'A128GCM', + }, + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + encrypted: expect.any(Uint8Array), + iv: expect.any(Uint8Array), + tag: expect.any(Uint8Array), + }) + + const decrypted = await service.decrypt(agentContext, { + key: encryptionKey.keyId, + decryption: { + algorithm: 'A128GCM', + iv: result.iv as Uint8Array, + tag: result.tag as Uint8Array, + }, + encrypted: result.encrypted, + }) + + expect(decrypted.data).toEqual(new Uint8Array([1, 2, 3])) + }) + + it('encrypts and decrypts with A128GCM and ECDH-ES key agreement', async () => { + const encryptionKey = await service.createKey(agentContext, { + type: { + kty: 'EC', + crv: 'P-256', + }, + }) + const recipientKey = await service.createKey(agentContext, { + type: { + kty: 'EC', + crv: 'P-256', + }, + }) + + const result = await service.encrypt(agentContext, { + key: { + keyId: encryptionKey.keyId, + algorithm: 'ECDH-ES', + externalPublicJwk: recipientKey.publicJwk, + }, + + encryption: { + algorithm: 'A128GCM', + }, + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + encrypted: expect.any(Uint8Array), + iv: expect.any(Uint8Array), + tag: expect.any(Uint8Array), + }) + + const decrypted = await service.decrypt(agentContext, { + key: { + keyId: encryptionKey.keyId, + algorithm: 'ECDH-ES', + externalPublicJwk: recipientKey.publicJwk, + }, + + decryption: { + algorithm: 'A128GCM', + iv: result.iv as Uint8Array, + tag: result.tag as Uint8Array, + }, + encrypted: result.encrypted, + }) + + expect(decrypted.data).toEqual(new Uint8Array([1, 2, 3])) + }) + + it('encrypts and decrypts with A256GCM and ECDH-EH+A128KW key agreement', async () => { + const encryptionKey = await service.createKey(agentContext, { + type: { + kty: 'EC', + crv: 'P-256', + }, + }) + const recipientKey = await service.createKey(agentContext, { + type: { + kty: 'EC', + crv: 'P-256', + }, + }) + + const result = await service.encrypt(agentContext, { + key: { + keyId: encryptionKey.keyId, + algorithm: 'ECDH-ES+A128KW', + externalPublicJwk: recipientKey.publicJwk, + }, + + encryption: { + algorithm: 'A256GCM', + }, + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + encryptedKey: { + encrypted: expect.any(Uint8Array), + iv: expect.any(Uint8Array), + tag: expect.any(Uint8Array), + }, + encrypted: expect.any(Uint8Array), + iv: expect.any(Uint8Array), + tag: expect.any(Uint8Array), + }) + + const decrypted = await service.decrypt(agentContext, { + key: { + keyId: encryptionKey.keyId, + algorithm: 'ECDH-ES+A128KW', + externalPublicJwk: recipientKey.publicJwk, + encryptedKey: result.encryptedKey as Kms.KmsEncryptedKey, + }, + + decryption: { + algorithm: 'A256GCM', + iv: result.iv as Uint8Array, + tag: result.tag as Uint8Array, + }, + encrypted: result.encrypted, + }) + + expect(decrypted.data).toEqual(new Uint8Array([1, 2, 3])) + }) + + it('encrypts and decrypts with XC20P and ECDH-EH+A256KW key agreement', async () => { + const senderKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + const recipientKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + + const result = await service.encrypt(agentContext, { + key: { + keyId: senderKey.keyId, + algorithm: 'ECDH-ES+A256KW', + externalPublicJwk: recipientKey.publicJwk, + }, + + encryption: { + algorithm: 'XC20P', + }, + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + encryptedKey: { + encrypted: expect.any(Uint8Array), + iv: expect.any(Uint8Array), + tag: expect.any(Uint8Array), + }, + encrypted: expect.any(Uint8Array), + iv: expect.any(Uint8Array), + tag: expect.any(Uint8Array), + }) + + const decrypted = await service.decrypt(agentContext, { + key: { + keyId: senderKey.keyId, + algorithm: 'ECDH-ES+A256KW', + externalPublicJwk: recipientKey.publicJwk, + encryptedKey: result.encryptedKey as Kms.KmsEncryptedKey, + }, + + decryption: { + algorithm: 'XC20P', + iv: result.iv as Uint8Array, + tag: result.tag as Uint8Array, + }, + encrypted: result.encrypted, + }) + + expect(decrypted.data).toEqual(new Uint8Array([1, 2, 3])) + }) + }) + + describe('didcomm', () => { + it('encrypts and decrypts DIDComm v1 Anoncrypt message', async () => { + const recipientKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + const { bytes: contentEncryptionKey } = service.randomBytes(agentContext, { length: 32 }) + + const { encrypted: encryptedKey } = await service.encrypt(agentContext, { + data: contentEncryptionKey, + encryption: { + algorithm: 'XSALSA20-POLY1305', + }, + key: { + algorithm: 'ECDH-HSALSA20', + externalPublicJwk: recipientKey.publicJwk, + }, + }) + + const { + encrypted: encryptedMessage, + iv, + tag, + } = await service.encrypt(agentContext, { + data: JsonEncoder.toBuffer({ + '@type': 'https://didcomm.org/message/1.0/message', + }), + encryption: { + algorithm: 'XC20P', + aad: JsonEncoder.toBuffer({ + the: 'header', + }), + }, + key: { + kty: 'oct', + k: TypedArrayEncoder.toBase64URL(contentEncryptionKey), + }, + }) + + if (!tag || !iv) throw new Error('expected tag and iv') + + const { data: decryptedKey } = await service.decrypt(agentContext, { + decryption: { + algorithm: 'XSALSA20-POLY1305', + }, + key: { + algorithm: 'ECDH-HSALSA20', + keyId: recipientKey.keyId, + }, + encrypted: encryptedKey, + }) + + expect(Buffer.from(decryptedKey).equals(Buffer.from(contentEncryptionKey))).toEqual(true) + + const { data: decryptedMessage } = await service.decrypt(agentContext, { + decryption: { + algorithm: 'XC20P', + iv, + tag, + aad: JsonEncoder.toBuffer({ + the: 'header', + }), + }, + encrypted: encryptedMessage, + key: { + kty: 'oct', + k: TypedArrayEncoder.toBase64URL(decryptedKey), + }, + }) + + expect(JsonEncoder.fromBuffer(decryptedMessage)).toEqual({ + '@type': 'https://didcomm.org/message/1.0/message', + }) + }) + + it('encrypts and decrypts DIDComm v1 Authcrypt message', async () => { + const recipientKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + const senderKey = await service.createKey(agentContext, { + type: { + kty: 'OKP', + crv: 'X25519', + }, + }) + const { bytes: contentEncryptionKey } = service.randomBytes(agentContext, { length: 32 }) + const senderPublicJwk = Kms.PublicJwk.fromPublicJwk(senderKey.publicJwk) + + const { encrypted: encryptedSender } = await service.encrypt(agentContext, { + data: TypedArrayEncoder.fromString(TypedArrayEncoder.toBase58(senderPublicJwk.publicKey.publicKey)), + encryption: { + algorithm: 'XSALSA20-POLY1305', + }, + key: { + algorithm: 'ECDH-HSALSA20', + externalPublicJwk: recipientKey.publicJwk, + }, + }) + + const { encrypted: encryptedKey, iv: encryptedKeyIv } = await service.encrypt(agentContext, { + data: contentEncryptionKey, + encryption: { + algorithm: 'XSALSA20-POLY1305', + }, + key: { + algorithm: 'ECDH-HSALSA20', + externalPublicJwk: recipientKey.publicJwk, + keyId: senderKey.keyId, + }, + }) + + const { + encrypted: encryptedMessage, + iv, + tag, + } = await service.encrypt(agentContext, { + data: JsonEncoder.toBuffer({ + '@type': 'https://didcomm.org/message/1.0/message', + }), + encryption: { + algorithm: 'XC20P', + aad: JsonEncoder.toBuffer({ + the: 'header', + }), + }, + key: { + kty: 'oct', + k: TypedArrayEncoder.toBase64URL(contentEncryptionKey), + }, + }) + + if (!tag || !iv) throw new Error('expected tag and iv') + + const { data: decryptedSender } = await service.decrypt(agentContext, { + decryption: { + algorithm: 'XSALSA20-POLY1305', + }, + key: { + algorithm: 'ECDH-HSALSA20', + keyId: recipientKey.keyId, + }, + encrypted: encryptedSender, + }) + + expect(TypedArrayEncoder.toUtf8String(decryptedSender)).toEqual( + TypedArrayEncoder.toBase58(senderPublicJwk.publicKey.publicKey) + ) + + const { data: decryptedKey } = await service.decrypt(agentContext, { + decryption: { + algorithm: 'XSALSA20-POLY1305', + iv: encryptedKeyIv, + }, + key: { + algorithm: 'ECDH-HSALSA20', + keyId: recipientKey.keyId, + externalPublicJwk: senderKey.publicJwk, + }, + encrypted: encryptedKey, + }) + + expect(Buffer.from(decryptedKey).equals(Buffer.from(contentEncryptionKey))).toEqual(true) + + const { data: decryptedMessage } = await service.decrypt(agentContext, { + decryption: { + algorithm: 'XC20P', + iv, + tag, + aad: JsonEncoder.toBuffer({ + the: 'header', + }), + }, + encrypted: encryptedMessage, + key: { + kty: 'oct', + k: TypedArrayEncoder.toBase64URL(decryptedKey), + }, + }) + + expect(JsonEncoder.fromBuffer(decryptedMessage)).toEqual({ + '@type': 'https://didcomm.org/message/1.0/message', + }) + }) + }) +}) diff --git a/packages/askar/src/kms/crypto/decrypt.ts b/packages/askar/src/kms/crypto/decrypt.ts new file mode 100644 index 0000000000..9bd68b9388 --- /dev/null +++ b/packages/askar/src/kms/crypto/decrypt.ts @@ -0,0 +1,30 @@ +import { Kms } from '@credo-ts/core' +import { Key } from '@openwallet-foundation/askar-shared' +import { jwkEncToAskarAlg } from '../../utils' + +// TODO: should we make these methods generic, so they can be reused across backends? +type AskarSupportedDecryptionOptions = Kms.KmsDecryptDataDecryption & { + algorithm: keyof typeof jwkEncToAskarAlg +} + +export function decrypt(options: { + key: Key + decryption: AskarSupportedDecryptionOptions + encrypted: Uint8Array +}) { + const { key, decryption, encrypted } = options + + const askarEncryptionAlgorithm = jwkEncToAskarAlg[decryption.algorithm] + if (!askarEncryptionAlgorithm) { + throw new Kms.KeyManagementAlgorithmNotSupportedError(`JWA decryption algorithm '${decryption.algorithm}'`, 'askar') + } + + const decrypted = key.aeadDecrypt({ + ciphertext: encrypted, + tag: decryption.tag, + aad: decryption.aad, + nonce: decryption.iv, + }) + + return decrypted +} diff --git a/packages/askar/src/kms/crypto/deriveKey.ts b/packages/askar/src/kms/crypto/deriveKey.ts new file mode 100644 index 0000000000..338f946c4f --- /dev/null +++ b/packages/askar/src/kms/crypto/deriveKey.ts @@ -0,0 +1,173 @@ +import { Kms, TypedArrayEncoder } from '@credo-ts/core' +import { Key, askar } from '@openwallet-foundation/askar-shared' +import { jwkEncToAskarAlg } from '../../utils' + +export const askarSupportedKeyAgreementAlgorithms = [ + 'ECDH-ES', + 'ECDH-ES+A128KW', + 'ECDH-ES+A256KW', + 'ECDH-HSALSA20', +] satisfies Kms.KnownJwaKeyAgreementAlgorithm[] + +type AskarSupportedKeyAgreementEncryptOptions = Kms.KmsKeyAgreementEncryptOptions & { + algorithm: (typeof askarSupportedKeyAgreementAlgorithms)[number] +} + +type AskarSupportedKeyAgreementDecryptOptions = Kms.KmsKeyAgreementDecryptOptions & { + algorithm: (typeof askarSupportedKeyAgreementAlgorithms)[number] +} + +export function deriveEncryptionKey(options: { + keyAgreement: AskarSupportedKeyAgreementEncryptOptions + senderKey: Key + recipientKey: Key + encryption: Kms.KmsEncryptDataEncryption +}) { + const { keyAgreement, encryption, senderKey, recipientKey } = options + + const askarEncryptionAlgorithm = jwkEncToAskarAlg[encryption.algorithm as keyof typeof jwkEncToAskarAlg] + if (!askarEncryptionAlgorithm) { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `encryption with algorithm '${encryption.algorithm}'`, + 'askar' + ) + } + + // This should be handled on a higher level as we only support combined key agreemnt + encryption + if (keyAgreement.algorithm === 'ECDH-HSALSA20') { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `derive key for algorithm '${keyAgreement.algorithm}' with encryption algorithm '${encryption.algorithm}'`, + 'askar' + ) + } + + const askarKeyWrappingAlgorithm = + keyAgreement.algorithm !== 'ECDH-ES' + ? jwkEncToAskarAlg[keyAgreement.algorithm.replace('ECDH-ES+', '') as keyof typeof jwkEncToAskarAlg] + : undefined + + const derivedKey = new Key( + askar.keyDeriveEcdhEs({ + algId: TypedArrayEncoder.fromString( + keyAgreement.algorithm === 'ECDH-ES' ? encryption.algorithm : keyAgreement.algorithm + ), + receive: false, + apv: keyAgreement.apv ?? new Uint8Array([]), + apu: keyAgreement.apu ?? new Uint8Array([]), + algorithm: askarKeyWrappingAlgorithm ?? askarEncryptionAlgorithm, + ephemeralKey: senderKey, + recipientKey: recipientKey, + }) + ) + let contentEncryptionKey: Key | undefined = undefined + let encryptedContentEncryptionKey: Kms.KmsEncryptedKey | undefined + try { + // Key wrapping + if (keyAgreement.algorithm !== 'ECDH-ES') { + contentEncryptionKey = Key.generate(askarEncryptionAlgorithm) + + const wrappedKey = derivedKey.wrapKey({ + other: contentEncryptionKey, + }) + encryptedContentEncryptionKey = { + encrypted: wrappedKey.ciphertext, + iv: wrappedKey.nonce, + tag: wrappedKey.tag, + } + } + + return { + contentEncryptionKey: contentEncryptionKey ?? derivedKey, + encryptedContentEncryptionKey, + } + } catch (error) { + if (contentEncryptionKey) { + contentEncryptionKey.handle.free() + } + // We only free the derived key if there is no content encryption key + // as in this case the derived key is already freed in the finally clause + else { + derivedKey.handle.free() + } + + throw error + } finally { + // If there is a content encryption key, it means we can free the + // derived key + if (contentEncryptionKey) { + derivedKey.handle.free() + } + } +} + +export function deriveDecryptionKey(options: { + keyAgreement: AskarSupportedKeyAgreementDecryptOptions + senderKey: Key + recipientKey: Key + decryption: Kms.KmsDecryptDataDecryption +}) { + const { keyAgreement, decryption, senderKey, recipientKey } = options + + const askarEncryptionAlgorithm = jwkEncToAskarAlg[decryption.algorithm as keyof typeof jwkEncToAskarAlg] + if (!askarEncryptionAlgorithm) { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `decryption with algorithm '${decryption.algorithm}'`, + 'askar' + ) + } + + if (keyAgreement.algorithm === 'ECDH-HSALSA20') { + // This should be handled on a higher level as we only support combined key agreemnt + encryption + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `derive key for algorithm '${keyAgreement.algorithm}' with encryption algorithm '${decryption.algorithm}'`, + 'askar' + ) + } + + const askarKeyWrappingAlgorithm = + keyAgreement.algorithm !== 'ECDH-ES' + ? jwkEncToAskarAlg[keyAgreement.algorithm.replace('ECDH-ES+', '') as keyof typeof jwkEncToAskarAlg] + : undefined + + const derivedKey = new Key( + askar.keyDeriveEcdhEs({ + algId: TypedArrayEncoder.fromString( + keyAgreement.algorithm === 'ECDH-ES' ? decryption.algorithm : keyAgreement.algorithm + ), + receive: true, + apv: keyAgreement.apv ?? new Uint8Array([]), + apu: keyAgreement.apu ?? new Uint8Array([]), + algorithm: askarKeyWrappingAlgorithm ?? askarEncryptionAlgorithm, + ephemeralKey: senderKey, + recipientKey: recipientKey, + }) + ) + + let contentEncryptionKey: Key | undefined = undefined + try { + // Key wrapping + if (keyAgreement.algorithm !== 'ECDH-ES') { + contentEncryptionKey = derivedKey.unwrapKey({ + ciphertext: keyAgreement.encryptedKey.encrypted, + algorithm: askarEncryptionAlgorithm, + nonce: keyAgreement.encryptedKey.iv, + tag: keyAgreement.encryptedKey.tag, + }) + } + + return { + contentEncryptionKey: contentEncryptionKey ?? derivedKey, + } + } catch (error) { + if (contentEncryptionKey) { + contentEncryptionKey.handle.free() + } else { + derivedKey.handle.free() + } + throw error + } finally { + if (contentEncryptionKey) { + derivedKey.handle.free() + } + } +} diff --git a/packages/askar/src/kms/crypto/encrypt.ts b/packages/askar/src/kms/crypto/encrypt.ts new file mode 100644 index 0000000000..ecf1fc0458 --- /dev/null +++ b/packages/askar/src/kms/crypto/encrypt.ts @@ -0,0 +1,32 @@ +import { Kms } from '@credo-ts/core' +import { Key } from '@openwallet-foundation/askar-shared' +import { jwkEncToAskarAlg } from '../../utils' + +export type AskarSupportedEncryptionOptions = Kms.KmsEncryptDataEncryption & { + algorithm: keyof typeof jwkEncToAskarAlg +} + +export function encrypt(options: { + key: Key + encryption: AskarSupportedEncryptionOptions + data: Uint8Array +}) { + const { key, encryption, data } = options + + const askarEncryptionAlgorithm = jwkEncToAskarAlg[encryption.algorithm] + if (!askarEncryptionAlgorithm) { + throw new Kms.KeyManagementAlgorithmNotSupportedError(`JWA encryption algorithm '${encryption.algorithm}'`, 'askar') + } + + const encrypted = key.aeadEncrypt({ + message: data, + aad: 'aad' in encryption ? encryption.aad : undefined, + nonce: 'iv' in encryption ? encryption.iv : undefined, + }) + + return { + encrypted: encrypted.ciphertext, + iv: encrypted.nonce, + tag: encrypted.tag, + } +} diff --git a/packages/askar/src/kms/crypto/randomBytes.ts b/packages/askar/src/kms/crypto/randomBytes.ts new file mode 100644 index 0000000000..19bbfbc06e --- /dev/null +++ b/packages/askar/src/kms/crypto/randomBytes.ts @@ -0,0 +1,16 @@ +import { CryptoBox } from '@openwallet-foundation/askar-shared' + +export function randomBytes(length: number): Uint8Array { + const buffer = new Uint8Array(length) + const CBOX_NONCE_LENGTH = 24 + + const genCount = Math.ceil(length / CBOX_NONCE_LENGTH) + const buf = new Uint8Array(genCount * CBOX_NONCE_LENGTH) + for (let i = 0; i < genCount; i++) { + const randomBytes = CryptoBox.randomNonce() + buf.set(randomBytes, CBOX_NONCE_LENGTH * i) + } + buffer.set(buf.subarray(0, length)) + + return buffer +} diff --git a/packages/askar/src/storage/AskarStorageService.ts b/packages/askar/src/storage/AskarStorageService.ts index 3e88d77b3d..bb4d1d4a6f 100644 --- a/packages/askar/src/storage/AskarStorageService.ts +++ b/packages/askar/src/storage/AskarStorageService.ts @@ -6,28 +6,33 @@ import type { QueryOptions, StorageService, } from '@credo-ts/core' - -import { JsonTransformer, RecordDuplicateError, RecordNotFoundError, WalletError, injectable } from '@credo-ts/core' +import { JsonTransformer, RecordDuplicateError, RecordNotFoundError, injectable } from '@credo-ts/core' +import { Session } from '@openwallet-foundation/askar-shared' import { Scan } from '@openwallet-foundation/askar-shared' +import { AskarStoreManager } from '../AskarStoreManager' import { AskarErrorCode, isAskarError } from '../utils/askarError' -import { assertAskarWallet } from '../utils/assertAskarWallet' +import { AskarError } from '../error' import { askarQueryFromSearchQuery, recordToInstance, transformFromRecordTagValues } from './utils' @injectable() export class AskarStorageService implements StorageService { + public constructor(private askarStoreManager: AskarStoreManager) {} + + private withSession(agentContext: AgentContext, callback: (session: Session) => Return) { + return this.askarStoreManager.withSession(agentContext, callback) + } + /** @inheritDoc */ public async save(agentContext: AgentContext, record: T) { - assertAskarWallet(agentContext.wallet) - record.updatedAt = new Date() const value = JsonTransformer.serialize(record) const tags = transformFromRecordTagValues(record.getTags()) as Record try { - await agentContext.wallet.withSession((session) => + await this.withSession(agentContext, (session) => session.insert({ category: record.type, name: record.id, value, tags }) ) } catch (error) { @@ -35,21 +40,19 @@ export class AskarStorageService implements StorageService throw new RecordDuplicateError(`Record with id ${record.id} already exists`, { recordType: record.type }) } - throw new WalletError('Error saving record', { cause: error }) + throw new AskarError('Error saving record', { cause: error }) } } /** @inheritDoc */ public async update(agentContext: AgentContext, record: T): Promise { - assertAskarWallet(agentContext.wallet) - record.updatedAt = new Date() const value = JsonTransformer.serialize(record) const tags = transformFromRecordTagValues(record.getTags()) as Record try { - await agentContext.wallet.withSession((session) => + await this.withSession(agentContext, (session) => session.replace({ category: record.type, name: record.id, value, tags }) ) } catch (error) { @@ -60,16 +63,14 @@ export class AskarStorageService implements StorageService }) } - throw new WalletError('Error updating record', { cause: error }) + throw new AskarError('Error updating record', { cause: error }) } } /** @inheritDoc */ public async delete(agentContext: AgentContext, record: T) { - assertAskarWallet(agentContext.wallet) - try { - await agentContext.wallet.withSession((session) => session.remove({ category: record.type, name: record.id })) + await this.withSession(agentContext, (session) => session.remove({ category: record.type, name: record.id })) } catch (error) { if (isAskarError(error, AskarErrorCode.NotFound)) { throw new RecordNotFoundError(`record with id ${record.id} not found.`, { @@ -77,7 +78,7 @@ export class AskarStorageService implements StorageService cause: error, }) } - throw new WalletError('Error deleting record', { cause: error }) + throw new AskarError('Error deleting record', { cause: error }) } } @@ -87,10 +88,8 @@ export class AskarStorageService implements StorageService recordClass: BaseRecordConstructor, id: string ): Promise { - assertAskarWallet(agentContext.wallet) - try { - await agentContext.wallet.withSession((session) => session.remove({ category: recordClass.type, name: id })) + await this.withSession(agentContext, (session) => session.remove({ category: recordClass.type, name: id })) } catch (error) { if (isAskarError(error, AskarErrorCode.NotFound)) { throw new RecordNotFoundError(`record with id ${id} not found.`, { @@ -98,16 +97,14 @@ export class AskarStorageService implements StorageService cause: error, }) } - throw new WalletError('Error deleting record', { cause: error }) + throw new AskarError('Error deleting record', { cause: error }) } } /** @inheritDoc */ public async getById(agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string): Promise { - assertAskarWallet(agentContext.wallet) - try { - const record = await agentContext.wallet.withSession((session) => + const record = await this.withSession(agentContext, (session) => session.fetch({ category: recordClass.type, name: id }) ) if (!record) { @@ -118,15 +115,13 @@ export class AskarStorageService implements StorageService return recordToInstance(record, recordClass) } catch (error) { if (error instanceof RecordNotFoundError) throw error - throw new WalletError(`Error getting record ${recordClass.name}`, { cause: error }) + throw new AskarError(`Error getting record ${recordClass.name}`, { cause: error }) } } /** @inheritDoc */ public async getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor): Promise { - assertAskarWallet(agentContext.wallet) - - const records = await agentContext.wallet.withSession((session) => session.fetchAll({ category: recordClass.type })) + const records = await this.withSession(agentContext, (session) => session.fetchAll({ category: recordClass.type })) const instances = [] for (const record of records) { @@ -142,16 +137,14 @@ export class AskarStorageService implements StorageService query: Query, queryOptions?: QueryOptions ): Promise { - const wallet = agentContext.wallet - assertAskarWallet(wallet) - const askarQuery = askarQueryFromSearchQuery(query) + const { store, profile } = await this.askarStoreManager.getInitializedStoreWithProfile(agentContext) const scan = new Scan({ category: recordClass.type, - store: wallet.store, + store, tagFilter: askarQuery, - profile: wallet.profile, + profile, offset: queryOptions?.offset, limit: queryOptions?.limit, }) @@ -164,7 +157,7 @@ export class AskarStorageService implements StorageService } return instances } catch (error) { - throw new WalletError(`Error executing query. ${error.message}`, { cause: error }) + throw new AskarError(`Error executing query. ${error.message}`, { cause: error }) } } } diff --git a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts index a9047cf69c..9fb27b9e53 100644 --- a/packages/askar/src/storage/__tests__/AskarStorageService.test.ts +++ b/packages/askar/src/storage/__tests__/AskarStorageService.test.ts @@ -1,38 +1,54 @@ import type { AgentContext, TagsBase } from '@credo-ts/core' -import { RecordDuplicateError, RecordNotFoundError, SigningProviderRegistry, TypedArrayEncoder } from '@credo-ts/core' +import { RecordDuplicateError, RecordNotFoundError, TypedArrayEncoder } from '@credo-ts/core' import { askar } from '@openwallet-foundation/askar-nodejs' import { TestRecord } from '../../../../core/src/storage/__tests__/TestRecord' -import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' -import { AskarWallet } from '../../wallet/AskarWallet' +import { getAgentConfig, getAgentContext, getAskarStoreConfig } from '../../../../core/tests/helpers' +import { NodeFileSystem } from '../../../../node/src/NodeFileSystem' +import { AskarModuleConfig } from '../../AskarModuleConfig' +import { AskarStoreManager } from '../../AskarStoreManager' import { AskarStorageService } from '../AskarStorageService' import { askarQueryFromSearchQuery } from '../utils' const startDate = Date.now() describe('AskarStorageService', () => { - let wallet: AskarWallet let storageService: AskarStorageService + let storeManager: AskarStoreManager let agentContext: AgentContext beforeEach(async () => { const agentConfig = getAgentConfig('AskarStorageServiceTest') - wallet = new AskarWallet(agentConfig.logger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) agentContext = getAgentContext({ - wallet, agentConfig, }) - await wallet.createAndOpen(agentConfig.walletConfig) - storageService = new AskarStorageService() + storeManager = new AskarStoreManager( + new NodeFileSystem(), + new AskarModuleConfig({ + askar, + store: getAskarStoreConfig('AskarStorageServiceTest', { + inMemory: true, + }), + }) + ) + storageService = new AskarStorageService(storeManager) + + await storeManager.provisionStore(agentContext) }) afterEach(async () => { - await wallet.delete() + await storeManager.deleteStore(agentContext) }) - const insertRecord = async ({ id, tags }: { id?: string; tags?: TagsBase }) => { + const insertRecord = async ({ + id, + tags, + }: { + id?: string + tags?: TagsBase + }) => { const props = { id, foo: 'bar', @@ -59,7 +75,7 @@ describe('AskarStorageService', () => { }, }) - const retrieveRecord = await wallet.withSession((session) => + const retrieveRecord = await storeManager.withSession(agentContext, (session) => askar.sessionFetch({ category: record.type, name: record.id, @@ -83,7 +99,8 @@ describe('AskarStorageService', () => { }) it('should correctly transform tag values from string after retrieving', async () => { - await wallet.withSession( + await storeManager.withSession( + agentContext, async (session) => await askar.sessionUpdate({ category: TestRecord.type, @@ -130,7 +147,7 @@ describe('AskarStorageService', () => { it('should throw RecordDuplicateError if a record with the id already exists', async () => { const record = await insertRecord({ id: 'test-id' }) - return expect(() => storageService.save(agentContext, record)).rejects.toThrowError(RecordDuplicateError) + return expect(() => storageService.save(agentContext, record)).rejects.toThrow(RecordDuplicateError) }) it('should save the record', async () => { @@ -148,7 +165,7 @@ describe('AskarStorageService', () => { describe('getById()', () => { it('should throw RecordNotFoundError if the record does not exist', async () => { - return expect(() => storageService.getById(agentContext, TestRecord, 'does-not-exist')).rejects.toThrowError( + return expect(() => storageService.getById(agentContext, TestRecord, 'does-not-exist')).rejects.toThrow( RecordNotFoundError ) }) @@ -169,7 +186,7 @@ describe('AskarStorageService', () => { tags: { some: 'tag' }, }) - return expect(() => storageService.update(agentContext, record)).rejects.toThrowError(RecordNotFoundError) + return expect(() => storageService.update(agentContext, record)).rejects.toThrow(RecordNotFoundError) }) it('should update the record', async () => { @@ -197,14 +214,14 @@ describe('AskarStorageService', () => { tags: { some: 'tag' }, }) - return expect(() => storageService.delete(agentContext, record)).rejects.toThrowError(RecordNotFoundError) + await expect(() => storageService.delete(agentContext, record)).rejects.toThrow(RecordNotFoundError) }) it('should delete the record', async () => { const record = await insertRecord({ id: 'test-id' }) await storageService.delete(agentContext, record) - return expect(() => storageService.getById(agentContext, TestRecord, record.id)).rejects.toThrowError( + await expect(() => storageService.getById(agentContext, TestRecord, record.id)).rejects.toThrow( RecordNotFoundError ) }) @@ -219,7 +236,6 @@ describe('AskarStorageService', () => { ) const records = await storageService.getAll(agentContext, TestRecord) - expect(records).toEqual(expect.arrayContaining(createdRecords)) }) }) @@ -237,7 +253,9 @@ describe('AskarStorageService', () => { }) it('finds records using $and statements', async () => { - const expectedRecord = await insertRecord({ tags: { myTag: 'foo', anotherTag: 'bar' } }) + const expectedRecord = await insertRecord({ + tags: { myTag: 'foo', anotherTag: 'bar' }, + }) await insertRecord({ tags: { myTag: 'notfoobar' } }) const records = await storageService.findByQuery(agentContext, TestRecord, { @@ -250,7 +268,9 @@ describe('AskarStorageService', () => { it('finds records using $or statements', async () => { const expectedRecord = await insertRecord({ tags: { myTag: 'foo' } }) - const expectedRecord2 = await insertRecord({ tags: { anotherTag: 'bar' } }) + const expectedRecord2 = await insertRecord({ + tags: { anotherTag: 'bar' }, + }) await insertRecord({ tags: { myTag: 'notfoobar' } }) const records = await storageService.findByQuery(agentContext, TestRecord, { @@ -263,7 +283,9 @@ describe('AskarStorageService', () => { it('finds records using $not statements', async () => { const expectedRecord = await insertRecord({ tags: { myTag: 'foo' } }) - const expectedRecord2 = await insertRecord({ tags: { anotherTag: 'bar' } }) + const expectedRecord2 = await insertRecord({ + tags: { anotherTag: 'bar' }, + }) await insertRecord({ tags: { myTag: 'notfoobar' } }) const records = await storageService.findByQuery(agentContext, TestRecord, { @@ -289,8 +311,18 @@ describe('AskarStorageService', () => { $or: undefined, $not: undefined, $and: [ - { theNumber: 'n__0', $and: undefined, $or: undefined, $not: undefined }, - { theNumber: 'n__1', $and: undefined, $or: undefined, $not: undefined }, + { + theNumber: 'n__0', + $and: undefined, + $or: undefined, + $not: undefined, + }, + { + theNumber: 'n__1', + $and: undefined, + $or: undefined, + $not: undefined, + }, ], }, ], @@ -303,7 +335,12 @@ describe('AskarStorageService', () => { $not: undefined, }, ], - $not: { myTag: 'notfoobar', $and: undefined, $or: undefined, $not: undefined }, + $not: { + myTag: 'notfoobar', + $and: undefined, + $or: undefined, + $not: undefined, + }, } expect( diff --git a/packages/askar/src/tenants.ts b/packages/askar/src/tenants.ts new file mode 100644 index 0000000000..57c7938745 --- /dev/null +++ b/packages/askar/src/tenants.ts @@ -0,0 +1,80 @@ +import type { AgentContext } from '@credo-ts/core' +import type { TenantsModule } from '@credo-ts/tenants' + +import { getApiForModuleByName } from '@credo-ts/core' +import { AskarError } from './error' + +const ASKAR_STORE_CONFIG_METADATA_KEY = '_askar/storeConfig' + +type TenantRecordAskarStoreConfig = { key: string } + +/** + * Store the aksar store config associated with a context correlation id. If multi-tenancy is not used + * this method won't do anything as we can just use the store config from the default context. However + * if multi-tenancy is used, we will store the askar store config in the tenant record metadata so it can + * be queried when a wallet is opened. + * + * This method will only be used when using the DatabasePerWallet database scheme, where each wallet has it's own + * database and also it's own encryption key. + */ +export async function storeAskarStoreConfigForContextCorrelationId( + agentContext: AgentContext, + config: TenantRecordAskarStoreConfig +) { + // It's kind of hacky, but we add support for the tenants module specifically here to map an actorId to + // a specific tenant. Otherwise we have to expose /:contextCorrelationId/:actorId in all the public URLs + // which is of course not so nice. + const tenantsApi = getApiForModuleByName(agentContext, 'TenantsModule') + if (!tenantsApi || agentContext.isRootAgentContext) { + throw new AskarError( + 'Tenants module is not registered, make sure to only call this method when the tenants module is enabled' + ) + } + + // TODO: we duplicate this logic, would be good to keep it in one place + const tenantId = agentContext.contextCorrelationId.replace('tenant-', '') + // We don't want to query the tenant record if the current context is the root context + const tenantRecord = await tenantsApi.getTenantById(tenantId) + + tenantRecord.metadata.set(ASKAR_STORE_CONFIG_METADATA_KEY, config) + await tenantsApi.updateTenant(tenantRecord) +} + +export async function getAskarStoreConfigForContextCorrelationId( + agentContext: AgentContext +): Promise { + // It's kind of hacky, but we add support for the tenants module specifically here + const tenantsApi = getApiForModuleByName(agentContext, 'TenantsModule') + if (!tenantsApi || agentContext.isRootAgentContext) { + throw new AskarError( + 'Tenants module is not registered, make sure to only call this method when the tenants module is enabled' + ) + } + + // TODO: we duplicate this logic, would be good to keep it in one place + const tenantId = agentContext.contextCorrelationId.replace('tenant-', '') + const tenantRecord = await tenantsApi.getTenantById(tenantId) + + const storeConfig = tenantRecord.metadata.get(ASKAR_STORE_CONFIG_METADATA_KEY) + + if (storeConfig) return storeConfig + + const { walletConfig } = tenantRecord.config as { + walletConfig?: { key: string } + } + + // for backwards compatibility we also look at the walletConfig.key + if (walletConfig) { + // Update so we can access it directly next time + tenantRecord.metadata.set(ASKAR_STORE_CONFIG_METADATA_KEY, { + key: walletConfig.key, + }) + await tenantsApi.updateTenant(tenantRecord) + + return { + key: walletConfig.key, + } + } + + throw new AskarError('Unable to extract askar store from tenant record') +} diff --git a/packages/askar/src/utils/askarKeyBackend.ts b/packages/askar/src/utils/askarKeyBackend.ts deleted file mode 100644 index be36aec06a..0000000000 --- a/packages/askar/src/utils/askarKeyBackend.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { KeyBackend as CredoKeyBackend } from '@credo-ts/core' -import { KeyBackend as AskarKeyBackend } from '@openwallet-foundation/askar-shared' - -export const convertToAskarKeyBackend = (credoKeyBackend: CredoKeyBackend) => { - switch (credoKeyBackend) { - case CredoKeyBackend.Software: - return AskarKeyBackend.Software - case CredoKeyBackend.SecureElement: - return AskarKeyBackend.SecureElement - } -} diff --git a/packages/askar/src/utils/askarKeyTypes.ts b/packages/askar/src/utils/askarKeyTypes.ts index 80ef2bef3c..12905560b8 100644 --- a/packages/askar/src/utils/askarKeyTypes.ts +++ b/packages/askar/src/utils/askarKeyTypes.ts @@ -1,49 +1,27 @@ -import { KeyType } from '@credo-ts/core' +import { Kms } from '@credo-ts/core' import { KeyAlgorithm } from '@openwallet-foundation/askar-shared' -export enum AskarKeyTypePurpose { - KeyManagement = 'KeyManagement', - Signing = 'Signing', - Encryption = 'Encryption', -} +export const jwkCrvToAskarAlg: Partial< + Record +> = { + // EC + secp256k1: KeyAlgorithm.EcSecp256k1, + 'P-256': KeyAlgorithm.EcSecp256r1, + 'P-384': KeyAlgorithm.EcSecp384r1, -const keyTypeToAskarAlg = { - [KeyType.Ed25519]: { - keyAlg: KeyAlgorithm.Ed25519, - purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing], - }, - [KeyType.X25519]: { - keyAlg: KeyAlgorithm.X25519, - purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing], - }, - [KeyType.Bls12381g1]: { - keyAlg: KeyAlgorithm.Bls12381G1, - purposes: [AskarKeyTypePurpose.KeyManagement], - }, - [KeyType.Bls12381g2]: { - keyAlg: KeyAlgorithm.Bls12381G2, - purposes: [AskarKeyTypePurpose.KeyManagement], - }, - [KeyType.Bls12381g1g2]: { - keyAlg: KeyAlgorithm.Bls12381G1, - purposes: [AskarKeyTypePurpose.KeyManagement], - }, - [KeyType.P256]: { - keyAlg: KeyAlgorithm.EcSecp256r1, - purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing, AskarKeyTypePurpose.Encryption], - }, - [KeyType.P384]: { - keyAlg: KeyAlgorithm.EcSecp384r1, - purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing, AskarKeyTypePurpose.Encryption], - }, - [KeyType.K256]: { - keyAlg: KeyAlgorithm.EcSecp256k1, - purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing], - }, + // OKP + X25519: KeyAlgorithm.X25519, + Ed25519: KeyAlgorithm.Ed25519, } -export const isKeyTypeSupportedByAskarForPurpose = (keyType: KeyType, purpose: AskarKeyTypePurpose) => - keyType in keyTypeToAskarAlg && - keyTypeToAskarAlg[keyType as keyof typeof keyTypeToAskarAlg].purposes.includes(purpose) +export const jwkEncToAskarAlg = { + 'A128CBC-HS256': KeyAlgorithm.AesA128CbcHs256, + A128GCM: KeyAlgorithm.AesA128Gcm, + 'A256CBC-HS512': KeyAlgorithm.AesA256CbcHs512, + A256GCM: KeyAlgorithm.AesA256Gcm, + C20P: KeyAlgorithm.Chacha20C20P, + XC20P: KeyAlgorithm.Chacha20XC20P, -export const keyTypesSupportedByAskar = Object.keys(keyTypeToAskarAlg) as KeyType[] + A128KW: KeyAlgorithm.AesA128Kw, + A256KW: KeyAlgorithm.AesA256Kw, +} satisfies Partial> diff --git a/packages/askar/src/utils/askarStoreConfig.ts b/packages/askar/src/utils/askarStoreConfig.ts new file mode 100644 index 0000000000..2599459304 --- /dev/null +++ b/packages/askar/src/utils/askarStoreConfig.ts @@ -0,0 +1,90 @@ +import type { AskarModuleConfigStoreOptions } from '../AskarModuleConfig' + +import { KdfMethod, StoreKeyMethod } from '@openwallet-foundation/askar-shared' + +import { isAskarPostgresStorageConfig, isAskarSqliteStorageConfig } from '../AskarStorageConfig' +import { AskarError } from '../error' + +/** + * Creates an askar wallet URI value based on store config + * @param credoDataPath framework data path (used in case walletConfig.storage.path is undefined) + * @returns string containing the askar wallet URI + */ +export const uriFromStoreConfig = ( + storeConfig: AskarModuleConfigStoreOptions, + credoDataPath: string +): { uri: string; path?: string } => { + let uri = '' + let path: string | undefined + + const urlParams = [] + + const database = storeConfig.database ?? { type: 'sqlite' } + if (isAskarSqliteStorageConfig(database)) { + if (database.config?.inMemory) { + uri = 'sqlite://:memory:' + } else { + path = database.config?.path ?? `${credoDataPath}/wallet/${storeConfig.id}/sqlite.db` + uri = `sqlite://${path}` + } + } else if (isAskarPostgresStorageConfig(database)) { + if (!database.config || !database.credentials) { + throw new AskarError('Invalid storage configuration for postgres wallet') + } + + if (database.config.connectTimeout !== undefined) { + urlParams.push(`connect_timeout=${encodeURIComponent(database.config.connectTimeout)}`) + } + if (database.config.idleTimeout !== undefined) { + urlParams.push(`idle_timeout=${encodeURIComponent(database.config.idleTimeout)}`) + } + if (database.credentials.adminAccount !== undefined) { + urlParams.push(`admin_account=${encodeURIComponent(database.credentials.adminAccount)}`) + } + if (database.credentials.adminPassword !== undefined) { + urlParams.push(`admin_password=${encodeURIComponent(database.credentials.adminPassword)}`) + } + + uri = `postgres://${encodeURIComponent(database.credentials.account)}:${encodeURIComponent( + database.credentials.password + )}@${database.config.host}/${encodeURIComponent(storeConfig.id)}` + } else { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + throw new WalletError(`Storage type not supported: ${database.type}`) + } + + // Common config options + if (database.config?.maxConnections !== undefined) { + urlParams.push(`max_connections=${encodeURIComponent(database.config.maxConnections)}`) + } + if (database.config?.minConnections !== undefined) { + urlParams.push(`min_connections=${encodeURIComponent(database.config.minConnections)}`) + } + + if (urlParams.length > 0) { + uri = `${uri}?${urlParams.join('&')}` + } + + return { uri, path } +} + +export function keyDerivationMethodFromStoreConfig( + keyDerivationMethod?: AskarModuleConfigStoreOptions['keyDerivationMethod'] +) { + return new StoreKeyMethod( + (keyDerivationMethod ?? KdfMethod.Argon2IMod) satisfies `${KdfMethod}` | KdfMethod as KdfMethod + ) +} + +export function isSqliteInMemoryUri(uri: string) { + return uri.startsWith('sqlite://:memory:') +} + +export function isSqliteFileUri(uri: string) { + return uri.startsWith('sqlite://') && !isSqliteInMemoryUri(uri) +} + +export function isPostgresUri(uri: string) { + return uri.startsWith('postgres://') +} diff --git a/packages/askar/src/utils/askarWalletConfig.ts b/packages/askar/src/utils/askarWalletConfig.ts deleted file mode 100644 index 1297e5d752..0000000000 --- a/packages/askar/src/utils/askarWalletConfig.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { WalletConfig } from '@credo-ts/core' - -import { KeyDerivationMethod, WalletError } from '@credo-ts/core' -import { KdfMethod, StoreKeyMethod } from '@openwallet-foundation/askar-shared' - -import { - isAskarWalletPostgresStorageConfig, - isAskarWalletSqliteStorageConfig, -} from '../wallet/AskarWalletStorageConfig' - -export const keyDerivationMethodToStoreKeyMethod = (keyDerivationMethod: KeyDerivationMethod) => { - const correspondenceTable = { - [KeyDerivationMethod.Raw]: KdfMethod.Raw, - [KeyDerivationMethod.Argon2IInt]: KdfMethod.Argon2IInt, - [KeyDerivationMethod.Argon2IMod]: KdfMethod.Argon2IMod, - } - - return new StoreKeyMethod(correspondenceTable[keyDerivationMethod]) -} - -/** - * Creates a proper askar wallet URI value based on walletConfig - * @param walletConfig WalletConfig object - * @param credoDataPath framework data path (used in case walletConfig.storage.path is undefined) - * @returns string containing the askar wallet URI - */ -export const uriFromWalletConfig = ( - walletConfig: WalletConfig, - credoDataPath: string -): { uri: string; path?: string } => { - let uri = '' - let path: string | undefined - - // By default use sqlite as database backend - if (!walletConfig.storage) { - walletConfig.storage = { type: 'sqlite' } - } - - const urlParams = [] - - const storageConfig = walletConfig.storage - if (isAskarWalletSqliteStorageConfig(storageConfig)) { - if (storageConfig.config?.inMemory) { - uri = 'sqlite://:memory:' - } else { - path = storageConfig.config?.path ?? `${credoDataPath}/wallet/${walletConfig.id}/sqlite.db` - uri = `sqlite://${path}` - } - } else if (isAskarWalletPostgresStorageConfig(storageConfig)) { - if (!storageConfig.config || !storageConfig.credentials) { - throw new WalletError('Invalid storage configuration for postgres wallet') - } - - if (storageConfig.config.connectTimeout !== undefined) { - urlParams.push(`connect_timeout=${encodeURIComponent(storageConfig.config.connectTimeout)}`) - } - if (storageConfig.config.idleTimeout !== undefined) { - urlParams.push(`idle_timeout=${encodeURIComponent(storageConfig.config.idleTimeout)}`) - } - if (storageConfig.credentials.adminAccount !== undefined) { - urlParams.push(`admin_account=${encodeURIComponent(storageConfig.credentials.adminAccount)}`) - } - if (storageConfig.credentials.adminPassword !== undefined) { - urlParams.push(`admin_password=${encodeURIComponent(storageConfig.credentials.adminPassword)}`) - } - - uri = `postgres://${encodeURIComponent(storageConfig.credentials.account)}:${encodeURIComponent( - storageConfig.credentials.password - )}@${storageConfig.config.host}/${encodeURIComponent(walletConfig.id)}` - } else { - throw new WalletError(`Storage type not supported: ${storageConfig.type}`) - } - - // Common config options - if (storageConfig.config?.maxConnections !== undefined) { - urlParams.push(`max_connections=${encodeURIComponent(storageConfig.config.maxConnections)}`) - } - if (storageConfig.config?.minConnections !== undefined) { - urlParams.push(`min_connections=${encodeURIComponent(storageConfig.config.minConnections)}`) - } - - if (urlParams.length > 0) { - uri = `${uri}?${urlParams.join('&')}` - } - - return { uri, path } -} diff --git a/packages/askar/src/utils/assertAskarWallet.ts b/packages/askar/src/utils/assertAskarWallet.ts deleted file mode 100644 index 80a9411edd..0000000000 --- a/packages/askar/src/utils/assertAskarWallet.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Wallet } from '@credo-ts/core' - -import { CredoError } from '@credo-ts/core' - -import { AskarProfileWallet, AskarWallet } from '../wallet' - -export function assertAskarWallet(wallet: Wallet): asserts wallet is AskarProfileWallet | AskarWallet { - if (!(wallet instanceof AskarProfileWallet) && !(wallet instanceof AskarWallet)) { - // biome-ignore lint/suspicious/noExplicitAny: - const walletClassName = (wallet as any).constructor?.name ?? 'unknown' - throw new CredoError( - `Expected wallet to be instance of AskarProfileWallet or AskarWallet, found ${walletClassName}` - ) - } -} diff --git a/packages/askar/src/utils/index.ts b/packages/askar/src/utils/index.ts index b9f658de82..4df3e6a4ea 100644 --- a/packages/askar/src/utils/index.ts +++ b/packages/askar/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './askarError' export * from './askarKeyTypes' -export * from './askarWalletConfig' +export * from './askarStoreConfig' +export * from './transformPrivateKey' diff --git a/packages/askar/src/utils/transformPrivateKey.ts b/packages/askar/src/utils/transformPrivateKey.ts new file mode 100644 index 0000000000..6729075bb5 --- /dev/null +++ b/packages/askar/src/utils/transformPrivateKey.ts @@ -0,0 +1,97 @@ +import { Buffer, CredoError, Kms } from '@credo-ts/core' +import { Key as AskarKey } from '@openwallet-foundation/askar-shared' +import { jwkCrvToAskarAlg } from './askarKeyTypes' + +/** + * Method to transform private key bytes into a private jwk, + * which allows the key to be imported in the KMS API. + * + * This method is to still allow private keys that were + * used before the KMS API was introduced, to be used and imported. + * + * @example + * ```ts + * import { transformPrivateKeyToPrivateJwk } from '@credo-ts/askar' + * + * const { privateJwk } = transformPrivateKeyToPrivateJwk({ + * type: { + * kty: 'EC', + * crv: 'P-256', + * }, + * privateKey: TypedArrayEncoder.fromString('00000000000000000000000000000My1') + * }) + * + * const { keyId } = await agent.kms.importKey({ + * privateJwk + * }) + * ``` + */ +export function transformPrivateKeyToPrivateJwk({ + type, + privateKey, +}: { + type: Type + privateKey: Buffer +}): { privateJwk: Kms.KmsJwkPrivateFromKmsJwkPublic> } { + const askarAlgorithm = jwkCrvToAskarAlg[type.crv] + if (!askarAlgorithm) { + throw new CredoError(`kty '${type.kty}' with crv '${type.crv}' not supported by Askar`) + } + + const privateJwk = AskarKey.fromSecretBytes({ + algorithm: askarAlgorithm, + secretKey: privateKey, + }).jwkSecret + + return { + // biome-ignore lint/suspicious/noExplicitAny: + privateJwk: privateJwk as any, + } +} + +/** + * Method to transform seed into a private jwk, + * which allows the key to be imported in the KMS API. + * + * This method is to still allow seeds that were + * used before the KMS API was introduced, to be used and imported. + * + * @example + * ```ts + * import { transformSeedToPrivateJwk } from '@credo-ts/askar' + * + * const { privateJwk } = transformSeedToPrivateJwk({ + * type: { + * kty: 'EC', + * crv: 'P-256', + * }, + * seed: TypedArrayEncoder.fromString('00000000000000000000000000000My1') + * }) + * + * const { keyId } = await agent.kms.importKey({ + * privateJwk + * }) + * ``` + */ +export function transformSeedToPrivateJwk({ + type, + seed, +}: { + type: Type + seed: Buffer +}): { privateJwk: Kms.KmsJwkPrivateFromKmsJwkPublic> } { + const askarAlgorithm = jwkCrvToAskarAlg[type.crv] + if (!askarAlgorithm) { + throw new CredoError(`kty '${type.kty}' with crv '${type.crv}' not supported by Askar`) + } + + const privateJwk = AskarKey.fromSeed({ + algorithm: askarAlgorithm, + seed, + }).jwkSecret + + return { + // biome-ignore lint/suspicious/noExplicitAny: + privateJwk: privateJwk as any, + } +} diff --git a/packages/askar/src/wallet/AskarBaseWallet.ts b/packages/askar/src/wallet/AskarBaseWallet.ts deleted file mode 100644 index 475fd3ec16..0000000000 --- a/packages/askar/src/wallet/AskarBaseWallet.ts +++ /dev/null @@ -1,735 +0,0 @@ -import type { - EncryptedMessage, - KeyPair, - Logger, - SigningProviderRegistry, - UnpackedMessageContext, - Wallet, - WalletConfig, - WalletConfigRekey, - WalletCreateKeyOptions, - WalletDirectEncryptCompactJwtEcdhEsOptions, - WalletExportImportConfig, - WalletSignOptions, - WalletVerifyOptions, -} from '@credo-ts/core' -import type { Session } from '@openwallet-foundation/askar-shared' - -import { - Buffer, - CredoError, - JsonEncoder, - Key, - KeyBackend, - KeyType, - TypedArrayEncoder, - WalletError, - WalletKeyExistsError, - isValidPrivateKey, - isValidSeed, - utils, -} from '@credo-ts/core' -import { - Key as AskarKey, - CryptoBox, - EcdhEs, - Jwk, - KeyAlgorithm, - Store, - keyAlgorithmFromString, -} from '@openwallet-foundation/askar-shared' - -import { importSecureEnvironment } from '../secureEnvironment' -import { - AskarErrorCode, - AskarKeyTypePurpose, - isAskarError, - isKeyTypeSupportedByAskarForPurpose, - keyTypesSupportedByAskar, -} from '../utils' - -import { didcommV1Pack, didcommV1Unpack } from './didcommV1' - -const isError = (error: unknown): error is Error => error instanceof Error - -export abstract class AskarBaseWallet implements Wallet { - protected logger: Logger - protected signingKeyProviderRegistry: SigningProviderRegistry - - public constructor(logger: Logger, signingKeyProviderRegistry: SigningProviderRegistry) { - this.logger = logger - this.signingKeyProviderRegistry = signingKeyProviderRegistry - } - - /** - * Abstract methods that need to be implemented by subclasses - */ - public abstract isInitialized: boolean - public abstract isProvisioned: boolean - public abstract create(walletConfig: WalletConfig): Promise - public abstract createAndOpen(walletConfig: WalletConfig): Promise - public abstract open(walletConfig: WalletConfig): Promise - public abstract rotateKey(walletConfig: WalletConfigRekey): Promise - public abstract close(): Promise - public abstract delete(): Promise - public abstract export(exportConfig: WalletExportImportConfig): Promise - public abstract import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig): Promise - public abstract dispose(): void | Promise - public abstract profile: string - - protected abstract store: Store - - /** - * Run callback with the session provided, the session will - * be closed once the callback resolves or rejects if it is not closed yet. - * - * TODO: update to new `using` syntax so we don't have to use a callback - */ - public async withSession(callback: (session: Session) => Return): Promise> { - let session: Session | undefined = undefined - try { - session = await this.store.session(this.profile).open() - - const result = await callback(session) - - return result - } finally { - if (session?.handle) { - await session.close() - } - } - } - - /** - * Run callback with a transaction. If the callback resolves the transaction - * will be committed if the transaction is not closed yet. If the callback rejects - * the transaction will be rolled back if the transaction is not closed yet. - * - * TODO: update to new `using` syntax so we don't have to use a callback - */ - public async withTransaction(callback: (transaction: Session) => Return): Promise> { - let session: Session | undefined = undefined - try { - session = await this.store.transaction(this.profile).open() - - const result = await callback(session) - - if (session.handle) { - await session.commit() - } - return result - } catch (error) { - if (session?.handle) { - await session?.rollback() - } - - throw error - } - } - - public get supportedKeyTypes() { - const signingKeyProviderSupportedKeyTypes = this.signingKeyProviderRegistry.supportedKeyTypes - - return Array.from(new Set([...keyTypesSupportedByAskar, ...signingKeyProviderSupportedKeyTypes])) - } - - /** - * Create a key with an optional seed and keyType. - * The keypair is also automatically stored in the wallet afterwards - */ - public async createKey({ - seed, - privateKey, - keyType, - keyId, - keyBackend = KeyBackend.Software, - }: WalletCreateKeyOptions): Promise { - try { - if (seed && privateKey) { - throw new WalletError('Only one of seed and privateKey can be set') - } - - if (seed && !isValidSeed(seed, keyType)) { - throw new WalletError('Invalid seed provided') - } - - if (privateKey && !isValidPrivateKey(privateKey, keyType)) { - throw new WalletError('Invalid private key provided') - } - - if (keyBackend === KeyBackend.SecureElement && keyType !== KeyType.P256) { - throw new WalletError(`Keytype '${keyType}' is not supported for the secure element`) - } - - if ( - isKeyTypeSupportedByAskarForPurpose(keyType, AskarKeyTypePurpose.KeyManagement) && - keyBackend === KeyBackend.Software - ) { - const algorithm = keyAlgorithmFromString(keyType) - - // Create key - let key: AskarKey | undefined - try { - const _key = privateKey - ? AskarKey.fromSecretBytes({ secretKey: privateKey, algorithm }) - : seed - ? AskarKey.fromSeed({ seed, algorithm }) - : AskarKey.generate(algorithm) - - // FIXME: we need to create a separate const '_key' so TS definitely knows _key is defined in the session callback. - // This will be fixed once we use the new 'using' syntax - key = _key - - const keyInstance = new Key(key.publicBytes, keyType) - - // Store key - await this.withSession((session) => - // NOTE: askar by default uses the compressed variant of EC keys. To not break existing wallets we keep using - // the compressed variant of the public key as the key identifier - session.insertKey({ key: _key, name: keyId ?? TypedArrayEncoder.toBase58(keyInstance.compressedPublicKey) }) - ) - - key.handle.free() - return keyInstance - } catch (error) { - key?.handle.free() - // Handle case where key already exists - if (isAskarError(error, AskarErrorCode.Duplicate)) { - throw new WalletKeyExistsError('Key already exists') - } - - // Otherwise re-throw error - throw error - } - } else if (keyBackend === KeyBackend.SecureElement && keyType === KeyType.P256) { - const secureEnvironment = importSecureEnvironment() - const kid = utils.uuid() - - // Generate a hardware-backed P-256 keypair - await secureEnvironment.generateKeypair(kid) - const compressedPublicKeyBytes = await secureEnvironment.getPublicBytesForKeyId(kid) - - const publicKeyInstance = new Key(compressedPublicKeyBytes, keyType) - - await this.storeSecureEnvironmentKeyById({ - keyType, - publicKeyBase58: TypedArrayEncoder.toBase58(publicKeyInstance.compressedPublicKey), - keyId: kid, - }) - - return publicKeyInstance - } else { - // Check if there is a signing key provider for the specified key type. - if (this.signingKeyProviderRegistry.hasProviderForKeyType(keyType)) { - const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(keyType) - - const keyPair = await signingKeyProvider.createKeyPair({ seed, privateKey }) - await this.storeKeyPair(keyPair) - return Key.fromPublicKeyBase58(keyPair.publicKeyBase58, keyType) - } - throw new WalletError(`Unsupported key type: '${keyType}'`) - } - } catch (error) { - // If already instance of `WalletError`, re-throw - if (error instanceof WalletError) throw error - - if (!isError(error)) { - throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError(`Error creating key with key type '${keyType}': ${error.message}`, { cause: error }) - } - } - - /** - * sign a Buffer with an instance of a Key class - * - * @param data Buffer The data that needs to be signed - * @param key Key The key that is used to sign the data - * - * @returns A signature for the data - */ - public async sign({ data, key }: WalletSignOptions): Promise { - let askarKey: AskarKey | null | undefined - let keyPair: KeyPair | null | undefined - - try { - if (isKeyTypeSupportedByAskarForPurpose(key.keyType, AskarKeyTypePurpose.KeyManagement)) { - askarKey = await this.withSession( - async (session) => - (await session.fetchKey({ name: TypedArrayEncoder.toBase58(key.compressedPublicKey) }))?.key - ) - } - - // FIXME: remove the custom KeyPair record now that we deprecate Indy SDK. - // We can do this in a migration script - - // Fallback to fetching key from the non-askar storage, this is to handle the case - // where a key wasn't supported at first by the wallet, but now is - if (!askarKey) { - // TODO: we should probably make retrieveKeyPair + insertKey + deleteKeyPair a transaction - keyPair = await this.retrieveKeyPair(TypedArrayEncoder.toBase58(key.compressedPublicKey)) - - // If we have the key stored in a custom record, but it is now supported by Askar, - // we 'import' the key into askar storage and remove the custom key record - if (keyPair && isKeyTypeSupportedByAskarForPurpose(keyPair.keyType, AskarKeyTypePurpose.KeyManagement)) { - const _askarKey = AskarKey.fromSecretBytes({ - secretKey: TypedArrayEncoder.fromBase58(keyPair.privateKeyBase58), - algorithm: keyAlgorithmFromString(keyPair.keyType), - }) - askarKey = _askarKey - - await this.withSession((session) => - session.insertKey({ - name: TypedArrayEncoder.toBase58(key.compressedPublicKey), - key: _askarKey, - }) - ) - - // Now we can remove it from the custom record as we have imported it into Askar - await this.deleteKeyPair(TypedArrayEncoder.toBase58(key.compressedPublicKey)) - keyPair = undefined - } else { - const { keyId } = await this.getSecureEnvironmentKey(TypedArrayEncoder.toBase58(key.compressedPublicKey)) - - if (Array.isArray(data[0])) { - throw new WalletError('Multi signature is not supported for the Secure Environment') - } - - return Buffer.from(await importSecureEnvironment().sign(keyId, new Uint8Array(data as Buffer))) - } - } - - if (!askarKey && !keyPair) { - throw new WalletError('Key entry not found') - } - - // Not all keys are supported for signing - if (isKeyTypeSupportedByAskarForPurpose(key.keyType, AskarKeyTypePurpose.Signing)) { - if (!TypedArrayEncoder.isTypedArray(data)) { - throw new WalletError('Currently not supporting signing of multiple messages') - } - - askarKey = - askarKey ?? - (keyPair - ? AskarKey.fromSecretBytes({ - secretKey: TypedArrayEncoder.fromBase58(keyPair.privateKeyBase58), - algorithm: keyAlgorithmFromString(keyPair.keyType), - }) - : undefined) - - if (!askarKey) { - throw new WalletError('Key entry not found') - } - - const signed = askarKey.signMessage({ message: data as Buffer }) - return Buffer.from(signed) - } - // Check if there is a signing key provider for the specified key type. - if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { - const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) - - // It could be that askar supports storing the key, but can't sign with it - // (in case of bls) - const privateKeyBase58 = - keyPair?.privateKeyBase58 ?? - (askarKey?.secretBytes ? TypedArrayEncoder.toBase58(askarKey.secretBytes) : undefined) - - if (!privateKeyBase58) { - throw new WalletError('Key entry not found') - } - const signed = await signingKeyProvider.sign({ - data, - privateKeyBase58: privateKeyBase58, - publicKeyBase58: key.publicKeyBase58, - }) - - return signed - } - throw new WalletError(`Unsupported keyType: ${key.keyType}`) - } catch (error) { - if (!isError(error)) { - throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError( - `Error signing data with key associated with publicKeyBase58 ${key.publicKeyBase58}. ${error.message}`, - { - cause: error, - } - ) - } finally { - askarKey?.handle.free() - } - } - - /** - * Verify the signature with the data and the used key - * - * @param data Buffer The data that has to be confirmed to be signed - * @param key Key The key that was used in the signing process - * @param signature Buffer The signature that was created by the signing process - * - * @returns A boolean whether the signature was created with the supplied data and key - * - * @throws {WalletError} When it could not do the verification - * @throws {WalletError} When an unsupported keytype is used - */ - public async verify({ data, key, signature }: WalletVerifyOptions): Promise { - let askarKey: AskarKey | undefined - try { - if (isKeyTypeSupportedByAskarForPurpose(key.keyType, AskarKeyTypePurpose.Signing)) { - if (!TypedArrayEncoder.isTypedArray(data)) { - throw new WalletError('Currently not supporting verification of multiple messages') - } - - askarKey = AskarKey.fromPublicBytes({ - algorithm: keyAlgorithmFromString(key.keyType), - publicKey: key.publicKey, - }) - const verified = askarKey.verifySignature({ message: data as Buffer, signature }) - askarKey.handle.free() - return verified - } - if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { - // Check if there is a signing key provider for the specified key type. - const signingKeyProvider = this.signingKeyProviderRegistry.getProviderForKeyType(key.keyType) - const signed = await signingKeyProvider.verify({ - data, - signature, - publicKeyBase58: key.publicKeyBase58, - }) - - return signed - } - throw new WalletError(`Unsupported keyType: ${key.keyType}`) - } catch (error) { - askarKey?.handle.free() - if (!isError(error)) { - throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError(`Error verifying signature of data signed with verkey ${key.publicKeyBase58}`, { - cause: error, - }) - } - } - - /** - * Pack a message using DIDComm V1 algorithm - * - * @param payload message to send - * @param recipientKeys array containing recipient keys in base58 - * @param senderVerkey sender key in base58 - * @returns JWE Envelope to send - */ - public async pack( - payload: Record, - recipientKeys: string[], - senderVerkey?: string // in base58 - ): Promise { - const senderKey = senderVerkey - ? await this.withSession((session) => session.fetchKey({ name: senderVerkey })) - : undefined - - try { - if (senderVerkey && !senderKey) { - throw new WalletError('Sender key not found') - } - - const envelope = didcommV1Pack(payload, recipientKeys, senderKey?.key) - - return envelope - } finally { - senderKey?.key.handle.free() - } - } - - /** - * Unpacks a JWE Envelope coded using DIDComm V1 algorithm - * - * @param messagePackage JWE Envelope - * @returns UnpackedMessageContext with plain text message, sender key and recipient key - */ - public async unpack(messagePackage: EncryptedMessage): Promise { - const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) - // biome-ignore lint/suspicious/noExplicitAny: - const recipientKids: string[] = protectedJson.recipients.map((r: any) => r.header.kid) - - // TODO: how long should sessions last? Just for the duration of the unpack? Or should each item in the recipientKids get a separate session? - const returnValue = await this.withSession(async (session) => { - for (const recipientKid of recipientKids) { - const recipientKeyEntry = await session.fetchKey({ name: recipientKid }) - try { - if (recipientKeyEntry) { - return didcommV1Unpack(messagePackage, recipientKeyEntry.key) - } - } finally { - recipientKeyEntry?.key.handle.free() - } - } - }) - - if (!returnValue) { - throw new WalletError('No corresponding recipient key found') - } - - return returnValue - } - - /** - * Method that enables JWE encryption using ECDH-ES and A256GCM/A128GCM,/A128CBC-HS256 and returns it as a compact JWE. - * This method is specifically added to support OpenID4VP response encryption using JARM and should later be - * refactored into a more generic method that supports encryption/decryption. - * - * @returns compact JWE - */ - public async directEncryptCompactJweEcdhEs({ - recipientKey, - encryptionAlgorithm, - apu, - apv, - data, - header, - }: WalletDirectEncryptCompactJwtEcdhEsOptions) { - if ( - encryptionAlgorithm !== 'A256GCM' && - encryptionAlgorithm !== 'A128GCM' && - encryptionAlgorithm !== 'A128CBC-HS256' - ) { - throw new WalletError( - `Encryption algorithm ${encryptionAlgorithm} is not supported. Only A128GCM, A256GCM and A128CBC-HS256 are supported` - ) - } - - const encAlg = - encryptionAlgorithm === 'A256GCM' - ? KeyAlgorithm.AesA256Gcm - : encryptionAlgorithm === 'A128GCM' - ? KeyAlgorithm.AesA128Gcm - : KeyAlgorithm.AesA128CbcHs256 - - // Create ephemeral key - const ephemeralKey = AskarKey.generate(keyAlgorithmFromString(recipientKey.keyType)) - - const _header = { - ...header, - apv, - apu, - enc: encryptionAlgorithm, - alg: 'ECDH-ES', - epk: ephemeralKey.jwkPublic, - } - - const encodedHeader = JsonEncoder.toBase64URL(_header) - - const ecdh = new EcdhEs({ - algId: Uint8Array.from(Buffer.from(encryptionAlgorithm)), - apu: apu ? Uint8Array.from(TypedArrayEncoder.fromBase64(apu)) : Uint8Array.from([]), - apv: apv ? Uint8Array.from(TypedArrayEncoder.fromBase64(apv)) : Uint8Array.from([]), - }) - - const { ciphertext, tag, nonce } = ecdh.encryptDirect({ - encryptionAlgorithm: encAlg, - ephemeralKey, - message: Uint8Array.from(data), - recipientKey: AskarKey.fromPublicBytes({ - algorithm: keyAlgorithmFromString(recipientKey.keyType), - publicKey: recipientKey.publicKey, - }), - // NOTE: aad is bytes of base64url encoded string. It SHOULD NOT be decoded as base64 - aad: Uint8Array.from(Buffer.from(encodedHeader)), - }) - - const compactJwe = `${encodedHeader}..${TypedArrayEncoder.toBase64URL(nonce)}.${TypedArrayEncoder.toBase64URL( - ciphertext - )}.${TypedArrayEncoder.toBase64URL(tag)}` - return compactJwe - } - - /** - * Method that enables JWE decryption using ECDH-ES and A256GCM/A128GCM,/A128CBC-HS256 and returns it as plaintext buffer with the header. - * The apv and apu values are extracted from the heaader, and thus on a higher level it should be checked that these - * values are correct. - */ - public async directDecryptCompactJweEcdhEs({ - compactJwe, - recipientKey, - }: { - compactJwe: string - recipientKey: Key - }): Promise<{ data: Buffer; header: Record }> { - // encryption key is not used (we don't use key wrapping) - const [encodedHeader /* encryptionKey */, , encodedIv, encodedCiphertext, encodedTag] = compactJwe.split('.') - - const header = JsonEncoder.fromBase64(encodedHeader) - - if (header.alg !== 'ECDH-ES') { - throw new WalletError('Only ECDH-ES alg value is supported') - } - if (header.enc !== 'A128GCM' && header.enc !== 'A256GCM' && header.enc !== 'A128CBC-HS256') { - throw new WalletError('Only A256GCM and A128CBC-HS256 enc values are supported') - } - if (!header.epk || typeof header.epk !== 'object') { - throw new WalletError('header epk value must contain a JWK') - } - - // NOTE: we don't support custom key storage record at the moment. - let askarKey: AskarKey | null | undefined - if (isKeyTypeSupportedByAskarForPurpose(recipientKey.keyType, AskarKeyTypePurpose.KeyManagement)) { - askarKey = await this.withSession( - async (session) => - (await session.fetchKey({ name: TypedArrayEncoder.toBase58(recipientKey.compressedPublicKey) }))?.key - ) - } - if (!askarKey) { - throw new WalletError('Key entry not found') - } - - const encAlg = - header.enc === 'A256GCM' - ? KeyAlgorithm.AesA256Gcm - : header.enc === 'A128GCM' - ? KeyAlgorithm.AesA128Gcm - : KeyAlgorithm.AesA128CbcHs256 - const ecdh = new EcdhEs({ - algId: Uint8Array.from(Buffer.from(header.enc)), - apu: header.apu ? Uint8Array.from(TypedArrayEncoder.fromBase64(header.apu)) : Uint8Array.from([]), - apv: header.apv ? Uint8Array.from(TypedArrayEncoder.fromBase64(header.apv)) : Uint8Array.from([]), - }) - - const plaintext = ecdh.decryptDirect({ - nonce: TypedArrayEncoder.fromBase64(encodedIv), - ciphertext: TypedArrayEncoder.fromBase64(encodedCiphertext), - encryptionAlgorithm: encAlg, - ephemeralKey: Jwk.fromJson(header.epk), - recipientKey: askarKey, - tag: TypedArrayEncoder.fromBase64(encodedTag), - // NOTE: aad is bytes of base64url encoded string. It SHOULD NOT be decoded as base64 - aad: TypedArrayEncoder.fromString(encodedHeader), - }) - - return { data: Buffer.from(plaintext), header } - } - - public async generateNonce(): Promise { - try { - // generate an 80-bit nonce suitable for AnonCreds proofs - const nonce = CryptoBox.randomNonce().slice(0, 10) - return nonce.reduce((acc, byte) => (acc << 8n) | BigInt(byte), 0n).toString() - } catch (error) { - if (!isError(error)) { - throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError('Error generating nonce', { cause: error }) - } - } - - public getRandomValues(length: number): Uint8Array { - try { - const buffer = new Uint8Array(length) - const CBOX_NONCE_LENGTH = 24 - - const genCount = Math.ceil(length / CBOX_NONCE_LENGTH) - const buf = new Uint8Array(genCount * CBOX_NONCE_LENGTH) - for (let i = 0; i < genCount; i++) { - const randomBytes = CryptoBox.randomNonce() - buf.set(randomBytes, CBOX_NONCE_LENGTH * i) - } - buffer.set(buf.subarray(0, length)) - - return buffer - } catch (error) { - if (!isError(error)) { - throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError('Error generating nonce', { cause: error }) - } - } - - public async generateWalletKey() { - try { - return Store.generateRawKey() - } catch (error) { - throw new WalletError('Error generating wallet key', { cause: error }) - } - } - - private async retrieveKeyPair(publicKeyBase58: string): Promise { - try { - const entryObject = await this.withSession((session) => - session.fetch({ category: 'KeyPairRecord', name: `key-${publicKeyBase58}` }) - ) - - if (!entryObject) return null - - return JsonEncoder.fromString(entryObject?.value as string) as KeyPair - } catch (error) { - throw new WalletError('Error retrieving KeyPair record', { cause: error }) - } - } - - private async getSecureEnvironmentKey(keyId: string): Promise<{ keyId: string }> { - try { - const entryObject = await this.withSession((session) => - session.fetch({ category: 'SecureEnvironmentKeyRecord', name: keyId }) - ) - - return JsonEncoder.fromString(entryObject?.value as string) as { keyId: string } - } catch (error) { - throw new WalletError('Error retrieving Secure Environment record', { cause: error }) - } - } - - private async deleteKeyPair(publicKeyBase58: string): Promise { - try { - await this.withSession((session) => session.remove({ category: 'KeyPairRecord', name: `key-${publicKeyBase58}` })) - } catch (error) { - throw new WalletError('Error removing KeyPair record', { cause: error }) - } - } - - private async storeKeyPair(keyPair: KeyPair): Promise { - try { - await this.withSession((session) => - session.insert({ - category: 'KeyPairRecord', - name: `key-${TypedArrayEncoder.toBase58( - Key.fromPublicKeyBase58(keyPair.publicKeyBase58, keyPair.keyType).compressedPublicKey - )}`, - value: JSON.stringify(keyPair), - tags: { - keyType: keyPair.keyType, - }, - }) - ) - } catch (error) { - if (isAskarError(error, AskarErrorCode.Duplicate)) { - throw new WalletKeyExistsError('Key already exists') - } - throw new WalletError('Error saving KeyPair record', { cause: error }) - } - } - - private async storeSecureEnvironmentKeyById(options: { - keyId: string - publicKeyBase58: string - keyType: KeyType - }): Promise { - try { - await this.withSession((session) => - session.insert({ - category: 'SecureEnvironmentKeyRecord', - name: options.publicKeyBase58, - value: JSON.stringify(options), - tags: { - keyType: options.keyType, - }, - }) - ) - } catch (error) { - if (isAskarError(error, AskarErrorCode.Duplicate)) { - throw new WalletKeyExistsError('Key already exists') - } - throw new WalletError('Error saving SecureEnvironment record', { cause: error }) - } - } -} diff --git a/packages/askar/src/wallet/AskarProfileWallet.ts b/packages/askar/src/wallet/AskarProfileWallet.ts deleted file mode 100644 index fa547ddabb..0000000000 --- a/packages/askar/src/wallet/AskarProfileWallet.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type { WalletConfig } from '@credo-ts/core' - -import { - InjectionSymbols, - Logger, - SigningProviderRegistry, - WalletDuplicateError, - WalletError, - WalletExportUnsupportedError, - WalletNotFoundError, -} from '@credo-ts/core' -import { Store } from '@openwallet-foundation/askar-shared' -import { inject, injectable } from 'tsyringe' - -import { AskarErrorCode, isAskarError } from '../utils' - -import { AskarBaseWallet } from './AskarBaseWallet' - -@injectable() -export class AskarProfileWallet extends AskarBaseWallet { - private walletConfig?: WalletConfig - public readonly store: Store - public isInitialized = false - - public constructor( - store: Store, - @inject(InjectionSymbols.Logger) logger: Logger, - signingKeyProviderRegistry: SigningProviderRegistry - ) { - super(logger, signingKeyProviderRegistry) - - this.store = store - } - - public get isProvisioned() { - return this.walletConfig !== undefined - } - - public get profile() { - if (!this.walletConfig) { - throw new WalletError('No profile configured.') - } - - return this.walletConfig.id - } - - /** - * Dispose method is called when an agent context is disposed. - */ - public async dispose() { - if (this.isInitialized) { - await this.close() - } - } - - public async create(walletConfig: WalletConfig): Promise { - this.logger.debug(`Creating wallet for profile '${walletConfig.id}'`) - - try { - await this.store.createProfile(walletConfig.id) - } catch (error) { - if (isAskarError(error, AskarErrorCode.Duplicate)) { - const errorMessage = `Wallet for profile '${walletConfig.id}' already exists` - this.logger.debug(errorMessage) - - throw new WalletDuplicateError(errorMessage, { - walletType: 'AskarProfileWallet', - cause: error, - }) - } - - const errorMessage = `Error creating wallet for profile '${walletConfig.id}'` - this.logger.error(errorMessage, { - error, - errorMessage: error.message, - }) - - throw new WalletError(errorMessage, { cause: error }) - } - - this.logger.debug(`Successfully created wallet for profile '${walletConfig.id}'`) - } - - public async open(walletConfig: WalletConfig): Promise { - this.logger.debug(`Opening wallet for profile '${walletConfig.id}'`) - - try { - this.walletConfig = walletConfig - - // TODO: what is faster? listProfiles or open and close session? - // I think open/close is more scalable (what if profiles is 10.000.000?) - // We just want to check if the profile exists. Because the wallet initialization logic - // first tries to open, and if it doesn't exist it will create it. So we must check here - // if the profile exists - await this.withSession(() => { - /* no-op */ - }) - this.isInitialized = true - } catch (error) { - // Profile does not exist - if (isAskarError(error, AskarErrorCode.NotFound)) { - const errorMessage = `Wallet for profile '${walletConfig.id}' not found` - this.logger.debug(errorMessage) - - throw new WalletNotFoundError(errorMessage, { - walletType: 'AskarProfileWallet', - cause: error, - }) - } - - const errorMessage = `Error opening wallet for profile '${walletConfig.id}'` - this.logger.error(errorMessage, { - error, - errorMessage: error.message, - }) - - throw new WalletError(errorMessage, { cause: error }) - } - - this.logger.debug(`Successfully opened wallet for profile '${walletConfig.id}'`) - } - - public async createAndOpen(walletConfig: WalletConfig): Promise { - await this.create(walletConfig) - await this.open(walletConfig) - } - - public async delete() { - if (!this.walletConfig) { - throw new WalletError( - 'Can not delete wallet that does not have wallet config set. Make sure to call create wallet before deleting the wallet' - ) - } - - this.logger.info(`Deleting profile '${this.profile}'`) - if (this.isInitialized) { - await this.close() - } - - try { - await this.store.removeProfile(this.profile) - } catch (error) { - const errorMessage = `Error deleting wallet for profile '${this.profile}': ${error.message}` - this.logger.error(errorMessage, { - error, - errorMessage: error.message, - }) - - throw new WalletError(errorMessage, { cause: error }) - } - } - - public async export() { - // This PR should help with this: https://github.com/openwallet-foundation/askar/pull/159 - throw new WalletExportUnsupportedError('Exporting a profile is not supported.') - } - - public async import() { - // This PR should help with this: https://github.com/openwallet-foundation/askar/pull/159 - throw new WalletError('Importing a profile is not supported.') - } - - public async rotateKey(): Promise { - throw new WalletError( - 'Rotating a key is not supported for a profile. You can rotate the key on the main askar wallet.' - ) - } - - public async close() { - this.logger.debug(`Closing wallet for profile ${this.walletConfig?.id}`) - - if (!this.isInitialized) { - throw new WalletError('Wallet is in invalid state, you are trying to close wallet that is not initialized.') - } - - this.isInitialized = false - } -} diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts deleted file mode 100644 index 6dfca95f9f..0000000000 --- a/packages/askar/src/wallet/AskarWallet.ts +++ /dev/null @@ -1,423 +0,0 @@ -import type { WalletConfig, WalletConfigRekey, WalletExportImportConfig } from '@credo-ts/core' - -import { - CredoError, - FileSystem, - InjectionSymbols, - KeyDerivationMethod, - Logger, - SigningProviderRegistry, - WalletDuplicateError, - WalletError, - WalletExportPathExistsError, - WalletExportUnsupportedError, - WalletImportPathExistsError, - WalletInvalidKeyError, - WalletNotFoundError, -} from '@credo-ts/core' -import { Store } from '@openwallet-foundation/askar-shared' -import { inject, injectable } from 'tsyringe' - -import { AskarErrorCode, isAskarError, keyDerivationMethodToStoreKeyMethod, uriFromWalletConfig } from '../utils' - -import { AskarBaseWallet } from './AskarBaseWallet' -import { isAskarWalletSqliteStorageConfig } from './AskarWalletStorageConfig' - -/** - * @todo: rename after 0.5.0, as we now have multiple types of AskarWallet - */ -@injectable() -export class AskarWallet extends AskarBaseWallet { - private fileSystem: FileSystem - - private walletConfig?: WalletConfig - private _store?: Store - - public constructor( - @inject(InjectionSymbols.Logger) logger: Logger, - @inject(InjectionSymbols.FileSystem) fileSystem: FileSystem, - signingKeyProviderRegistry: SigningProviderRegistry - ) { - super(logger, signingKeyProviderRegistry) - this.fileSystem = fileSystem - } - - public get isProvisioned() { - return this.walletConfig !== undefined - } - - public get isInitialized() { - return this._store !== undefined - } - - public get store() { - if (!this._store) { - throw new CredoError( - 'Wallet has not been initialized yet. Make sure to await agent.initialize() before using the agent.' - ) - } - - return this._store - } - - public get profile() { - if (!this.walletConfig) { - throw new WalletError('No profile configured.') - } - - return this.walletConfig.id - } - - /** - * Dispose method is called when an agent context is disposed. - */ - public async dispose() { - if (this.isInitialized) { - await this.close() - } - } - - /** - * @throws {WalletDuplicateError} if the wallet already exists - * @throws {WalletError} if another error occurs - */ - public async create(walletConfig: WalletConfig): Promise { - await this.createAndOpen(walletConfig) - await this.close() - } - - /** - * @throws {WalletDuplicateError} if the wallet already exists - * @throws {WalletError} if another error occurs - */ - public async createAndOpen(walletConfig: WalletConfig): Promise { - this.logger.debug(`Creating wallet '${walletConfig.id}`) - - const askarWalletConfig = await this.getAskarWalletConfig(walletConfig) - - // Check if database exists - const { path: filePath } = uriFromWalletConfig(walletConfig, this.fileSystem.dataPath) - if (filePath && (await this.fileSystem.exists(filePath))) { - throw new WalletDuplicateError(`Wallet '${walletConfig.id}' already exists.`, { - walletType: 'AskarWallet', - }) - } - try { - // Make sure path exists before creating the wallet - if (filePath) { - await this.fileSystem.createDirectory(filePath) - } - - this._store = await Store.provision({ - recreate: false, - uri: askarWalletConfig.uri, - profile: askarWalletConfig.profile, - keyMethod: askarWalletConfig.keyMethod, - passKey: askarWalletConfig.passKey, - }) - - // TODO: Should we do something to check if it exists? - // Like this.withSession()? - - this.walletConfig = walletConfig - } catch (error) { - // FIXME: Askar should throw a Duplicate error code, but is currently returning Encryption - // And if we provide the very same wallet key, it will open it without any error - if ( - isAskarError(error) && - (error.code === AskarErrorCode.Encryption || error.code === AskarErrorCode.Duplicate) - ) { - const errorMessage = `Wallet '${walletConfig.id}' already exists` - this.logger.debug(errorMessage) - - throw new WalletDuplicateError(errorMessage, { - walletType: 'AskarWallet', - cause: error, - }) - } - - const errorMessage = `Error creating wallet '${walletConfig.id}'` - this.logger.error(errorMessage, { - error, - errorMessage: error.message, - }) - - throw new WalletError(errorMessage, { cause: error }) - } - - this.logger.debug(`Successfully created wallet '${walletConfig.id}'`) - } - - /** - * @throws {WalletNotFoundError} if the wallet does not exist - * @throws {WalletError} if another error occurs - */ - public async open(walletConfig: WalletConfig): Promise { - await this._open(walletConfig) - } - - /** - * @throws {WalletNotFoundError} if the wallet does not exist - * @throws {WalletError} if another error occurs - */ - public async rotateKey(walletConfig: WalletConfigRekey): Promise { - if (!walletConfig.rekey) { - throw new WalletError('Wallet rekey undefined!. Please specify the new wallet key') - } - await this._open( - { - id: walletConfig.id, - key: walletConfig.key, - keyDerivationMethod: walletConfig.keyDerivationMethod, - }, - walletConfig.rekey, - walletConfig.rekeyDerivationMethod - ) - } - - /** - * @throws {WalletNotFoundError} if the wallet does not exist - * @throws {WalletError} if another error occurs - */ - private async _open( - walletConfig: WalletConfig, - rekey?: string, - rekeyDerivation?: KeyDerivationMethod - ): Promise { - if (this._store) { - throw new WalletError( - 'Wallet instance already opened. Close the currently opened wallet before re-opening the wallet' - ) - } - - const askarWalletConfig = await this.getAskarWalletConfig(walletConfig) - - try { - this._store = await Store.open({ - uri: askarWalletConfig.uri, - keyMethod: askarWalletConfig.keyMethod, - passKey: askarWalletConfig.passKey, - }) - - if (rekey) { - await this._store.rekey({ - passKey: rekey, - keyMethod: keyDerivationMethodToStoreKeyMethod(rekeyDerivation ?? KeyDerivationMethod.Argon2IMod), - }) - } - - // TODO: Should we do something to check if it exists? - // Like this.withSession()? - - this.walletConfig = walletConfig - } catch (error) { - if ( - isAskarError(error) && - (error.code === AskarErrorCode.NotFound || - (error.code === AskarErrorCode.Backend && - isAskarWalletSqliteStorageConfig(walletConfig.storage) && - walletConfig.storage.config?.inMemory)) - ) { - const errorMessage = `Wallet '${walletConfig.id}' not found` - this.logger.debug(errorMessage) - - throw new WalletNotFoundError(errorMessage, { - walletType: 'AskarWallet', - cause: error, - }) - } - if (isAskarError(error) && error.code === AskarErrorCode.Encryption) { - const errorMessage = `Incorrect key for wallet '${walletConfig.id}'` - this.logger.debug(errorMessage) - throw new WalletInvalidKeyError(errorMessage, { - walletType: 'AskarWallet', - cause: error, - }) - } - throw new WalletError(`Error opening wallet ${walletConfig.id}: ${error.message}`, { cause: error }) - } - - this.logger.debug(`Wallet '${walletConfig.id}' opened with handle '${this._store.handle.handle}'`) - } - - /** - * @throws {WalletNotFoundError} if the wallet does not exist - * @throws {WalletError} if another error occurs - */ - public async delete(): Promise { - if (!this.walletConfig) { - throw new WalletError( - 'Can not delete wallet that does not have wallet config set. Make sure to call create wallet before deleting the wallet' - ) - } - - this.logger.info(`Deleting wallet '${this.walletConfig.id}'`) - if (this._store) { - await this.close() - } - - try { - const { uri } = uriFromWalletConfig(this.walletConfig, this.fileSystem.dataPath) - await Store.remove(uri) - } catch (error) { - const errorMessage = `Error deleting wallet '${this.walletConfig.id}': ${error.message}` - this.logger.error(errorMessage, { - error, - errorMessage: error.message, - }) - - throw new WalletError(errorMessage, { cause: error }) - } - } - - public async export(exportConfig: WalletExportImportConfig) { - if (!this.walletConfig) { - throw new WalletError( - 'Can not export wallet that does not have wallet config set. Make sure to open it before exporting' - ) - } - - const { path: destinationPath, key: exportKey } = exportConfig - - const { path: sourcePath } = uriFromWalletConfig(this.walletConfig, this.fileSystem.dataPath) - - if (isAskarWalletSqliteStorageConfig(this.walletConfig.storage) && this.walletConfig.storage?.inMemory) { - throw new WalletExportUnsupportedError('Export is not supported for in memory wallet') - } - if (!sourcePath) { - throw new WalletExportUnsupportedError('Export is only supported for SQLite backend') - } - - try { - // Export path already exists - if (await this.fileSystem.exists(destinationPath)) { - throw new WalletExportPathExistsError( - `Unable to create export, wallet export at path '${exportConfig.path}' already exists` - ) - } - const exportedWalletConfig = await this.getAskarWalletConfig({ - ...this.walletConfig, - key: exportKey, - storage: { type: 'sqlite', config: { path: destinationPath } }, - }) - - // Make sure destination path exists - await this.fileSystem.createDirectory(destinationPath) - - await this.store.copyTo({ - recreate: false, - uri: exportedWalletConfig.uri, - keyMethod: exportedWalletConfig.keyMethod, - passKey: exportedWalletConfig.passKey, - }) - } catch (error) { - const errorMessage = `Error exporting wallet '${this.walletConfig.id}': ${error.message}` - this.logger.error(errorMessage, { - error, - errorMessage: error.message, - }) - - if (error instanceof WalletExportPathExistsError) throw error - - throw new WalletError(errorMessage, { cause: error }) - } - } - - public async import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig) { - const { path: sourcePath, key: importKey } = importConfig - const { path: destinationPath } = uriFromWalletConfig(walletConfig, this.fileSystem.dataPath) - - if (!destinationPath) { - throw new WalletError('Import is only supported for SQLite backend') - } - - let sourceWalletStore: Store | undefined = undefined - try { - const importWalletConfig = await this.getAskarWalletConfig(walletConfig) - - // Import path already exists - if (await this.fileSystem.exists(destinationPath)) { - throw new WalletExportPathExistsError(`Unable to import wallet. Path '${destinationPath}' already exists`) - } - - // Make sure destination path exists - await this.fileSystem.createDirectory(destinationPath) - // Open imported wallet and copy to destination - sourceWalletStore = await Store.open({ - uri: `sqlite://${sourcePath}`, - keyMethod: importWalletConfig.keyMethod, - passKey: importKey, - }) - - const defaultProfile = await sourceWalletStore.getDefaultProfile() - if (defaultProfile !== importWalletConfig.profile) { - throw new WalletError( - `Trying to import wallet with walletConfig.id ${importWalletConfig.profile}, however the wallet contains a default profile with id ${defaultProfile}. The walletConfig.id MUST match with the default profile. In the future this behavior may be changed. See https://github.com/openwallet-foundation/askar/issues/221 for more information.` - ) - } - - await sourceWalletStore.copyTo({ - recreate: false, - uri: importWalletConfig.uri, - keyMethod: importWalletConfig.keyMethod, - passKey: importWalletConfig.passKey, - }) - - await sourceWalletStore.close() - } catch (error) { - await sourceWalletStore?.close() - const errorMessage = `Error importing wallet '${walletConfig.id}': ${error.message}` - this.logger.error(errorMessage, { - error, - errorMessage: error.message, - }) - - if (error instanceof WalletImportPathExistsError) throw error - - // Cleanup any wallet file we could have created - if (await this.fileSystem.exists(destinationPath)) { - await this.fileSystem.delete(destinationPath) - } - - throw new WalletError(errorMessage, { cause: error }) - } - } - - /** - * @throws {WalletError} if the wallet is already closed or another error occurs - */ - public async close(): Promise { - this.logger.debug(`Closing wallet ${this.walletConfig?.id}`) - if (!this._store) { - throw new WalletError('Wallet is in invalid state, you are trying to close wallet that has no handle.') - } - - try { - await this.store.close() - this._store = undefined - } catch (error) { - const errorMessage = `Error closing wallet': ${error.message}` - this.logger.error(errorMessage, { - error, - errorMessage: error.message, - }) - - throw new WalletError(errorMessage, { cause: error }) - } - } - - private async getAskarWalletConfig(walletConfig: WalletConfig) { - const { uri, path } = uriFromWalletConfig(walletConfig, this.fileSystem.dataPath) - - return { - uri, - path, - profile: walletConfig.id, - // FIXME: Default derivation method should be set somewhere in either agent config or some constants - keyMethod: keyDerivationMethodToStoreKeyMethod( - walletConfig.keyDerivationMethod ?? KeyDerivationMethod.Argon2IMod - ), - passKey: walletConfig.key, - } - } -} diff --git a/packages/askar/src/wallet/AskarWalletStorageConfig.ts b/packages/askar/src/wallet/AskarWalletStorageConfig.ts deleted file mode 100644 index be73af4546..0000000000 --- a/packages/askar/src/wallet/AskarWalletStorageConfig.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { WalletStorageConfig } from '@credo-ts/core' - -export interface AskarWalletPostgresConfig { - host: string - connectTimeout?: number - idleTimeout?: number - maxConnections?: number - minConnections?: number -} - -export interface AskarWalletSqliteConfig { - // TODO: add other sqlite config options - maxConnections?: number - minConnections?: number - inMemory?: boolean - path?: string -} - -export interface AskarWalletPostgresCredentials { - account: string - password: string - adminAccount?: string - adminPassword?: string -} - -export interface AskarWalletPostgresStorageConfig extends WalletStorageConfig { - type: 'postgres' - config: AskarWalletPostgresConfig - credentials: AskarWalletPostgresCredentials -} - -export interface AskarWalletSqliteStorageConfig extends WalletStorageConfig { - type: 'sqlite' - config?: AskarWalletSqliteConfig -} - -export function isAskarWalletSqliteStorageConfig( - config?: WalletStorageConfig -): config is AskarWalletSqliteStorageConfig { - return config?.type === 'sqlite' -} - -export function isAskarWalletPostgresStorageConfig( - config?: WalletStorageConfig -): config is AskarWalletPostgresStorageConfig { - return config?.type === 'postgres' -} diff --git a/packages/askar/src/wallet/JweEnvelope.ts b/packages/askar/src/wallet/JweEnvelope.ts deleted file mode 100644 index 96561e9479..0000000000 --- a/packages/askar/src/wallet/JweEnvelope.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' -import { Expose, Type } from 'class-transformer' - -export class JweRecipient { - @Expose({ name: 'encrypted_key' }) - public encryptedKey!: string - public header?: Record - - public constructor(options: { encryptedKey: Uint8Array; header?: Record }) { - if (options) { - this.encryptedKey = TypedArrayEncoder.toBase64URL(options.encryptedKey) - - this.header = options.header - } - } -} - -export interface JweEnvelopeOptions { - protected: string - unprotected?: string - recipients?: JweRecipient[] - ciphertext: string - iv: string - tag: string - aad?: string - header?: string[] - encryptedKey?: string -} - -export class JweEnvelope { - public protected!: string - public unprotected?: string - - @Type(() => JweRecipient) - public recipients?: JweRecipient[] - public ciphertext!: string - public iv!: string - public tag!: string - public aad?: string - public header?: string[] - - @Expose({ name: 'encrypted_key' }) - public encryptedKey?: string - - public constructor(options: JweEnvelopeOptions) { - if (options) { - this.protected = options.protected - this.unprotected = options.unprotected - this.recipients = options.recipients - this.ciphertext = options.ciphertext - this.iv = options.iv - this.tag = options.tag - this.aad = options.aad - this.header = options.header - this.encryptedKey = options.encryptedKey - } - } - - public toJson() { - return JsonTransformer.toJSON(this) - } -} diff --git a/packages/askar/src/wallet/__tests__/AskarProfileWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarProfileWallet.test.ts deleted file mode 100644 index c501057ab3..0000000000 --- a/packages/askar/src/wallet/__tests__/AskarProfileWallet.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { WalletConfig } from '@credo-ts/core' - -import { KeyDerivationMethod, SigningProviderRegistry, WalletDuplicateError, WalletNotFoundError } from '@credo-ts/core' - -import { agentDependencies, testLogger } from '../../../../core/tests' -import { AskarProfileWallet } from '../AskarProfileWallet' -import { AskarWallet } from '../AskarWallet' - -// use raw key derivation method to speed up wallet creating / opening / closing between tests -const rootWalletConfig: WalletConfig = { - id: 'Wallet: AskarProfileWalletTest', - // generated using indy.generateWalletKey - key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', - keyDerivationMethod: KeyDerivationMethod.Raw, -} - -describe('AskarWallet management', () => { - let rootAskarWallet: AskarWallet - let profileAskarWallet: AskarProfileWallet - - afterEach(async () => { - if (profileAskarWallet) { - await profileAskarWallet.delete() - } - - if (rootAskarWallet) { - await rootAskarWallet.delete() - } - }) - - test('Create, open, close, delete', async () => { - const signingProviderRegistry = new SigningProviderRegistry([]) - rootAskarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), signingProviderRegistry) - - // Create and open wallet - await rootAskarWallet.createAndOpen(rootWalletConfig) - - profileAskarWallet = new AskarProfileWallet(rootAskarWallet.store, testLogger, signingProviderRegistry) - - // Create, open and close profile - await profileAskarWallet.create({ ...rootWalletConfig, id: 'profile-id' }) - await profileAskarWallet.open({ ...rootWalletConfig, id: 'profile-id' }) - await profileAskarWallet.close() - - // try to re-create it - await expect(profileAskarWallet.createAndOpen({ ...rootWalletConfig, id: 'profile-id' })).rejects.toThrowError( - WalletDuplicateError - ) - - // Re-open profile - await profileAskarWallet.open({ ...rootWalletConfig, id: 'profile-id' }) - - // try to open non-existent wallet - await expect(profileAskarWallet.open({ ...rootWalletConfig, id: 'non-existent-profile-id' })).rejects.toThrowError( - WalletNotFoundError - ) - }) -}) diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts deleted file mode 100644 index e9282cd6fc..0000000000 --- a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts +++ /dev/null @@ -1,441 +0,0 @@ -import type { - CreateKeyPairOptions, - KeyPair, - SignOptions, - SigningProvider, - VerifyOptions, - WalletConfig, -} from '@credo-ts/core' -import type { JwkProps } from '@openwallet-foundation/askar-shared' - -import { readFileSync } from 'fs' -import path from 'path' -import { - Buffer, - JsonEncoder, - Key, - KeyDerivationMethod, - KeyType, - SigningProviderRegistry, - TypedArrayEncoder, - WalletDuplicateError, - WalletError, - WalletInvalidKeyError, - WalletKeyExistsError, - WalletNotFoundError, -} from '@credo-ts/core' -import { Key as AskarKey } from '@openwallet-foundation/askar-nodejs' -import { Jwk, Store } from '@openwallet-foundation/askar-shared' - -import { KeyBackend } from '../../../../core/src/crypto/KeyBackend' -import { encodeToBase58 } from '../../../../core/src/utils/base58' -import { agentDependencies } from '../../../../core/tests/helpers' -import testLogger from '../../../../core/tests/logger' -import { AskarWallet } from '../AskarWallet' - -// use raw key derivation method to speed up wallet creating / opening / closing between tests -const walletConfig: WalletConfig = { - id: 'Wallet: AskarWalletTest', - // generated using indy.generateWalletKey - key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', - keyDerivationMethod: KeyDerivationMethod.Raw, -} - -describe('AskarWallet basic operations', () => { - let askarWallet: AskarWallet - - const seed = TypedArrayEncoder.fromString('sample-seed-min-of-32-bytes-long') - const privateKey = TypedArrayEncoder.fromString('2103de41b4ae37e8e28586d84a342b67') - const message = TypedArrayEncoder.fromString('sample-message') - - beforeEach(async () => { - askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) - await askarWallet.createAndOpen(walletConfig) - }) - - afterEach(async () => { - await askarWallet.delete() - }) - - test('supportedKeyTypes', () => { - expect(askarWallet.supportedKeyTypes).toEqual([ - KeyType.Ed25519, - KeyType.X25519, - KeyType.Bls12381g1, - KeyType.Bls12381g2, - KeyType.Bls12381g1g2, - KeyType.P256, - KeyType.P384, - KeyType.K256, - ]) - }) - - test('Get the wallet store', () => { - expect(askarWallet.store).toEqual(expect.any(Store)) - }) - - test('Generate Nonce', async () => { - const nonce = await askarWallet.generateNonce() - - expect(nonce).toMatch(/[0-9]+/) - }) - - test('Create ed25519 keypair from seed', async () => { - const key = await askarWallet.createKey({ - seed, - keyType: KeyType.Ed25519, - }) - - expect(key).toMatchObject({ - keyType: KeyType.Ed25519, - }) - }) - - test('Create ed25519 keypair from private key', async () => { - const key = await askarWallet.createKey({ - privateKey, - keyType: KeyType.Ed25519, - }) - - expect(key).toMatchObject({ - keyType: KeyType.Ed25519, - }) - }) - - test('Attempt to create ed25519 keypair from both seed and private key', async () => { - await expect( - askarWallet.createKey({ - privateKey, - seed, - keyType: KeyType.Ed25519, - }) - ).rejects.toThrow() - }) - - test('Create x25519 keypair', async () => { - await expect(askarWallet.createKey({ seed, keyType: KeyType.X25519 })).resolves.toMatchObject({ - keyType: KeyType.X25519, - }) - }) - - test('Create P-256 keypair', async () => { - await expect( - askarWallet.createKey({ seed: Buffer.concat([seed, seed]), keyType: KeyType.P256 }) - ).resolves.toMatchObject({ - keyType: KeyType.P256, - }) - }) - - test('throws WalletKeyExistsError when a key already exists', async () => { - const privateKey = TypedArrayEncoder.fromString('2103de41b4ae37e8e28586d84a342b68') - await expect(askarWallet.createKey({ privateKey, keyType: KeyType.Ed25519 })).resolves.toEqual(expect.any(Key)) - await expect(askarWallet.createKey({ privateKey, keyType: KeyType.Ed25519 })).rejects.toThrow(WalletKeyExistsError) - }) - - test('Fail to create a P384 keypair', async () => { - await expect(askarWallet.createKey({ seed, keyType: KeyType.P384 })).rejects.toThrow(WalletError) - }) - - test('Fail to create a P256 keypair in the secure environment', async () => { - await expect( - askarWallet.createKey({ keyType: KeyType.P256, keyBackend: KeyBackend.SecureElement }) - ).rejects.toThrow(WalletError) - }) - - test('Create a signature with a ed25519 keypair', async () => { - const ed25519Key = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) - const signature = await askarWallet.sign({ - data: message, - key: ed25519Key, - }) - expect(signature.length).toStrictEqual(64) - }) - - test('Verify a signed message with a ed25519 publicKey', async () => { - const ed25519Key = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) - const signature = await askarWallet.sign({ - data: message, - key: ed25519Key, - }) - await expect(askarWallet.verify({ key: ed25519Key, data: message, signature })).resolves.toStrictEqual(true) - }) - - test('Create K-256 keypair', async () => { - await expect( - askarWallet.createKey({ seed: Buffer.concat([seed, seed]), keyType: KeyType.K256 }) - ).resolves.toMatchObject({ - keyType: KeyType.K256, - }) - }) - - test('Verify a signed message with a k256 publicKey', async () => { - const k256Key = await askarWallet.createKey({ keyType: KeyType.K256 }) - const signature = await askarWallet.sign({ - data: message, - key: k256Key, - }) - await expect(askarWallet.verify({ key: k256Key, data: message, signature })).resolves.toStrictEqual(true) - }) - - test('Encrypt and decrypt using JWE ECDH-ES A256GCM', async () => { - const recipientKey = await askarWallet.createKey({ - keyType: KeyType.P256, - }) - - const apv = TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString('nonce-from-auth-request')) - const apu = TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString(await askarWallet.generateNonce())) - - const compactJwe = await askarWallet.directEncryptCompactJweEcdhEs({ - data: JsonEncoder.toBuffer({ vp_token: ['something'] }), - apu, - apv, - encryptionAlgorithm: 'A256GCM', - header: { - kid: 'some-kid', - }, - recipientKey, - }) - - const { data, header } = await askarWallet.directDecryptCompactJweEcdhEs({ - compactJwe, - recipientKey, - }) - - expect(header).toEqual({ - kid: 'some-kid', - apv, - apu, - enc: 'A256GCM', - alg: 'ECDH-ES', - epk: { - kty: 'EC', - crv: 'P-256', - x: expect.any(String), - y: expect.any(String), - }, - }) - expect(JsonEncoder.fromBuffer(data)).toEqual({ vp_token: ['something'] }) - }) - - test('Encrypt and decrypt using JWE ECDH-ES A128CBC-HS256', async () => { - const recipientKey = await askarWallet.createKey({ - keyType: KeyType.P256, - }) - - const apv = TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString('nonce-from-auth-request')) - const apu = TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString(await askarWallet.generateNonce())) - - const compactJwe = await askarWallet.directEncryptCompactJweEcdhEs({ - data: JsonEncoder.toBuffer({ vp_token: ['something'] }), - apu, - apv, - encryptionAlgorithm: 'A128CBC-HS256', - header: { - kid: 'some-kid', - }, - recipientKey, - }) - - const { data, header } = await askarWallet.directDecryptCompactJweEcdhEs({ - compactJwe, - recipientKey, - }) - - expect(header).toEqual({ - kid: 'some-kid', - apv, - apu, - enc: 'A128CBC-HS256', - alg: 'ECDH-ES', - epk: { - kty: 'EC', - crv: 'P-256', - x: expect.any(String), - y: expect.any(String), - }, - }) - expect(JsonEncoder.fromBuffer(data)).toEqual({ vp_token: ['something'] }) - }) - - test('decrypt using JWE ECDH-ES based on test vector from OpenID Conformance test', async () => { - const { - compactJwe, - decodedPayload, - privateKeyJwk, - header: expectedHeader, - } = JSON.parse( - readFileSync(path.join(__dirname, '__fixtures__/jarm-jwe-encrypted-response.json')).toString('utf-8') - ) as { - compactJwe: string - decodedPayload: Record - privateKeyJwk: JwkProps - header: string - } - - const key = AskarKey.fromJwk({ jwk: Jwk.fromJson(privateKeyJwk) }) - const recipientKey = await askarWallet.createKey({ - keyType: KeyType.P256, - privateKey: Buffer.from(key.secretBytes), - }) - - const { data, header } = await askarWallet.directDecryptCompactJweEcdhEs({ - compactJwe, - recipientKey, - }) - - expect(header).toEqual(expectedHeader) - expect(JsonEncoder.fromBuffer(data)).toEqual(decodedPayload) - }) -}) - -describe.skip('Currently, all KeyTypes are supported by Askar natively', () => { - describe('AskarWallet with custom signing provider', () => { - let askarWallet: AskarWallet - - const seed = TypedArrayEncoder.fromString('sample-seed') - const message = TypedArrayEncoder.fromString('sample-message') - - class DummySigningProvider implements SigningProvider { - public keyType: KeyType = KeyType.Bls12381g1g2 - - public async createKeyPair(options: CreateKeyPairOptions): Promise { - return { - publicKeyBase58: encodeToBase58(Buffer.from(options.seed || TypedArrayEncoder.fromString('publicKeyBase58'))), - privateKeyBase58: 'privateKeyBase58', - keyType: KeyType.Bls12381g1g2, - } - } - - public async sign(_options: SignOptions): Promise { - return new Buffer('signed') - } - - public async verify(_options: VerifyOptions): Promise { - return true - } - } - - beforeEach(async () => { - askarWallet = new AskarWallet( - testLogger, - new agentDependencies.FileSystem(), - new SigningProviderRegistry([new DummySigningProvider()]) - ) - await askarWallet.createAndOpen(walletConfig) - }) - - afterEach(async () => { - await askarWallet.delete() - }) - - test('Create custom keypair and use it for signing', async () => { - const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) - expect(key.keyType).toBe(KeyType.Bls12381g1g2) - expect(key.publicKeyBase58).toBe(encodeToBase58(Buffer.from(seed))) - - const signature = await askarWallet.sign({ - data: message, - key, - }) - - expect(signature).toBeInstanceOf(Buffer) - }) - - test('Create custom keypair and use it for verifying', async () => { - const key = await askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) - expect(key.keyType).toBe(KeyType.Bls12381g1g2) - expect(key.publicKeyBase58).toBe(encodeToBase58(Buffer.from(seed))) - - const signature = await askarWallet.verify({ - data: message, - signature: new Buffer('signature'), - key, - }) - - expect(signature).toBeTruthy() - }) - - test('Attempt to create the same custom keypair twice', async () => { - await askarWallet.createKey({ seed: TypedArrayEncoder.fromString('keybase58'), keyType: KeyType.Bls12381g1g2 }) - - await expect( - askarWallet.createKey({ seed: TypedArrayEncoder.fromString('keybase58'), keyType: KeyType.Bls12381g1g2 }) - ).rejects.toThrow(WalletError) - }) - }) -}) - -describe('AskarWallet management', () => { - let askarWallet: AskarWallet - - afterEach(async () => { - if (askarWallet) { - await askarWallet.delete() - } - }) - - test('Create', async () => { - askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) - - const initialKey = Store.generateRawKey() - const anotherKey = Store.generateRawKey() - - // Create and open wallet - await askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Create', key: initialKey }) - - // Close and try to re-create it - await askarWallet.close() - await expect( - askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Create', key: anotherKey }) - ).rejects.toThrow(WalletDuplicateError) - }) - - test('Open', async () => { - askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) - - const initialKey = Store.generateRawKey() - const wrongKey = Store.generateRawKey() - - // Create and open wallet - await askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Open', key: initialKey }) - - // Close and try to re-opening it with a wrong key - await askarWallet.close() - await expect(askarWallet.open({ ...walletConfig, id: 'AskarWallet Open', key: wrongKey })).rejects.toThrow( - WalletInvalidKeyError - ) - - // Try to open a non existent wallet - await expect( - askarWallet.open({ ...walletConfig, id: 'AskarWallet Open - Non existent', key: initialKey }) - ).rejects.toThrow(WalletNotFoundError) - }) - - test('Rotate key', async () => { - askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) - - const initialKey = Store.generateRawKey() - await askarWallet.createAndOpen({ ...walletConfig, id: 'AskarWallet Key Rotation', key: initialKey }) - - await askarWallet.close() - - const newKey = Store.generateRawKey() - await askarWallet.rotateKey({ - ...walletConfig, - id: 'AskarWallet Key Rotation', - key: initialKey, - rekey: newKey, - rekeyDerivationMethod: KeyDerivationMethod.Raw, - }) - - await askarWallet.close() - - await expect( - askarWallet.open({ ...walletConfig, id: 'AskarWallet Key Rotation', key: initialKey }) - ).rejects.toThrow(WalletInvalidKeyError) - - await askarWallet.open({ ...walletConfig, id: 'AskarWallet Key Rotation', key: newKey }) - - await askarWallet.close() - }) -}) diff --git a/packages/askar/src/wallet/__tests__/packing.test.ts b/packages/askar/src/wallet/__tests__/packing.test.ts deleted file mode 100644 index 156c36ebf0..0000000000 --- a/packages/askar/src/wallet/__tests__/packing.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { WalletConfig } from '@credo-ts/core' - -import { JsonTransformer, KeyDerivationMethod, KeyType, SigningProviderRegistry } from '@credo-ts/core' - -import { agentDependencies } from '../../../../core/tests/helpers' -import testLogger from '../../../../core/tests/logger' -import { BasicMessage } from '../../../../didcomm' -import { AskarWallet } from '../AskarWallet' - -// use raw key derivation method to speed up wallet creating / opening / closing between tests -const walletConfig: WalletConfig = { - id: 'Askar Wallet Packing', - // generated using indy.generateWalletKey - key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', - keyDerivationMethod: KeyDerivationMethod.Raw, -} - -describe('askarWallet packing', () => { - let askarWallet: AskarWallet - - beforeEach(async () => { - askarWallet = new AskarWallet(testLogger, new agentDependencies.FileSystem(), new SigningProviderRegistry([])) - await askarWallet.createAndOpen(walletConfig) - }) - - afterEach(async () => { - await askarWallet.delete() - }) - - test('DIDComm V1 packing and unpacking', async () => { - // Create both sender and recipient keys - const senderKey = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) - const recipientKey = await askarWallet.createKey({ keyType: KeyType.Ed25519 }) - - const message = new BasicMessage({ content: 'hello' }) - - const encryptedMessage = await askarWallet.pack( - message.toJSON(), - [recipientKey.publicKeyBase58], - senderKey.publicKeyBase58 - ) - - const plainTextMessage = await askarWallet.unpack(encryptedMessage) - - expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, BasicMessage)).toEqual(message) - }) -}) diff --git a/packages/askar/src/wallet/didcommV1.ts b/packages/askar/src/wallet/didcommV1.ts deleted file mode 100644 index cce8728639..0000000000 --- a/packages/askar/src/wallet/didcommV1.ts +++ /dev/null @@ -1,179 +0,0 @@ -import type { EncryptedMessage } from '@credo-ts/core' - -import { Buffer, JsonEncoder, JsonTransformer, Key, KeyType, TypedArrayEncoder, WalletError } from '@credo-ts/core' -import { Key as AskarKey, CryptoBox, KeyAlgorithm } from '@openwallet-foundation/askar-shared' - -import { JweEnvelope, JweRecipient } from './JweEnvelope' - -export function didcommV1Pack(payload: Record, recipientKeys: string[], senderKey?: AskarKey) { - let cek: AskarKey | undefined - let senderExchangeKey: AskarKey | undefined - - try { - cek = AskarKey.generate(KeyAlgorithm.Chacha20C20P) - - senderExchangeKey = senderKey ? senderKey.convertkey({ algorithm: KeyAlgorithm.X25519 }) : undefined - - const recipients: JweRecipient[] = [] - - for (const recipientKey of recipientKeys) { - let targetExchangeKey: AskarKey | undefined - try { - targetExchangeKey = AskarKey.fromPublicBytes({ - publicKey: Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519).publicKey, - algorithm: KeyAlgorithm.Ed25519, - }).convertkey({ algorithm: KeyAlgorithm.X25519 }) - - if (senderKey && senderExchangeKey) { - const encryptedSender = CryptoBox.seal({ - recipientKey: targetExchangeKey, - message: TypedArrayEncoder.fromString(TypedArrayEncoder.toBase58(senderKey.publicBytes)), - }) - const nonce = CryptoBox.randomNonce() - const encryptedCek = CryptoBox.cryptoBox({ - recipientKey: targetExchangeKey, - senderKey: senderExchangeKey, - message: cek.secretBytes, - nonce, - }) - - recipients.push( - new JweRecipient({ - encryptedKey: encryptedCek, - header: { - kid: recipientKey, - sender: TypedArrayEncoder.toBase64URL(encryptedSender), - iv: TypedArrayEncoder.toBase64URL(nonce), - }, - }) - ) - } else { - const encryptedCek = CryptoBox.seal({ - recipientKey: targetExchangeKey, - message: cek.secretBytes, - }) - recipients.push( - new JweRecipient({ - encryptedKey: encryptedCek, - header: { - kid: recipientKey, - }, - }) - ) - } - } finally { - targetExchangeKey?.handle.free() - } - } - - const protectedJson = { - enc: 'xchacha20poly1305_ietf', - typ: 'JWM/1.0', - alg: senderKey ? 'Authcrypt' : 'Anoncrypt', - recipients: recipients.map((item) => JsonTransformer.toJSON(item)), - } - - const { ciphertext, tag, nonce } = cek.aeadEncrypt({ - message: Buffer.from(JSON.stringify(payload)), - aad: Buffer.from(JsonEncoder.toBase64URL(protectedJson)), - }).parts - - const envelope = new JweEnvelope({ - ciphertext: TypedArrayEncoder.toBase64URL(ciphertext), - iv: TypedArrayEncoder.toBase64URL(nonce), - protected: JsonEncoder.toBase64URL(protectedJson), - tag: TypedArrayEncoder.toBase64URL(tag), - }).toJson() - - return envelope as EncryptedMessage - } finally { - cek?.handle.free() - senderExchangeKey?.handle.free() - } -} - -export function didcommV1Unpack(messagePackage: EncryptedMessage, recipientKey: AskarKey) { - const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) - - const alg = protectedJson.alg - if (!['Anoncrypt', 'Authcrypt'].includes(alg)) { - throw new WalletError(`Unsupported pack algorithm: ${alg}`) - } - - const recipient = protectedJson.recipients.find( - // biome-ignore lint/suspicious/noExplicitAny: - (r: any) => r.header.kid === TypedArrayEncoder.toBase58(recipientKey.publicBytes) - ) - - if (!recipient) { - throw new WalletError('No corresponding recipient key found') - } - - const sender = recipient?.header.sender ? TypedArrayEncoder.fromBase64(recipient.header.sender) : undefined - const iv = recipient?.header.iv ? TypedArrayEncoder.fromBase64(recipient.header.iv) : undefined - const encrypted_key = TypedArrayEncoder.fromBase64(recipient.encrypted_key) - - if (sender && !iv) { - throw new WalletError('Missing IV') - } - if (!sender && iv) { - throw new WalletError('Unexpected IV') - } - - let payloadKey: Uint8Array - let senderKey: string | undefined - - let sender_x: AskarKey | undefined - let recip_x: AskarKey | undefined - - try { - recip_x = recipientKey.convertkey({ algorithm: KeyAlgorithm.X25519 }) - - if (sender && iv) { - senderKey = TypedArrayEncoder.toUtf8String( - CryptoBox.sealOpen({ - recipientKey: recip_x, - ciphertext: sender, - }) - ) - sender_x = AskarKey.fromPublicBytes({ - algorithm: KeyAlgorithm.Ed25519, - publicKey: TypedArrayEncoder.fromBase58(senderKey), - }).convertkey({ algorithm: KeyAlgorithm.X25519 }) - - payloadKey = CryptoBox.open({ - recipientKey: recip_x, - senderKey: sender_x, - message: encrypted_key, - nonce: iv, - }) - } else { - payloadKey = CryptoBox.sealOpen({ ciphertext: encrypted_key, recipientKey: recip_x }) - } - } finally { - sender_x?.handle.free() - recip_x?.handle.free() - } - - if (!senderKey && alg === 'Authcrypt') { - throw new WalletError('Sender public key not provided for Authcrypt') - } - - let cek: AskarKey | undefined - try { - cek = AskarKey.fromSecretBytes({ algorithm: KeyAlgorithm.Chacha20C20P, secretKey: payloadKey }) - const message = cek.aeadDecrypt({ - ciphertext: TypedArrayEncoder.fromBase64(messagePackage.ciphertext), - nonce: TypedArrayEncoder.fromBase64(messagePackage.iv), - tag: TypedArrayEncoder.fromBase64(messagePackage.tag), - aad: TypedArrayEncoder.fromString(messagePackage.protected), - }) - return { - plaintextMessage: JsonEncoder.fromBuffer(message), - senderKey, - recipientKey: TypedArrayEncoder.toBase58(recipientKey.publicBytes), - } - } finally { - cek?.handle.free() - } -} diff --git a/packages/askar/src/wallet/index.ts b/packages/askar/src/wallet/index.ts deleted file mode 100644 index 49e1da0a79..0000000000 --- a/packages/askar/src/wallet/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { AskarWallet } from './AskarWallet' -export { AskarProfileWallet } from './AskarProfileWallet' -export * from './AskarWalletStorageConfig' diff --git a/packages/askar/tests/askar-inmemory.test.ts b/packages/askar/tests/askar-inmemory.test.ts index 51e0c25ed9..d8e3cbb664 100644 --- a/packages/askar/tests/askar-inmemory.test.ts +++ b/packages/askar/tests/askar-inmemory.test.ts @@ -26,18 +26,16 @@ const bobInMemoryAgentOptions = getAskarSqliteAgentOptions( ) describe('Askar In Memory agents', () => { - let aliceAgent: Agent - let bobAgent: Agent + let aliceAgent: Agent<(typeof aliceInMemoryAgentOptions)['modules']> + let bobAgent: Agent<(typeof bobInMemoryAgentOptions)['modules']> afterAll(async () => { if (bobAgent) { await bobAgent.shutdown() - await bobAgent.wallet.delete() } if (aliceAgent) { await aliceAgent.shutdown() - await aliceAgent.wallet.delete() } }) diff --git a/packages/askar/tests/askar-postgres.e2e.test.ts b/packages/askar/tests/askar-postgres.e2e.test.ts index 9457d91b88..02a8272e1b 100644 --- a/packages/askar/tests/askar-postgres.e2e.test.ts +++ b/packages/askar/tests/askar-postgres.e2e.test.ts @@ -22,20 +22,20 @@ const bobPostgresAgentOptions = getAskarPostgresAgentOptions( ) describe('Askar Postgres agents', () => { - let aliceAgent: Agent - let bobAgent: Agent - - afterAll(async () => { - if (bobAgent) { - await bobAgent.shutdown() - await bobAgent.wallet.delete() - } - - if (aliceAgent) { - await aliceAgent.shutdown() - await aliceAgent.wallet.delete() - } - }) + let aliceAgent: Agent<(typeof alicePostgresAgentOptions)['modules']> + let bobAgent: Agent<(typeof bobPostgresAgentOptions)['modules']> + + // afterAll(async () => { + // if (bobAgent) { + // await bobAgent.shutdown() + // // await bobAgent.modules.askar.deleteStore() + // } + + // if (aliceAgent) { + // await aliceAgent.shutdown() + // // await aliceAgent.modules.askar.deleteStore() + // } + // }) test('Postgres Askar wallets E2E test', async () => { const aliceMessages = new Subject() diff --git a/packages/askar/tests/askar-sqlite.test.ts b/packages/askar/tests/askar-sqlite.test.ts deleted file mode 100644 index 2473cbac41..0000000000 --- a/packages/askar/tests/askar-sqlite.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { tmpdir } from 'os' -import path from 'path' -import { - Agent, - KeyDerivationMethod, - TypedArrayEncoder, - WalletDuplicateError, - WalletInvalidKeyError, - WalletNotFoundError, - utils, -} from '@credo-ts/core' -import { Store } from '@openwallet-foundation/askar-shared' - -import { BasicMessageRecord, BasicMessageRepository, BasicMessageRole } from '../..//didcomm' - -import { getAskarSqliteAgentOptions } from './helpers' - -const aliceAgentOptions = getAskarSqliteAgentOptions('AgentsAlice') -const bobAgentOptions = getAskarSqliteAgentOptions('AgentsBob') - -describe('Askar SQLite agents', () => { - let aliceAgent: Agent - let bobAgent: Agent - - beforeEach(async () => { - aliceAgent = new Agent(aliceAgentOptions) - bobAgent = new Agent(bobAgentOptions) - }) - - afterEach(async () => { - await aliceAgent.shutdown() - await bobAgent.shutdown() - - if (aliceAgent.wallet.isProvisioned) { - await aliceAgent.wallet.delete() - } - if (bobAgent.wallet.isProvisioned) { - await bobAgent.wallet.delete() - } - }) - - test('open, create and open wallet with different wallet key that it is in agent config', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey-0', - } - - try { - await aliceAgent.wallet.open(walletConfig) - } catch (error) { - if (error instanceof WalletNotFoundError) { - await aliceAgent.wallet.create(walletConfig) - await aliceAgent.wallet.open(walletConfig) - } - } - - await aliceAgent.initialize() - - expect(aliceAgent.isInitialized).toBe(true) - }) - - test('when opening non-existing wallet throw WalletNotFoundError', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey-1', - } - - await expect(aliceAgent.wallet.open(walletConfig)).rejects.toThrowError(WalletNotFoundError) - }) - - test('when create wallet and shutdown, wallet is closed', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey-2', - } - - await aliceAgent.wallet.create(walletConfig) - - await aliceAgent.shutdown() - - await expect(aliceAgent.wallet.open(walletConfig)).resolves.toBeUndefined() - }) - - test('create wallet with custom key derivation method', async () => { - const walletConfig = { - id: 'mywallet', - key: Store.generateRawKey(TypedArrayEncoder.fromString('mysecretwalletkey')), - keyDerivationMethod: KeyDerivationMethod.Raw, - } - - await aliceAgent.wallet.createAndOpen(walletConfig) - - expect(aliceAgent.wallet.isInitialized).toBe(true) - }) - - test('when exporting and importing a wallet, content is copied', async () => { - await bobAgent.initialize() - const bobBasicMessageRepository = bobAgent.dependencyManager.resolve(BasicMessageRepository) - - const basicMessageRecord = new BasicMessageRecord({ - id: 'some-id', - connectionId: 'connId', - content: 'hello', - role: BasicMessageRole.Receiver, - sentTime: 'sentIt', - }) - - // Save in wallet - await bobBasicMessageRepository.save(bobAgent.context, basicMessageRecord) - - if (!bobAgent.config.walletConfig) { - throw new Error('No wallet config on bobAgent') - } - - const backupKey = 'someBackupKey' - const backupWalletName = `backup-${utils.uuid()}` - const backupPath = path.join(tmpdir(), backupWalletName) - - // Create backup and delete wallet - await bobAgent.wallet.export({ path: backupPath, key: backupKey }) - await bobAgent.wallet.delete() - - // Initialize the wallet again and assert record does not exist - // This should create a new wallet - await bobAgent.wallet.initialize(bobAgent.config.walletConfig) - expect(await bobBasicMessageRepository.findById(bobAgent.context, basicMessageRecord.id)).toBeNull() - await bobAgent.wallet.delete() - - // Import backup with SAME wallet id and initialize - await bobAgent.wallet.import(bobAgent.config.walletConfig, { path: backupPath, key: backupKey }) - await bobAgent.wallet.initialize(bobAgent.config.walletConfig) - - // Expect same basic message record to exist in new wallet - expect(await bobBasicMessageRepository.getById(bobAgent.context, basicMessageRecord.id)).toMatchObject({ - id: basicMessageRecord.id, - connectionId: basicMessageRecord.connectionId, - content: basicMessageRecord.content, - createdAt: basicMessageRecord.createdAt, - updatedAt: basicMessageRecord.updatedAt, - type: basicMessageRecord.type, - }) - }) - - test('throws error when exporting a wallet and importing it with a different walletConfig.id', async () => { - await bobAgent.initialize() - - if (!bobAgent.config.walletConfig) { - throw new Error('No wallet config on bobAgent') - } - - const backupKey = 'someBackupKey' - const backupWalletName = `backup-${utils.uuid()}` - const backupPath = path.join(tmpdir(), backupWalletName) - - // Create backup and delete wallet - await bobAgent.wallet.export({ path: backupPath, key: backupKey }) - await bobAgent.wallet.delete() - - // Import backup with different wallet id and initialize - await expect( - bobAgent.wallet.import({ id: backupWalletName, key: backupWalletName }, { path: backupPath, key: backupKey }) - ).rejects.toThrow( - `Error importing wallet '${backupWalletName}': Trying to import wallet with walletConfig.id ${backupWalletName}, however the wallet contains a default profile with id ${bobAgent.config.walletConfig.id}. The walletConfig.id MUST match with the default profile. In the future this behavior may be changed. See https://github.com/openwallet-foundation/askar/issues/221 for more information.` - ) - }) - - test('throws error when attempting to export and import to existing paths', async () => { - await bobAgent.initialize() - - if (!bobAgent.config.walletConfig) { - throw new Error('No wallet config on bobAgent') - } - - const backupKey = 'someBackupKey' - const backupWalletName = `backup-${utils.uuid()}` - const backupPath = path.join(tmpdir(), backupWalletName) - - // Create backup and try to export it again to the same path - await bobAgent.wallet.export({ path: backupPath, key: backupKey }) - await expect(bobAgent.wallet.export({ path: backupPath, key: backupKey })).rejects.toThrow( - /Unable to create export/ - ) - - await bobAgent.wallet.delete() - - // Import backup with different wallet id and initialize - await bobAgent.wallet.import(bobAgent.config.walletConfig, { path: backupPath, key: backupKey }) - await bobAgent.wallet.initialize(bobAgent.config.walletConfig) - await bobAgent.wallet.close() - - // Try to import again an existing wallet - await expect( - bobAgent.wallet.import(bobAgent.config.walletConfig, { path: backupPath, key: backupKey }) - ).rejects.toThrow(/Unable to import wallet/) - }) - - test('throws error when attempting to import using wrong key', async () => { - await bobAgent.initialize() - - if (!bobAgent.config.walletConfig) { - throw new Error('No wallet config on bobAgent') - } - - const backupKey = 'someBackupKey' - const wrongBackupKey = 'wrongBackupKey' - const backupWalletName = `backup-${utils.uuid()}` - const backupPath = path.join(tmpdir(), backupWalletName) - - // Create backup and try to export it again to the same path - await bobAgent.wallet.export({ path: backupPath, key: backupKey }) - await bobAgent.wallet.delete() - - // Try to import backup with wrong key - await expect( - bobAgent.wallet.import(bobAgent.config.walletConfig, { path: backupPath, key: wrongBackupKey }) - ).rejects.toThrow() - - // Try to import again using the correct key - await bobAgent.wallet.import(bobAgent.config.walletConfig, { path: backupPath, key: backupKey }) - await bobAgent.wallet.initialize(bobAgent.config.walletConfig) - await bobAgent.wallet.close() - }) - - test('changing wallet key', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey', - } - - await aliceAgent.wallet.createAndOpen(walletConfig) - await aliceAgent.initialize() - - //Close agent - const walletConfigRekey = { - id: 'mywallet', - key: 'mysecretwalletkey', - rekey: '123', - } - - await aliceAgent.shutdown() - await aliceAgent.wallet.rotateKey(walletConfigRekey) - await aliceAgent.initialize() - - expect(aliceAgent.isInitialized).toBe(true) - }) - - test('when creating already existing wallet throw WalletDuplicateError', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey-2', - } - - await aliceAgent.wallet.create(walletConfig) - await expect(aliceAgent.wallet.create(walletConfig)).rejects.toThrowError(WalletDuplicateError) - }) - - test('when opening wallet with invalid key throw WalletInvalidKeyError', async () => { - const walletConfig = { - id: 'mywallet', - key: 'mysecretwalletkey-3', - } - - await aliceAgent.wallet.create(walletConfig) - await expect(aliceAgent.wallet.open({ ...walletConfig, key: 'abcd' })).rejects.toThrowError(WalletInvalidKeyError) - }) -}) diff --git a/packages/askar/tests/askar-store-api.test.ts b/packages/askar/tests/askar-store-api.test.ts new file mode 100644 index 0000000000..c6d0c7205c --- /dev/null +++ b/packages/askar/tests/askar-store-api.test.ts @@ -0,0 +1,286 @@ +import { tmpdir } from 'os' +import path from 'path' +import { Agent, utils } from '@credo-ts/core' + +import { BasicMessageRecord, BasicMessageRepository, BasicMessageRole } from '../../didcomm/src' + +import { AskarStoreDuplicateError, AskarStoreInvalidKeyError, AskarStoreNotFoundError } from '../src/error' +import { getAskarSqliteAgentOptions } from './helpers' + +const aliceAgentOptions = getAskarSqliteAgentOptions('AgentsAlice') +const bobAgentOptions = getAskarSqliteAgentOptions('AgentsBob') + +describe('Askar SQLite agents', () => { + let aliceAgent: Agent<(typeof aliceAgentOptions)['modules']> + let bobAgent: Agent<(typeof bobAgentOptions)['modules']> + + beforeEach(async () => { + aliceAgent = new Agent(aliceAgentOptions) + bobAgent = new Agent(bobAgentOptions) + }) + + afterEach(async () => { + if (aliceAgent.modules.askar.isStoreOpen) { + await aliceAgent.shutdown() + await aliceAgent.modules.askar.deleteStore() + } + + if (bobAgent.modules.askar.isStoreOpen) { + await bobAgent.shutdown() + await bobAgent.modules.askar.deleteStore() + } + }) + + test('when opening non-existing store throw AskarStoreNotFoundError', async () => { + // @ts-expect-error + aliceAgentOptions.modules.askar.config.options.store = { + id: 'mywallet', + key: 'mysecretwalletkey-0', + } + + await expect(aliceAgent.modules.askar.openStore()).rejects.toThrow(AskarStoreNotFoundError) + }) + + test('when create store and shutdown, store is closed', async () => { + // @ts-expect-error + aliceAgentOptions.modules.askar.config.options.store = { + id: 'mywallet', + key: 'mysecretwalletkey-0', + } + + await aliceAgent.modules.askar.provisionStore() + await aliceAgent.shutdown() + + await expect(aliceAgent.modules.askar.openStore()).resolves.toBeUndefined() + + await aliceAgent.modules.askar.deleteStore() + }) + + test('create store with custom key derivation method', async () => { + // @ts-expect-error + aliceAgentOptions.modules.askar.config.options.store = { + id: 'mywallet', + key: 'mysecretwalletkey-0', + keyDerivationMethod: 'kdf:argon2i:int', + } + + await aliceAgent.modules.askar.provisionStore() + + expect(aliceAgent.modules.askar.isStoreOpen).toBe(true) + + await aliceAgent.modules.askar.deleteStore() + }) + + test('when exporting and importing a store, content is copied', async () => { + await bobAgent.initialize() + const bobBasicMessageRepository = bobAgent.dependencyManager.resolve(BasicMessageRepository) + + const basicMessageRecord = new BasicMessageRecord({ + id: 'some-id', + connectionId: 'connId', + content: 'hello', + role: BasicMessageRole.Receiver, + sentTime: 'sentIt', + }) + + // Save in wallet + await bobBasicMessageRepository.save(bobAgent.context, basicMessageRecord) + + const backupKey = 'someBackupKey' + const backupWalletName = `backup-${utils.uuid()}` + const backupPath = path.join(tmpdir(), backupWalletName) + + // Create backup and delete wallet + await bobAgent.modules.askar.exportStore({ + exportToStore: { id: 'newwallet', key: backupKey, database: { type: 'sqlite', config: { path: backupPath } } }, + }) + await bobAgent.modules.askar.deleteStore() + + // Initialize the wallet again and assert record does not exist + // This should create a new wallet + await bobAgent.modules.askar.provisionStore() + expect(await bobBasicMessageRepository.findById(bobAgent.context, basicMessageRecord.id)).toBeNull() + await bobAgent.modules.askar.deleteStore() + + // Import backup with SAME wallet id and initialize + await bobAgent.modules.askar.importStore({ + importFromStore: { id: 'newwallet', key: backupKey, database: { type: 'sqlite', config: { path: backupPath } } }, + }) + await bobAgent.modules.askar.openStore() + + // Expect same basic message record to exist in new wallet + expect(await bobBasicMessageRepository.getById(bobAgent.context, basicMessageRecord.id)).toMatchObject({ + id: basicMessageRecord.id, + connectionId: basicMessageRecord.connectionId, + content: basicMessageRecord.content, + createdAt: basicMessageRecord.createdAt, + updatedAt: basicMessageRecord.updatedAt, + type: basicMessageRecord.type, + }) + await aliceAgent.modules.askar.deleteStore() + }) + + test('throws error when attempting to export and import to existing paths', async () => { + await bobAgent.initialize() + + const backupKey = 'someBackupKey' + const backupWalletName = `backup-${utils.uuid()}` + const backupPath = path.join(tmpdir(), backupWalletName) + + // Create backup and try to export it again to the same path + await bobAgent.modules.askar.exportStore({ + exportToStore: { + key: backupKey, + id: 'new-wallet-id', + database: { + type: 'sqlite', + config: { + path: backupPath, + }, + }, + }, + }) + await expect( + bobAgent.modules.askar.exportStore({ + exportToStore: { + key: backupKey, + id: 'new-wallet-id', + database: { + type: 'sqlite', + config: { + path: backupPath, + }, + }, + }, + }) + ).rejects.toThrow(/Unable to create export/) + + await bobAgent.modules.askar.deleteStore() + + // Import backup with different wallet id and initialize + await bobAgent.modules.askar.importStore({ + importFromStore: { + key: backupKey, + id: 'new-wallet-id', + database: { + type: 'sqlite', + config: { + path: backupPath, + }, + }, + }, + }) + await bobAgent.modules.askar.openStore() + await bobAgent.modules.askar.closeStore() + + // Try to import again an existing wallet + await expect( + bobAgent.modules.askar.importStore({ + importFromStore: { + key: backupKey, + id: 'new-wallet-id', + database: { + type: 'sqlite', + config: { + path: backupPath, + }, + }, + }, + }) + ).rejects.toThrow(/Unable to import store/) + + await aliceAgent.modules.askar.deleteStore() + }) + + test('throws error when attempting to import using wrong key', async () => { + await bobAgent.initialize() + + const backupKey = 'someBackupKey' + const wrongBackupKey = 'wrongBackupKey' + const backupWalletName = `backup-${utils.uuid()}` + const backupPath = path.join(tmpdir(), backupWalletName) + + // Create backup and try to export it again to the same path + await bobAgent.modules.askar.exportStore({ + exportToStore: { + key: backupKey, + id: 'new-wallet-id', + database: { + type: 'sqlite', + config: { + path: backupPath, + }, + }, + }, + }) + await bobAgent.modules.askar.deleteStore() + + // Try to import backup with wrong key + await expect( + bobAgent.modules.askar.importStore({ + importFromStore: { + key: wrongBackupKey, + id: 'new-wallet-id', + database: { + type: 'sqlite', + config: { + path: backupPath, + }, + }, + }, + }) + ).rejects.toThrow() + + // Try to import again using the correct key + await bobAgent.modules.askar.importStore({ + importFromStore: { + key: backupKey, + id: 'new-wallet-id', + database: { + type: 'sqlite', + config: { + path: backupPath, + }, + }, + }, + }) + await bobAgent.modules.askar.openStore() + await bobAgent.modules.askar.closeStore() + await aliceAgent.modules.askar.deleteStore() + }) + + test('changing store key', async () => { + await aliceAgent.modules.askar.provisionStore() + await aliceAgent.initialize() + + await aliceAgent.modules.askar.rotateStoreKey({ newKey: 'mysecretwalletkey' }) + + expect(aliceAgent.isInitialized).toBe(true) + + await aliceAgent.modules.askar.deleteStore() + }) + + test('when creating already existing store throw AskarStoreDuplicateError', async () => { + await aliceAgent.modules.askar.provisionStore() + await aliceAgent.modules.askar.closeStore() + await expect(aliceAgent.modules.askar.provisionStore()).rejects.toThrow(AskarStoreDuplicateError) + + await aliceAgent.modules.askar.deleteStore() + }) + + test('when opening store with invalid key throw AskarStoreInvalidKeyError', async () => { + await aliceAgent.modules.askar.provisionStore() + await aliceAgent.modules.askar.closeStore() + + // @ts-expect-error + aliceAgentOptions.modules.askar.config.options.store = { + // @ts-expect-error + ...aliceAgentOptions.modules.askar.config.options.store, + key: 'some-random-key', + } + + await expect(aliceAgent.modules.askar.openStore()).rejects.toThrow(AskarStoreInvalidKeyError) + + await aliceAgent.modules.askar.deleteStore() + }) +}) diff --git a/packages/askar/tests/helpers.ts b/packages/askar/tests/helpers.ts index 51981a1fc6..9039ff2d60 100644 --- a/packages/askar/tests/helpers.ts +++ b/packages/askar/tests/helpers.ts @@ -1,6 +1,5 @@ import type { Agent, InitConfig } from '@credo-ts/core' import type { DidCommModuleConfig } from '../..//didcomm' -import type { AskarWalletPostgresStorageConfig } from '../src/wallet' import path from 'path' import { LogLevel, utils } from '@credo-ts/core' @@ -12,26 +11,19 @@ import { TestLogger } from '../../core/tests/logger' import { ConnectionsModule, HandshakeProtocol } from '../../didcomm' import { getDefaultDidcommModules } from '../../didcomm/src/util/modules' import { agentDependencies } from '../../node/src' +import { AskarPostgresStorageConfig } from '../src' import { AskarModule } from '../src/AskarModule' -import { AskarModuleConfig } from '../src/AskarModuleConfig' -import { AskarWallet } from '../src/wallet' -export const askarModuleConfig = new AskarModuleConfig({ askar }) -registerAskar({ askar: askarModuleConfig.askar }) -export const askarModule = new AskarModule(askarModuleConfig) +registerAskar({ askar }) export { askar } -// When using the AskarWallet directly, the native dependency won't be loaded by default. -// So in tests depending on Askar, we import this wallet so we're sure the native dependency is loaded. -export const RegisteredAskarTestWallet = AskarWallet - export const genesisPath = process.env.GENESIS_TXN_PATH ? path.resolve(process.env.GENESIS_TXN_PATH) : path.join(__dirname, '../../../../network/genesis/local-genesis.txn') export const publicDidSeed = process.env.TEST_AGENT_PUBLIC_DID_SEED ?? '000000000000000000000000Trustee9' -export const askarPostgresStorageConfig: AskarWalletPostgresStorageConfig = { +export const askarPostgresStorageConfig: AskarPostgresStorageConfig = { type: 'postgres', config: { host: 'localhost:5432', @@ -45,17 +37,12 @@ export const askarPostgresStorageConfig: AskarWalletPostgresStorageConfig = { export function getAskarPostgresAgentOptions( name: string, didcommConfig: Partial, - storageConfig: AskarWalletPostgresStorageConfig, + storageConfig: AskarPostgresStorageConfig, extraConfig: Partial = {} ) { const random = utils.uuid().slice(0, 4) const config: InitConfig = { label: `PostgresAgent: ${name} - ${random}`, - walletConfig: { - id: `PostgresWallet${name}${random}`, - key: `Key${name}`, - storage: storageConfig, - }, autoUpdateStorageOnStartup: false, logger: new TestLogger(LogLevel.off, name), ...extraConfig, @@ -65,7 +52,14 @@ export function getAskarPostgresAgentOptions( dependencies: agentDependencies, modules: { ...getDefaultDidcommModules(didcommConfig), - askar: new AskarModule(askarModuleConfig), + askar: new AskarModule({ + askar, + store: { + id: `PostgresWallet${name}${random}`, + key: `Key${name}`, + database: storageConfig, + }, + }), connections: new ConnectionsModule({ autoAcceptConnections: true, }), @@ -82,11 +76,6 @@ export function getAskarSqliteAgentOptions( const random = utils.uuid().slice(0, 4) const config: InitConfig = { label: `SQLiteAgent: ${name} - ${random}`, - walletConfig: { - id: `SQLiteWallet${name} - ${random}`, - key: `Key${name}`, - storage: { type: 'sqlite', inMemory }, - }, autoUpdateStorageOnStartup: false, logger: new TestLogger(LogLevel.off, name), ...extraConfig, @@ -96,7 +85,14 @@ export function getAskarSqliteAgentOptions( dependencies: agentDependencies, modules: { ...getDefaultDidcommModules(didcommConfig), - askar: new AskarModule(askarModuleConfig), + askar: new AskarModule({ + askar, + store: { + id: `SQLiteWallet${name} - ${random}`, + key: `Key${name}`, + database: { type: 'sqlite', config: { inMemory } }, + }, + }), connections: new ConnectionsModule({ autoAcceptConnections: true, }), diff --git a/packages/bbs-signatures/CHANGELOG.md b/packages/bbs-signatures/CHANGELOG.md deleted file mode 100644 index 9d773570f5..0000000000 --- a/packages/bbs-signatures/CHANGELOG.md +++ /dev/null @@ -1,128 +0,0 @@ -# Changelog - -## 0.5.13 - -### Patch Changes - -- Updated dependencies [595c3d6] - - @credo-ts/core@0.5.13 - -## 0.5.12 - -### Patch Changes - -- Updated dependencies [3c85565] -- Updated dependencies [3c85565] -- Updated dependencies [7d51fcb] -- Updated dependencies [9756a4a] - - @credo-ts/core@0.5.12 - -## 0.5.11 - -### Patch Changes - -- @credo-ts/core@0.5.11 - -## 0.5.10 - -### Patch Changes - -- Updated dependencies [fa62b74] - - @credo-ts/core@0.5.10 - -## 0.5.9 - -### Patch Changes - -- @credo-ts/core@0.5.9 - -## 0.5.8 - -### Patch Changes - -- Updated dependencies [3819eb2] -- Updated dependencies [15d0a54] -- Updated dependencies [a5235e7] - - @credo-ts/core@0.5.8 - -## 0.5.7 - -### Patch Changes - -- Updated dependencies [352383f] -- Updated dependencies [1044c9d] - - @credo-ts/core@0.5.7 - -## 0.5.6 - -### Patch Changes - -- 66e696d: Fix build issue causing error with importing packages in 0.5.5 release -- Updated dependencies [66e696d] - - @credo-ts/core@0.5.6 - -## 0.5.5 - -### Patch Changes - -- 482a630: - feat: allow serving dids from did record (#1856) - - fix: set created at for anoncreds records (#1862) - - feat: add goal to public api for credential and proof (#1867) - - fix(oob): only reuse connection if enabled (#1868) - - fix: issuer id query anoncreds w3c (#1870) - - feat: sd-jwt issuance without holder binding (#1871) - - chore: update oid4vci deps (#1873) - - fix: query for qualified/unqualified forms in revocation notification (#1866) - - fix: wrong schema id is stored for credentials (#1884) - - fix: process credential or proof problem report message related to connectionless or out of band exchange (#1859) - - fix: unqualified indy revRegDefId in migration (#1887) - - feat: verify SD-JWT Token status list and SD-JWT VC fixes (#1872) - - fix(anoncreds): combine creds into one proof (#1893) - - fix: AnonCreds proof requests with unqualified dids (#1891) - - fix: WebSocket priority in Message Pick Up V2 (#1888) - - fix: anoncreds predicate only proof with unqualified dids (#1907) - - feat: add pagination params to storage service (#1883) - - feat: add message handler middleware and fallback (#1894) -- Updated dependencies [3239ef3] -- Updated dependencies [d548fa4] -- Updated dependencies [482a630] - - @credo-ts/core@0.5.5 - -## [0.5.3](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.2...v0.5.3) (2024-05-01) - -**Note:** Version bump only for package @credo-ts/bbs-signatures - -## [0.5.2](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.1...v0.5.2) (2024-04-26) - -**Note:** Version bump only for package @credo-ts/bbs-signatures - -## [0.5.1](https://github.com/openwallet-foundation/credo-ts/compare/v0.5.0...v0.5.1) (2024-03-28) - -**Note:** Version bump only for package @credo-ts/bbs-signatures - -# [0.5.0](https://github.com/openwallet-foundation/credo-ts/compare/v0.4.2...v0.5.0) (2024-03-13) - -### Bug Fixes - -- jsonld document loader node 18 ([#1454](https://github.com/openwallet-foundation/credo-ts/issues/1454)) ([3656d49](https://github.com/openwallet-foundation/credo-ts/commit/3656d4902fb832e5e75142b1846074d4f39c11a2)) -- unused imports ([#1733](https://github.com/openwallet-foundation/credo-ts/issues/1733)) ([e0b971e](https://github.com/openwallet-foundation/credo-ts/commit/e0b971e86b506bb78dafa21f76ae3b193abe9a9d)) - -## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) - -**Note:** Version bump only for package @credo-ts/bbs-signatures - -## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) - -**Note:** Version bump only for package @credo-ts/bbs-signatures - -# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) - -### Bug Fixes - -- jsonld credential format identifier version ([#1412](https://github.com/hyperledger/aries-framework-javascript/issues/1412)) ([c46a6b8](https://github.com/hyperledger/aries-framework-javascript/commit/c46a6b81b8a1e28e05013c27ffe2eeaee4724130)) -- seed and private key validation and return type in registrars ([#1324](https://github.com/hyperledger/aries-framework-javascript/issues/1324)) ([c0e5339](https://github.com/hyperledger/aries-framework-javascript/commit/c0e5339edfa32df92f23fb9c920796b4b59adf52)) - -### Features - -- **core:** add W3cCredentialsApi ([c888736](https://github.com/hyperledger/aries-framework-javascript/commit/c888736cb6b51014e23f5520fbc4074cf0e49e15)) -- **openid4vc:** jwt format and more crypto ([#1472](https://github.com/hyperledger/aries-framework-javascript/issues/1472)) ([bd4932d](https://github.com/hyperledger/aries-framework-javascript/commit/bd4932d34f7314a6d49097b6460c7570e1ebc7a8)) diff --git a/packages/bbs-signatures/README.md b/packages/bbs-signatures/README.md deleted file mode 100644 index 8aa81032d8..0000000000 --- a/packages/bbs-signatures/README.md +++ /dev/null @@ -1,90 +0,0 @@ -

-
- Credo Logo -

-

Credo BBS+ Module

-

- License - typescript - @credo-ts/bbs-signatures version - -

-
- -Credo BBS Module provides an optional addon to Credo to use BBS signatures in W3C VC exchange. - -## Installation - -```sh -# or npm/yarn -pnpm add @credo-ts/bbs-signatures -``` - -### React Native - -When using Credo inside the React Native environment, temporarily, a dependency for creating keys, signing and verifying, with bbs keys must be swapped. Inside your `package.json` the following must be added. This is only needed for React Native environments - -#### yarn - -```diff -+ "resolutions": { -+ "@mattrglobal/bbs-signatures": "@animo-id/react-native-bbs-signatures@^0.1.0", -+ }, - "dependencies": { - ... -+ "@animo-id/react-native-bbs-signatures": "^0.1.0", - } -``` - -#### npm - -```diff -+ "overrides": { -+ "@mattrglobal/bbs-signatures": "@animo-id/react-native-bbs-signatures@^0.1.0", -+ }, - "dependencies": { - ... -+ "@animo-id/react-native-bbs-signatures": "^0.1.0", - } -``` - -#### pnpm - -```diff -+ "pnpm": { -+ overrides": { -+ "@mattrglobal/bbs-signatures": "npm:@animo-id/react-native-bbs-signatures@^0.1.0", -+ } -+ }, - "dependencies": { - ... -+ "@animo-id/react-native-bbs-signatures": "^0.1.0", - } -``` - -The resolution field says that any instance of `@mattrglobal/bbs-signatures` in any child dependency must be swapped with `@animo-id/react-native-bbs-signatures`. - -The added dependency is required for autolinking and should be the same as the one used in the resolution. - -[React Native Bbs Signature](https://github.com/animo/react-native-bbs-signatures) has some quirks with setting it up correctly. If any errors occur while using this library, please refer to their README for the installation guide. - -### Issue with `node-bbs-signatures` - -Right now some platforms will see an "error" when installing the `@credo-ts/bbs-signatures` package. This is because the BBS signatures library that we use under the hood is built for Linux x86 and MacOS x86 (and not Windows and MacOS arm). This means that it will show that it could not download the binary. This is not an error for developers, the library that fails is `node-bbs-signatures` and is an optional dependency for performance improvements. It will fallback to a (slower) wasm build. diff --git a/packages/bbs-signatures/src/BbsModule.ts b/packages/bbs-signatures/src/BbsModule.ts deleted file mode 100644 index 273340965d..0000000000 --- a/packages/bbs-signatures/src/BbsModule.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { DependencyManager, Module } from '@credo-ts/core' - -import { - AgentConfig, - KeyType, - SignatureSuiteToken, - SigningProviderToken, - VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020, -} from '@credo-ts/core' - -import { Bls12381g2SigningProvider } from './Bls12381g2SigningProvider' -import { BbsBlsSignature2020, BbsBlsSignatureProof2020 } from './signature-suites' - -export class BbsModule implements Module { - /** - * Registers the dependencies of the bbs module on the dependency manager. - */ - public register(dependencyManager: DependencyManager) { - // Warn about experimental module - dependencyManager - .resolve(AgentConfig) - .logger.warn( - "The '@credo-ts/bbs-signatures' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." - ) - - // Signing providers. - dependencyManager.registerSingleton(SigningProviderToken, Bls12381g2SigningProvider) - - // Signature suites. - dependencyManager.registerInstance(SignatureSuiteToken, { - suiteClass: BbsBlsSignature2020, - proofType: 'BbsBlsSignature2020', - verificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], - keyTypes: [KeyType.Bls12381g2], - }) - dependencyManager.registerInstance(SignatureSuiteToken, { - suiteClass: BbsBlsSignatureProof2020, - proofType: 'BbsBlsSignatureProof2020', - verificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], - keyTypes: [KeyType.Bls12381g2], - }) - } -} diff --git a/packages/bbs-signatures/src/Bls12381g2SigningProvider.ts b/packages/bbs-signatures/src/Bls12381g2SigningProvider.ts deleted file mode 100644 index 633b35a7d4..0000000000 --- a/packages/bbs-signatures/src/Bls12381g2SigningProvider.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { CreateKeyPairOptions, KeyPair, SignOptions, SigningProvider, VerifyOptions } from '@credo-ts/core' - -import { Buffer, KeyType, SigningProviderError, TypedArrayEncoder, injectable } from '@credo-ts/core' -import { bls12381toBbs, generateBls12381G2KeyPair, sign, verify } from '@mattrglobal/bbs-signatures' - -/** - * This will be extracted to the bbs package. - */ -@injectable() -export class Bls12381g2SigningProvider implements SigningProvider { - public readonly keyType = KeyType.Bls12381g2 - - /** - * Create a KeyPair with type Bls12381g2 - * - * @throws {SigningProviderError} When a key could not be created - */ - public async createKeyPair({ seed, privateKey }: CreateKeyPairOptions): Promise { - if (privateKey) { - throw new SigningProviderError('Cannot create keypair from private key') - } - - const blsKeyPair = await generateBls12381G2KeyPair(seed) - - return { - keyType: KeyType.Bls12381g2, - publicKeyBase58: TypedArrayEncoder.toBase58(blsKeyPair.publicKey), - privateKeyBase58: TypedArrayEncoder.toBase58(blsKeyPair.secretKey), - } - } - - /** - * Sign an arbitrary amount of messages, in byte form, with a keypair - * - * @param messages Buffer[] List of messages in Buffer form - * @param publicKey Buffer Publickey required for the signing process - * @param privateKey Buffer PrivateKey required for the signing process - * - * @returns A Buffer containing the signature of the messages - * - * @throws {SigningProviderError} When there are no supplied messages - */ - public async sign({ data, publicKeyBase58, privateKeyBase58 }: SignOptions): Promise { - if (data.length === 0) throw new SigningProviderError('Unable to create a signature without any messages') - // Check if it is a single message or list and if it is a single message convert it to a list - const normalizedMessages = (TypedArrayEncoder.isTypedArray(data) ? [data as Buffer] : data) as Buffer[] - - // Get the Uint8Array variant of all the messages - const messageBuffers = normalizedMessages.map((m) => Uint8Array.from(m)) - - const publicKey = TypedArrayEncoder.fromBase58(publicKeyBase58) - const privateKey = TypedArrayEncoder.fromBase58(privateKeyBase58) - - const bbsKeyPair = await bls12381toBbs({ - keyPair: { publicKey: Uint8Array.from(publicKey), secretKey: Uint8Array.from(privateKey) }, - messageCount: normalizedMessages.length, - }) - - // Sign the messages via the keyPair - const signature = await sign({ - keyPair: bbsKeyPair, - messages: messageBuffers, - }) - - // Convert the Uint8Array signature to a Buffer type - return Buffer.from(signature) - } - - /** - * Verify an arbitrary amount of messages with their signature created with their key pair - * - * @param publicKey Buffer The public key used to sign the messages - * @param messages Buffer[] The messages that have to be verified if they are signed - * @param signature Buffer The signature that has to be verified if it was created with the messages and public key - * - * @returns A boolean whether the signature is create with the public key over the messages - * - * @throws {SigningProviderError} When the message list is empty - * @throws {SigningProviderError} When the verification process failed - */ - public async verify({ data, publicKeyBase58, signature }: VerifyOptions): Promise { - if (data.length === 0) throw new SigningProviderError('Unable to create a signature without any messages') - // Check if it is a single message or list and if it is a single message convert it to a list - const normalizedMessages = (TypedArrayEncoder.isTypedArray(data) ? [data as Buffer] : data) as Buffer[] - - const publicKey = TypedArrayEncoder.fromBase58(publicKeyBase58) - - // Get the Uint8Array variant of all the messages - const messageBuffers = normalizedMessages.map((m) => Uint8Array.from(m)) - - const bbsKeyPair = await bls12381toBbs({ - keyPair: { publicKey: Uint8Array.from(publicKey) }, - messageCount: normalizedMessages.length, - }) - - // Verify the signature against the messages with their public key - const { verified, error } = await verify({ signature, messages: messageBuffers, publicKey: bbsKeyPair.publicKey }) - - // If the messages could not be verified and an error occurred - if (!verified && error) { - throw new SigningProviderError(`Could not verify the signature against the messages: ${error}`) - } - - return verified - } -} diff --git a/packages/bbs-signatures/src/__tests__/BbsModule.test.ts b/packages/bbs-signatures/src/__tests__/BbsModule.test.ts deleted file mode 100644 index 5d1519daea..0000000000 --- a/packages/bbs-signatures/src/__tests__/BbsModule.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { DependencyManager } from '@credo-ts/core' - -import { - KeyType, - SignatureSuiteToken, - SigningProviderToken, - VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020, -} from '@credo-ts/core' - -import { BbsModule } from '../BbsModule' -import { Bls12381g2SigningProvider } from '../Bls12381g2SigningProvider' -import { BbsBlsSignature2020, BbsBlsSignatureProof2020 } from '../signature-suites' - -const dependencyManager = { - registerInstance: jest.fn(), - registerSingleton: jest.fn(), - resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), -} as unknown as DependencyManager - -describe('BbsModule', () => { - test('registers dependencies on the dependency manager', () => { - const bbsModule = new BbsModule() - bbsModule.register(dependencyManager) - - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(SigningProviderToken, Bls12381g2SigningProvider) - - expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(2) - expect(dependencyManager.registerInstance).toHaveBeenCalledWith(SignatureSuiteToken, { - suiteClass: BbsBlsSignature2020, - proofType: 'BbsBlsSignature2020', - verificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], - keyTypes: [KeyType.Bls12381g2], - }) - expect(dependencyManager.registerInstance).toHaveBeenCalledWith(SignatureSuiteToken, { - suiteClass: BbsBlsSignatureProof2020, - proofType: 'BbsBlsSignatureProof2020', - verificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], - keyTypes: [KeyType.Bls12381g2], - }) - }) -}) diff --git a/packages/bbs-signatures/src/index.ts b/packages/bbs-signatures/src/index.ts deleted file mode 100644 index 0b218fb3d0..0000000000 --- a/packages/bbs-signatures/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './signature-suites' -export * from './BbsModule' -export * from './Bls12381g2SigningProvider' -export * from './types' diff --git a/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts b/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts deleted file mode 100644 index 28b8985ee1..0000000000 --- a/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts +++ /dev/null @@ -1,403 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DocumentLoader, JsonObject, Proof, VerificationMethod } from '@credo-ts/core' -import type { - CanonizeOptions, - CreateProofOptions, - CreateVerifyDataOptions, - SignatureSuiteOptions, - SuiteSignOptions, - VerifyProofOptions, - VerifySignatureOptions, -} from '../types' - -import { - CredoError, - SECURITY_CONTEXT_BBS_URL, - SECURITY_CONTEXT_URL, - TypedArrayEncoder, - vcLibraries, - w3cDate, -} from '@credo-ts/core' - -const { jsonld, jsonldSignatures } = vcLibraries -const LinkedDataProof = jsonldSignatures.suites.LinkedDataProof - -/** - * A BBS+ signature suite for use with BLS12-381 key pairs - */ -export class BbsBlsSignature2020 extends LinkedDataProof { - private proof: Record - /** - * Default constructor - * @param options {SignatureSuiteOptions} options for constructing the signature suite - */ - public constructor(options: SignatureSuiteOptions = {}) { - const { verificationMethod, signer, key, date, useNativeCanonize, LDKeyClass } = options - // validate common options - if (verificationMethod !== undefined && typeof verificationMethod !== 'string') { - throw new TypeError('"verificationMethod" must be a URL string.') - } - super({ - type: 'BbsBlsSignature2020', - }) - - this.proof = { - '@context': [ - { - sec: 'https://w3id.org/security#', - proof: { - '@id': 'sec:proof', - '@type': '@id', - '@container': '@graph', - }, - }, - SECURITY_CONTEXT_BBS_URL, - ], - type: 'BbsBlsSignature2020', - } - - this.LDKeyClass = LDKeyClass - this.signer = signer - this.verificationMethod = verificationMethod - this.proofSignatureKey = 'proofValue' - if (key) { - if (verificationMethod === undefined) { - this.verificationMethod = key.id - } - this.key = key - if (typeof key.signer === 'function') { - this.signer = key.signer() - } - if (typeof key.verifier === 'function') { - this.verifier = key.verifier() - } - } - if (date) { - this.date = new Date(date) - - if (Number.isNaN(this.date)) { - throw TypeError(`"date" "${date}" is not a valid date.`) - } - } - this.useNativeCanonize = useNativeCanonize - } - - public ensureSuiteContext({ document }: { document: Record }) { - if ( - document['@context'] === SECURITY_CONTEXT_BBS_URL || - (Array.isArray(document['@context']) && document['@context'].includes(SECURITY_CONTEXT_BBS_URL)) - ) { - // document already includes the required context - return - } - throw new TypeError(`The document to be signed must contain this suite's @context, "${SECURITY_CONTEXT_BBS_URL}".`) - } - - /** - * @param options {CreateProofOptions} options for creating the proof - * - * @returns {Promise} Resolves with the created proof object. - */ - public async createProof(options: CreateProofOptions): Promise> { - const { document, purpose, documentLoader, compactProof } = options - - let proof: JsonObject - - // use proof JSON-LD document passed to API - if (this.proof) { - proof = await jsonld.compact(this.proof, SECURITY_CONTEXT_URL, { - documentLoader, - compactToRelative: true, - }) - } else { - // create proof JSON-LD document - proof = { '@context': SECURITY_CONTEXT_URL } - } - - // ensure proof type is set - proof.type = this.type - - // set default `now` date if not given in `proof` or `options` - let date = this.date - if (proof.created === undefined && date === undefined) { - date = new Date() - } - - // ensure date is in string format - if (date !== undefined && typeof date !== 'string') { - date = w3cDate(date) - } - - // add API overrides - if (date !== undefined) { - proof.created = date - } - - if (this.verificationMethod !== undefined) { - proof.verificationMethod = this.verificationMethod - } - - // allow purpose to update the proof; the `proof` is in the - // SECURITY_CONTEXT_URL `@context` -- therefore the `purpose` must - // ensure any added fields are also represented in that same `@context` - proof = await purpose.update(proof, { - document, - suite: this, - documentLoader, - }) - - // create data to sign - const verifyData = ( - await this.createVerifyData({ - document, - proof, - documentLoader, - - compactProof, - }) - ).map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) - - // sign data - proof = await this.sign({ - verifyData, - document, - proof, - documentLoader, - }) - - // biome-ignore lint/performance/noDelete: - delete proof['@context'] - - return proof - } - - /** - * @param options {object} options for verifying the proof. - * - * @returns {Promise<{object}>} Resolves with the verification result. - */ - public async verifyProof(options: VerifyProofOptions): Promise> { - const { proof, document, documentLoader, purpose } = options - - try { - // create data to verify - const verifyData = ( - await this.createVerifyData({ - document, - proof, - documentLoader, - compactProof: false, - }) - ).map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) - - // fetch verification method - const verificationMethod = await this.getVerificationMethod({ - proof, - documentLoader, - }) - - // verify signature on data - const verified = await this.verifySignature({ - verifyData, - verificationMethod, - document, - proof, - documentLoader, - }) - if (!verified) { - throw new Error('Invalid signature.') - } - - // ensure proof was performed for a valid purpose - const { valid, error } = await purpose.validate(proof, { - document, - suite: this, - verificationMethod, - documentLoader, - }) - if (!valid) { - throw error - } - - return { verified: true } - } catch (error) { - return { verified: false, error } - } - } - - public async canonize(input: Record, options: CanonizeOptions): Promise { - const { documentLoader, skipExpansion } = options - return jsonld.canonize(input, { - algorithm: 'URDNA2015', - format: 'application/n-quads', - documentLoader, - skipExpansion, - useNative: this.useNativeCanonize, - }) - } - - public async canonizeProof(proof: Record, options: CanonizeOptions): Promise { - const { documentLoader } = options - // biome-ignore lint/style/noParameterAssign: - proof = { ...proof } - delete proof[this.proofSignatureKey] - return this.canonize(proof, { - documentLoader, - skipExpansion: false, - }) - } - - /** - * @param document {CreateVerifyDataOptions} options to create verify data - * - * @returns {Promise<{string[]>}. - */ - public async createVerifyData(options: CreateVerifyDataOptions): Promise { - const { proof, document, documentLoader } = options - - const proof2 = { ...proof, '@context': document['@context'] } - - const proofStatements = await this.createVerifyProofData(proof2, { - documentLoader, - }) - const documentStatements = await this.createVerifyDocumentData(document, { - documentLoader, - }) - - // concatenate c14n proof options and c14n document - return proofStatements.concat(documentStatements) - } - - /** - * @param proof to canonicalize - * @param options to create verify data - * - * @returns {Promise<{string[]>}. - */ - public async createVerifyProofData( - proof: Record, - { documentLoader }: { documentLoader?: DocumentLoader } - ): Promise { - const c14nProofOptions = await this.canonizeProof(proof, { - documentLoader, - }) - - return c14nProofOptions.split('\n').filter((_) => _.length > 0) - } - - /** - * @param document to canonicalize - * @param options to create verify data - * - * @returns {Promise<{string[]>}. - */ - public async createVerifyDocumentData( - document: Record, - { documentLoader }: { documentLoader?: DocumentLoader } - ): Promise { - const c14nDocument = await this.canonize(document, { - documentLoader, - }) - - return c14nDocument.split('\n').filter((_) => _.length > 0) - } - - /** - * @param document {object} to be signed. - * @param proof {object} - * @param documentLoader {function} - */ - public async getVerificationMethod({ - proof, - documentLoader, - }: { - proof: Proof - documentLoader?: DocumentLoader - }): Promise { - let { verificationMethod } = proof - - if (typeof verificationMethod === 'object' && verificationMethod !== null) { - verificationMethod = verificationMethod.id - } - - if (!verificationMethod) { - throw new Error('No "verificationMethod" found in proof.') - } - - if (!documentLoader) { - throw new CredoError('Missing custom document loader. This is required for resolving verification methods.') - } - - const { document } = await documentLoader(verificationMethod) - - if (!document) { - throw new Error(`Verification method ${verificationMethod} not found.`) - } - - // ensure verification method has not been revoked - if (document.revoked !== undefined) { - throw new Error('The verification method has been revoked.') - } - - return document as unknown as VerificationMethod - } - - /** - * @param options {SuiteSignOptions} Options for signing. - * - * @returns {Promise<{object}>} the proof containing the signature value. - */ - public async sign(options: SuiteSignOptions): Promise { - const { verifyData, proof } = options - - if (!(this.signer && typeof this.signer.sign === 'function')) { - throw new Error('A signer API with sign function has not been specified.') - } - - const proofValue: Uint8Array = await this.signer.sign({ - data: verifyData, - }) - - proof[this.proofSignatureKey] = TypedArrayEncoder.toBase64(proofValue) - - return proof as Proof - } - - /** - * @param verifyData {VerifySignatureOptions} Options to verify the signature. - * - * @returns {Promise} - */ - public async verifySignature(options: VerifySignatureOptions): Promise { - const { verificationMethod, verifyData, proof } = options - let { verifier } = this - - if (!verifier) { - const key = await this.LDKeyClass.from(verificationMethod) - verifier = key.verifier(key, this.alg, this.type) - } - - return await verifier.verify({ - data: verifyData, - signature: new Uint8Array(TypedArrayEncoder.fromBase64(proof[this.proofSignatureKey] as string)), - }) - } - - public static proofType = [ - 'BbsBlsSignature2020', - 'sec:BbsBlsSignature2020', - 'https://w3id.org/security#BbsBlsSignature2020', - ] -} diff --git a/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts b/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts deleted file mode 100644 index dc5cc04663..0000000000 --- a/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DocumentLoader, JsonObject, Proof } from '@credo-ts/core' -import type { CanonizeOptions, CreateVerifyDataOptions, DeriveProofOptions, VerifyProofOptions } from '../types' -import type { VerifyProofResult } from '../types/VerifyProofResult' - -import { CredoError, SECURITY_CONTEXT_URL, TypedArrayEncoder, vcLibraries } from '@credo-ts/core' -import { blsCreateProof, blsVerifyProof } from '@mattrglobal/bbs-signatures' -import { Bls12381G2KeyPair } from '@mattrglobal/bls12381-key-pair' -import { randomBytes } from '@stablelib/random' - -import { BbsBlsSignature2020 } from './BbsBlsSignature2020' - -const { jsonld, jsonldSignatures } = vcLibraries -const LinkedDataProof = jsonldSignatures.suites.LinkedDataProof - -export class BbsBlsSignatureProof2020 extends LinkedDataProof { - public constructor({ useNativeCanonize, key, LDKeyClass }: Record = {}) { - super({ - type: 'BbsBlsSignatureProof2020', - }) - - this.proof = { - '@context': [ - { - sec: 'https://w3id.org/security#', - proof: { - '@id': 'sec:proof', - '@type': '@id', - '@container': '@graph', - }, - }, - 'https://w3id.org/security/bbs/v1', - ], - type: 'BbsBlsSignatureProof2020', - } - this.mappedDerivedProofType = 'BbsBlsSignature2020' - this.supportedDeriveProofType = BbsBlsSignatureProof2020.supportedDerivedProofType - - this.LDKeyClass = LDKeyClass ?? Bls12381G2KeyPair - this.proofSignatureKey = 'proofValue' - this.key = key - this.useNativeCanonize = useNativeCanonize - } - - /** - * Derive a proof from a proof and reveal document - * - * @param options {object} options for deriving a proof. - * - * @returns {Promise} Resolves with the derived proof object. - */ - public async deriveProof(options: DeriveProofOptions): Promise> { - const { document, proof, revealDocument, documentLoader } = options - let { nonce } = options - - const proofType = proof.type - - if (typeof proofType !== 'string') { - throw new TypeError(`Expected proof.type to be of type 'string', got ${typeof proofType} instead.`) - } - - // Validate that the input proof document has a proof compatible with this suite - if (!BbsBlsSignatureProof2020.supportedDerivedProofType.includes(proofType)) { - throw new TypeError( - `proof document proof incompatible, expected proof types of ${JSON.stringify( - BbsBlsSignatureProof2020.supportedDerivedProofType - )} received ${proof.type}` - ) - } - - const signatureBase58 = proof[this.proofSignatureKey] - - if (typeof signatureBase58 !== 'string') { - throw new TypeError(`Expected signature to be a base58 encoded string, got ${typeof signatureBase58} instead.`) - } - - //Extract the BBS signature from the input proof - const signature = TypedArrayEncoder.fromBase64(signatureBase58) - - //Initialize the BBS signature suite - const suite = new BbsBlsSignature2020() - - //Initialize the derived proof - // biome-ignore lint/suspicious/noImplicitAnyLet: - let derivedProof - if (this.proof) { - // use proof JSON-LD document passed to API - derivedProof = await jsonld.compact(this.proof, SECURITY_CONTEXT_URL, { - documentLoader, - compactToRelative: false, - }) - } else { - // create proof JSON-LD document - derivedProof = { '@context': SECURITY_CONTEXT_URL } - } - - // ensure proof type is set - derivedProof.type = this.type - - // Get the input document statements - const documentStatements = await suite.createVerifyDocumentData(document, { - documentLoader, - }) - - // Get the proof statements - const proofStatements = await suite.createVerifyProofData(proof, { - documentLoader, - }) - - // Transform any blank node identifiers for the input - // document statements into actual node identifiers - // e.g _:c14n0 => urn:bnid:_:c14n0 - const transformedInputDocumentStatements = documentStatements.map((element) => - element.replace(/(_:c14n[0-9]+)/g, '') - ) - - //Transform the resulting RDF statements back into JSON-LD - const compactInputProofDocument = await jsonld.fromRDF(transformedInputDocumentStatements.join('\n')) - - // Frame the result to create the reveal document result - const revealDocumentResult = await jsonld.frame(compactInputProofDocument, revealDocument, { documentLoader }) - - // Canonicalize the resulting reveal document - const revealDocumentStatements = await suite.createVerifyDocumentData(revealDocumentResult, { - documentLoader, - }) - - //Get the indicies of the revealed statements from the transformed input document offset - //by the number of proof statements - const numberOfProofStatements = proofStatements.length - - //Always reveal all the statements associated to the original proof - //these are always the first statements in the normalized form - const proofRevealIndicies = Array.from(Array(numberOfProofStatements).keys()) - - //Reveal the statements indicated from the reveal document - const documentRevealIndicies = revealDocumentStatements.map( - (key) => transformedInputDocumentStatements.indexOf(key) + numberOfProofStatements - ) - - // Check there is not a mismatch - if (documentRevealIndicies.length !== revealDocumentStatements.length) { - throw new Error('Some statements in the reveal document not found in original proof') - } - - // Combine all indicies to get the resulting list of revealed indicies - const revealIndicies = proofRevealIndicies.concat(documentRevealIndicies) - - // Create a nonce if one is not supplied - if (!nonce) { - nonce = randomBytes(50) - } - - // Set the nonce on the derived proof - // derivedProof.nonce = Buffer.from(nonce).toString('base64') - derivedProof.nonce = TypedArrayEncoder.toBase64(nonce) - - //Combine all the input statements that - //were originally signed to generate the proof - const allInputStatements: Uint8Array[] = proofStatements - .concat(documentStatements) - .map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) - - // Fetch the verification method - const verificationMethod = await this.getVerificationMethod({ - proof, - documentLoader, - }) - - // Construct a key pair class from the returned verification method - const key = verificationMethod.publicKeyJwk - ? await this.LDKeyClass.fromJwk(verificationMethod) - : await this.LDKeyClass.from(verificationMethod) - - // Compute the proof - const outputProof = await blsCreateProof({ - signature, - publicKey: Uint8Array.from(key.publicKeyBuffer), - messages: allInputStatements, - nonce, - revealed: revealIndicies, - }) - - // Set the proof value on the derived proof - derivedProof.proofValue = TypedArrayEncoder.toBase64(outputProof) - - // Set the relevant proof elements on the derived proof from the input proof - derivedProof.verificationMethod = proof.verificationMethod - derivedProof.proofPurpose = proof.proofPurpose - derivedProof.created = proof.created - - return { - document: { ...revealDocumentResult }, - proof: derivedProof, - } - } - - /** - * @param options {object} options for verifying the proof. - * - * @returns {Promise<{object}>} Resolves with the verification result. - */ - public async verifyProof(options: VerifyProofOptions): Promise { - const { document, documentLoader, purpose } = options - const { proof } = options - - try { - proof.type = this.mappedDerivedProofType - - const proofIncludingDocumentContext = { ...proof, '@context': document['@context'] } - - // Get the proof statements - const proofStatements = await this.createVerifyProofData(proofIncludingDocumentContext, { - documentLoader, - }) - - // Get the document statements - const documentStatements = await this.createVerifyProofData(document, { - documentLoader, - }) - - // Transform the blank node identifier placeholders for the document statements - // back into actual blank node identifiers - const transformedDocumentStatements = documentStatements.map((element) => - element.replace(//g, '$1') - ) - - // Combine all the statements to be verified - const statementsToVerify: Uint8Array[] = proofStatements - .concat(transformedDocumentStatements) - .map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) - - // Fetch the verification method - const verificationMethod = await this.getVerificationMethod({ - proof, - documentLoader, - }) - - // Construct a key pair class from the returned verification method - const key = verificationMethod.publicKeyJwk - ? await this.LDKeyClass.fromJwk(verificationMethod) - : await this.LDKeyClass.from(verificationMethod) - - const proofValue = proof.proofValue - - if (typeof proofValue !== 'string') { - throw new CredoError(`Expected proof.proofValue to be of type 'string', got ${typeof proof}`) - } - - // Verify the proof - const verified = await blsVerifyProof({ - proof: TypedArrayEncoder.fromBase64(proofValue), - publicKey: key.publicKeyBuffer, - messages: statementsToVerify, - nonce: TypedArrayEncoder.fromBase64(proof.nonce as string), - }) - - // Ensure proof was performed for a valid purpose - const { valid, error } = await purpose.validate(proof, { - document, - suite: this, - verificationMethod, - documentLoader, - }) - if (!valid) { - throw error - } - - return verified - } catch (error) { - return { verified: false, error } - } - } - - public async canonize(input: JsonObject, options: CanonizeOptions): Promise { - const { documentLoader, skipExpansion } = options - return jsonld.canonize(input, { - algorithm: 'URDNA2015', - format: 'application/n-quads', - documentLoader, - skipExpansion, - useNative: this.useNativeCanonize, - }) - } - - public async canonizeProof(proof: JsonObject, options: CanonizeOptions): Promise { - const { documentLoader } = options - // biome-ignore lint/style/noParameterAssign: - proof = { ...proof } - - // biome-ignore lint/performance/noDelete: - delete proof.nonce - // biome-ignore lint/performance/noDelete: - delete proof.proofValue - - return this.canonize(proof, { - documentLoader, - skipExpansion: false, - }) - } - - /** - * @param document {CreateVerifyDataOptions} options to create verify data - * - * @returns {Promise<{string[]>}. - */ - public async createVerifyData(options: CreateVerifyDataOptions): Promise { - const { proof, document, documentLoader } = options - - const proofStatements = await this.createVerifyProofData(proof, { - documentLoader, - }) - const documentStatements = await this.createVerifyDocumentData(document, { - documentLoader, - }) - - // concatenate c14n proof options and c14n document - return proofStatements.concat(documentStatements) - } - - /** - * @param proof to canonicalize - * @param options to create verify data - * - * @returns {Promise<{string[]>}. - */ - public async createVerifyProofData( - proof: JsonObject, - { documentLoader }: { documentLoader?: DocumentLoader } - ): Promise { - const c14nProofOptions = await this.canonizeProof(proof, { - documentLoader, - }) - - return c14nProofOptions.split('\n').filter((_) => _.length > 0) - } - - /** - * @param document to canonicalize - * @param options to create verify data - * - * @returns {Promise<{string[]>}. - */ - public async createVerifyDocumentData( - document: JsonObject, - { documentLoader }: { documentLoader?: DocumentLoader } - ): Promise { - const c14nDocument = await this.canonize(document, { - documentLoader, - }) - - return c14nDocument.split('\n').filter((_) => _.length > 0) - } - - public async getVerificationMethod(options: { proof: Proof; documentLoader?: DocumentLoader }) { - if (this.key) { - // This happens most often during sign() operations. For verify(), - // the expectation is that the verification method will be fetched - // by the documentLoader (below), not provided as a `key` parameter. - return this.key.export({ publicKey: true }) - } - - let { verificationMethod } = options.proof - - if (typeof verificationMethod === 'object' && verificationMethod !== null) { - verificationMethod = verificationMethod.id - } - - if (!verificationMethod) { - throw new Error('No "verificationMethod" found in proof.') - } - - if (!options.documentLoader) { - throw new CredoError('Missing custom document loader. This is required for resolving verification methods.') - } - - const { document } = await options.documentLoader(verificationMethod) - - verificationMethod = typeof document === 'string' ? JSON.parse(document) : document - - // await this.assertVerificationMethod(verificationMethod) - return verificationMethod - } - - public static proofType = [ - 'BbsBlsSignatureProof2020', - 'sec:BbsBlsSignatureProof2020', - 'https://w3id.org/security#BbsBlsSignatureProof2020', - ] - - public static supportedDerivedProofType = [ - 'BbsBlsSignature2020', - 'sec:BbsBlsSignature2020', - 'https://w3id.org/security#BbsBlsSignature2020', - ] -} diff --git a/packages/bbs-signatures/src/signature-suites/index.ts b/packages/bbs-signatures/src/signature-suites/index.ts deleted file mode 100644 index 932af48e2f..0000000000 --- a/packages/bbs-signatures/src/signature-suites/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { Bls12381G2KeyPair } from '@mattrglobal/bls12381-key-pair' -export { BbsBlsSignature2020 } from './BbsBlsSignature2020' -export { BbsBlsSignatureProof2020 } from './BbsBlsSignatureProof2020' diff --git a/packages/bbs-signatures/src/types/CanonizeOptions.ts b/packages/bbs-signatures/src/types/CanonizeOptions.ts deleted file mode 100644 index f03a2a9a20..0000000000 --- a/packages/bbs-signatures/src/types/CanonizeOptions.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DocumentLoader } from '@credo-ts/core' - -/** - * Options for canonizing a document - */ -export interface CanonizeOptions { - /** - * Optional custom document loader - */ - documentLoader?: DocumentLoader - - /** - * Indicates whether to skip expansion during canonization - */ - readonly skipExpansion?: boolean -} diff --git a/packages/bbs-signatures/src/types/CreateProofOptions.ts b/packages/bbs-signatures/src/types/CreateProofOptions.ts deleted file mode 100644 index 4697a54d25..0000000000 --- a/packages/bbs-signatures/src/types/CreateProofOptions.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DocumentLoader, JsonObject, ProofPurpose } from '@credo-ts/core' - -/** - * Options for creating a proof - */ -export interface CreateProofOptions { - /** - * Document to create the proof for - */ - readonly document: JsonObject - /** - * The proof purpose to specify for the generated proof - */ - readonly purpose: ProofPurpose - /** - * Optional custom document loader - */ - documentLoader?: DocumentLoader - /** - * Indicates whether to compact the resulting proof - */ - readonly compactProof: boolean -} diff --git a/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts b/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts deleted file mode 100644 index b628c8661b..0000000000 --- a/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DocumentLoader, JsonObject } from '@credo-ts/core' - -/** - * Options for creating a proof - */ -export interface CreateVerifyDataOptions { - /** - * Document to create the proof for - */ - readonly document: JsonObject - - /** - * The proof - */ - readonly proof: JsonObject - - /** - * Optional custom document loader - */ - documentLoader?: DocumentLoader - - /** - * Indicates whether to compact the proof - */ - readonly compactProof: boolean -} diff --git a/packages/bbs-signatures/src/types/DeriveProofOptions.ts b/packages/bbs-signatures/src/types/DeriveProofOptions.ts deleted file mode 100644 index 51d3faf5c3..0000000000 --- a/packages/bbs-signatures/src/types/DeriveProofOptions.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DocumentLoader, JsonObject, Proof } from '@credo-ts/core' - -/** - * Options for creating a proof - */ -export interface DeriveProofOptions { - /** - * Document outlining what statements to reveal - */ - readonly revealDocument: JsonObject - /** - * The document featuring the proof to derive from - */ - readonly document: JsonObject - /** - * The proof for the document - */ - readonly proof: Proof - /** - * Optional custom document loader - */ - documentLoader?: DocumentLoader - - /** - * Nonce to include in the derived proof - */ - readonly nonce?: Uint8Array - /** - * Indicates whether to compact the resulting proof - */ - readonly skipProofCompaction?: boolean -} diff --git a/packages/bbs-signatures/src/types/DidDocumentPublicKey.ts b/packages/bbs-signatures/src/types/DidDocumentPublicKey.ts deleted file mode 100644 index d8a7476e1f..0000000000 --- a/packages/bbs-signatures/src/types/DidDocumentPublicKey.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { PublicJsonWebKey } from './JsonWebKey' - -/** - * Interface for the public key definition entry in a DID Document. - * @see https://w3c-ccg.github.io/did-spec/#public-keys - */ -export interface DidDocumentPublicKey { - /** - * Fully qualified identifier of this public key, e.g. did:example:entity.id#keys-1 - */ - readonly id: string - - /** - * The type of this public key, as defined in: https://w3c-ccg.github.io/ld-cryptosuite-registry/ - */ - readonly type: string - - /** - * The DID of the controller of this key. - */ - readonly controller?: string - - /** - * The value of the public key in Base58 format. Only one value field will be present. - */ - readonly publicKeyBase58?: string - - /** - * Public key in JWK format. - * @see https://w3c-ccg.github.io/did-spec/#public-keys - */ - readonly publicKeyJwk?: PublicJsonWebKey - - /** - * Public key in HEX format. - * @see https://w3c-ccg.github.io/did-spec/#public-keys - */ - readonly publicKeyHex?: string -} diff --git a/packages/bbs-signatures/src/types/JsonWebKey.ts b/packages/bbs-signatures/src/types/JsonWebKey.ts deleted file mode 100644 index a027778879..0000000000 --- a/packages/bbs-signatures/src/types/JsonWebKey.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export enum JwkKty { - OctetKeyPair = 'OKP', - EC = 'EC', - RSA = 'RSA', -} - -export interface JwkEc { - readonly kty: JwkKty.EC - readonly crv: string - readonly d?: string - readonly x?: string - readonly y?: string - readonly kid?: string -} - -export interface JwkOctetKeyPair { - readonly kty: JwkKty.OctetKeyPair - readonly crv: string - readonly d?: string - readonly x?: string - readonly y?: string - readonly kid?: string -} - -export interface JwkRsa { - readonly kty: JwkKty.RSA - readonly e: string - readonly n: string -} - -export interface JwkRsaPrivate extends JwkRsa { - readonly d: string - readonly p: string - readonly q: string - readonly dp: string - readonly dq: string - readonly qi: string -} -export type JsonWebKey = JwkOctetKeyPair | JwkEc | JwkRsa | JwkRsaPrivate -export type PublicJsonWebKey = JwkOctetKeyPair | JwkEc | JwkRsa diff --git a/packages/bbs-signatures/src/types/KeyPairOptions.ts b/packages/bbs-signatures/src/types/KeyPairOptions.ts deleted file mode 100644 index 624029cd9c..0000000000 --- a/packages/bbs-signatures/src/types/KeyPairOptions.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Options for constructing a key pair - */ -export interface KeyPairOptions { - /** - * The key id - */ - readonly id?: string - /** - * The key controller - */ - readonly controller?: string - /** - * Base58 encoding of the private key - */ - readonly privateKeyBase58?: string - /** - * Base58 encoding of the public key - */ - readonly publicKeyBase58: string -} diff --git a/packages/bbs-signatures/src/types/KeyPairSigner.ts b/packages/bbs-signatures/src/types/KeyPairSigner.ts deleted file mode 100644 index 2aaa37f7cf..0000000000 --- a/packages/bbs-signatures/src/types/KeyPairSigner.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Key pair signer - */ -export interface KeyPairSigner { - /** - * Signer function - */ - readonly sign: (options: KeyPairSignerOptions) => Promise -} - -/** - * Key pair signer options - */ -export interface KeyPairSignerOptions { - readonly data: Uint8Array | Uint8Array[] -} diff --git a/packages/bbs-signatures/src/types/KeyPairVerifier.ts b/packages/bbs-signatures/src/types/KeyPairVerifier.ts deleted file mode 100644 index ed89f3bffe..0000000000 --- a/packages/bbs-signatures/src/types/KeyPairVerifier.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Key pair verifier - */ -export interface KeyPairVerifier { - /** - * Key pair verify function - */ - readonly verify: (options: KeyPairVerifierOptions) => Promise -} - -/** - * Key pair verifier options - */ -export interface KeyPairVerifierOptions { - readonly data: Uint8Array | Uint8Array[] - readonly signature: Uint8Array -} diff --git a/packages/bbs-signatures/src/types/SignatureSuiteOptions.ts b/packages/bbs-signatures/src/types/SignatureSuiteOptions.ts deleted file mode 100644 index 0b74ec07c1..0000000000 --- a/packages/bbs-signatures/src/types/SignatureSuiteOptions.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { JsonArray, LdKeyPair } from '@credo-ts/core' -import type { Bls12381G2KeyPair } from '@mattrglobal/bls12381-key-pair' -import type { KeyPairSigner } from './KeyPairSigner' - -/** - * Options for constructing a signature suite - */ -export interface SignatureSuiteOptions { - /** - * An optional signer interface for handling the sign operation - */ - readonly signer?: KeyPairSigner - /** - * The key pair used to generate the proof - */ - readonly key?: Bls12381G2KeyPair - /** - * A key id URL to the paired public key used for verifying the proof - */ - readonly verificationMethod?: string - /** - * The `created` date to report in generated proofs - */ - readonly date?: string | Date - /** - * Indicates whether to use the native implementation - * of RDF Dataset Normalization - */ - readonly useNativeCanonize?: boolean - /** - * Additional proof elements - */ - readonly proof?: JsonArray - /** - * Linked Data Key class implementation - */ - readonly LDKeyClass?: LdKeyPair -} diff --git a/packages/bbs-signatures/src/types/SuiteSignOptions.ts b/packages/bbs-signatures/src/types/SuiteSignOptions.ts deleted file mode 100644 index 53ae0c7ca9..0000000000 --- a/packages/bbs-signatures/src/types/SuiteSignOptions.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DocumentLoader, JsonObject } from '@credo-ts/core' - -/** - * Options for signing using a signature suite - */ -export interface SuiteSignOptions { - /** - * Input document to sign - */ - readonly document: JsonObject - /** - * Optional custom document loader - */ - documentLoader?: DocumentLoader - - /** - * The array of statements to sign - */ - readonly verifyData: readonly Uint8Array[] - /** - * The proof - */ - readonly proof: JsonObject -} diff --git a/packages/bbs-signatures/src/types/VerifyProofOptions.ts b/packages/bbs-signatures/src/types/VerifyProofOptions.ts deleted file mode 100644 index 196def1957..0000000000 --- a/packages/bbs-signatures/src/types/VerifyProofOptions.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DocumentLoader, JsonObject, Proof, ProofPurpose } from '@credo-ts/core' - -/** - * Options for verifying a proof - */ -export interface VerifyProofOptions { - /** - * The proof - */ - readonly proof: Proof - /** - * The document - */ - readonly document: JsonObject - /** - * The proof purpose to specify for the generated proof - */ - readonly purpose: ProofPurpose - /** - * Optional custom document loader - */ - documentLoader?: DocumentLoader -} diff --git a/packages/bbs-signatures/src/types/VerifyProofResult.ts b/packages/bbs-signatures/src/types/VerifyProofResult.ts deleted file mode 100644 index 96996d006d..0000000000 --- a/packages/bbs-signatures/src/types/VerifyProofResult.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Result of calling verify proof - */ -export interface VerifyProofResult { - /** - * A boolean indicating if the verification was successful - */ - readonly verified: boolean - /** - * A string representing the error if the verification failed - */ - readonly error?: unknown -} diff --git a/packages/bbs-signatures/src/types/VerifySignatureOptions.ts b/packages/bbs-signatures/src/types/VerifySignatureOptions.ts deleted file mode 100644 index 435e0769fa..0000000000 --- a/packages/bbs-signatures/src/types/VerifySignatureOptions.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DocumentLoader, JsonObject, Proof, VerificationMethod } from '@credo-ts/core' - -/** - * Options for verifying a signature - */ -export interface VerifySignatureOptions { - /** - * Document to verify - */ - readonly document: JsonObject - /** - * Array of statements to verify - */ - readonly verifyData: Uint8Array[] - /** - * Verification method to verify the signature against - */ - readonly verificationMethod: VerificationMethod - /** - * Proof to verify - */ - readonly proof: Proof - /** - * Optional custom document loader - */ - documentLoader?: DocumentLoader -} diff --git a/packages/bbs-signatures/src/types/index.ts b/packages/bbs-signatures/src/types/index.ts deleted file mode 100644 index 60575814bb..0000000000 --- a/packages/bbs-signatures/src/types/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2020 - MATTR Limited - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { KeyPairOptions } from './KeyPairOptions' -export { KeyPairSigner } from './KeyPairSigner' -export { KeyPairVerifier } from './KeyPairVerifier' -export { SignatureSuiteOptions } from './SignatureSuiteOptions' -export { CreateProofOptions } from './CreateProofOptions' -export { VerifyProofOptions } from './VerifyProofOptions' -export { CanonizeOptions } from './CanonizeOptions' -export { CreateVerifyDataOptions } from './CreateVerifyDataOptions' -export { VerifySignatureOptions } from './VerifySignatureOptions' -export { SuiteSignOptions } from './SuiteSignOptions' -export { DeriveProofOptions } from './DeriveProofOptions' -export { DidDocumentPublicKey } from './DidDocumentPublicKey' diff --git a/packages/bbs-signatures/tests/bbs-signatures.test.ts b/packages/bbs-signatures/tests/bbs-signatures.test.ts deleted file mode 100644 index 3c4744ea6f..0000000000 --- a/packages/bbs-signatures/tests/bbs-signatures.test.ts +++ /dev/null @@ -1,292 +0,0 @@ -import type { AgentContext, W3cJwtCredentialService, Wallet } from '@credo-ts/core' -import type { W3cCredentialRepository } from '../../core/src/modules/vc/repository/W3cCredentialRepository' - -import { - ClaimFormat, - CredentialIssuancePurpose, - DidKey, - Ed25519Signature2018, - JsonTransformer, - KeyType, - SigningProviderRegistry, - TypedArrayEncoder, - VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020, - VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, - W3cCredential, - W3cCredentialService, - W3cJsonLdVerifiableCredential, - W3cJsonLdVerifiablePresentation, - W3cPresentation, - vcLibraries, -} from '@credo-ts/core' - -import { RegisteredAskarTestWallet } from '../../askar/tests/helpers' -import { W3cCredentialsModuleConfig } from '../../core/src/modules/vc/W3cCredentialsModuleConfig' -import { SignatureSuiteRegistry } from '../../core/src/modules/vc/data-integrity/SignatureSuiteRegistry' -import { W3cJsonLdCredentialService } from '../../core/src/modules/vc/data-integrity/W3cJsonLdCredentialService' -import { customDocumentLoader } from '../../core/src/modules/vc/data-integrity/__tests__/documentLoader' -import { LinkedDataProof } from '../../core/src/modules/vc/data-integrity/models/LinkedDataProof' -import { agentDependencies, getAgentConfig, getAgentContext } from '../../core/tests/helpers' -import { BbsBlsSignature2020, BbsBlsSignatureProof2020, Bls12381g2SigningProvider } from '../src' - -import { BbsBlsSignature2020Fixtures } from './fixtures' -import { describeSkipNode18 } from './util' - -const { jsonldSignatures } = vcLibraries -const { purposes } = jsonldSignatures - -const signatureSuiteRegistry = new SignatureSuiteRegistry([ - { - suiteClass: BbsBlsSignature2020, - proofType: 'BbsBlsSignature2020', - verificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], - keyTypes: [KeyType.Bls12381g2], - }, - { - suiteClass: BbsBlsSignatureProof2020, - proofType: 'BbsBlsSignatureProof2020', - verificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], - keyTypes: [KeyType.Bls12381g2], - }, - { - suiteClass: Ed25519Signature2018, - proofType: 'Ed25519Signature2018', - verificationMethodTypes: [VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018], - keyTypes: [KeyType.Ed25519], - }, -]) - -const signingProviderRegistry = new SigningProviderRegistry([new Bls12381g2SigningProvider()]) - -const agentConfig = getAgentConfig('BbsSignaturesE2eTest') - -describeSkipNode18('BBS W3cCredentialService', () => { - let wallet: Wallet - let agentContext: AgentContext - let w3cJsonLdCredentialService: W3cJsonLdCredentialService - let w3cCredentialService: W3cCredentialService - const privateKey = TypedArrayEncoder.fromString('testseed000000000000000000000001') - - beforeAll(async () => { - // Use askar wallet so we can use the signing provider registry - // TODO: support signing provider registry in memory wallet - // so we don't have to use askar here - wallet = new RegisteredAskarTestWallet( - agentConfig.logger, - new agentDependencies.FileSystem(), - signingProviderRegistry - ) - await wallet.createAndOpen(agentConfig.walletConfig) - agentContext = getAgentContext({ - agentConfig, - wallet, - }) - w3cJsonLdCredentialService = new W3cJsonLdCredentialService( - signatureSuiteRegistry, - new W3cCredentialsModuleConfig({ - documentLoader: customDocumentLoader, - }) - ) - w3cCredentialService = new W3cCredentialService( - {} as unknown as W3cCredentialRepository, - w3cJsonLdCredentialService, - {} as unknown as W3cJwtCredentialService - ) - }) - - afterAll(async () => { - await wallet.delete() - }) - - describe('Utility methods', () => { - describe('getKeyTypesByProofType', () => { - it('should return the correct key types for BbsBlsSignature2020 proof type', async () => { - const keyTypes = w3cJsonLdCredentialService.getKeyTypesByProofType('BbsBlsSignature2020') - expect(keyTypes).toEqual([KeyType.Bls12381g2]) - }) - it('should return the correct key types for BbsBlsSignatureProof2020 proof type', async () => { - const keyTypes = w3cJsonLdCredentialService.getKeyTypesByProofType('BbsBlsSignatureProof2020') - expect(keyTypes).toEqual([KeyType.Bls12381g2]) - }) - }) - - describe('getVerificationMethodTypesByProofType', () => { - it('should return the correct key types for BbsBlsSignature2020 proof type', async () => { - const verificationMethodTypes = - w3cJsonLdCredentialService.getVerificationMethodTypesByProofType('BbsBlsSignature2020') - expect(verificationMethodTypes).toEqual([VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020]) - }) - it('should return the correct key types for BbsBlsSignatureProof2020 proof type', async () => { - const verificationMethodTypes = - w3cJsonLdCredentialService.getVerificationMethodTypesByProofType('BbsBlsSignatureProof2020') - expect(verificationMethodTypes).toEqual([VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020]) - }) - }) - }) - - describe('BbsBlsSignature2020', () => { - let issuerDidKey: DidKey - let verificationMethod: string - - beforeAll(async () => { - // FIXME: askar doesn't create the same privateKey based on the same seed as when generated used askar BBS library... - // See https://github.com/openwallet-foundation/askar/issues/219 - const key = await wallet.createKey({ - keyType: KeyType.Bls12381g2, - privateKey: TypedArrayEncoder.fromBase58('2szQ7zB4tKLJPsGK3YTp9SNQ6hoWYFG5rGhmgfQM4nb7'), - }) - - issuerDidKey = new DidKey(key) - verificationMethod = `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}` - }) - - describe('signCredential', () => { - it('should return a successfully signed credential bbs', async () => { - const credentialJson = BbsBlsSignature2020Fixtures.TEST_LD_DOCUMENT - credentialJson.issuer = issuerDidKey.did - - const credential = JsonTransformer.fromJSON(credentialJson, W3cCredential) - - const vc = await w3cJsonLdCredentialService.signCredential(agentContext, { - format: ClaimFormat.LdpVc, - credential, - proofType: 'BbsBlsSignature2020', - verificationMethod, - }) - - expect(vc).toBeInstanceOf(W3cJsonLdVerifiableCredential) - expect(vc.issuer).toEqual(issuerDidKey.did) - expect(Array.isArray(vc.proof)).toBe(false) - expect(vc.proof).toBeInstanceOf(LinkedDataProof) - - vc.proof = vc.proof as LinkedDataProof - expect(vc.proof.verificationMethod).toEqual(verificationMethod) - }) - }) - - describe('verifyCredential', () => { - it('should verify the credential successfully', async () => { - const result = await w3cJsonLdCredentialService.verifyCredential(agentContext, { - credential: JsonTransformer.fromJSON( - BbsBlsSignature2020Fixtures.TEST_LD_DOCUMENT_SIGNED, - W3cJsonLdVerifiableCredential - ), - proofPurpose: new purposes.AssertionProofPurpose(), - }) - - expect(result.isValid).toEqual(true) - }) - }) - - describe('deriveProof', () => { - it('should derive proof successfully', async () => { - const credentialJson = BbsBlsSignature2020Fixtures.TEST_LD_DOCUMENT_SIGNED - - const vc = JsonTransformer.fromJSON(credentialJson, W3cJsonLdVerifiableCredential) - - const revealDocument = { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/citizenship/v1', - 'https://w3id.org/security/bbs/v1', - ], - type: ['VerifiableCredential', 'PermanentResidentCard'], - credentialSubject: { - '@explicit': true, - type: ['PermanentResident', 'Person'], - givenName: {}, - familyName: {}, - gender: {}, - }, - } - - const result = await w3cJsonLdCredentialService.deriveProof(agentContext, { - credential: vc, - revealDocument: revealDocument, - verificationMethod: verificationMethod, - }) - - result.proof = result.proof as LinkedDataProof - expect(result.proof.verificationMethod).toBe( - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN' - ) - }) - }) - - describe('verifyDerived', () => { - it('should verify the derived proof successfully', async () => { - const result = await w3cJsonLdCredentialService.verifyCredential(agentContext, { - credential: JsonTransformer.fromJSON( - BbsBlsSignature2020Fixtures.TEST_VALID_DERIVED, - W3cJsonLdVerifiableCredential - ), - proofPurpose: new purposes.AssertionProofPurpose(), - }) - expect(result.isValid).toEqual(true) - }) - }) - - describe('createPresentation', () => { - it('should create a presentation successfully', async () => { - const vc = JsonTransformer.fromJSON( - BbsBlsSignature2020Fixtures.TEST_VALID_DERIVED, - W3cJsonLdVerifiableCredential - ) - const result = await w3cCredentialService.createPresentation({ credentials: [vc] }) - - expect(result).toBeInstanceOf(W3cPresentation) - - expect(result.type).toEqual(expect.arrayContaining(['VerifiablePresentation'])) - - expect(result.verifiableCredential).toHaveLength(1) - expect(result.verifiableCredential).toEqual(expect.arrayContaining([vc])) - }) - }) - - describe('signPresentation', () => { - it('should sign the presentation successfully', async () => { - const signingKey = await wallet.createKey({ - privateKey, - keyType: KeyType.Ed25519, - }) - const signingDidKey = new DidKey(signingKey) - const verificationMethod = `${signingDidKey.did}#${signingDidKey.key.fingerprint}` - const presentation = JsonTransformer.fromJSON(BbsBlsSignature2020Fixtures.TEST_VP_DOCUMENT, W3cPresentation) - - const purpose = new CredentialIssuancePurpose({ - controller: { - id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', - }, - date: new Date().toISOString(), - }) - - const verifiablePresentation = await w3cJsonLdCredentialService.signPresentation(agentContext, { - format: ClaimFormat.LdpVp, - presentation: presentation, - proofPurpose: purpose, - proofType: 'Ed25519Signature2018', - challenge: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', - verificationMethod: verificationMethod, - }) - - expect(verifiablePresentation).toBeInstanceOf(W3cJsonLdVerifiablePresentation) - }) - }) - - describe('verifyPresentation', () => { - it('should successfully verify a presentation containing a single verifiable credential bbs', async () => { - const vp = JsonTransformer.fromJSON( - BbsBlsSignature2020Fixtures.TEST_VP_DOCUMENT_SIGNED, - W3cJsonLdVerifiablePresentation - ) - - const result = await w3cJsonLdCredentialService.verifyPresentation(agentContext, { - presentation: vp, - challenge: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', - }) - - expect(result.isValid).toBe(true) - }) - }) - }) -}) diff --git a/packages/bbs-signatures/tests/bbs-signing-provider.test.ts b/packages/bbs-signatures/tests/bbs-signing-provider.test.ts deleted file mode 100644 index c7b0b14859..0000000000 --- a/packages/bbs-signatures/tests/bbs-signing-provider.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { Wallet, WalletConfig } from '@credo-ts/core' - -import { KeyDerivationMethod, KeyType, SigningProviderRegistry, TypedArrayEncoder } from '@credo-ts/core' -import { BBS_SIGNATURE_LENGTH } from '@mattrglobal/bbs-signatures' - -import { RegisteredAskarTestWallet } from '../../askar/tests/helpers' -import { agentDependencies, testLogger } from '../../core/tests' -import { Bls12381g2SigningProvider } from '../src' - -import { describeSkipNode18 } from './util' - -// use raw key derivation method to speed up wallet creating / opening / closing between tests -const walletConfig: WalletConfig = { - id: 'Wallet: BBS Signing Provider', - // generated using indy.generateWalletKey - key: 'CwNJroKHTSSj3XvE7ZAnuKiTn2C4QkFvxEqfm5rzhNrb', - keyDerivationMethod: KeyDerivationMethod.Raw, -} - -describeSkipNode18('BBS Signing Provider', () => { - let wallet: Wallet - const seed = TypedArrayEncoder.fromString('sample-seed-min-of-32-bytes-long') - const message = TypedArrayEncoder.fromString('sample-message') - - beforeEach(async () => { - wallet = new RegisteredAskarTestWallet( - testLogger, - new agentDependencies.FileSystem(), - new SigningProviderRegistry([new Bls12381g2SigningProvider()]) - ) - await wallet.createAndOpen(walletConfig) - }) - - afterEach(async () => { - await wallet.delete() - }) - - test('Create bls12381g2 keypair', async () => { - const key = await wallet.createKey({ seed, keyType: KeyType.Bls12381g2 }) - expect(key.keyType).toStrictEqual(KeyType.Bls12381g2) - expect(key.publicKeyBase58).toStrictEqual( - 'yVLZ92FeZ3AYco43LXtJgtM8kUD1WPUyQPw4VwxZ1iYSak85GYGSJwURhVJM4R6ASRGuM9vjjSU91pKbaqTWQgLjPJjFuK8HdDmAHi3thYun9QUGjarrK7BzC11LurcpYqD' - ) - }) - - test('Fail to sign with bls12381g1g2 keypair', async () => { - const key = await wallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 }) - - await expect( - wallet.sign({ - data: message, - key, - }) - ).rejects.toThrow( - 'Error signing data with key associated with publicKeyBase58 AeAihfn5UFf7y9oesemKE1oLmTwKMRv7fafTepespr3qceF4RUMggAbogkoC8n6rXgtJytq4oGy59DsVHxmNj9WGWwkiRnP3Sz2r924RLVbc2NdP4T7yEPsSFZPsWmLjgnP1vXHpj4bVXNcTmkUmF6mSXinF3HehnQVip14vRFuMzYVxMUh28ofTJzbtUqxMWZQRu. Unsupported keyType: bls12381g1g2' - ) - }) - - test('Create a signature with a bls12381g2 keypair', async () => { - const bls12381g2Key = await wallet.createKey({ seed, keyType: KeyType.Bls12381g2 }) - const signature = await wallet.sign({ - data: message, - key: bls12381g2Key, - }) - expect(signature.length).toStrictEqual(BBS_SIGNATURE_LENGTH) - }) - - test('Verify a signed message with a bls12381g2 publicKey', async () => { - const bls12381g2Key = await wallet.createKey({ seed, keyType: KeyType.Bls12381g2 }) - const signature = await wallet.sign({ - data: message, - key: bls12381g2Key, - }) - await expect(wallet.verify({ key: bls12381g2Key, data: message, signature })).resolves.toStrictEqual(true) - }) -}) diff --git a/packages/bbs-signatures/tests/fixtures.ts b/packages/bbs-signatures/tests/fixtures.ts deleted file mode 100644 index 18430eb592..0000000000 --- a/packages/bbs-signatures/tests/fixtures.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { CREDENTIALS_CONTEXT_V1_URL, SECURITY_CONTEXT_BBS_URL } from '@credo-ts/core' - -export const BbsBlsSignature2020Fixtures = { - TEST_LD_DOCUMENT: { - '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], - id: 'https://issuer.oidp.uscis.gov/credentials/83627465', - type: ['VerifiableCredential', 'PermanentResidentCard'], - issuer: '', - identifier: '83627465', - name: 'Permanent Resident Card', - description: 'Government of Example Permanent Resident Card.', - issuanceDate: '2019-12-03T12:19:52Z', - expirationDate: '2029-12-03T12:19:52Z', - credentialSubject: { - id: 'did:example:b34ca6cd37bbf23', - type: ['PermanentResident', 'Person'], - givenName: 'JOHN', - familyName: 'SMITH', - gender: 'Male', - image: 'data:image/png;base64,iVBORw0KGgokJggg==', - residentSince: '2015-01-01', - lprCategory: 'C09', - lprNumber: '999-999-999', - commuterClassification: 'C1', - birthCountry: 'Bahamas', - birthDate: '1958-07-17', - }, - }, - - TEST_LD_DOCUMENT_SIGNED: { - '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], - id: 'https://issuer.oidp.uscis.gov/credentials/83627465', - type: ['VerifiableCredential', 'PermanentResidentCard'], - issuer: - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - identifier: '83627465', - name: 'Permanent Resident Card', - description: 'Government of Example Permanent Resident Card.', - issuanceDate: '2019-12-03T12:19:52Z', - expirationDate: '2029-12-03T12:19:52Z', - credentialSubject: { - id: 'did:example:b34ca6cd37bbf23', - type: ['PermanentResident', 'Person'], - givenName: 'JOHN', - familyName: 'SMITH', - gender: 'Male', - image: 'data:image/png;base64,iVBORw0KGgokJggg==', - residentSince: '2015-01-01', - lprCategory: 'C09', - lprNumber: '999-999-999', - commuterClassification: 'C1', - birthCountry: 'Bahamas', - birthDate: '1958-07-17', - }, - proof: { - type: 'BbsBlsSignature2020', - created: '2022-04-13T13:47:47Z', - verificationMethod: - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - proofPurpose: 'assertionMethod', - proofValue: - 'hoNNnnRIoEoaY9Fvg3pGVG2eWTAHnR1kIM01nObEL2FdI2IkkpM3246jn3VBD8KBYUHlKfzccE4m7waZyoLEkBLFiK2g54Q2i+CdtYBgDdkUDsoULSBMcH1MwGHwdjfXpldFNFrHFx/IAvLVniyeMQ==', - }, - }, - TEST_LD_DOCUMENT_BAD_SIGNED: { - '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], - id: 'https://issuer.oidp.uscis.gov/credentials/83627465', - type: ['VerifiableCredential', 'PermanentResidentCard'], - issuer: - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - identifier: '83627465', - name: 'Permanent Resident Card', - description: 'Government of Example Permanent Resident Card.', - issuanceDate: '2019-12-03T12:19:52Z', - expirationDate: '2029-12-03T12:19:52Z', - credentialSubject: { - id: 'did:example:b34ca6cd37bbf23', - type: ['PermanentResident', 'Person'], - givenName: 'JOHN', - familyName: 'SMITH', - gender: 'Male', - image: 'data:image/png;base64,iVBORw0KGgokJggg==', - residentSince: '2015-01-01', - lprCategory: 'C09', - lprNumber: '999-999-999', - commuterClassification: 'C1', - birthCountry: 'Bahamas', - birthDate: '1958-07-17', - }, - proof: { - type: 'BbsBlsSignature2020', - created: '2022-04-13T13:47:47Z', - verificationMethod: - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - proofPurpose: 'assertionMethod', - proofValue: - 'gU44r/fmvGpkOyMRZX4nwRB6IsbrL7zbVTs+yu6bZGeCNJuiJqS5U6fCPuvGQ+iNYUHlKfzccE4m7waZyoLEkBLFiK2g54Q2i+CdtYBgDdkUDsoULSBMcH1MwGHwdjfXpldFNFrHFx/IAvLVniyeMQ==', - }, - }, - - TEST_VALID_DERIVED: { - '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], - id: 'https://issuer.oidp.uscis.gov/credentials/83627465', - type: ['PermanentResidentCard', 'VerifiableCredential'], - description: 'Government of Example Permanent Resident Card.', - identifier: '83627465', - name: 'Permanent Resident Card', - credentialSubject: { - id: 'did:example:b34ca6cd37bbf23', - type: ['Person', 'PermanentResident'], - familyName: 'SMITH', - gender: 'Male', - givenName: 'JOHN', - }, - expirationDate: '2029-12-03T12:19:52Z', - issuanceDate: '2019-12-03T12:19:52Z', - issuer: - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - proof: { - type: 'BbsBlsSignatureProof2020', - created: '2022-04-13T13:47:47Z', - nonce: 'GfuRhH8hSAcWm5RWgUQYNQNWjQBsWuVgMCJrhTCD3kSpnHmQOkHcnNAoBsgyMAT4UUI=', - proofPurpose: 'assertionMethod', - proofValue: - 'ABkB/wbvkcCcbPRE5vrXc++orru4MsgrS4ESsZ30RNCs3noqLwm94/RZNp62I6Hyf0Kmht0Vog70HDtnNzbnMAj/zD9oT/N53pOADrtn5v+xZgP3cK4N2d6amg6h3LXem29gidW9hMrROPLit5cWEIL4/HOzxPxQQGYiwEXdW++Aja5ZuwJoMsIx7ysn4C4ekN7JXZtnAAAAdJR/oeDShxRdSBlnCSUHkE4Ol+Z3AhXBKkxb4AxiMKOiNmBreMTjJUGwNAPNU2aKnAAAAAIBUuKV0W0YBQZY/mwLmwCcyOWMiaEpjnVhYip4jhBBZw1aPBe8GzsG9zv3Sf9XAyGEAvVFe3OvwvMwYY5nZYdYoLSR4TLl1aLw0oChiPm2zb6ApXypCEEVd8KhJMATyssTlY48bEljDNixAD2rVDaoAAAACWjyrWp3b62M5Onuwo9EItCrBjPD68xC12q1agqgwFTnOI0+MfEwVGNZsA0IqkCGrZmo3AyRpcRm51IYDWYorM4hued5EcVHeCGd6NrnLSxTFPEu8lnmCoMXcxBWDCZFRGb//M5WlncbsYiz01itHbSs1nmpj3o+DYlF2ZyOYphvLo5A9T4rWVwHRK1+LeCDEawOnI03DWLyN8U4ZpbpcdZNK421IwNjseYY+ptvvL3juZ2uQR84maAZYy/OjMuHNyzqHPXNgsLLqtrvPo0kncefp+x1jgA0J/b5xfT72+vhKZAN1R48/uPf+DySC3avwD3T+YHjePn1bBOidhCWMjwzI9LYO8VvhcWXzH7nBWh5MeUch+Wkl777KrsLhrXnCg==', - verificationMethod: - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - }, - }, - - TEST_VP_DOCUMENT: { - '@context': [CREDENTIALS_CONTEXT_V1_URL], - type: ['VerifiablePresentation'], - verifiableCredential: [ - { - '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], - id: 'https://issuer.oidp.uscis.gov/credentials/83627465', - type: ['PermanentResidentCard', 'VerifiableCredential'], - description: 'Government of Example Permanent Resident Card.', - identifier: '83627465', - name: 'Permanent Resident Card', - credentialSubject: { - id: 'did:example:b34ca6cd37bbf23', - type: ['Person', 'PermanentResident'], - familyName: 'SMITH', - gender: 'Male', - givenName: 'JOHN', - }, - expirationDate: '2029-12-03T12:19:52Z', - issuanceDate: '2019-12-03T12:19:52Z', - issuer: - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - proof: { - type: 'BbsBlsSignatureProof2020', - created: '2022-04-13T13:47:47Z', - nonce: 'GfuRhH8hSAcWm5RWgUQYNQNWjQBsWuVgMCJrhTCD3kSpnHmQOkHcnNAoBsgyMAT4UUI=', - proofPurpose: 'assertionMethod', - proofValue: - 'ABkB/wbvkcCcbPRE5vrXc++orru4MsgrS4ESsZ30RNCs3noqLwm94/RZNp62I6Hyf0Kmht0Vog70HDtnNzbnMAj/zD9oT/N53pOADrtn5v+xZgP3cK4N2d6amg6h3LXem29gidW9hMrROPLit5cWEIL4/HOzxPxQQGYiwEXdW++Aja5ZuwJoMsIx7ysn4C4ekN7JXZtnAAAAdJR/oeDShxRdSBlnCSUHkE4Ol+Z3AhXBKkxb4AxiMKOiNmBreMTjJUGwNAPNU2aKnAAAAAIBUuKV0W0YBQZY/mwLmwCcyOWMiaEpjnVhYip4jhBBZw1aPBe8GzsG9zv3Sf9XAyGEAvVFe3OvwvMwYY5nZYdYoLSR4TLl1aLw0oChiPm2zb6ApXypCEEVd8KhJMATyssTlY48bEljDNixAD2rVDaoAAAACWjyrWp3b62M5Onuwo9EItCrBjPD68xC12q1agqgwFTnOI0+MfEwVGNZsA0IqkCGrZmo3AyRpcRm51IYDWYorM4hued5EcVHeCGd6NrnLSxTFPEu8lnmCoMXcxBWDCZFRGb//M5WlncbsYiz01itHbSs1nmpj3o+DYlF2ZyOYphvLo5A9T4rWVwHRK1+LeCDEawOnI03DWLyN8U4ZpbpcdZNK421IwNjseYY+ptvvL3juZ2uQR84maAZYy/OjMuHNyzqHPXNgsLLqtrvPo0kncefp+x1jgA0J/b5xfT72+vhKZAN1R48/uPf+DySC3avwD3T+YHjePn1bBOidhCWMjwzI9LYO8VvhcWXzH7nBWh5MeUch+Wkl777KrsLhrXnCg==', - verificationMethod: - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - }, - }, - ], - }, - TEST_VP_DOCUMENT_SIGNED: { - '@context': [CREDENTIALS_CONTEXT_V1_URL], - type: ['VerifiablePresentation'], - verifiableCredential: [ - { - '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], - id: 'https://issuer.oidp.uscis.gov/credentials/83627465', - type: ['PermanentResidentCard', 'VerifiableCredential'], - description: 'Government of Example Permanent Resident Card.', - identifier: '83627465', - name: 'Permanent Resident Card', - credentialSubject: { - id: 'did:example:b34ca6cd37bbf23', - type: ['Person', 'PermanentResident'], - familyName: 'SMITH', - gender: 'Male', - givenName: 'JOHN', - }, - expirationDate: '2029-12-03T12:19:52Z', - issuanceDate: '2019-12-03T12:19:52Z', - issuer: - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - proof: { - type: 'BbsBlsSignatureProof2020', - created: '2022-04-13T13:47:47Z', - nonce: 'GfuRhH8hSAcWm5RWgUQYNQNWjQBsWuVgMCJrhTCD3kSpnHmQOkHcnNAoBsgyMAT4UUI=', - proofPurpose: 'assertionMethod', - proofValue: - 'ABkB/wbvkcCcbPRE5vrXc++orru4MsgrS4ESsZ30RNCs3noqLwm94/RZNp62I6Hyf0Kmht0Vog70HDtnNzbnMAj/zD9oT/N53pOADrtn5v+xZgP3cK4N2d6amg6h3LXem29gidW9hMrROPLit5cWEIL4/HOzxPxQQGYiwEXdW++Aja5ZuwJoMsIx7ysn4C4ekN7JXZtnAAAAdJR/oeDShxRdSBlnCSUHkE4Ol+Z3AhXBKkxb4AxiMKOiNmBreMTjJUGwNAPNU2aKnAAAAAIBUuKV0W0YBQZY/mwLmwCcyOWMiaEpjnVhYip4jhBBZw1aPBe8GzsG9zv3Sf9XAyGEAvVFe3OvwvMwYY5nZYdYoLSR4TLl1aLw0oChiPm2zb6ApXypCEEVd8KhJMATyssTlY48bEljDNixAD2rVDaoAAAACWjyrWp3b62M5Onuwo9EItCrBjPD68xC12q1agqgwFTnOI0+MfEwVGNZsA0IqkCGrZmo3AyRpcRm51IYDWYorM4hued5EcVHeCGd6NrnLSxTFPEu8lnmCoMXcxBWDCZFRGb//M5WlncbsYiz01itHbSs1nmpj3o+DYlF2ZyOYphvLo5A9T4rWVwHRK1+LeCDEawOnI03DWLyN8U4ZpbpcdZNK421IwNjseYY+ptvvL3juZ2uQR84maAZYy/OjMuHNyzqHPXNgsLLqtrvPo0kncefp+x1jgA0J/b5xfT72+vhKZAN1R48/uPf+DySC3avwD3T+YHjePn1bBOidhCWMjwzI9LYO8VvhcWXzH7nBWh5MeUch+Wkl777KrsLhrXnCg==', - verificationMethod: - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - }, - }, - ], - proof: { - verificationMethod: - 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', - type: 'Ed25519Signature2018', - created: '2022-04-21T10:15:38Z', - proofPurpose: 'authentication', - challenge: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', - jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..wGtR9yuTRfhrsvCthUOn-fg_lK0mZIe2IOO2Lv21aOXo5YUAbk50qMBLk4C1iqoOx-Jz6R0g4aa4cuqpdXzkBw', - }, - }, -} diff --git a/packages/bbs-signatures/tests/setup.ts b/packages/bbs-signatures/tests/setup.ts deleted file mode 100644 index 78143033f2..0000000000 --- a/packages/bbs-signatures/tests/setup.ts +++ /dev/null @@ -1,3 +0,0 @@ -import 'reflect-metadata' - -jest.setTimeout(120000) diff --git a/packages/bbs-signatures/tests/util.ts b/packages/bbs-signatures/tests/util.ts deleted file mode 100644 index efe9f799bd..0000000000 --- a/packages/bbs-signatures/tests/util.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function describeSkipNode18(...parameters: Parameters) { - const version = process.version - - if (version.startsWith('v18.')) { - describe.skip(...parameters) - } else { - describe(...parameters) - } -} diff --git a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts deleted file mode 100644 index c2123062b9..0000000000 --- a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts +++ /dev/null @@ -1,281 +0,0 @@ -import type { EventReplaySubject, JsonLdTestsAgent } from '../../core/tests' -import type { V2IssueCredentialMessage } from '../../didcomm' - -import { TypedArrayEncoder } from '../../core/src' -import { KeyType } from '../../core/src/crypto' -import { CREDENTIALS_CONTEXT_V1_URL, SECURITY_CONTEXT_BBS_URL } from '../../core/src/modules/vc' -import { JsonTransformer } from '../../core/src/utils/JsonTransformer' -import { setupJsonLdTests, testLogger, waitForCredentialRecordSubject } from '../../core/tests' -import { CredentialExchangeRecord, CredentialState } from '../../didcomm' - -import { describeSkipNode18 } from './util' - -let faberAgent: JsonLdTestsAgent -let faberReplay: EventReplaySubject -let aliceAgent: JsonLdTestsAgent -let aliceReplay: EventReplaySubject -let aliceConnectionId: string -let aliceCredentialRecord: CredentialExchangeRecord -let faberCredentialRecord: CredentialExchangeRecord - -const signCredentialOptions = { - credential: { - '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], - id: 'https://issuer.oidp.uscis.gov/credentials/83627465', - type: ['VerifiableCredential', 'PermanentResidentCard'], - issuer: - 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', - issuanceDate: '2019-12-03T12:19:52Z', - expirationDate: '2029-12-03T12:19:52Z', - identifier: '83627465', - name: 'Permanent Resident Card', - credentialSubject: { - id: 'did:example:b34ca6cd37bbf23', - type: ['PermanentResident', 'Person'], - givenName: 'JOHN', - familyName: 'SMITH', - gender: 'Male', - image: 'data:image/png;base64,iVBORw0KGgokJggg==', - residentSince: '2015-01-01', - description: 'Government of Example Permanent Resident Card.', - lprCategory: 'C09', - lprNumber: '999-999-999', - commuterClassification: 'C1', - birthCountry: 'Bahamas', - birthDate: '1958-07-17', - }, - }, - options: { - proofType: 'BbsBlsSignature2020', - proofPurpose: 'assertionMethod', - }, -} - -describeSkipNode18('credentials, BBS+ signature', () => { - beforeAll(async () => { - ;({ - issuerAgent: faberAgent, - issuerReplay: faberReplay, - holderAgent: aliceAgent, - holderReplay: aliceReplay, - holderIssuerConnectionId: aliceConnectionId, - } = await setupJsonLdTests({ - issuerName: 'Faber Agent Credentials LD BBS+', - holderName: 'Alice Agent Credentials LD BBS+', - useBbs: true, - })) - - await faberAgent.context.wallet.createKey({ - keyType: KeyType.Ed25519, - privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), - }) - // FIXME: askar doesn't create the same privateKey based on the same seed as when generated used askar BBS library... - // See https://github.com/openwallet-foundation/askar/issues/219 - await faberAgent.context.wallet.createKey({ - keyType: KeyType.Bls12381g2, - privateKey: TypedArrayEncoder.fromBase58('2szQ7zB4tKLJPsGK3YTp9SNQ6hoWYFG5rGhmgfQM4nb7'), - }) - }) - - afterAll(async () => { - await faberAgent.shutdown() - await faberAgent.wallet.delete() - await aliceAgent.shutdown() - await aliceAgent.wallet.delete() - }) - - test('Alice starts with V2 (ld format, BbsBlsSignature2020 signature) credential proposal to Faber', async () => { - testLogger.test('Alice sends (v2 jsonld) credential proposal to Faber') - const credentialExchangeRecord = await aliceAgent.modules.credentials.proposeCredential({ - connectionId: aliceConnectionId, - protocolVersion: 'v2', - credentialFormats: { - jsonld: signCredentialOptions, - }, - comment: 'v2 propose credential test for W3C Credentials', - }) - - expect(credentialExchangeRecord.connectionId).toEqual(aliceConnectionId) - expect(credentialExchangeRecord.protocolVersion).toEqual('v2') - expect(credentialExchangeRecord.state).toEqual(CredentialState.ProposalSent) - expect(credentialExchangeRecord.threadId).not.toBeNull() - - testLogger.test('Faber waits for credential proposal from Alice') - faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { - threadId: credentialExchangeRecord.threadId, - state: CredentialState.ProposalReceived, - }) - - testLogger.test('Faber sends credential offer to Alice') - await faberAgent.modules.credentials.acceptProposal({ - credentialRecordId: faberCredentialRecord.id, - comment: 'V2 W3C Offer', - }) - - testLogger.test('Alice waits for credential offer from Faber') - aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.OfferReceived, - }) - - const offerMessage = await faberAgent.modules.credentials.findOfferMessage(faberCredentialRecord.id) - expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ - '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', - '@id': expect.any(String), - comment: 'V2 W3C Offer', - formats: [ - { - attach_id: expect.any(String), - format: 'aries/ld-proof-vc-detail@v1.0', - }, - ], - 'offers~attach': [ - { - '@id': expect.any(String), - 'mime-type': 'application/json', - data: expect.any(Object), - lastmod_time: undefined, - byte_count: undefined, - }, - ], - '~thread': { - thid: expect.any(String), - pthid: undefined, - sender_order: undefined, - received_orders: undefined, - }, - '~service': undefined, - '~attach': undefined, - '~please_ack': undefined, - '~timing': undefined, - '~transport': undefined, - '~l10n': undefined, - credential_preview: expect.any(Object), - replacement_id: undefined, - }) - expect(aliceCredentialRecord.id).not.toBeNull() - expect(aliceCredentialRecord.type).toBe(CredentialExchangeRecord.type) - - const offerCredentialExchangeRecord = await aliceAgent.modules.credentials.acceptOffer({ - credentialRecordId: aliceCredentialRecord.id, - credentialFormats: { - jsonld: undefined, - }, - }) - - expect(offerCredentialExchangeRecord.connectionId).toEqual(aliceConnectionId) - expect(offerCredentialExchangeRecord.protocolVersion).toEqual('v2') - expect(offerCredentialExchangeRecord.state).toEqual(CredentialState.RequestSent) - expect(offerCredentialExchangeRecord.threadId).not.toBeNull() - - testLogger.test('Faber waits for credential request from Alice') - await waitForCredentialRecordSubject(faberReplay, { - threadId: aliceCredentialRecord.threadId, - state: CredentialState.RequestReceived, - }) - - testLogger.test('Faber sends credential to Alice') - await faberAgent.modules.credentials.acceptRequest({ - credentialRecordId: faberCredentialRecord.id, - comment: 'V2 W3C Offer', - }) - - testLogger.test('Alice waits for credential from Faber') - aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.CredentialReceived, - }) - - testLogger.test('Alice sends credential ack to Faber') - await aliceAgent.modules.credentials.acceptCredential({ credentialRecordId: aliceCredentialRecord.id }) - - testLogger.test('Faber waits for credential ack from Alice') - faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.Done, - }) - expect(aliceCredentialRecord).toMatchObject({ - type: CredentialExchangeRecord.type, - id: expect.any(String), - createdAt: expect.any(Date), - threadId: expect.any(String), - connectionId: expect.any(String), - state: CredentialState.CredentialReceived, - }) - - const credentialMessage = await faberAgent.modules.credentials.findCredentialMessage(faberCredentialRecord.id) - const w3cCredential = (credentialMessage as V2IssueCredentialMessage).credentialAttachments[0].getDataAsJson() - - expect(w3cCredential).toMatchObject({ - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/citizenship/v1', - 'https://w3id.org/security/bbs/v1', - ], - id: 'https://issuer.oidp.uscis.gov/credentials/83627465', - type: ['VerifiableCredential', 'PermanentResidentCard'], - issuer: - 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', - issuanceDate: '2019-12-03T12:19:52Z', - expirationDate: '2029-12-03T12:19:52Z', - identifier: '83627465', - name: 'Permanent Resident Card', - credentialSubject: { - id: 'did:example:b34ca6cd37bbf23', - type: ['PermanentResident', 'Person'], - givenName: 'JOHN', - familyName: 'SMITH', - gender: 'Male', - image: 'data:image/png;base64,iVBORw0KGgokJggg==', - residentSince: '2015-01-01', - description: 'Government of Example Permanent Resident Card.', - lprCategory: 'C09', - lprNumber: '999-999-999', - commuterClassification: 'C1', - birthCountry: 'Bahamas', - birthDate: '1958-07-17', - }, - proof: { - type: 'BbsBlsSignature2020', - created: expect.any(String), - verificationMethod: - 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', - proofPurpose: 'assertionMethod', - proofValue: expect.any(String), - }, - }) - - expect(JsonTransformer.toJSON(credentialMessage)).toMatchObject({ - '@type': 'https://didcomm.org/issue-credential/2.0/issue-credential', - '@id': expect.any(String), - comment: 'V2 W3C Offer', - formats: [ - { - attach_id: expect.any(String), - format: 'aries/ld-proof-vc@v1.0', - }, - ], - 'credentials~attach': [ - { - '@id': expect.any(String), - 'mime-type': 'application/json', - data: expect.any(Object), - lastmod_time: undefined, - byte_count: undefined, - }, - ], - '~thread': { - thid: expect.any(String), - pthid: undefined, - sender_order: undefined, - received_orders: undefined, - }, - '~please_ack': { on: ['RECEIPT'] }, - '~service': undefined, - '~attach': undefined, - '~timing': undefined, - '~transport': undefined, - '~l10n': undefined, - }) - }) -}) diff --git a/packages/cheqd/src/dids/CheqdDidRegistrar.ts b/packages/cheqd/src/dids/CheqdDidRegistrar.ts index 0a4f628b3e..8f0d1b04ad 100644 --- a/packages/cheqd/src/dids/CheqdDidRegistrar.ts +++ b/packages/cheqd/src/dids/CheqdDidRegistrar.ts @@ -1,35 +1,37 @@ -import type { CheqdNetwork, DIDDocument, DidStdFee, TVerificationKey, VerificationMethods } from '@cheqd/sdk' +import { CheqdNetwork, DIDDocument, DidStdFee, VerificationMethods } from '@cheqd/sdk' import type { SignInfo } from '@cheqd/ts-proto/cheqd/did/v2' -import type { +import { AgentContext, DidCreateOptions, DidCreateResult, DidDeactivateResult, + DidDocumentKey, DidRegistrar, DidUpdateOptions, DidUpdateResult, + Kms, + XOR, + getKmsKeyIdForVerifiacationMethod, + getPublicJwkFromVerificationMethod, } from '@credo-ts/core' import { MethodSpecificIdAlgo, createDidVerificationMethod } from '@cheqd/sdk' import { MsgCreateResourcePayload } from '@cheqd/ts-proto/cheqd/resource/v2' import { - Buffer, DidDocument, DidDocumentRole, DidRecord, DidRepository, JsonTransformer, - KeyType, TypedArrayEncoder, VerificationMethod, - getKeyFromVerificationMethod, - isValidPrivateKey, utils, } from '@credo-ts/core' import { parseCheqdDid } from '../anoncreds/utils/identifiers' import { CheqdLedgerService } from '../ledger' +import { KmsJwkPublicOkp } from '@credo-ts/core/src/modules/kms' import { createMsgCreateDidDocPayloadToSign, createMsgDeactivateDidDocPayloadToSign, @@ -47,48 +49,115 @@ export class CheqdDidRegistrar implements DidRegistrar { let didDocument: DidDocument const versionId = options.options?.versionId ?? utils.uuid() + let keys: DidDocumentKey[] = [] + try { - if (options.didDocument && validateSpecCompliantPayload(options.didDocument)) { + if (options.didDocument) { + const isSpecCompliantPayload = validateSpecCompliantPayload(options.didDocument) + if (!isSpecCompliantPayload.valid) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `Invalid did document provided. ${isSpecCompliantPayload.error}`, + }, + } + } + didDocument = options.didDocument + const authenticationIds = didDocument.authentication?.map((v) => (typeof v === 'string' ? v : v.id)) ?? [] + const didDocumentRelativeKeyIds = options.options.keys.map((key) => key.didDocumentRelativeKeyId) + keys = options.options.keys - const cheqdDid = parseCheqdDid(options.didDocument.id) - if (!cheqdDid) { + // Ensure all keys are present in the did document + for (const didDocumentKeyId of didDocumentRelativeKeyIds) { + didDocument.dereferenceKey(didDocumentKeyId) + } + + if (!authenticationIds.every((id) => didDocumentRelativeKeyIds.includes(id.replace(didDocument.id, '')))) { return { didDocumentMetadata: {}, didRegistrationMetadata: {}, didState: { state: 'failed', - reason: `Unable to parse cheqd did ${options.didDocument.id}`, + reason: `For all 'authentication' verification methods in the did document a 'key' entry in the options MUST be provided that link the did document key id with the kms key id`, }, } } - } else if (options.secret?.verificationMethod) { - const withoutDidDocumentOptions = options as CheqdDidCreateWithoutDidDocumentOptions - const verificationMethod = withoutDidDocumentOptions.secret.verificationMethod - const methodSpecificIdAlgo = withoutDidDocumentOptions.options.methodSpecificIdAlgo - const privateKey = verificationMethod.privateKey - if (privateKey && !isValidPrivateKey(privateKey, KeyType.Ed25519)) { + + const cheqdDid = parseCheqdDid(options.didDocument.id) + + if (!cheqdDid) { return { didDocumentMetadata: {}, didRegistrationMetadata: {}, didState: { state: 'failed', - reason: 'Invalid private key provided', + reason: `Unable to parse cheqd did ${options.didDocument.id}`, }, } } + } else if (options.options.createKey || options.options.keyId) { + const methodSpecificIdAlgo = options.options.methodSpecificIdAlgo + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) + + let publicJwk: KmsJwkPublicOkp & { crv: 'Ed25519' } + if (options.options.createKey) { + const createKeyResult = await kms.createKey(options.options.createKey) + publicJwk = createKeyResult.publicJwk + keys.push({ + kmsKeyId: createKeyResult.keyId, + didDocumentRelativeKeyId: '#key-1', + }) + } else { + const _publicJwk = await kms.getPublicKey({ + keyId: options.options.keyId, + }) + keys.push({ + kmsKeyId: options.options.keyId, + didDocumentRelativeKeyId: '#key-1', + }) + if (!_publicJwk) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notFound: key with key id '${options.options.keyId}' not found`, + }, + } + } + + if (_publicJwk.kty !== 'OKP' || _publicJwk.crv !== 'Ed25519') { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `key with key id '${options.options.keyId}' uses unsupported ${Kms.getJwkHumanDescription( + _publicJwk + )} for did:cheqd`, + }, + } + } + + publicJwk = { + ..._publicJwk, + crv: _publicJwk.crv, + } + } - const key = await agentContext.wallet.createKey({ - keyType: KeyType.Ed25519, - privateKey: privateKey, - }) + // TODO: make this configureable + const verificationMethod = VerificationMethods.JWK + const jwk = Kms.PublicJwk.fromPublicJwk(publicJwk) didDocument = generateDidDoc({ - verificationMethod: verificationMethod.type as VerificationMethods, - verificationMethodId: verificationMethod.id || 'key-1', + verificationMethod, + verificationMethodId: 'key-1', methodSpecificIdAlgo: (methodSpecificIdAlgo as MethodSpecificIdAlgo) || MethodSpecificIdAlgo.Uuid, - network: withoutDidDocumentOptions.options.network as CheqdNetwork, - publicKey: TypedArrayEncoder.toHex(key.publicKey), + network: options.options.network as CheqdNetwork, + publicKey: TypedArrayEncoder.toHex(jwk.publicKey.publicKey), }) const contextMapping = { @@ -96,7 +165,7 @@ export class CheqdDidRegistrar implements DidRegistrar { Ed25519VerificationKey2020: 'https://w3id.org/security/suites/ed25519-2020/v1', JsonWebKey2020: 'https://w3id.org/security/suites/jws-2020/v1', } - const contextUrl = contextMapping[verificationMethod.type] + const contextUrl = contextMapping[verificationMethod] // Add the context to the did document // NOTE: cheqd sdk uses https://www.w3.org/ns/did/v1 while Credo did doc uses https://w3id.org/did/v1 @@ -108,15 +177,29 @@ export class CheqdDidRegistrar implements DidRegistrar { didRegistrationMetadata: {}, didState: { state: 'failed', - reason: 'Provide a didDocument or at least one verificationMethod with seed in secret', + reason: 'Provide a didDocument or provide createKey or keyId in options', }, } } const didDocumentJson = didDocument.toJSON() as DIDDocument - const payloadToSign = await createMsgCreateDidDocPayloadToSign(didDocumentJson, versionId) - const signInputs = await this.signPayload(agentContext, payloadToSign, didDocument.verificationMethod) + + const authentication = didDocument.authentication?.map((authentication) => + typeof authentication === 'string' ? didDocument.dereferenceVerificationMethod(authentication) : authentication + ) + if (!authentication || authentication.length === 0) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: "No keys to sign with in 'authentication' of DID document", + }, + } + } + + const signInputs = await this.signPayload(agentContext, payloadToSign, authentication, keys) const response = await cheqdLedgerService.create(didDocumentJson, signInputs, versionId) if (response.code !== 0) { @@ -128,6 +211,7 @@ export class CheqdDidRegistrar implements DidRegistrar { did: didDocument.id, role: DidDocumentRole.Created, didDocument, + keys, }) await didRepository.save(agentContext, didRecord) @@ -159,12 +243,23 @@ export class CheqdDidRegistrar implements DidRegistrar { const cheqdLedgerService = agentContext.dependencyManager.resolve(CheqdLedgerService) const versionId = options.options?.versionId || utils.uuid() - const verificationMethod = options.secret?.verificationMethod let didDocument: DidDocument let didRecord: DidRecord | null try { - if (options.didDocument && validateSpecCompliantPayload(options.didDocument)) { + if (options.didDocument) { + const isSpecCompliantPayload = validateSpecCompliantPayload(options.didDocument) + if (!isSpecCompliantPayload.valid) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `Invalid did document provided. ${isSpecCompliantPayload.error}`, + }, + } + } + didDocument = options.didDocument const resolvedDocument = await cheqdLedgerService.resolve(didDocument.id) didRecord = await didRepository.findCreatedDid(agentContext, didDocument.id) @@ -179,34 +274,83 @@ export class CheqdDidRegistrar implements DidRegistrar { } } - if (verificationMethod) { - const privateKey = verificationMethod.privateKey - if (privateKey && !isValidPrivateKey(privateKey, KeyType.Ed25519)) { + const keys = didRecord.keys ?? [] + if (options.options?.createKey || options.options?.keyId) { + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) + let createdKey: DidDocumentKey + + let publicJwk: KmsJwkPublicOkp & { crv: 'Ed25519' } + if (options.options.createKey) { + const createKeyResult = await kms.createKey(options.options.createKey) + publicJwk = createKeyResult.publicJwk + + createdKey = { + didDocumentRelativeKeyId: `#${utils.uuid()}-1`, + kmsKeyId: createKeyResult.keyId, + } + } else if (options.options.keyId) { + const _publicJwk = await kms.getPublicKey({ + keyId: options.options.keyId, + }) + createdKey = { + didDocumentRelativeKeyId: `#${utils.uuid()}-1`, + kmsKeyId: options.options.keyId, + } + if (!_publicJwk) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notFound: key with key id '${options.options.keyId}' not found`, + }, + } + } + + if (_publicJwk.kty !== 'OKP' || _publicJwk.crv !== 'Ed25519') { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `key with key id '${options.options.keyId}' uses unsupported ${Kms.getJwkHumanDescription( + _publicJwk + )} for did:cheqd`, + }, + } + } + + publicJwk = { + ..._publicJwk, + crv: _publicJwk.crv, + } + } else { + // This will never happen, but to make TS happy return { didDocumentMetadata: {}, didRegistrationMetadata: {}, didState: { state: 'failed', - reason: 'Invalid private key provided', + reason: 'Expect options.createKey or options.keyId', }, } } - const key = await agentContext.wallet.createKey({ - keyType: KeyType.Ed25519, - privateKey: privateKey, - }) + // TODO: make this configureable + const verificationMethod = VerificationMethods.JWK + const jwk = Kms.PublicJwk.fromPublicJwk(publicJwk) + keys.push(createdKey) didDocument.verificationMethod?.concat( JsonTransformer.fromJSON( createDidVerificationMethod( - [verificationMethod.type as VerificationMethods], + [verificationMethod], [ { methodSpecificId: didDocument.id.split(':')[3], didUrl: didDocument.id, - keyId: `${didDocument.id}#${verificationMethod.id}`, - publicKey: TypedArrayEncoder.toHex(key.publicKey), + keyId: `${didDocument.id}${createdKey.didDocumentRelativeKeyId}` as `${string}#${string}-${number}`, + publicKey: TypedArrayEncoder.toHex(jwk.publicKey.publicKey), }, ] ), @@ -225,10 +369,57 @@ export class CheqdDidRegistrar implements DidRegistrar { } } - const payloadToSign = await createMsgCreateDidDocPayloadToSign(didDocument as DIDDocument, versionId) - const signInputs = await this.signPayload(agentContext, payloadToSign, didDocument.verificationMethod) + // Filter out all keys that are not present in the did document anymore + didRecord.keys = didRecord.keys?.filter(({ didDocumentRelativeKeyId }) => { + try { + didDocument.dereferenceKey(didDocumentRelativeKeyId) + return true + } catch (_error) { + return false + } + }) - const response = await cheqdLedgerService.update(didDocument as DIDDocument, signInputs, versionId) + // TODO: we don't know which keys are managed by Credo. Should we + // create a keys array for all keys within the did document set to the legacy key id + // TODO: we need some sort of migration plan, otherwise we will have to support + // legacy key ids forever + // const authenticationIds = didDocument.authentication?.map(a => typeof a === 'string' ? a : a.id) ?? [] + // const didDocumentKeyIds = didRecord.keys?.map(({didDocumentRelativeKeyId}) => didDocumentRelativeKeyId) + // if (!authenticationIds.every((id) => didDocumentKeyIds?.includes(id))) { + // return { + // didDocumentMetadata: {}, + // didRegistrationMetadata: {}, + // didState: { + // state: "failed", + // reason: `For all 'authentication' verification methods in the did document a 'key' entry in the options MUST be provided that link the did document key id with the kms key id`, + // }, + // }; + // } + + const payloadToSign = await createMsgCreateDidDocPayloadToSign(didDocument.toJSON() as DIDDocument, versionId) + + const authentication = didDocument.authentication?.map((authentication) => + typeof authentication === 'string' ? didDocument.dereferenceVerificationMethod(authentication) : authentication + ) + if (!authentication || authentication.length === 0) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: "No keys to sign with in 'authentication' of DID document", + }, + } + } + const signInputs = await this.signPayload( + agentContext, + payloadToSign, + // TOOD: we should also sign with the authentication entries that are removed (so we should diff) + authentication, + didRecord.keys + ) + + const response = await cheqdLedgerService.update(didDocument.toJSON() as DIDDocument, signInputs, versionId) if (response.code !== 0) { throw new Error(`${response.rawLog}`) } @@ -285,8 +476,24 @@ export class CheqdDidRegistrar implements DidRegistrar { } } const payloadToSign = createMsgDeactivateDidDocPayloadToSign(didDocument, versionId) - const didDocumentInstance = JsonTransformer.fromJSON(didDocument, DidDocument) - const signInputs = await this.signPayload(agentContext, payloadToSign, didDocumentInstance.verificationMethod) + const didDocumentInstance = DidDocument.fromJSON(didDocument) + + const authentication = didDocumentInstance.authentication?.map((authentication) => + typeof authentication === 'string' + ? didDocumentInstance.dereferenceVerificationMethod(authentication) + : authentication + ) + if (!authentication || authentication.length === 0) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: "No keys to sign with in 'authentication' of DID document", + }, + } + } + const signInputs = await this.signPayload(agentContext, payloadToSign, authentication, didRecord.keys) const response = await cheqdLedgerService.deactivate(didDocument, signInputs, versionId) if (response.code !== 0) { throw new Error(`${response.rawLog}`) @@ -355,7 +562,12 @@ export class CheqdDidRegistrar implements DidRegistrar { const payloadToSign = MsgCreateResourcePayload.encode(resourcePayload).finish() const didDocumentInstance = JsonTransformer.fromJSON(didDocument, DidDocument) - const signInputs = await this.signPayload(agentContext, payloadToSign, didDocumentInstance.verificationMethod) + const signInputs = await this.signPayload( + agentContext, + payloadToSign, + didDocumentInstance.verificationMethod, + didRecord.keys + ) const response = await cheqdLedgerService.createResource(did, resourcePayload, signInputs) if (response.code !== 0) { throw new Error(`${response.rawLog}`) @@ -385,40 +597,65 @@ export class CheqdDidRegistrar implements DidRegistrar { private async signPayload( agentContext: AgentContext, payload: Uint8Array, - verificationMethod: VerificationMethod[] = [] + verificationMethod: VerificationMethod[] = [], + keys?: DidDocumentKey[] ) { + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) return await Promise.all( verificationMethod.map(async (method) => { - const key = getKeyFromVerificationMethod(method) + const publicJwk = getPublicJwkFromVerificationMethod(method) + const kmsKeyId = getKmsKeyIdForVerifiacationMethod(method, keys) ?? publicJwk.legacyKeyId + + const { signature } = await kms.sign({ + data: payload, + algorithm: publicJwk.signatureAlgorithm, + keyId: kmsKeyId, + }) + + // EC signatures need to be sent as DER encoded for Cheqd + const jwk = publicJwk.toJson() + if (jwk.kty === 'EC') { + return { + verificationMethodId: method.id, + signature: Kms.rawEcSignatureToDer(signature, jwk.crv), + } + } + return { verificationMethodId: method.id, - signature: await agentContext.wallet.sign({ data: Buffer.from(payload), key }), + signature, } satisfies SignInfo }) ) } } +type KmsCreateKeyOptionsOkpEd25519 = Kms.KmsCreateKeyOptions + export interface CheqdDidCreateWithoutDidDocumentOptions extends DidCreateOptions { method: 'cheqd' - did?: undefined - didDocument?: undefined + did?: never + didDocument?: never + secret?: never + options: { network: `${CheqdNetwork}` fee?: DidStdFee versionId?: string methodSpecificIdAlgo?: `${MethodSpecificIdAlgo}` - } - secret: { - verificationMethod: IVerificationMethod - } + } & XOR<{ createKey: KmsCreateKeyOptionsOkpEd25519 }, { keyId: string }> } export interface CheqdDidCreateFromDidDocumentOptions extends DidCreateOptions { method: 'cheqd' did?: undefined didDocument: DidDocument - options?: { + options: { + /** + * The linking between the did document keys and the kms keys. For cheqd dids ALL authentication entries MUST sign the request + * and thus it is required to a mapping for all keys. + */ + keys: DidDocumentKey[] fee?: DidStdFee versionId?: string } @@ -429,13 +666,18 @@ export type CheqdDidCreateOptions = CheqdDidCreateFromDidDocumentOptions | Cheqd export interface CheqdDidUpdateOptions extends DidUpdateOptions { did: string didDocument: DidDocument - options: { + secret?: never + + options?: { + /** + * The linking between the did document keys and the kms keys. The existing keys will be filtered based on the keys not present + * in the did document anymore, and this new list will be merged into it. + */ + keys?: DidDocumentKey[] + fee?: DidStdFee versionId?: string - } - secret?: { - verificationMethod: IVerificationMethod - } + } & XOR<{ createKey?: KmsCreateKeyOptionsOkpEd25519 }, { keyId?: string }> } export interface CheqdDidDeactivateOptions extends DidCreateOptions { @@ -453,9 +695,3 @@ export interface CheqdCreateResourceOptions extends Pick - privateKey?: Buffer -} diff --git a/packages/cheqd/src/ledger/CheqdLedgerService.ts b/packages/cheqd/src/ledger/CheqdLedgerService.ts index ac05a88723..a0732897fc 100644 --- a/packages/cheqd/src/ledger/CheqdLedgerService.ts +++ b/packages/cheqd/src/ledger/CheqdLedgerService.ts @@ -50,6 +50,17 @@ export class CheqdLedgerService { } } + public async disconnect() { + for (const network of this.networks) { + const _a = await network.sdk + if (!network.sdk) { + await this.initializeSdkForNetwork(network) + } else { + this.logger.debug(`Not connecting to network ${network} as SDK already initialized`) + } + } + } + private async getSdk(did: string) { const parsedDid = parseCheqdDid(did) if (!parsedDid) { diff --git a/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts b/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts index cadb671b4c..23f2051482 100644 --- a/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts +++ b/packages/cheqd/tests/cheqd-data-integrity.e2e.test.ts @@ -24,9 +24,7 @@ describe('anoncreds w3c data integrity e2e tests', () => { afterEach(async () => { await issuerAgent.shutdown() - await issuerAgent.wallet.delete() await holderAgent.shutdown() - await holderAgent.wallet.delete() }) test('cheqd issuance and verification flow starting from offer without revocation', async () => { diff --git a/packages/cheqd/tests/cheqd-did-registrar.e2e.test.ts b/packages/cheqd/tests/cheqd-did-registrar.e2e.test.ts index dc41a94ae5..d4ca6dfdaf 100644 --- a/packages/cheqd/tests/cheqd-did-registrar.e2e.test.ts +++ b/packages/cheqd/tests/cheqd-did-registrar.e2e.test.ts @@ -1,24 +1,24 @@ import type { DidDocument } from '@credo-ts/core' -import type { CheqdDidCreateOptions } from '../src' +import type { CheqdDidCreateOptions, CheqdDidUpdateOptions } from '../src' import { Agent, DidDocumentBuilder, - KeyType, + Kms, SECURITY_JWS_CONTEXT_URL, TypedArrayEncoder, getEd25519VerificationKey2018, getJsonWebKey2020, utils, } from '@credo-ts/core' -import { generateKeyPairFromSeed } from '@stablelib/ed25519' -import { getInMemoryAgentOptions } from '../../core/tests/helpers' +import { getAgentOptions } from '../../core/tests/helpers' +import { transformPrivateKeyToPrivateJwk } from '../../askar/src' import { validService } from './setup' import { cheqdPayerSeeds, getCheqdModules } from './setupCheqdModule' -const agentOptions = getInMemoryAgentOptions('Faber Dids Registrar', {}, {}, getCheqdModules(cheqdPayerSeeds[0])) +const agentOptions = getAgentOptions('Faber Dids Registrar', {}, {}, getCheqdModules(cheqdPayerSeeds[0])) describe('Cheqd DID registrar', () => { let agent: Agent> @@ -30,7 +30,6 @@ describe('Cheqd DID registrar', () => { afterAll(async () => { await agent.shutdown() - await agent.wallet.delete() }) it('should create a did:cheqd did', async () => { @@ -41,18 +40,16 @@ describe('Cheqd DID registrar', () => { .join(`${Math.random().toString(36)}00000000000000000`.slice(2, 18)) .slice(0, 32) ) - const publicKeyEd25519 = generateKeyPairFromSeed(privateKey).publicKey - const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(publicKeyEd25519) + const { privateJwk } = transformPrivateKeyToPrivateJwk({ type: { crv: 'Ed25519', kty: 'OKP' }, privateKey }) + const createdKey = await agent.kms.importKey({ privateJwk }) + + // @ts-ignore + const { kid, d, ...publicJwk } = createdKey.publicJwk + const did = await agent.dids.create({ method: 'cheqd', - secret: { - verificationMethod: { - id: 'key-1', - type: 'Ed25519VerificationKey2018', - privateKey, - }, - }, options: { + keyId: createdKey.keyId, network: 'testnet', methodSpecificIdAlgo: 'base58btc', }, @@ -63,8 +60,8 @@ describe('Cheqd DID registrar', () => { didDocument: { verificationMethod: [ { - type: 'Ed25519VerificationKey2018', - publicKeyBase58: ed25519PublicKeyBase58, + type: 'JsonWebKey2020', + publicKeyJwk: publicJwk, }, ], }, @@ -75,13 +72,14 @@ describe('Cheqd DID registrar', () => { it('should create a did:cheqd using Ed25519VerificationKey2020', async () => { const did = await agent.dids.create({ method: 'cheqd', - secret: { - verificationMethod: { - id: 'key-1', - type: 'Ed25519VerificationKey2020', - }, - }, options: { + createKey: { + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + keyId: 'custom-key-id', + }, network: 'testnet', methodSpecificIdAlgo: 'uuid', }, @@ -92,13 +90,14 @@ describe('Cheqd DID registrar', () => { it('should create a did:cheqd using JsonWebKey2020', async () => { const createResult = await agent.dids.create({ method: 'cheqd', - secret: { - verificationMethod: { - id: 'key-11', - type: 'JsonWebKey2020', - }, - }, + options: { + createKey: { + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }, network: 'testnet', methodSpecificIdAlgo: 'base58btc', }, @@ -117,9 +116,10 @@ describe('Cheqd DID registrar', () => { const didDocument = createResult.didState.didDocument as DidDocument didDocument.service = [validService(did)] - const updateResult = await agent.dids.update({ + const updateResult = await agent.dids.update({ did, didDocument, + options: {}, }) expect(updateResult).toMatchObject({ didState: { @@ -141,24 +141,36 @@ describe('Cheqd DID registrar', () => { it('should create a did:cheqd did using custom did document containing Ed25519 key', async () => { const did = `did:cheqd:testnet:${utils.uuid()}` - const ed25519Key = await agent.wallet.createKey({ - keyType: KeyType.Ed25519, + const ed25519Key = await agent.kms.createKey({ + type: { + crv: 'Ed25519', + kty: 'OKP', + }, }) + const publicJwk = Kms.PublicJwk.fromPublicJwk(ed25519Key.publicJwk) const createResult = await agent.dids.create({ method: 'cheqd', didDocument: new DidDocumentBuilder(did) .addContext(SECURITY_JWS_CONTEXT_URL) .addController(did) - .addAuthentication(`${did}#${ed25519Key.fingerprint}`) + .addAuthentication(`${did}#${publicJwk.fingerprint}`) .addVerificationMethod( getEd25519VerificationKey2018({ - key: ed25519Key, + publicJwk, controller: did, - id: `${did}#${ed25519Key.fingerprint}`, + id: `${did}#${publicJwk.fingerprint}`, }) ) .build(), + options: { + keys: [ + { + didDocumentRelativeKeyId: `#${publicJwk.fingerprint}`, + kmsKeyId: ed25519Key.keyId, + }, + ], + }, }) expect(createResult).toMatchObject({ @@ -173,7 +185,7 @@ describe('Cheqd DID registrar', () => { { controller: did, type: 'Ed25519VerificationKey2018', - publicKeyBase58: ed25519Key.publicKeyBase58, + publicKeyBase58: TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey), }, ], }) @@ -182,31 +194,39 @@ describe('Cheqd DID registrar', () => { it('should create a did:cheqd did using custom did document containing P256 key', async () => { const did = `did:cheqd:testnet:${utils.uuid()}` - const p256Key = await agent.wallet.createKey({ - keyType: KeyType.P256, + const p256Key = await agent.kms.createKey({ + type: { kty: 'EC', crv: 'P-256' }, }) + const publicJwk = Kms.PublicJwk.fromPublicJwk(p256Key.publicJwk) const createResult = await agent.dids.create({ method: 'cheqd', + options: { + keys: [ + { + didDocumentRelativeKeyId: `#${publicJwk.fingerprint}`, + kmsKeyId: p256Key.keyId, + }, + ], + }, didDocument: new DidDocumentBuilder(did) .addContext(SECURITY_JWS_CONTEXT_URL) .addController(did) - .addAuthentication(`${did}#${p256Key.fingerprint}`) + .addAuthentication(`${did}#${publicJwk.fingerprint}`) .addVerificationMethod( getJsonWebKey2020({ did, - key: p256Key, - verificationMethodId: `${did}#${p256Key.fingerprint}`, + publicJwk, + verificationMethodId: `${did}#${publicJwk.fingerprint}`, }) ) .build(), }) - // FIXME: the ES256 signature generated by Credo is invalid for Cheqd - // need to dive deeper into it, but for now adding a failing test so we can fix it in the future + // Somehow this only works with the Node KMS expect(createResult).toMatchObject({ didState: { - state: 'failed', + state: 'finished', }, }) }) diff --git a/packages/cheqd/tests/cheqd-did-resolver.e2e.test.ts b/packages/cheqd/tests/cheqd-did-resolver.e2e.test.ts index 24f1e7cfbc..95499f7f42 100644 --- a/packages/cheqd/tests/cheqd-did-resolver.e2e.test.ts +++ b/packages/cheqd/tests/cheqd-did-resolver.e2e.test.ts @@ -2,16 +2,14 @@ import type { CheqdDidCreateOptions } from '../src' import { Agent, JsonTransformer, utils } from '@credo-ts/core' -import { getInMemoryAgentOptions } from '../../core/tests/helpers' +import { getAgentOptions } from '../../core/tests/helpers' import { CheqdDidRegistrar } from '../src' import { getClosestResourceVersion } from '../src/dids/didCheqdUtil' import { cheqdPayerSeeds, getCheqdModules } from './setupCheqdModule' // biome-ignore lint/suspicious/noExportsInTest: -export const resolverAgent = new Agent( - getInMemoryAgentOptions('Cheqd resolver', {}, {}, getCheqdModules(cheqdPayerSeeds[1])) -) +export const resolverAgent = new Agent(getAgentOptions('Cheqd resolver', {}, {}, getCheqdModules(cheqdPayerSeeds[1]))) describe('Cheqd DID resolver', () => { let did: string @@ -25,13 +23,8 @@ describe('Cheqd DID resolver', () => { const didResult = await resolverAgent.dids.create({ method: 'cheqd', - secret: { - verificationMethod: { - id: 'key-1', - type: 'Ed25519VerificationKey2020', - }, - }, options: { + createKey: { type: { kty: 'OKP', crv: 'Ed25519' } }, network: 'testnet', methodSpecificIdAlgo: 'uuid', }, @@ -74,7 +67,6 @@ describe('Cheqd DID resolver', () => { afterAll(async () => { await resolverAgent.shutdown() - await resolverAgent.wallet.delete() }) it('should resolve a did:cheqd did from local testnet', async () => { @@ -83,15 +75,19 @@ describe('Cheqd DID resolver', () => { }) expect(JsonTransformer.toJSON(resolveResult)).toMatchObject({ didDocument: { - '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/ed25519-2020/v1'], + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], id: did, controller: [did], verificationMethod: [ { controller: did, id: `${did}#key-1`, - publicKeyMultibase: expect.any(String), - type: 'Ed25519VerificationKey2020', + publicKeyJwk: { + kty: 'OKP', + crv: 'Ed25519', + x: expect.any(String), + }, + type: 'JsonWebKey2020', }, ], authentication: [`${did}#key-1`], diff --git a/packages/cheqd/tests/cheqd-sdk-anoncreds-registry.e2e.test.ts b/packages/cheqd/tests/cheqd-sdk-anoncreds-registry.e2e.test.ts index 79fce1a58c..e73a7ca7cb 100644 --- a/packages/cheqd/tests/cheqd-sdk-anoncreds-registry.e2e.test.ts +++ b/packages/cheqd/tests/cheqd-sdk-anoncreds-registry.e2e.test.ts @@ -2,12 +2,13 @@ import type { CheqdDidCreateOptions } from '../src' import { Agent, JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' -import { getInMemoryAgentOptions } from '../../core/tests/helpers' +import { getAgentOptions } from '../../core/tests/helpers' import { CheqdAnonCredsRegistry } from '../src/anoncreds' +import { transformPrivateKeyToPrivateJwk } from '../../askar/src' import { cheqdPayerSeeds, getCheqdModules } from './setupCheqdModule' -const agent = new Agent(getInMemoryAgentOptions('cheqdAnonCredsRegistry', {}, {}, getCheqdModules(cheqdPayerSeeds[2]))) +const agent = new Agent(getAgentOptions('cheqdAnonCredsRegistry', {}, {}, getCheqdModules(cheqdPayerSeeds[2]))) const cheqdAnonCredsRegistry = new CheqdAnonCredsRegistry() @@ -20,7 +21,6 @@ describe('cheqdAnonCredsRegistry', () => { afterAll(async () => { await agent.shutdown() - await agent.wallet.delete() }) let credentialDefinitionId: string @@ -28,17 +28,22 @@ describe('cheqdAnonCredsRegistry', () => { // One test as the credential definition depends on the schema test('register and resolve a schema and credential definition', async () => { const privateKey = TypedArrayEncoder.fromString('000000000000000000000000000cheqd') + const { privateJwk } = transformPrivateKeyToPrivateJwk({ + privateKey, + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }) + + const createdKey = await agent.kms.importKey({ + privateJwk, + }) const did = await agent.dids.create({ method: 'cheqd', - secret: { - verificationMethod: { - id: 'key-10', - type: 'Ed25519VerificationKey2020', - privateKey, - }, - }, options: { + keyId: createdKey.keyId, network: 'testnet', methodSpecificIdAlgo: 'uuid', }, diff --git a/packages/cheqd/tests/setup.ts b/packages/cheqd/tests/setup.ts index 0c0c9b380a..c3c7e2283c 100644 --- a/packages/cheqd/tests/setup.ts +++ b/packages/cheqd/tests/setup.ts @@ -17,7 +17,7 @@ export function validService(did: string) { return new DidDocumentService({ id: `${did}#service-1`, type: 'CustomType', - serviceEndpoint: 'https://rand.io', + serviceEndpoint: ['https://rand.io'], }) } diff --git a/packages/core/package.json b/packages/core/package.json index e46da5057e..ffa8965023 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,7 +33,8 @@ "@multiformats/base-x": "^4.0.1", "@noble/curves": "^1.8.1", "@noble/hashes": "^1.7.1", - "@peculiar/asn1-ecc": "^2.3.13", + "@peculiar/asn1-ecc": "^2.3.14", + "@peculiar/asn1-rsa": "^2.3.15", "@peculiar/asn1-schema": "^2.3.13", "@peculiar/asn1-x509": "^2.3.13", "@peculiar/x509": "^1.12.1", @@ -44,7 +45,6 @@ "@sd-jwt/sd-jwt-vc": "^0.7.2", "@sd-jwt/types": "^0.7.2", "@sd-jwt/utils": "^0.7.2", - "dcql": "^0.2.20", "@sphereon/pex-models": "^2.3.2", "@sphereon/ssi-types": "0.33.0", "@stablelib/ed25519": "^1.0.2", @@ -53,6 +53,7 @@ "buffer": "^6.0.3", "class-transformer": "0.5.1", "class-validator": "0.14.1", + "dcql": "^0.2.20", "did-resolver": "^4.1.0", "ec-compression": "0.0.1-alpha.12", "lru_map": "^0.4.1", @@ -64,7 +65,8 @@ "uuid": "^9.0.0", "varint": "^6.0.0", "web-did-resolver": "^2.0.21", - "webcrypto-core": "^1.8.0" + "webcrypto-core": "^1.8.0", + "zod": "^3.24.2" }, "devDependencies": { "@types/events": "^3.0.0", diff --git a/packages/core/src/agent/Agent.ts b/packages/core/src/agent/Agent.ts index 3891cffcbb..35d759bc70 100644 --- a/packages/core/src/agent/Agent.ts +++ b/packages/core/src/agent/Agent.ts @@ -1,4 +1,3 @@ -import type { Module } from '../plugins' import type { InitConfig } from '../types' import type { AgentDependencies } from './AgentDependencies' import type { AgentModulesInput } from './AgentModules' @@ -6,11 +5,10 @@ import type { AgentModulesInput } from './AgentModules' import { Subject } from 'rxjs' import { InjectionSymbols } from '../constants' -import { SigningProviderToken } from '../crypto' import { JwsService } from '../crypto/JwsService' import { CredoError } from '../error' import { DependencyManager } from '../plugins' -import { StorageUpdateService, StorageVersionRepository } from '../storage' +import { StorageUpdateService, StorageVersionRepository, UpdateAssistant } from '../storage' import { AgentConfig } from './AgentConfig' import { extendModulesWithDefaultModules } from './AgentModules' @@ -37,13 +35,6 @@ export class Agent extends BaseAge dependencyManager.registerSingleton(StorageVersionRepository) dependencyManager.registerSingleton(StorageUpdateService) - // This is a really ugly hack to make tsyringe work without any SigningProviders registered - // It is currently impossible to use @injectAll if there are no instances registered for the - // token. We register a value of `default` by default and will filter that out in the registry. - // Once we have a signing provider that should always be registered we can remove this. We can make an ed25519 - // signer using the @stablelib/ed25519 library. - dependencyManager.registerInstance(SigningProviderToken, 'default') - dependencyManager.registerInstance(AgentConfig, agentConfig) dependencyManager.registerInstance(InjectionSymbols.AgentDependencies, agentConfig.agentDependencies) dependencyManager.registerInstance(InjectionSymbols.Stop$, new Subject()) @@ -52,12 +43,6 @@ export class Agent extends BaseAge // Register all modules. This will also include the default modules dependencyManager.registerModules(modulesWithDefaultModules) - // Register possibly already defined services - if (!dependencyManager.isRegistered(InjectionSymbols.Wallet)) { - throw new CredoError( - "Missing required dependency: 'Wallet'. You can register it using the AskarModule, or implement your own." - ) - } if (!dependencyManager.isRegistered(InjectionSymbols.Logger)) { dependencyManager.registerInstance(InjectionSymbols.Logger, agentConfig.logger) } @@ -74,6 +59,7 @@ export class Agent extends BaseAge new AgentContext({ dependencyManager, contextCorrelationId: 'default', + isRootAgentContext: true, }) ) @@ -90,32 +76,52 @@ export class Agent extends BaseAge } public async initialize() { - await super.initialize() + if (this._isInitialized) { + throw new CredoError( + 'Agent already initialized. Currently it is not supported to re-initialize an already initialized agent.' + ) + } + + // We first initialize all the modules + await this.dependencyManager.initializeModules(this.agentContext) + + // Then we initialize the root agent context + await this.dependencyManager.initializeAgentContext(this.agentContext) - for (const [, module] of Object.entries(this.dependencyManager.registeredModules) as [string, Module][]) { - if (module.initialize) { - await module.initialize(this.agentContext) - } + // Make sure the storage is up to date + const storageUpdateService = this.dependencyManager.resolve(StorageUpdateService) + const isStorageUpToDate = await storageUpdateService.isUpToDate(this.agentContext) + this.logger.info(`Agent storage is ${isStorageUpToDate ? '' : 'not '}up to date.`) + + if (!isStorageUpToDate && this.agentConfig.autoUpdateStorageOnStartup) { + const updateAssistant = new UpdateAssistant(this) + + await updateAssistant.initialize() + await updateAssistant.update() + } else if (!isStorageUpToDate) { + const currentVersion = await storageUpdateService.getCurrentStorageVersion(this.agentContext) + + // Close agent context to prevent un-initialized agent with initialized agent context + await this.dependencyManager.closeAgentContext(this.agentContext) + + throw new CredoError( + // TODO: add link to where documentation on how to update can be found. + `Current agent storage is not up to date. To prevent the framework state from getting corrupted the agent initialization is aborted. Make sure to update the agent storage (currently at ${currentVersion}) to the latest version (${UpdateAssistant.frameworkStorageVersion}). You can also downgrade your version of Credo.` + ) } this._isInitialized = true } public async shutdown() { + // TODO: relace stop$, should be replaced by module specific lifecycle methods const stop$ = this.dependencyManager.resolve>(InjectionSymbols.Stop$) // All observables use takeUntil with the stop$ observable // this means all observables will stop running if a value is emitted on this observable stop$.next(true) - for (const [, module] of Object.entries(this.dependencyManager.registeredModules) as [string, Module][]) { - if (module.shutdown) { - await module.shutdown(this.agentContext) - } - } - - if (this.wallet.isInitialized) { - await this.wallet.close() - } + await this.dependencyManager.shutdownModules(this.agentContext) + await this.dependencyManager.closeAgentContext(this.agentContext) this._isInitialized = false } diff --git a/packages/core/src/agent/AgentConfig.ts b/packages/core/src/agent/AgentConfig.ts index 862814b833..0c7b71df8e 100644 --- a/packages/core/src/agent/AgentConfig.ts +++ b/packages/core/src/agent/AgentConfig.ts @@ -17,13 +17,6 @@ export class AgentConfig { this.agentDependencies = agentDependencies } - /** - * @todo move to context configuration - */ - public get walletConfig() { - return this.initConfig.walletConfig - } - public get allowInsecureHttpUrls() { return this.initConfig.allowInsecureHttpUrls ?? false } @@ -32,10 +25,6 @@ export class AgentConfig { return this.initConfig.autoUpdateStorageOnStartup ?? false } - public get backupBeforeStorageUpdate() { - return this.initConfig.backupBeforeStorageUpdate ?? true - } - public extend(config: Partial): AgentConfig { return new AgentConfig( { ...this.initConfig, logger: this.logger, label: this.label, ...config }, @@ -46,14 +35,6 @@ export class AgentConfig { public toJSON() { return { ...this.initConfig, - walletConfig: { - ...this.walletConfig, - key: this.walletConfig?.key ? '[*****]' : undefined, - storage: { - ...this.walletConfig?.storage, - credentials: this.walletConfig?.storage?.credentials ? '[*****]' : undefined, - }, - }, logger: this.logger.logLevel, agentDependencies: Boolean(this.agentDependencies), label: this.label, diff --git a/packages/core/src/agent/AgentModules.ts b/packages/core/src/agent/AgentModules.ts index 9ecb9d47e6..6336656b2c 100644 --- a/packages/core/src/agent/AgentModules.ts +++ b/packages/core/src/agent/AgentModules.ts @@ -2,16 +2,16 @@ import type { ApiModule, DependencyManager, Module } from '../plugins' import type { IsAny } from '../types' import type { Constructor } from '../utils/mixins' -import { CacheModule } from '../modules/cache' +import { CacheModule, SingleContextStorageLruCache } from '../modules/cache' import { DcqlModule } from '../modules/dcql/DcqlModule' import { DidsModule } from '../modules/dids' import { DifPresentationExchangeModule } from '../modules/dif-presentation-exchange' import { GenericRecordsModule } from '../modules/generic-records' +import { KeyManagementModule } from '../modules/kms' import { MdocModule } from '../modules/mdoc/MdocModule' import { SdJwtVcModule } from '../modules/sd-jwt-vc' import { W3cCredentialsModule } from '../modules/vc' import { X509Module } from '../modules/x509' -import { WalletModule } from '../wallet' /** * Simple utility type that represent a map of modules. This is used to map from moduleKey (api key) to the api in the framework. @@ -106,13 +106,13 @@ function getDefaultAgentModules() { dcql: () => new DcqlModule(), genericRecords: () => new GenericRecordsModule(), dids: () => new DidsModule(), - wallet: () => new WalletModule(), w3cCredentials: () => new W3cCredentialsModule(), - cache: () => new CacheModule(), + cache: () => new CacheModule({ cache: new SingleContextStorageLruCache({ limit: 500 }) }), pex: () => new DifPresentationExchangeModule(), sdJwtVc: () => new SdJwtVcModule(), x509: () => new X509Module(), mdoc: () => new MdocModule(), + kms: () => new KeyManagementModule({}), } as const } @@ -125,18 +125,21 @@ function getDefaultAgentModules() { export function extendModulesWithDefaultModules( modules?: AgentModules ): AgentModules & DefaultAgentModules { - const extendedModules: Record = { ...modules } const defaultAgentModules = getDefaultAgentModules() + const defaultAgentModuleKeys = Object.keys(defaultAgentModules) + + const defaultModules: Array<[string, Module]> = [] + const customModules: Array<[string, Module]> = Object.entries(modules ?? {}).filter( + ([key]) => !defaultAgentModuleKeys.includes(key) + ) // Register all default modules, if not registered yet for (const [moduleKey, getConfiguredModule] of Object.entries(defaultAgentModules)) { - // Do not register if the module is already registered. - if (modules?.[moduleKey]) continue - - extendedModules[moduleKey] = getConfiguredModule() + // Prefer user-registered module, otherwise initialize the default module + defaultModules.push([moduleKey, modules?.[moduleKey] ?? getConfiguredModule()]) } - return extendedModules as AgentModules & DefaultAgentModules + return Object.fromEntries([...defaultModules, ...customModules]) as AgentModules & DefaultAgentModules } /** diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index af5798d6d3..541907d905 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -1,28 +1,21 @@ import type { Logger } from '../logger' -import type { DependencyManager } from '../plugins' -import type { AgentConfig } from './AgentConfig' -import type { AgentApi, EmptyModuleMap, ModulesMap, WithoutDefaultModules } from './AgentModules' - -import { CredoError } from '../error' import { DidsApi } from '../modules/dids' import { GenericRecordsApi } from '../modules/generic-records' +import { KeyManagementApi } from '../modules/kms' import { MdocApi } from '../modules/mdoc' import { SdJwtVcApi } from '../modules/sd-jwt-vc' import { W3cCredentialsApi } from '../modules/vc/W3cCredentialsApi' import { X509Api } from '../modules/x509' -import { StorageUpdateService } from '../storage' -import { UpdateAssistant } from '../storage/migration/UpdateAssistant' -import { WalletApi } from '../wallet' -import { WalletError } from '../wallet/error' +import type { DependencyManager } from '../plugins' +import type { AgentConfig } from './AgentConfig' +import type { AgentApi, EmptyModuleMap, ModulesMap, WithoutDefaultModules } from './AgentModules' import { getAgentApi } from './AgentModules' import { EventEmitter } from './EventEmitter' import { AgentContext } from './context' export abstract class BaseAgent { - protected agentConfig: AgentConfig protected logger: Logger - public readonly dependencyManager: DependencyManager protected eventEmitter: EventEmitter protected _isInitialized = false protected agentContext: AgentContext @@ -30,51 +23,43 @@ export abstract class BaseAgent> - public constructor(agentConfig: AgentConfig, dependencyManager: DependencyManager) { - this.dependencyManager = dependencyManager - - this.agentConfig = agentConfig + public constructor( + protected agentConfig: AgentConfig, + public readonly dependencyManager: DependencyManager + ) { this.logger = this.agentConfig.logger this.logger.info('Creating agent with config', { agentConfig: agentConfig.toJSON(), }) - if (!this.agentConfig.walletConfig) { - this.logger.warn( - 'Wallet config has not been set on the agent config. ' + - 'Make sure to initialize the wallet yourself before initializing the agent, ' + - 'or provide the required wallet configuration in the agent constructor' - ) - } - // Resolve instances after everything is registered this.eventEmitter = this.dependencyManager.resolve(EventEmitter) this.agentContext = this.dependencyManager.resolve(AgentContext) this.genericRecords = this.dependencyManager.resolve(GenericRecordsApi) this.dids = this.dependencyManager.resolve(DidsApi) - this.wallet = this.dependencyManager.resolve(WalletApi) this.w3cCredentials = this.dependencyManager.resolve(W3cCredentialsApi) this.sdJwtVc = this.dependencyManager.resolve(SdJwtVcApi) this.x509 = this.dependencyManager.resolve(X509Api) this.mdoc = this.dependencyManager.resolve(MdocApi) + this.kms = this.dependencyManager.resolve(KeyManagementApi) const defaultApis = [ this.genericRecords, this.dids, - this.wallet, this.w3cCredentials, this.sdJwtVc, this.x509, this.mdoc, + this.kms, ] // Set the api of the registered modules on the agent, excluding the default apis @@ -82,47 +67,7 @@ export abstract class BaseAgent { describe('Initialization', () => { let agent: Agent - afterEach(async () => { - const wallet = agent.context.wallet - - if (wallet.isInitialized) { - await wallet.delete() - } - }) - it('isInitialized should only return true after initialization', async () => { expect.assertions(2) @@ -116,42 +107,6 @@ describe('Agent', () => { await agent.initialize() expect(agent.isInitialized).toBe(true) }) - - it('wallet isInitialized should return true after agent initialization if wallet config is set in agent constructor', async () => { - expect.assertions(4) - - agent = new Agent(agentOptions) - const wallet = agent.context.wallet - - expect(agent.isInitialized).toBe(false) - expect(wallet.isInitialized).toBe(false) - await agent.initialize() - expect(agent.isInitialized).toBe(true) - expect(wallet.isInitialized).toBe(true) - }) - - it('wallet must be initialized if wallet config is not set before agent can be initialized', async () => { - expect.assertions(9) - - const { walletConfig, ...withoutWalletConfig } = agentOptions.config - agent = new Agent({ ...agentOptions, config: withoutWalletConfig }) - - expect(agent.isInitialized).toBe(false) - expect(agent.wallet.isInitialized).toBe(false) - - expect(agent.initialize()).rejects.toThrowError(WalletError) - expect(agent.isInitialized).toBe(false) - expect(agent.wallet.isInitialized).toBe(false) - - // biome-ignore lint/style/noNonNullAssertion: - await agent.wallet.initialize(walletConfig!) - expect(agent.isInitialized).toBe(false) - expect(agent.wallet.isInitialized).toBe(true) - - await agent.initialize() - expect(agent.wallet.isInitialized).toBe(true) - expect(agent.isInitialized).toBe(true) - }) }) describe('Dependency Injection', () => { diff --git a/packages/core/src/agent/__tests__/AgentModules.test.ts b/packages/core/src/agent/__tests__/AgentModules.test.ts index 836fa03c88..3455f4a205 100644 --- a/packages/core/src/agent/__tests__/AgentModules.test.ts +++ b/packages/core/src/agent/__tests__/AgentModules.test.ts @@ -5,12 +5,12 @@ import { DcqlModule } from '../../modules/dcql' import { DidsModule } from '../../modules/dids' import { DifPresentationExchangeModule } from '../../modules/dif-presentation-exchange' import { GenericRecordsModule } from '../../modules/generic-records' +import { KeyManagementModule } from '../../modules/kms' import { MdocModule } from '../../modules/mdoc' import { SdJwtVcModule } from '../../modules/sd-jwt-vc' import { W3cCredentialsModule } from '../../modules/vc' import { X509Module } from '../../modules/x509' import { DependencyManager, injectable } from '../../plugins' -import { WalletModule } from '../../wallet' import { extendModulesWithDefaultModules, getAgentApi } from '../AgentModules' @injectable() @@ -56,7 +56,7 @@ describe('AgentModules', () => { pex: expect.any(DifPresentationExchangeModule), genericRecords: expect.any(GenericRecordsModule), dids: expect.any(DidsModule), - wallet: expect.any(WalletModule), + kms: expect.any(KeyManagementModule), w3cCredentials: expect.any(W3cCredentialsModule), sdJwtVc: expect.any(SdJwtVcModule), mdoc: expect.any(MdocModule), @@ -76,7 +76,7 @@ describe('AgentModules', () => { pex: expect.any(DifPresentationExchangeModule), genericRecords: expect.any(GenericRecordsModule), dids: expect.any(DidsModule), - wallet: expect.any(WalletModule), + kms: expect.any(KeyManagementModule), w3cCredentials: expect.any(W3cCredentialsModule), cache: expect.any(CacheModule), sdJwtVc: expect.any(SdJwtVcModule), @@ -99,7 +99,7 @@ describe('AgentModules', () => { pex: expect.any(DifPresentationExchangeModule), dcql: expect.any(DcqlModule), dids: expect.any(DidsModule), - wallet: expect.any(WalletModule), + kms: expect.any(KeyManagementModule), w3cCredentials: expect.any(W3cCredentialsModule), cache: expect.any(CacheModule), sdJwtVc: expect.any(SdJwtVcModule), diff --git a/packages/core/src/agent/context/AgentContext.ts b/packages/core/src/agent/context/AgentContext.ts index 5c45061194..7232f0123f 100644 --- a/packages/core/src/agent/context/AgentContext.ts +++ b/packages/core/src/agent/context/AgentContext.ts @@ -1,5 +1,4 @@ -import type { DependencyManager } from '../../plugins' -import type { Wallet } from '../../wallet' +import type { DependencyManager, InjectionToken } from '../../plugins' import type { AgentContextProvider } from './AgentContextProvider' import { InjectionSymbols } from '../../constants' @@ -23,15 +22,20 @@ export class AgentContext { */ public readonly contextCorrelationId: string + public readonly isRootAgentContext: boolean + public constructor({ dependencyManager, contextCorrelationId, + isRootAgentContext = false, }: { dependencyManager: DependencyManager contextCorrelationId: string + isRootAgentContext?: boolean }) { this.dependencyManager = dependencyManager this.contextCorrelationId = contextCorrelationId + this.isRootAgentContext = isRootAgentContext } /** @@ -41,13 +45,6 @@ export class AgentContext { return this.dependencyManager.resolve(AgentConfig) } - /** - * Convenience method to access the wallet for the current context. - */ - public get wallet() { - return this.dependencyManager.resolve(InjectionSymbols.Wallet) - } - /** * End session the current agent context */ @@ -67,4 +64,11 @@ export class AgentContext { contextCorrelationId: this.contextCorrelationId, } } + + /** + * Resolve a dependency + */ + public resolve(token: InjectionToken): T { + return this.dependencyManager.resolve(token) + } } diff --git a/packages/core/src/agent/context/AgentContextProvider.ts b/packages/core/src/agent/context/AgentContextProvider.ts index 14ba9984c5..a83dd32fd4 100644 --- a/packages/core/src/agent/context/AgentContextProvider.ts +++ b/packages/core/src/agent/context/AgentContextProvider.ts @@ -27,4 +27,6 @@ export interface AgentContextProvider { * called once for every session and the agent context MUST not be used after this method is called. */ endSessionForAgentContext(agentContext: AgentContext): Promise + + deleteAgentContext(agentContext: AgentContext): Promise } diff --git a/packages/core/src/agent/context/DefaultAgentContextProvider.ts b/packages/core/src/agent/context/DefaultAgentContextProvider.ts index 7f9ec4d918..180ec412a6 100644 --- a/packages/core/src/agent/context/DefaultAgentContextProvider.ts +++ b/packages/core/src/agent/context/DefaultAgentContextProvider.ts @@ -50,6 +50,17 @@ export class DefaultAgentContextProvider implements AgentContextProvider { ) } - // We won't dispose the agent context as we don't keep track of the total number of sessions for the root agent context.65 + // We won't dispose the agent context as we don't keep track of the total number of sessions for the root agent context. + } + + public async deleteAgentContext(agentContext: AgentContext): Promise { + // Throw an error if the context correlation id does not match to prevent misuse. + if (agentContext.contextCorrelationId !== this.agentContext.contextCorrelationId) { + throw new CredoError( + `Could not delete agent context with contextCorrelationId '${agentContext.contextCorrelationId}'. Only contextCorrelationId '${this.agentContext.contextCorrelationId}' is provided by this provider.` + ) + } + + await agentContext.dependencyManager.deleteAgentContext(agentContext) } } diff --git a/packages/core/src/crypto/JwsService.ts b/packages/core/src/crypto/JwsService.ts index e8f18ba49d..c7eebc2eaf 100644 --- a/packages/core/src/crypto/JwsService.ts +++ b/packages/core/src/crypto/JwsService.ts @@ -6,55 +6,62 @@ import type { JwsGeneralFormat, JwsProtectedHeaderOptions, } from './JwsTypes' -import type { Key } from './Key' -import type { JwkJson } from './jose/jwk/Jwk' import { CredoError } from '../error' import { EncodedX509Certificate, X509ModuleConfig } from '../modules/x509' import { injectable } from '../plugins' import { Buffer, JsonEncoder, TypedArrayEncoder, isJsonObject } from '../utils' -import { WalletError } from '../wallet/error' +import { + KeyManagementApi, + KeyManagementError, + KnownJwaSignatureAlgorithm, + PublicJwk, + assertJwkAsymmetric, + assymetricPublicJwkMatches, + getJwkHumanDescription, +} from '../modules/kms' +import { isKnownJwaSignatureAlgorithm } from '../modules/kms/jwk/jwa' import { X509Service } from './../modules/x509/X509Service' import { JwsSigner, JwsSignerWithJwk } from './JwsSigner' import { JWS_COMPACT_FORMAT_MATCHER } from './JwsTypes' -import { JwaSignatureAlgorithm } from './jose' -import { getJwkFromJson, getJwkFromKey } from './jose/jwk' import { JwtPayload } from './jose/jwt' @injectable() export class JwsService { private async createJwsBase(agentContext: AgentContext, options: CreateJwsBaseOptions) { const { jwk, alg, x5c } = options.protectedHeaderOptions - const keyJwk = getJwkFromKey(options.key) + + const kms = agentContext.dependencyManager.resolve(KeyManagementApi) + + const key = await kms.getPublicKey({ keyId: options.keyId }) + assertJwkAsymmetric(key) + + const publicJwk = PublicJwk.fromPublicJwk(key) // Make sure the options.x5c and x5c from protectedHeader are the same. if (x5c) { - const certificate = X509Service.getLeafCertificate(agentContext, { certificateChain: x5c }) - if ( - certificate.publicKey.keyType !== options.key.keyType || - !Buffer.from(certificate.publicKey.publicKey).equals(Buffer.from(options.key.publicKey)) - ) { + const certificate = X509Service.getLeafCertificate(agentContext, { + certificateChain: x5c, + }) + + if (!assymetricPublicJwkMatches(certificate.publicJwk.toJson(), key)) { throw new CredoError('Protected header x5c does not match key for signing.') } } + const jwkInstance = jwk instanceof PublicJwk ? jwk : jwk ? PublicJwk.fromUnknown(jwk) : undefined // Make sure the options.key and jwk from protectedHeader are the same. - if ( - jwk && - (jwk.key.keyType !== options.key.keyType || - !Buffer.from(jwk.key.publicKey).equals(Buffer.from(options.key.publicKey))) - ) { + if (jwkInstance && !assymetricPublicJwkMatches(jwkInstance.toJson(), key)) { throw new CredoError('Protected header JWK does not match key for signing.') } // Validate the options.key used for signing against the jws options - // We use keyJwk instead of jwk, as the user could also use kid instead of jwk - if (keyJwk && !keyJwk.supportsSignatureAlgorithm(alg)) { + if (!publicJwk.supportedSignatureAlgorithms.includes(alg)) { throw new CredoError( - `alg '${alg}' is not a valid JWA signature algorithm for this jwk with keyType ${ - keyJwk.keyType - }. Supported algorithms are ${keyJwk.supportedSignatureAlgorithms.join(', ')}` + `alg '${alg}' is not a valid JWA signature algorithm for this jwk with ${publicJwk.jwkTypehumanDescription}. Supported algorithms are ${publicJwk.supportedSignatureAlgorithms.join( + ', ' + )}` ) } @@ -64,12 +71,12 @@ export class JwsService { const base64Payload = TypedArrayEncoder.toBase64URL(payload) const base64UrlProtectedHeader = JsonEncoder.toBase64URL(this.buildProtected(options.protectedHeaderOptions)) - const signature = TypedArrayEncoder.toBase64URL( - await agentContext.wallet.sign({ - data: TypedArrayEncoder.fromString(`${base64UrlProtectedHeader}.${base64Payload}`), - key: options.key, - }) - ) + const signResult = await kms.sign({ + algorithm: alg, + data: TypedArrayEncoder.fromString(`${base64UrlProtectedHeader}.${base64Payload}`), + keyId: options.keyId, + }) + const signature = TypedArrayEncoder.toBase64URL(signResult.signature) return { base64Payload, @@ -80,11 +87,11 @@ export class JwsService { public async createJws( agentContext: AgentContext, - { payload, key, header, protectedHeaderOptions }: CreateJwsOptions + { payload, keyId, header, protectedHeaderOptions }: CreateJwsOptions ): Promise { const { base64UrlProtectedHeader, signature, base64Payload } = await this.createJwsBase(agentContext, { payload, - key, + keyId, protectedHeaderOptions, }) @@ -101,11 +108,11 @@ export class JwsService { * */ public async createJwsCompact( agentContext: AgentContext, - { payload, key, protectedHeaderOptions }: CreateCompactJwsOptions + { payload, keyId, protectedHeaderOptions }: CreateCompactJwsOptions ): Promise { const { base64Payload, base64UrlProtectedHeader, signature } = await this.createJwsBase(agentContext, { payload, - key, + keyId, protectedHeaderOptions, }) return `${base64UrlProtectedHeader}.${base64Payload}.${signature}` @@ -129,7 +136,9 @@ export class JwsService { if (expectedJwsSigner && !allowedJwsSignerMethods.includes(expectedJwsSigner.method)) { throw new CredoError( - `jwsSigner provided with method '${expectedJwsSigner.method}', but allowed jws signer methods are ${allowedJwsSignerMethods.join(', ')}.` + `jwsSigner provided with method '${ + expectedJwsSigner.method + }', but allowed jws signer methods are ${allowedJwsSignerMethods.join(', ')}.` ) } @@ -191,11 +200,9 @@ export class JwsService { trustedCertificates, }) - if (!jwsSigner.jwk.supportsSignatureAlgorithm(protectedJson.alg)) { + if (!jwsSigner.jwk.supportedSignatureAlgorithms.includes(protectedJson.alg as KnownJwaSignatureAlgorithm)) { throw new CredoError( - `alg '${protectedJson.alg}' is not a valid JWA signature algorithm for this jwk with keyType ${ - jwsSigner.jwk.keyType - }. Supported algorithms are ${jwsSigner.jwk.supportedSignatureAlgorithms.join(', ')}` + `alg '${protectedJson.alg}' is not a valid JWA signature algorithm for this jwk ${getJwkHumanDescription(jwsSigner.jwk.toJson())}. Supported algorithms are ${jwsSigner.jwk.supportedSignatureAlgorithms.join(', ')}` ) } @@ -203,10 +210,17 @@ export class JwsService { const signature = TypedArrayEncoder.fromBase64(jws.signature) jwsSigners.push(jwsSigner) - try { - const isValid = await agentContext.wallet.verify({ key: jwsSigner.jwk.key, data, signature }) + const kms = agentContext.dependencyManager.resolve(KeyManagementApi) - if (!isValid) { + try { + const { verified } = await kms.verify({ + key: jwsSigner.jwk.toJson(), + data, + signature, + algorithm: protectedJson.alg as KnownJwaSignatureAlgorithm, + }) + + if (!verified) { return { isValid: false, jwsSigners: [], @@ -215,8 +229,8 @@ export class JwsService { } } catch (error) { // WalletError probably means signature verification failed. Would be useful to add - // more specific error type in wallet.verify method - if (error instanceof WalletError) { + // more specific error type in kms.verify method + if (error instanceof KeyManagementError) { return { isValid: false, jwsSigners: [], @@ -235,7 +249,7 @@ export class JwsService { return { ...options, alg: options.alg, - jwk: options.jwk?.toJson(), + jwk: options.jwk instanceof PublicJwk ? options.jwk?.toJson() : options.jwk, kid: options.kid, } } @@ -278,8 +292,8 @@ export class JwsService { ): Promise { const { protectedHeader, resolveJwsSigner, jws, payload, allowedJwsSignerMethods } = options - const alg = protectedHeader.alg as JwaSignatureAlgorithm - if (!Object.values(JwaSignatureAlgorithm).includes(alg)) { + const alg = protectedHeader.alg + if (!isKnownJwaSignatureAlgorithm(alg)) { throw new CredoError(`Unsupported JWA signature algorithm '${protectedHeader.alg}'`) } @@ -291,10 +305,12 @@ export class JwsService { throw new CredoError('x5c header is not a valid JSON array of strings.') } - const certificate = X509Service.getLeafCertificate(agentContext, { certificateChain: protectedHeader.x5c }) + const certificate = X509Service.getLeafCertificate(agentContext, { + certificateChain: protectedHeader.x5c, + }) return { method: 'x5c', - jwk: getJwkFromKey(certificate.publicKey), + jwk: certificate.publicJwk, x5c: protectedHeader.x5c, } } @@ -303,7 +319,7 @@ export class JwsService { if (protectedHeader.jwk && allowedJwsSignerMethods.includes('jwk')) { if (!isJsonObject(protectedHeader.jwk)) throw new CredoError('JWK is not a valid JSON object.') - const protectedJwk = getJwkFromJson(protectedHeader.jwk as JwkJson) + const protectedJwk = PublicJwk.fromUnknown(protectedHeader.jwk) return { method: 'jwk', @@ -327,7 +343,9 @@ export class JwsService { if (!allowedJwsSignerMethods.includes(jwsSigner.method)) { throw new CredoError( - `resolveJwsSigner returned jws signer with method '${jwsSigner.method}', but allowed jws signer methods are ${allowedJwsSignerMethods.join(', ')}.` + `resolveJwsSigner returned jws signer with method '${ + jwsSigner.method + }', but allowed jws signer methods are ${allowedJwsSignerMethods.join(', ')}.` ) } @@ -341,8 +359,8 @@ export class JwsService { } export interface CreateJwsOptions { - key: Key payload: Buffer | JwtPayload + keyId: string header: Record protectedHeaderOptions: JwsProtectedHeaderOptions } @@ -385,7 +403,12 @@ export interface VerifyJwsOptions { export type JwsSignerResolver = (options: { jws: JwsDetachedFormat payload: string - protectedHeader: { alg: JwaSignatureAlgorithm; jwk?: string; kid?: string; [key: string]: unknown } + protectedHeader: { + alg: KnownJwaSignatureAlgorithm + jwk?: string + kid?: string + [key: string]: unknown + } }) => Promise | JwsSignerWithJwk export interface VerifyJwsResult { diff --git a/packages/core/src/crypto/JwsSigner.ts b/packages/core/src/crypto/JwsSigner.ts index 1a33cb15ee..9e19f93879 100644 --- a/packages/core/src/crypto/JwsSigner.ts +++ b/packages/core/src/crypto/JwsSigner.ts @@ -1,4 +1,4 @@ -import { Jwk } from './jose/jwk' +import { PublicJwk } from '../modules/kms' export interface JwsSignerDid { method: 'did' @@ -19,8 +19,8 @@ export interface JwsSignerX5c { export interface JwsSignerJwk { method: 'jwk' - jwk: Jwk + jwk: PublicJwk } export type JwsSigner = JwsSignerDid | JwsSignerX5c | JwsSignerJwk -export type JwsSignerWithJwk = JwsSigner & { jwk: Jwk } +export type JwsSignerWithJwk = JwsSigner & { jwk: PublicJwk } diff --git a/packages/core/src/crypto/JwsTypes.ts b/packages/core/src/crypto/JwsTypes.ts index 3339ace459..bd8c6cb7d0 100644 --- a/packages/core/src/crypto/JwsTypes.ts +++ b/packages/core/src/crypto/JwsTypes.ts @@ -1,12 +1,11 @@ -import type { JwaSignatureAlgorithm } from './jose/jwa' -import type { Jwk } from './jose/jwk' +import { Jwk, KnownJwaSignatureAlgorithm, PublicJwk } from '../modules/kms' export type Kid = string export interface JwsProtectedHeaderOptions { - alg: JwaSignatureAlgorithm | string + alg: KnownJwaSignatureAlgorithm kid?: Kid - jwk?: Jwk + jwk?: PublicJwk | Jwk x5c?: string[] [key: string]: unknown } diff --git a/packages/core/src/crypto/Key.ts b/packages/core/src/crypto/Key.ts deleted file mode 100644 index 1661c72138..0000000000 --- a/packages/core/src/crypto/Key.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { KeyType } from './KeyType' - -import { compressPublicKeyIfPossible, decompressPublicKeyIfPossible } from 'ec-compression' - -import { MultiBaseEncoder, TypedArrayEncoder, VarintEncoder } from '../utils' - -import { isEncryptionSupportedForKeyType, isSigningSupportedForKeyType } from './keyUtils' -import { getKeyTypeByMultiCodecPrefix, getMultiCodecPrefixByKeyType } from './multiCodecKey' - -export class Key { - public readonly publicKey: Uint8Array - public readonly keyType: KeyType - - public constructor(publicKey: Uint8Array, keyType: KeyType) { - this.publicKey = decompressPublicKeyIfPossible(publicKey, keyType) - this.keyType = keyType - } - - public get compressedPublicKey() { - return compressPublicKeyIfPossible(this.publicKey, this.keyType) - } - - public static fromPublicKey(publicKey: Uint8Array, keyType: KeyType) { - return new Key(publicKey, keyType) - } - - public static fromPublicKeyBase58(publicKey: string, keyType: KeyType) { - const publicKeyBytes = Uint8Array.from(TypedArrayEncoder.fromBase58(publicKey)) - - return Key.fromPublicKey(publicKeyBytes, keyType) - } - - public static fromFingerprint(fingerprint: string) { - const { data } = MultiBaseEncoder.decode(fingerprint) - const [code, byteLength] = VarintEncoder.decode(data) - - const publicKey = data.slice(byteLength) - const keyType = getKeyTypeByMultiCodecPrefix(code) - - return new Key(publicKey, keyType) - } - - public get prefixedPublicKey() { - const multiCodecPrefix = getMultiCodecPrefixByKeyType(this.keyType) - - // Create Buffer with length of the prefix bytes, then use varint to fill the prefix bytes - const prefixBytes = VarintEncoder.encode(multiCodecPrefix) - - // Combine prefix with public key - // Multicodec requires compressable keys to be compressed - return new Uint8Array([...prefixBytes, ...this.compressedPublicKey]) - } - - public get fingerprint() { - return `z${TypedArrayEncoder.toBase58(this.prefixedPublicKey)}` - } - - public get publicKeyBase58() { - return TypedArrayEncoder.toBase58(this.publicKey) - } - - public get supportsEncrypting() { - return isEncryptionSupportedForKeyType(this.keyType) - } - - public get supportsSigning() { - return isSigningSupportedForKeyType(this.keyType) - } - - // We return an object structure based on the key, so that when this object is - // serialized to JSON it will be nicely formatted instead of the bytes printed - private toJSON() { - return { - keyType: this.keyType, - publicKeyBase58: this.publicKeyBase58, - } - } -} diff --git a/packages/core/src/crypto/KeyBackend.ts b/packages/core/src/crypto/KeyBackend.ts deleted file mode 100644 index 76b13ec540..0000000000 --- a/packages/core/src/crypto/KeyBackend.ts +++ /dev/null @@ -1,20 +0,0 @@ -export enum KeyBackend { - /** - * - * Generate a key using common software-based implementations. - * Key material will be instantiated in memory. - * - * Supported for almost all, if not all, key types. - * - */ - Software = 'Software', - - /** - * - * Generate a key within the secure element of the device. - * - * For now, this is only supported using Aries Askar in iOS or Android for `KeyType.P256`. - * - */ - SecureElement = 'SecureElement', -} diff --git a/packages/core/src/crypto/KeyType.ts b/packages/core/src/crypto/KeyType.ts deleted file mode 100644 index cb85ab608d..0000000000 --- a/packages/core/src/crypto/KeyType.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum KeyType { - Ed25519 = 'ed25519', - Bls12381g1g2 = 'bls12381g1g2', - Bls12381g1 = 'bls12381g1', - Bls12381g2 = 'bls12381g2', - X25519 = 'x25519', - P256 = 'p256', - P384 = 'p384', - P521 = 'p521', - K256 = 'k256', -} diff --git a/packages/core/src/crypto/WalletKeyPair.ts b/packages/core/src/crypto/KmsKeyPair.ts similarity index 56% rename from packages/core/src/crypto/WalletKeyPair.ts rename to packages/core/src/crypto/KmsKeyPair.ts index e5c83721da..c4abb27093 100644 --- a/packages/core/src/crypto/WalletKeyPair.ts +++ b/packages/core/src/crypto/KmsKeyPair.ts @@ -1,32 +1,29 @@ import type { LdKeyPairOptions } from '../modules/vc/data-integrity/models/LdKeyPair' -import type { Wallet } from '../wallet' -import type { Key } from './Key' +import { AgentContext } from '../agent' +import { CredoError } from '../error' import { VerificationMethod } from '../modules/dids' -import { getKeyFromVerificationMethod } from '../modules/dids/domain/key-type/keyDidMapping' +import { getPublicJwkFromVerificationMethod } from '../modules/dids/domain/key-type/keyDidMapping' +import { KeyManagementApi, PublicJwk } from '../modules/kms' import { LdKeyPair } from '../modules/vc/data-integrity/models/LdKeyPair' import { JsonTransformer, MessageValidator } from '../utils' import { Buffer } from '../utils/buffer' -interface WalletKeyPairOptions extends LdKeyPairOptions { - wallet: Wallet - key: Key +interface KmsKeyPairOptions extends LdKeyPairOptions { + publicJwk: PublicJwk } -export function createWalletKeyPairClass(wallet: Wallet) { - return class WalletKeyPair extends LdKeyPair { - public wallet: Wallet - public key: Key - public type: string +export function createKmsKeyPairClass(agentContext: AgentContext) { + return class KmsKeyPair extends LdKeyPair { + public publicJwk: PublicJwk + public type = 'KmsKeyPair' - public constructor(options: WalletKeyPairOptions) { + public constructor(options: KmsKeyPairOptions) { super(options) - this.wallet = options.wallet - this.key = options.key - this.type = options.key.keyType + this.publicJwk = options.publicJwk } - public static async generate(): Promise { + public static async generate(): Promise { throw new Error('Not implemented') } @@ -38,16 +35,15 @@ export function createWalletKeyPairClass(wallet: Wallet) { throw new Error('Method not implemented.') } - public static async from(verificationMethod: VerificationMethod): Promise { + public static async from(verificationMethod: VerificationMethod): Promise { const vMethod = JsonTransformer.fromJSON(verificationMethod, VerificationMethod) MessageValidator.validateSync(vMethod) - const key = getKeyFromVerificationMethod(vMethod) + const publicJwk = getPublicJwkFromVerificationMethod(vMethod) - return new WalletKeyPair({ + return new KmsKeyPair({ id: vMethod.id, controller: vMethod.controller, - wallet: wallet, - key: key, + publicJwk, }) } @@ -57,23 +53,18 @@ export function createWalletKeyPairClass(wallet: Wallet) { public signer(): { sign: (data: { data: Uint8Array | Uint8Array[] }) => Promise } { // wrap function for conversion const wrappedSign = async (data: { data: Uint8Array | Uint8Array[] }): Promise => { - let converted: Buffer | Buffer[] = [] - - // convert uint8array to buffer if (Array.isArray(data.data)) { - converted = data.data.map((d) => Buffer.from(d)) - } else { - converted = Buffer.from(data.data) + throw new CredoError('Signing array of data entries is not supported') } + const kms = agentContext.dependencyManager.resolve(KeyManagementApi) - // sign - const result = await wallet.sign({ - data: converted, - key: this.key, + const result = await kms.sign({ + data: data.data, + keyId: this.publicJwk.keyId, + algorithm: this.publicJwk.signatureAlgorithm, }) - // convert result buffer to uint8array - return Uint8Array.from(result) + return result.signature } return { @@ -91,21 +82,19 @@ export function createWalletKeyPairClass(wallet: Wallet) { data: Uint8Array | Uint8Array[] signature: Uint8Array }): Promise => { - let converted: Buffer | Buffer[] = [] - - // convert uint8array to buffer if (Array.isArray(data.data)) { - converted = data.data.map((d) => Buffer.from(d)) - } else { - converted = Buffer.from(data.data) + throw new CredoError('Verifying array of data entries is not supported') } + const kms = agentContext.dependencyManager.resolve(KeyManagementApi) - // verify - return wallet.verify({ - data: converted, + const { verified } = await kms.verify({ + data: data.data, signature: Buffer.from(data.signature), - key: this.key, + key: this.publicJwk.toJson(), + algorithm: this.publicJwk.signatureAlgorithm, }) + + return verified } return { verify: wrappedVerify.bind(this), @@ -113,7 +102,13 @@ export function createWalletKeyPairClass(wallet: Wallet) { } public get publicKeyBuffer(): Uint8Array { - return new Uint8Array(this.key.publicKey) + const publicKey = this.publicJwk.publicKey + + if (publicKey.kty === 'RSA') { + throw new CredoError(`kty 'RSA' not supported for publicKeyBuffer`) + } + + return publicKey.publicKey } } } diff --git a/packages/core/src/crypto/__tests__/JwsService.test.ts b/packages/core/src/crypto/__tests__/JwsService.test.ts index 599caa5c29..e3d1e7720c 100644 --- a/packages/core/src/crypto/__tests__/JwsService.test.ts +++ b/packages/core/src/crypto/__tests__/JwsService.test.ts @@ -1,46 +1,78 @@ -import type { Key, Wallet, X509Certificate } from '@credo-ts/core' +import type { X509Certificate } from '@credo-ts/core' import type { AgentContext } from '../../agent' -import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' -import { getAgentConfig, getAgentContext } from '../../../tests/helpers' +import { getAgentConfig, getAgentContext, getAskarStoreConfig } from '../../../tests/helpers' import { DidKey } from '../../modules/dids' import { JsonEncoder, TypedArrayEncoder } from '../../utils' import { JwsService } from '../JwsService' -import { KeyType } from '../KeyType' -import { JwaSignatureAlgorithm } from '../jose/jwa' -import { getJwkFromKey } from '../jose/jwk' import * as didJwsz6Mkf from './__fixtures__/didJwsz6Mkf' import * as didJwsz6Mkv from './__fixtures__/didJwsz6Mkv' import * as didJwszDnaey from './__fixtures__/didJwszDnaey' import { CredoError, X509ModuleConfig, X509Service } from '@credo-ts/core' +import { askar } from '@openwallet-foundation/askar-nodejs' +import { AksarKeyManagementService, AskarModuleConfig, transformPrivateKeyToPrivateJwk } from '../../../../askar/src' +import { AskarStoreManager } from '../../../../askar/src/AskarStoreManager' +import { NodeFileSystem } from '../../../../node/src/NodeFileSystem' +import { + Ed25519PublicJwk, + KeyManagementApi, + KnownJwaSignatureAlgorithms, + P256PublicJwk, + PublicJwk, +} from '../../modules/kms' + +// NOTE: we use askar for the KMS in this test since the signatures with the +// node KMS are different, but it does correctly verify. It's probably something +// to do with the encoding of the signature? describe('JwsService', () => { - let wallet: Wallet let agentContext: AgentContext let jwsService: JwsService - let didJwsz6MkfKey: Key + let didJwsz6MkfKey: PublicJwk let didJwsz6MkfCertificate: X509Certificate - let didJwsz6MkvKey: Key + let didJwsz6MkvKey: PublicJwk let didJwsz6MkvCertificate: X509Certificate - let didJwszDnaeyKey: Key + let didJwszDnaeyKey: PublicJwk beforeAll(async () => { - const config = getAgentConfig('JwsService') - wallet = new InMemoryWallet() agentContext = getAgentContext({ - wallet, - registerInstances: [[X509ModuleConfig, new X509ModuleConfig()]], + registerInstances: [ + [X509ModuleConfig, new X509ModuleConfig()], + + [ + AskarStoreManager, + new AskarStoreManager( + new NodeFileSystem(), + new AskarModuleConfig({ + askar, + store: getAskarStoreConfig('JwsService'), + }) + ), + ], + ], + kmsBackends: [new AksarKeyManagementService()], + agentConfig: getAgentConfig('JwsService'), }) - await wallet.createAndOpen(config.walletConfig) + const kms = agentContext.resolve(KeyManagementApi) jwsService = new JwsService() - didJwsz6MkfKey = await wallet.createKey({ - privateKey: TypedArrayEncoder.fromString(didJwsz6Mkf.SEED), - keyType: KeyType.Ed25519, - }) + didJwsz6MkfKey = PublicJwk.fromPublicJwk( + ( + await kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString(didJwsz6Mkf.SEED), + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + }).privateJwk, + }) + ).publicJwk + ) + didJwsz6MkfCertificate = await X509Service.createCertificate(agentContext, { authorityKey: didJwsz6MkfKey, issuer: { @@ -48,10 +80,20 @@ describe('JwsService', () => { }, }) - didJwsz6MkvKey = await wallet.createKey({ - privateKey: TypedArrayEncoder.fromString(didJwsz6Mkv.SEED), - keyType: KeyType.Ed25519, - }) + didJwsz6MkvKey = PublicJwk.fromPublicJwk( + ( + await kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString(didJwsz6Mkv.SEED), + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + }).privateJwk, + }) + ).publicJwk + ) + didJwsz6MkvCertificate = await X509Service.createCertificate(agentContext, { authorityKey: didJwsz6MkvKey, issuer: { @@ -59,14 +101,19 @@ describe('JwsService', () => { }, }) - didJwszDnaeyKey = await wallet.createKey({ - privateKey: TypedArrayEncoder.fromString(didJwszDnaey.SEED), - keyType: KeyType.P256, - }) - }) - - afterAll(async () => { - await wallet.delete() + didJwszDnaeyKey = PublicJwk.fromPublicJwk( + ( + await kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString(didJwszDnaey.SEED), + type: { + kty: 'EC', + crv: 'P-256', + }, + }).privateJwk, + }) + ).publicJwk + ) }) it('creates a jws for the payload using Ed25519 key', async () => { @@ -75,11 +122,11 @@ describe('JwsService', () => { const jws = await jwsService.createJws(agentContext, { payload, - key: didJwsz6MkfKey, + keyId: didJwsz6MkfKey.keyId, header: { kid }, protectedHeaderOptions: { - alg: JwaSignatureAlgorithm.EdDSA, - jwk: getJwkFromKey(didJwsz6MkfKey), + alg: KnownJwaSignatureAlgorithms.EdDSA, + jwk: didJwsz6MkfKey.toJson({ includeKid: false }), }, }) @@ -92,11 +139,11 @@ describe('JwsService', () => { const jws = await jwsService.createJws(agentContext, { payload, - key: didJwszDnaeyKey, + keyId: didJwszDnaeyKey.keyId, header: { kid }, protectedHeaderOptions: { - alg: JwaSignatureAlgorithm.ES256, - jwk: getJwkFromKey(didJwszDnaeyKey), + alg: KnownJwaSignatureAlgorithms.ES256, + jwk: didJwszDnaeyKey.toJson({ includeKid: false }), }, }) @@ -108,10 +155,10 @@ describe('JwsService', () => { const jws = await jwsService.createJwsCompact(agentContext, { payload, - key: didJwsz6MkfKey, + keyId: didJwsz6MkfKey.keyId, protectedHeaderOptions: { - alg: JwaSignatureAlgorithm.EdDSA, - jwk: getJwkFromKey(didJwsz6MkfKey), + alg: KnownJwaSignatureAlgorithms.EdDSA, + jwk: didJwsz6MkfKey.toJson({ includeKid: false }), }, }) @@ -125,10 +172,10 @@ describe('JwsService', () => { const signed1 = await jwsService.createJwsCompact(agentContext, { payload, - key: didJwsz6MkfKey, + keyId: didJwsz6MkfKey.keyId, protectedHeaderOptions: { - alg: JwaSignatureAlgorithm.EdDSA, - jwk: getJwkFromKey(didJwsz6MkfKey), + alg: KnownJwaSignatureAlgorithms.EdDSA, + jwk: didJwsz6MkfKey, kid: 'something', }, }) @@ -139,9 +186,9 @@ describe('JwsService', () => { const signed2 = await jwsService.createJwsCompact(agentContext, { payload, - key: didJwsz6MkfKey, + keyId: didJwsz6MkfKey.keyId, protectedHeaderOptions: { - alg: JwaSignatureAlgorithm.EdDSA, + alg: KnownJwaSignatureAlgorithms.EdDSA, x5c: [didJwsz6MkfCertificate.toString('base64url')], kid: 'something', }, @@ -161,7 +208,7 @@ describe('JwsService', () => { allowedJwsSignerMethods: ['did'], jwsSigner: { didUrl: `did:key:${didJwsz6MkfKey.fingerprint}#${didJwsz6MkfKey.fingerprint}`, - jwk: getJwkFromKey(didJwsz6MkfKey), + jwk: didJwsz6MkfKey, method: 'did', }, }) @@ -171,7 +218,7 @@ describe('JwsService', () => { { method: 'did', didUrl: `did:key:${didJwsz6MkfKey.fingerprint}#${didJwsz6MkfKey.fingerprint}`, - jwk: getJwkFromKey(didJwsz6MkfKey), + jwk: didJwsz6MkfKey, }, ]) }) @@ -181,7 +228,7 @@ describe('JwsService', () => { jws: `${didJwsz6Mkf.JWS_JSON.protected}.${didJwsz6Mkf.JWS_JSON.payload}.${didJwsz6Mkf.JWS_JSON.signature}`, jwsSigner: { didUrl: `did:key:${didJwsz6MkfKey.fingerprint}#${didJwsz6MkfKey.fingerprint}`, - jwk: getJwkFromKey(didJwsz6MkfKey), + jwk: didJwsz6MkfKey, method: 'did', }, }) @@ -191,7 +238,7 @@ describe('JwsService', () => { { method: 'did', didUrl: `did:key:${didJwsz6MkfKey.fingerprint}#${didJwsz6MkfKey.fingerprint}`, - jwk: getJwkFromKey(didJwsz6MkfKey), + jwk: didJwsz6MkfKey, }, ]) }) @@ -204,7 +251,7 @@ describe('JwsService', () => { if (jws.header.kid === `did:key:${didJwsz6MkfKey.fingerprint}`) { return { method: 'did', - jwk: getJwkFromKey(didJwsz6MkfKey), + jwk: didJwsz6MkfKey, didUrl: `did:key:${didJwsz6MkfKey.fingerprint}#${didJwsz6MkfKey.fingerprint}`, } } @@ -212,7 +259,7 @@ describe('JwsService', () => { if (jws.header.kid === `did:key:${didJwsz6MkvKey.fingerprint}`) { return { method: 'did', - jwk: getJwkFromKey(didJwsz6MkvKey), + jwk: didJwsz6MkvKey, didUrl: `did:key:${didJwsz6MkvKey.fingerprint}#${didJwsz6MkvKey.fingerprint}`, } } @@ -226,12 +273,12 @@ describe('JwsService', () => { { method: 'did', didUrl: `did:key:${didJwsz6MkfKey.fingerprint}#${didJwsz6MkfKey.fingerprint}`, - jwk: getJwkFromKey(didJwsz6MkfKey), + jwk: didJwsz6MkfKey, }, { method: 'did', didUrl: `did:key:${didJwsz6MkvKey.fingerprint}#${didJwsz6MkvKey.fingerprint}`, - jwk: getJwkFromKey(didJwsz6MkvKey), + jwk: didJwsz6MkvKey, }, ]) }) @@ -263,7 +310,7 @@ describe('JwsService', () => { jws: { signatures: [], payload: '' }, jwsSigner: { method: 'jwk', - jwk: getJwkFromKey(didJwsz6MkfKey), + jwk: didJwsz6MkfKey, }, }) ).rejects.toThrow("jwsSigner provided with method 'jwk', but allowed jws signer methods are x5c.") @@ -274,7 +321,7 @@ describe('JwsService', () => { jws: didJwsz6Mkf.JWS_JSON, jwsSigner: { method: 'jwk', - jwk: getJwkFromKey(didJwsz6MkvKey), + jwk: didJwsz6MkvKey, }, }) @@ -287,9 +334,9 @@ describe('JwsService', () => { const signed = await jwsService.createJwsCompact(agentContext, { payload, - key: didJwsz6MkfKey, + keyId: didJwsz6MkfKey.keyId, protectedHeaderOptions: { - alg: JwaSignatureAlgorithm.EdDSA, + alg: KnownJwaSignatureAlgorithms.EdDSA, x5c: [didJwsz6MkfCertificate.toString('base64url')], }, }) @@ -309,7 +356,7 @@ describe('JwsService', () => { jwsSigner: { method: 'x5c', x5c: ['invalid'], - jwk: getJwkFromKey(didJwsz6MkfKey), + jwk: didJwsz6MkfKey, }, trustedCertificates: [didJwsz6MkfCertificate.toString('base64url')], }) @@ -323,7 +370,7 @@ describe('JwsService', () => { jwsSigner: { method: 'x5c', x5c: [didJwsz6MkfCertificate.toString('base64url')], - jwk: getJwkFromKey(didJwsz6MkfKey), + jwk: didJwsz6MkfKey, }, trustedCertificates: [didJwsz6MkvCertificate.toString('base64url')], }) diff --git a/packages/core/src/crypto/index.ts b/packages/core/src/crypto/index.ts index 55efa96214..309e60efd5 100644 --- a/packages/core/src/crypto/index.ts +++ b/packages/core/src/crypto/index.ts @@ -2,15 +2,8 @@ export { JwsService } from './JwsService' export { JwsDetachedFormat, JwsFlattenedDetachedFormat, JwsGeneralFormat, JwsProtectedHeaderOptions } from './JwsTypes' export { JwsSigner, JwsSignerDid, JwsSignerJwk, JwsSignerWithJwk, JwsSignerX5c } from './JwsSigner' -export * from './keyUtils' - -export { KeyBackend } from './KeyBackend' -export { KeyType } from './KeyType' -export { Key } from './Key' export * from './jose' -export * from './signing-provider' - export * from './webcrypto' export * from './hashes' diff --git a/packages/core/src/crypto/jose/index.ts b/packages/core/src/crypto/jose/index.ts index 0dd0dedecf..6700c430ce 100644 --- a/packages/core/src/crypto/jose/index.ts +++ b/packages/core/src/crypto/jose/index.ts @@ -1,3 +1 @@ -export * from './jwa' -export * from './jwk' export * from './jwt' diff --git a/packages/core/src/crypto/jose/jwa/alg.ts b/packages/core/src/crypto/jose/jwa/alg.ts deleted file mode 100644 index 07e32d98da..0000000000 --- a/packages/core/src/crypto/jose/jwa/alg.ts +++ /dev/null @@ -1,39 +0,0 @@ -export enum JwaSignatureAlgorithm { - HS256 = 'HS256', - HS384 = 'HS384', - HS512 = 'HS512', - RS256 = 'RS256', - RS384 = 'RS384', - RS512 = 'RS512', - ES256 = 'ES256', - ES384 = 'ES384', - ES512 = 'ES512', - PS256 = 'PS256', - PS384 = 'PS384', - PS512 = 'PS512', - EdDSA = 'EdDSA', - ES256K = 'ES256K', - None = 'none', -} - -export enum JwaEncryptionAlgorithm { - RSA15 = 'RSA1_5', - RSAOAEP = 'RSA-OAEP', - RSAOAEP256 = 'RSA-OAEP-256', - A128KW = 'A128KW', - A192KW = 'A192KW', - A256KW = 'A256KW', - Dir = 'dir', - ECDHES = 'ECDH-ES', - ECDHESA128KW = 'ECDH-ES+A128KW', - ECDHESA192KW = 'ECDH-ES+A192KW', - ECDHESA256KW = 'ECDH-ES+A256KW', - A128GCMKW = 'A128GCMKW', - A192GCMKW = 'A192GCMKW', - A256GCMKW = 'A256GCMKW', - PBES2HS256A128KW = 'PBES2-HS256+A128KW', - PBES2HS384A192KW = 'PBES2-HS384+A192KW', - PBES2HS512A256KW = 'PBES2-HS512+A256KW', -} - -export type JwaAlgorithm = JwaSignatureAlgorithm | JwaEncryptionAlgorithm diff --git a/packages/core/src/crypto/jose/jwa/crv.ts b/packages/core/src/crypto/jose/jwa/crv.ts deleted file mode 100644 index d663c2ebb4..0000000000 --- a/packages/core/src/crypto/jose/jwa/crv.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum JwaCurve { - P256 = 'P-256', - P384 = 'P-384', - P521 = 'P-521', - Ed25519 = 'Ed25519', - X25519 = 'X25519', - Secp256k1 = 'secp256k1', -} diff --git a/packages/core/src/crypto/jose/jwa/index.ts b/packages/core/src/crypto/jose/jwa/index.ts deleted file mode 100644 index 9aa115a084..0000000000 --- a/packages/core/src/crypto/jose/jwa/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { JwaAlgorithm, JwaEncryptionAlgorithm, JwaSignatureAlgorithm } from './alg' -export { JwaKeyType } from './kty' -export { JwaCurve } from './crv' diff --git a/packages/core/src/crypto/jose/jwa/kty.ts b/packages/core/src/crypto/jose/jwa/kty.ts deleted file mode 100644 index 0601fb7b02..0000000000 --- a/packages/core/src/crypto/jose/jwa/kty.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum JwaKeyType { - EC = 'EC', - RSA = 'RSA', - oct = 'oct', - OKP = 'OKP', -} diff --git a/packages/core/src/crypto/jose/jwk/Ed25519Jwk.ts b/packages/core/src/crypto/jose/jwk/Ed25519Jwk.ts deleted file mode 100644 index f77eb87bac..0000000000 --- a/packages/core/src/crypto/jose/jwk/Ed25519Jwk.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { JwaEncryptionAlgorithm } from '../jwa/alg' -import type { JwkJson } from './Jwk' - -import { TypedArrayEncoder } from '../../../utils' -import { KeyType } from '../../KeyType' -import { JwaCurve, JwaKeyType } from '../jwa' -import { JwaSignatureAlgorithm } from '../jwa/alg' - -import { Jwk } from './Jwk' -import { hasCrv, hasKty, hasValidUse, hasX } from './validate' - -export class Ed25519Jwk extends Jwk { - public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = [] - public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [JwaSignatureAlgorithm.EdDSA] - public static readonly keyType = KeyType.Ed25519 - - private readonly _x: Uint8Array - - public constructor({ x }: { x: string | Uint8Array }) { - super() - - this._x = typeof x === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(x)) : x - } - - public get x() { - return TypedArrayEncoder.toBase64URL(this._x) - } - - public get kty() { - return JwaKeyType.OKP as const - } - - public get crv() { - return JwaCurve.Ed25519 as const - } - - public get publicKey() { - return this._x - } - - public get keyType() { - return Ed25519Jwk.keyType - } - - public get supportedEncryptionAlgorithms() { - return Ed25519Jwk.supportedEncryptionAlgorithms - } - - public get supportedSignatureAlgorithms() { - return Ed25519Jwk.supportedSignatureAlgorithms - } - - public toJson() { - return { - ...super.toJson(), - crv: this.crv, - x: this.x, - } as Ed25519JwkJson - } - - public static fromJson(jwkJson: JwkJson) { - if (!isValidEd25519JwkPublicKey(jwkJson)) { - throw new Error("Invalid 'Ed25519' JWK.") - } - - return new Ed25519Jwk({ - x: jwkJson.x, - }) - } - - public static fromPublicKey(publicKey: Uint8Array) { - return new Ed25519Jwk({ x: publicKey }) - } -} - -export interface Ed25519JwkJson extends JwkJson { - kty: JwaKeyType.OKP - crv: JwaCurve.Ed25519 - x: string - use?: 'sig' -} - -function isValidEd25519JwkPublicKey(jwk: JwkJson): jwk is Ed25519JwkJson { - return ( - hasKty(jwk, JwaKeyType.OKP) && - hasCrv(jwk, JwaCurve.Ed25519) && - hasX(jwk) && - hasValidUse(jwk, { - supportsEncrypting: false, - supportsSigning: true, - }) - ) -} diff --git a/packages/core/src/crypto/jose/jwk/Jwk.ts b/packages/core/src/crypto/jose/jwk/Jwk.ts deleted file mode 100644 index 27a23d9c0a..0000000000 --- a/packages/core/src/crypto/jose/jwk/Jwk.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { KeyType } from '../../KeyType' -import type { JwaEncryptionAlgorithm, JwaKeyType, JwaSignatureAlgorithm } from '../jwa' - -import { Key } from '../../Key' - -export interface JwkJson { - kty: string - use?: string - [key: string]: unknown -} - -export abstract class Jwk { - public abstract publicKey: Uint8Array - public abstract supportedSignatureAlgorithms: JwaSignatureAlgorithm[] - public abstract supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] - - /** - * keyType as used by the rest of the framework, can be used in the - * `Wallet`, `Key` and other classes. - */ - public abstract keyType: KeyType - - /** - * key type as defined in [JWA Specification](https://tools.ietf.org/html/rfc7518#section-6.1) - */ - public abstract kty: JwaKeyType - public use?: string - - public toJson(): JwkJson { - return { use: this.use, kty: this.kty } - } - - public get key() { - return new Key(this.publicKey, this.keyType) - } - - public supportsSignatureAlgorithm(algorithm: JwaSignatureAlgorithm | string) { - return this.supportedSignatureAlgorithms.includes(algorithm as JwaSignatureAlgorithm) - } - - public supportsEncryptionAlgorithm(algorithm: JwaEncryptionAlgorithm | string) { - return this.supportedEncryptionAlgorithms.includes(algorithm as JwaEncryptionAlgorithm) - } -} diff --git a/packages/core/src/crypto/jose/jwk/K256Jwk.ts b/packages/core/src/crypto/jose/jwk/K256Jwk.ts deleted file mode 100644 index 75247c6acb..0000000000 --- a/packages/core/src/crypto/jose/jwk/K256Jwk.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { JwaEncryptionAlgorithm } from '../jwa/alg' -import type { JwkJson } from './Jwk' - -import { - AffinePoint, - Secp256k1, - isValidCompressedPublicKeyFormat, - isValidDecompressedPublicKeyFormat, -} from 'ec-compression' - -import { CredoError } from '../../../error' -import { TypedArrayEncoder } from '../../../utils' -import { KeyType } from '../../KeyType' -import { JwaCurve, JwaKeyType } from '../jwa' -import { JwaSignatureAlgorithm } from '../jwa/alg' - -import { Jwk } from './Jwk' -import { hasCrv, hasKty, hasValidUse, hasX, hasY } from './validate' - -export class K256Jwk extends Jwk { - public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = [] - public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [JwaSignatureAlgorithm.ES256K] - public static readonly keyType = KeyType.K256 - - private readonly affinePoint: AffinePoint - - public constructor({ x, y }: { x: string | Uint8Array; y: string | Uint8Array }) { - super() - - const xAsBytes = typeof x === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(x)) : x - const yAsBytes = typeof y === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(y)) : y - - this.affinePoint = new AffinePoint(xAsBytes, yAsBytes) - } - - public get kty() { - return JwaKeyType.EC as const - } - - public get crv() { - return JwaCurve.Secp256k1 as const - } - - public get x() { - return TypedArrayEncoder.toBase64URL(this.affinePoint.xBytes) - } - - public get y() { - return TypedArrayEncoder.toBase64URL(this.affinePoint.yBytes) - } - - /** - * Returns the uncompressed public key of the P-256 JWK. - */ - public get publicKey() { - return this.affinePoint.decompressedForm - } - - /** - * Returns the compressed public key of the K-256 JWK. - */ - public get publicKeyCompressed() { - return this.affinePoint.compressedForm - } - - public get keyType() { - return K256Jwk.keyType - } - - public get supportedEncryptionAlgorithms() { - return K256Jwk.supportedEncryptionAlgorithms - } - - public get supportedSignatureAlgorithms() { - return K256Jwk.supportedSignatureAlgorithms - } - - public toJson() { - return { - ...super.toJson(), - crv: this.crv, - x: this.x, - y: this.y, - } as K256JwkJson - } - - public static fromJson(jwkJson: JwkJson) { - if (!isValidK256JwkPublicKey(jwkJson)) { - throw new Error("Invalid 'K-256' JWK.") - } - - return new K256Jwk({ - x: jwkJson.x, - y: jwkJson.y, - }) - } - - public static fromPublicKey(publicKey: Uint8Array) { - if (isValidCompressedPublicKeyFormat(publicKey, Secp256k1)) { - const affinePoint = AffinePoint.fromCompressedPoint(publicKey, Secp256k1) - return new K256Jwk({ x: affinePoint.xBytes, y: affinePoint.yBytes }) - } - - if (isValidDecompressedPublicKeyFormat(publicKey, Secp256k1)) { - const affinePoint = AffinePoint.fromDecompressedPoint(publicKey, Secp256k1) - return new K256Jwk({ x: affinePoint.xBytes, y: affinePoint.yBytes }) - } - - throw new CredoError( - `${K256Jwk.keyType} public key is neither a valid compressed or uncompressed key. Key prefix '${publicKey[0]}', key length '${publicKey.length}'` - ) - } -} - -export interface K256JwkJson extends JwkJson { - kty: JwaKeyType.EC - crv: JwaCurve.Secp256k1 - x: string - y: string - use?: 'sig' | 'enc' -} - -export function isValidK256JwkPublicKey(jwk: JwkJson): jwk is K256JwkJson { - return ( - hasKty(jwk, JwaKeyType.EC) && - hasCrv(jwk, JwaCurve.Secp256k1) && - hasX(jwk) && - hasY(jwk) && - hasValidUse(jwk, { - supportsEncrypting: true, - supportsSigning: true, - }) - ) -} diff --git a/packages/core/src/crypto/jose/jwk/P256Jwk.ts b/packages/core/src/crypto/jose/jwk/P256Jwk.ts deleted file mode 100644 index ec04d325ab..0000000000 --- a/packages/core/src/crypto/jose/jwk/P256Jwk.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { JwaEncryptionAlgorithm } from '../jwa/alg' -import type { JwkJson } from './Jwk' - -import { - AffinePoint, - Secp256r1, - isValidCompressedPublicKeyFormat, - isValidDecompressedPublicKeyFormat, -} from 'ec-compression' - -import { CredoError } from '../../../error' -import { TypedArrayEncoder } from '../../../utils' -import { KeyType } from '../../KeyType' -import { JwaCurve, JwaKeyType } from '../jwa' -import { JwaSignatureAlgorithm } from '../jwa/alg' - -import { Jwk } from './Jwk' -import { hasCrv, hasKty, hasValidUse, hasX, hasY } from './validate' - -export class P256Jwk extends Jwk { - public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = [] - public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [JwaSignatureAlgorithm.ES256] - public static readonly keyType = KeyType.P256 - - private readonly affinePoint: AffinePoint - - public constructor({ x, y }: { x: string | Uint8Array; y: string | Uint8Array }) { - super() - - const xAsBytes = typeof x === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(x)) : x - const yAsBytes = typeof y === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(y)) : y - - this.affinePoint = new AffinePoint(xAsBytes, yAsBytes) - } - - public get kty() { - return JwaKeyType.EC as const - } - - public get crv() { - return JwaCurve.P256 as const - } - - public get x() { - return TypedArrayEncoder.toBase64URL(this.affinePoint.xBytes) - } - - public get y() { - return TypedArrayEncoder.toBase64URL(this.affinePoint.yBytes) - } - - /** - * Returns the uncompressed public key of the P-256 JWK. - */ - public get publicKey() { - return this.affinePoint.decompressedForm - } - - /** - * Returns the compressed public key of the P-256 JWK. - */ - public get publicKeyCompressed() { - return this.affinePoint.compressedForm - } - - public get keyType() { - return P256Jwk.keyType - } - - public get supportedEncryptionAlgorithms() { - return P256Jwk.supportedEncryptionAlgorithms - } - - public get supportedSignatureAlgorithms() { - return P256Jwk.supportedSignatureAlgorithms - } - - public toJson() { - return { - ...super.toJson(), - crv: this.crv, - x: this.x, - y: this.y, - } as P256JwkJson - } - - public static fromJson(jwkJson: JwkJson) { - if (!isValidP256JwkPublicKey(jwkJson)) { - throw new Error("Invalid 'P-256' JWK.") - } - - return new P256Jwk({ - x: jwkJson.x, - y: jwkJson.y, - }) - } - - public static fromPublicKey(publicKey: Uint8Array) { - if (isValidCompressedPublicKeyFormat(publicKey, Secp256r1)) { - const affinePoint = AffinePoint.fromCompressedPoint(publicKey, Secp256r1) - return new P256Jwk({ x: affinePoint.xBytes, y: affinePoint.yBytes }) - } - - if (isValidDecompressedPublicKeyFormat(publicKey, Secp256r1)) { - const affinePoint = AffinePoint.fromDecompressedPoint(publicKey, Secp256r1) - return new P256Jwk({ x: affinePoint.xBytes, y: affinePoint.yBytes }) - } - - throw new CredoError( - `${P256Jwk.keyType} public key is neither a valid compressed or uncompressed key. Key prefix '${publicKey[0]}', key length '${publicKey.length}'` - ) - } -} - -export interface P256JwkJson extends JwkJson { - kty: JwaKeyType.EC - crv: JwaCurve.P256 - x: string - y: string - use?: 'sig' | 'enc' -} - -export function isValidP256JwkPublicKey(jwk: JwkJson): jwk is P256JwkJson { - return ( - hasKty(jwk, JwaKeyType.EC) && - hasCrv(jwk, JwaCurve.P256) && - hasX(jwk) && - hasY(jwk) && - hasValidUse(jwk, { - supportsEncrypting: true, - supportsSigning: true, - }) - ) -} diff --git a/packages/core/src/crypto/jose/jwk/P384Jwk.ts b/packages/core/src/crypto/jose/jwk/P384Jwk.ts deleted file mode 100644 index 7610203044..0000000000 --- a/packages/core/src/crypto/jose/jwk/P384Jwk.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { JwaEncryptionAlgorithm } from '../jwa/alg' -import type { JwkJson } from './Jwk' - -import { - AffinePoint, - Secp384r1, - isValidCompressedPublicKeyFormat, - isValidDecompressedPublicKeyFormat, -} from 'ec-compression' - -import { CredoError } from '../../../error' -import { TypedArrayEncoder } from '../../../utils' -import { KeyType } from '../../KeyType' -import { JwaCurve, JwaKeyType } from '../jwa' -import { JwaSignatureAlgorithm } from '../jwa/alg' - -import { Jwk } from './Jwk' -import { hasCrv, hasKty, hasValidUse, hasX, hasY } from './validate' - -export class P384Jwk extends Jwk { - public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = [] - public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [JwaSignatureAlgorithm.ES384] - public static readonly keyType = KeyType.P384 - - private readonly affinePoint: AffinePoint - - public constructor({ x, y }: { x: string | Uint8Array; y: string | Uint8Array }) { - super() - - const xAsBytes = typeof x === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(x)) : x - const yAsBytes = typeof y === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(y)) : y - - this.affinePoint = new AffinePoint(xAsBytes, yAsBytes) - } - - public get kty() { - return JwaKeyType.EC as const - } - - public get crv() { - return JwaCurve.P384 as const - } - - public get keyType() { - return P384Jwk.keyType - } - - public get supportedEncryptionAlgorithms() { - return P384Jwk.supportedEncryptionAlgorithms - } - - public get supportedSignatureAlgorithms() { - return P384Jwk.supportedSignatureAlgorithms - } - - public get x() { - return TypedArrayEncoder.toBase64URL(this.affinePoint.xBytes) - } - - public get y() { - return TypedArrayEncoder.toBase64URL(this.affinePoint.yBytes) - } - - /** - * Returns the uncompressed public key of the P-384 JWK. - */ - public get publicKey() { - return this.affinePoint.decompressedForm - } - - /** - * Returns the compressed public key of the P-384 JWK. - */ - public get publicKeyCompressed() { - return this.affinePoint.compressedForm - } - - public toJson() { - return { - ...super.toJson(), - crv: this.crv, - x: this.x, - y: this.y, - } as P384JwkJson - } - - public static fromJson(jwk: JwkJson) { - if (!isValidP384JwkPublicKey(jwk)) { - throw new Error("Invalid 'P-384' JWK.") - } - - return new P384Jwk({ - x: jwk.x, - y: jwk.y, - }) - } - - public static fromPublicKey(publicKey: Uint8Array) { - if (isValidCompressedPublicKeyFormat(publicKey, Secp384r1)) { - const affinePoint = AffinePoint.fromCompressedPoint(publicKey, Secp384r1) - return new P384Jwk({ x: affinePoint.xBytes, y: affinePoint.yBytes }) - } - - if (isValidDecompressedPublicKeyFormat(publicKey, Secp384r1)) { - const affinePoint = AffinePoint.fromDecompressedPoint(publicKey, Secp384r1) - return new P384Jwk({ x: affinePoint.xBytes, y: affinePoint.yBytes }) - } - - throw new CredoError( - `${P384Jwk.keyType} public key is neither a valid compressed or uncompressed key. Key prefix '${publicKey[0]}', key length '${publicKey.length}'` - ) - } -} - -export interface P384JwkJson extends JwkJson { - kty: JwaKeyType.EC - crv: JwaCurve.P384 - x: string - y: string - use?: 'sig' | 'enc' -} - -export function isValidP384JwkPublicKey(jwk: JwkJson): jwk is P384JwkJson { - return ( - hasKty(jwk, JwaKeyType.EC) && - hasCrv(jwk, JwaCurve.P384) && - hasX(jwk) && - hasY(jwk) && - hasValidUse(jwk, { - supportsEncrypting: true, - supportsSigning: true, - }) - ) -} diff --git a/packages/core/src/crypto/jose/jwk/P521Jwk.ts b/packages/core/src/crypto/jose/jwk/P521Jwk.ts deleted file mode 100644 index 198a1ce1aa..0000000000 --- a/packages/core/src/crypto/jose/jwk/P521Jwk.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { JwaEncryptionAlgorithm } from '../jwa/alg' -import type { JwkJson } from './Jwk' - -import { - AffinePoint, - Secp521r1, - isValidCompressedPublicKeyFormat, - isValidDecompressedPublicKeyFormat, -} from 'ec-compression' - -import { CredoError } from '../../../error' -import { TypedArrayEncoder } from '../../../utils' -import { KeyType } from '../../KeyType' -import { JwaCurve, JwaKeyType } from '../jwa' -import { JwaSignatureAlgorithm } from '../jwa/alg' - -import { Jwk } from './Jwk' -import { hasCrv, hasKty, hasValidUse, hasX, hasY } from './validate' - -export class P521Jwk extends Jwk { - public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = [] - public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [JwaSignatureAlgorithm.ES512] - public static readonly keyType = KeyType.P521 - - private readonly affinePoint: AffinePoint - - public constructor({ x, y }: { x: string | Uint8Array; y: string | Uint8Array }) { - super() - - const xAsBytes = typeof x === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(x)) : x - const yAsBytes = typeof y === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(y)) : y - - this.affinePoint = new AffinePoint(xAsBytes, yAsBytes) - } - - public get kty() { - return JwaKeyType.EC as const - } - - public get crv() { - return JwaCurve.P521 as const - } - - public get keyType() { - return P521Jwk.keyType - } - - public get supportedEncryptionAlgorithms() { - return P521Jwk.supportedEncryptionAlgorithms - } - - public get supportedSignatureAlgorithms() { - return P521Jwk.supportedSignatureAlgorithms - } - - public get x() { - return TypedArrayEncoder.toBase64URL(this.affinePoint.xBytes) - } - - public get y() { - return TypedArrayEncoder.toBase64URL(this.affinePoint.yBytes) - } - - /** - * Returns the uncompressed public key of the P-521 JWK. - */ - public get publicKey() { - return this.affinePoint.decompressedForm - } - - /** - * Returns the compressed public key of the P-521 JWK. - */ - public get publicKeyCompressed() { - return this.affinePoint.compressedForm - } - - public toJson() { - return { - ...super.toJson(), - crv: this.crv, - x: this.x, - y: this.y, - } as P521JwkJson - } - - public static fromJson(jwk: JwkJson) { - if (!isValidP521JwkPublicKey(jwk)) { - throw new Error("Invalid 'P-521' JWK.") - } - - return new P521Jwk({ - x: jwk.x, - y: jwk.y, - }) - } - - public static fromPublicKey(publicKey: Uint8Array) { - if (isValidCompressedPublicKeyFormat(publicKey, Secp521r1)) { - const affinePoint = AffinePoint.fromCompressedPoint(publicKey, Secp521r1) - return new P521Jwk({ x: affinePoint.xBytes, y: affinePoint.yBytes }) - } - - if (isValidDecompressedPublicKeyFormat(publicKey, Secp521r1)) { - const affinePoint = AffinePoint.fromDecompressedPoint(publicKey, Secp521r1) - return new P521Jwk({ x: affinePoint.xBytes, y: affinePoint.yBytes }) - } - - throw new CredoError( - `${P521Jwk.keyType} public key is neither a valid compressed or uncompressed key. Key prefix '${publicKey[0]}', key length '${publicKey.length}'` - ) - } -} - -export interface P521JwkJson extends JwkJson { - kty: JwaKeyType.EC - crv: JwaCurve.P521 - x: string - y: string - use?: 'sig' | 'enc' -} - -export function isValidP521JwkPublicKey(jwk: JwkJson): jwk is P521JwkJson { - return ( - hasKty(jwk, JwaKeyType.EC) && - hasCrv(jwk, JwaCurve.P521) && - hasX(jwk) && - hasY(jwk) && - hasValidUse(jwk, { - supportsEncrypting: true, - supportsSigning: true, - }) - ) -} diff --git a/packages/core/src/crypto/jose/jwk/X25519Jwk.ts b/packages/core/src/crypto/jose/jwk/X25519Jwk.ts deleted file mode 100644 index 54c953cf31..0000000000 --- a/packages/core/src/crypto/jose/jwk/X25519Jwk.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { JwaSignatureAlgorithm } from '../jwa' -import type { JwkJson } from './Jwk' - -import { TypedArrayEncoder } from '../../../utils' -import { KeyType } from '../../KeyType' -import { JwaCurve, JwaEncryptionAlgorithm, JwaKeyType } from '../jwa' - -import { Jwk } from './Jwk' -import { hasCrv, hasKty, hasValidUse, hasX } from './validate' - -export class X25519Jwk extends Jwk { - public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = [ - JwaEncryptionAlgorithm.ECDHESA128KW, - JwaEncryptionAlgorithm.ECDHESA192KW, - JwaEncryptionAlgorithm.ECDHESA256KW, - JwaEncryptionAlgorithm.ECDHES, - ] - public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [] - public static readonly keyType = KeyType.X25519 - - private readonly _x: Uint8Array - - public constructor({ x }: { x: string | Uint8Array }) { - super() - - this._x = typeof x === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(x)) : x - } - - public get x() { - return TypedArrayEncoder.toBase64URL(this._x) - } - - public get kty() { - return JwaKeyType.OKP as const - } - - public get crv() { - return JwaCurve.X25519 as const - } - - public get keyType() { - return X25519Jwk.keyType - } - - public get supportedEncryptionAlgorithms() { - return X25519Jwk.supportedEncryptionAlgorithms - } - - public get supportedSignatureAlgorithms() { - return X25519Jwk.supportedSignatureAlgorithms - } - - public get publicKey() { - return this._x - } - - public toJson() { - return { - ...super.toJson(), - crv: this.crv, - x: this.x, - } as X25519JwkJson - } - - public static fromJson(jwk: JwkJson) { - if (!isValidX25519JwkPublicKey(jwk)) { - throw new Error("Invalid 'X25519' JWK.") - } - - return new X25519Jwk({ - x: jwk.x, - }) - } - - public static fromPublicKey(publicKey: Uint8Array) { - return new X25519Jwk({ x: publicKey }) - } -} - -export interface X25519JwkJson extends JwkJson { - kty: JwaKeyType.OKP - crv: JwaCurve.X25519 - x: string - use?: 'enc' -} - -function isValidX25519JwkPublicKey(jwk: JwkJson): jwk is X25519JwkJson { - return ( - hasKty(jwk, JwaKeyType.OKP) && - hasCrv(jwk, JwaCurve.X25519) && - hasX(jwk) && - hasValidUse(jwk, { - supportsEncrypting: true, - supportsSigning: false, - }) - ) -} diff --git a/packages/core/src/crypto/jose/jwk/__tests__/Ed25519Jwk.test.ts b/packages/core/src/crypto/jose/jwk/__tests__/Ed25519Jwk.test.ts deleted file mode 100644 index e5903063a6..0000000000 --- a/packages/core/src/crypto/jose/jwk/__tests__/Ed25519Jwk.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { TypedArrayEncoder } from '../../../../utils' -import { KeyType } from '../../../KeyType' -import { Ed25519Jwk } from '../Ed25519Jwk' - -const jwkJson = { - kty: 'OKP', - crv: 'Ed25519', - x: 'O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik', -} - -describe('Ed25519JWk', () => { - test('has correct properties', () => { - const jwk = new Ed25519Jwk({ x: jwkJson.x }) - - expect(jwk.kty).toEqual('OKP') - expect(jwk.crv).toEqual('Ed25519') - expect(jwk.keyType).toEqual(KeyType.Ed25519) - expect(jwk.publicKey).toEqual(Uint8Array.from(TypedArrayEncoder.fromBase64(jwkJson.x))) - expect(jwk.supportedEncryptionAlgorithms).toEqual([]) - expect(jwk.supportedSignatureAlgorithms).toEqual(['EdDSA']) - expect(jwk.key.keyType).toEqual(KeyType.Ed25519) - expect(jwk.toJson()).toEqual(jwkJson) - }) - - test('fromJson', () => { - const jwk = Ed25519Jwk.fromJson(jwkJson) - expect(jwk.x).toEqual(jwkJson.x) - - expect(() => Ed25519Jwk.fromJson({ ...jwkJson, kty: 'test' })).toThrow("Invalid 'Ed25519' JWK.") - }) - - test('fromPublicKey', () => { - const jwk = Ed25519Jwk.fromPublicKey(TypedArrayEncoder.fromBase64(jwkJson.x)) - expect(jwk.x).toEqual(jwkJson.x) - }) -}) diff --git a/packages/core/src/crypto/jose/jwk/__tests__/K_256Jwk.test.ts b/packages/core/src/crypto/jose/jwk/__tests__/K_256Jwk.test.ts deleted file mode 100644 index 87206c877d..0000000000 --- a/packages/core/src/crypto/jose/jwk/__tests__/K_256Jwk.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { compressPublicKeyIfPossible } from 'ec-compression' - -import { TypedArrayEncoder } from '../../../../utils' -import { KeyType } from '../../../KeyType' -import { K256Jwk } from '../K256Jwk' - -// Generated with https://mkjwk.org -const jwkJson = { - kty: 'EC', - crv: 'secp256k1', - x: '0CtFvFuEzkEhPOTKHi3k2OvEgJmQ1dH-IXXme3JBzVY', - y: 'vIr8423MqTswmAebHhCaOoiYdp1kyOiduZinD3JBXxU', -} - -const uncompressedPublicKey = new Uint8Array([ - 0x04, - ...TypedArrayEncoder.fromBase64(jwkJson.x), - ...TypedArrayEncoder.fromBase64(jwkJson.y), -]) -const compressedPublicKey = compressPublicKeyIfPossible(uncompressedPublicKey, 'k-256') - -describe('K_256JWk', () => { - test('has correct properties', () => { - const jwk = new K256Jwk({ x: jwkJson.x, y: jwkJson.y }) - - expect(jwk.kty).toEqual('EC') - expect(jwk.crv).toEqual('secp256k1') - expect(jwk.keyType).toEqual(KeyType.K256) - expect(jwk.supportedEncryptionAlgorithms).toEqual([]) - expect(jwk.supportedSignatureAlgorithms).toEqual(['ES256K']) - expect(jwk.key.keyType).toEqual(KeyType.K256) - expect(jwk.toJson()).toEqual(jwkJson) - - expect(jwk.publicKey).toEqual(uncompressedPublicKey) - expect(jwk.publicKey.length).toEqual(65) - expect(jwk.publicKeyCompressed.length).toEqual(33) - }) - - test('fromJson', () => { - const jwk = K256Jwk.fromJson(jwkJson) - expect(jwk.x).toEqual(jwkJson.x) - expect(jwk.y).toEqual(jwkJson.y) - - expect(() => K256Jwk.fromJson({ ...jwkJson, kty: 'test' })).toThrow("Invalid 'K-256' JWK.") - }) - - test('fromUncompressedPublicKey', () => { - const jwk = K256Jwk.fromPublicKey(uncompressedPublicKey) - expect(jwk.x).toEqual(jwkJson.x) - expect(jwk.y).toEqual(jwkJson.y) - }) - - test('fromCompressedPublicKey', () => { - const jwk = K256Jwk.fromPublicKey(compressedPublicKey) - expect(jwk.x).toEqual(jwkJson.x) - expect(jwk.y).toEqual(jwkJson.y) - }) -}) diff --git a/packages/core/src/crypto/jose/jwk/__tests__/P_256Jwk.test.ts b/packages/core/src/crypto/jose/jwk/__tests__/P_256Jwk.test.ts deleted file mode 100644 index e65bae02e6..0000000000 --- a/packages/core/src/crypto/jose/jwk/__tests__/P_256Jwk.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { compressPublicKeyIfPossible } from 'ec-compression' - -import { TypedArrayEncoder } from '../../../../utils' -import { KeyType } from '../../../KeyType' -import { P256Jwk } from '../P256Jwk' - -// Generated with https://mkjwk.org -const jwkJson = { - kty: 'EC', - crv: 'P-256', - x: 'YKIJKqnGI22osL86OZUIGmwW7Bh0ZsUpTVBLVRNyThQ', - y: 'booCsoNXVs1W8GBt9V7DvEktjyWPUV2NFvDrW2aqMfI', -} - -const uncompressedPublicKey = new Uint8Array([ - 0x04, - ...TypedArrayEncoder.fromBase64(jwkJson.x), - ...TypedArrayEncoder.fromBase64(jwkJson.y), -]) -const compressedPublicKey = compressPublicKeyIfPossible(uncompressedPublicKey, 'p-256') - -describe('P_256JWk', () => { - test('has correct properties', () => { - const jwk = new P256Jwk({ x: jwkJson.x, y: jwkJson.y }) - - expect(jwk.kty).toEqual('EC') - expect(jwk.crv).toEqual('P-256') - expect(jwk.keyType).toEqual(KeyType.P256) - expect(jwk.supportedEncryptionAlgorithms).toEqual([]) - expect(jwk.supportedSignatureAlgorithms).toEqual(['ES256']) - expect(jwk.key.keyType).toEqual(KeyType.P256) - expect(jwk.toJson()).toEqual(jwkJson) - - expect(jwk.publicKey).toEqual(uncompressedPublicKey) - expect(jwk.publicKey.length).toEqual(65) - expect(jwk.publicKeyCompressed.length).toEqual(33) - }) - - test('fromJson', () => { - const jwk = P256Jwk.fromJson(jwkJson) - expect(jwk.x).toEqual(jwkJson.x) - expect(jwk.y).toEqual(jwkJson.y) - - expect(() => P256Jwk.fromJson({ ...jwkJson, kty: 'test' })).toThrow("Invalid 'P-256' JWK.") - }) - - test('fromUncompressedPublicKey', () => { - const jwk = P256Jwk.fromPublicKey(uncompressedPublicKey) - expect(jwk.x).toEqual(jwkJson.x) - expect(jwk.y).toEqual(jwkJson.y) - }) - - test('fromCompressedPublicKey', () => { - const jwk = P256Jwk.fromPublicKey(compressedPublicKey) - expect(jwk.x).toEqual(jwkJson.x) - expect(jwk.y).toEqual(jwkJson.y) - }) -}) diff --git a/packages/core/src/crypto/jose/jwk/__tests__/P_384Jwk.test.ts b/packages/core/src/crypto/jose/jwk/__tests__/P_384Jwk.test.ts deleted file mode 100644 index 5028070cca..0000000000 --- a/packages/core/src/crypto/jose/jwk/__tests__/P_384Jwk.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { compressPublicKeyIfPossible } from 'ec-compression' - -import { TypedArrayEncoder } from '../../../../utils' -import { KeyType } from '../../../KeyType' -import { P384Jwk } from '../P384Jwk' - -// Generated with https://mkjwk.org -const jwkJson = { - kty: 'EC', - crv: 'P-384', - x: 'Rl0BbVOvE0zcytPVSGgM39tihXnlYjuaLin3SjhD6cLRL_IK-3tHTCljCiJBbSX9', - y: '282rUQMBuCkLb0t9PbReApadoP7Jo-sVcZDNGglYg4iMsqNPvyq-WIzxSUb1USpc', -} - -const uncompressedPublicKey = new Uint8Array([ - 0x04, - ...TypedArrayEncoder.fromBase64(jwkJson.x), - ...TypedArrayEncoder.fromBase64(jwkJson.y), -]) -const compressedPublicKey = compressPublicKeyIfPossible(uncompressedPublicKey, 'p-384') - -describe('P_384JWk', () => { - test('has correct properties', () => { - const jwk = new P384Jwk({ x: jwkJson.x, y: jwkJson.y }) - - expect(jwk.kty).toEqual('EC') - expect(jwk.crv).toEqual('P-384') - expect(jwk.keyType).toEqual(KeyType.P384) - expect(jwk.supportedEncryptionAlgorithms).toEqual([]) - expect(jwk.supportedSignatureAlgorithms).toEqual(['ES384']) - expect(jwk.key.keyType).toEqual(KeyType.P384) - expect(jwk.toJson()).toEqual(jwkJson) - - expect(jwk.publicKey).toEqual(uncompressedPublicKey) - expect(jwk.publicKey.length).toEqual(97) - expect(jwk.publicKeyCompressed.length).toEqual(49) - }) - - test('fromJson', () => { - const jwk = P384Jwk.fromJson(jwkJson) - expect(jwk.x).toEqual(jwkJson.x) - expect(jwk.y).toEqual(jwkJson.y) - - expect(() => P384Jwk.fromJson({ ...jwkJson, kty: 'test' })).toThrow("Invalid 'P-384' JWK.") - }) - - test('fromUncompressedPublicKey', () => { - const jwk = P384Jwk.fromPublicKey(uncompressedPublicKey) - expect(jwk.x).toEqual(jwkJson.x) - expect(jwk.y).toEqual(jwkJson.y) - }) - - test('fromCompressedPublicKey', () => { - const jwk = P384Jwk.fromPublicKey(compressedPublicKey) - expect(jwk.x).toEqual(jwkJson.x) - expect(jwk.y).toEqual(jwkJson.y) - }) -}) diff --git a/packages/core/src/crypto/jose/jwk/__tests__/P_521Jwk.test.ts b/packages/core/src/crypto/jose/jwk/__tests__/P_521Jwk.test.ts deleted file mode 100644 index 9f8a473820..0000000000 --- a/packages/core/src/crypto/jose/jwk/__tests__/P_521Jwk.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { compressPublicKeyIfPossible } from 'ec-compression' - -import { TypedArrayEncoder } from '../../../../utils' -import { KeyType } from '../../../KeyType' -import { P521Jwk } from '../P521Jwk' - -// Generated with https://mkjwk.org -const jwkJson = { - kty: 'EC', - crv: 'P-521', - x: 'AAyV8qWafv5UPexMB3ohAPSFuz_zFdaHAjb-XlzO8qBkx-lZtN1PN1E9AHipP6esSNBPilGOAkiZYnQ48hPJgJQG', - y: 'AccbmJnVXJhxJ8vFS4GcG1eM27XtSOjKz1dX52wbJ0YN6U5KEOPQ-3krxvLAqlFG2BCbZkpnrfateEdervmp3Q3G', -} - -const uncompressedPublicKey = new Uint8Array([ - 0x04, - ...TypedArrayEncoder.fromBase64(jwkJson.x), - ...TypedArrayEncoder.fromBase64(jwkJson.y), -]) -const compressedPublicKey = compressPublicKeyIfPossible(uncompressedPublicKey, 'p-521') - -describe('P_521JWk', () => { - test('has correct properties', () => { - const jwk = new P521Jwk({ x: jwkJson.x, y: jwkJson.y }) - - expect(jwk.kty).toEqual('EC') - expect(jwk.crv).toEqual('P-521') - expect(jwk.keyType).toEqual(KeyType.P521) - expect(jwk.supportedEncryptionAlgorithms).toEqual([]) - expect(jwk.supportedSignatureAlgorithms).toEqual(['ES512']) - expect(jwk.key.keyType).toEqual(KeyType.P521) - expect(jwk.toJson()).toEqual(jwkJson) - - expect(jwk.publicKey).toEqual(uncompressedPublicKey) - expect(jwk.publicKey.length).toEqual(133) - expect(jwk.publicKeyCompressed.length).toEqual(67) - }) - - test('fromJson', () => { - const jwk = P521Jwk.fromJson(jwkJson) - expect(jwk.x).toEqual(jwkJson.x) - expect(jwk.y).toEqual(jwkJson.y) - - expect(() => P521Jwk.fromJson({ ...jwkJson, kty: 'test' })).toThrow("Invalid 'P-521' JWK.") - }) - - test('fromUncompressedPublicKey', () => { - const jwk = P521Jwk.fromPublicKey(uncompressedPublicKey) - expect(jwk.x).toEqual(jwkJson.x) - expect(jwk.y).toEqual(jwkJson.y) - }) - - test('fromCompressedPublicKey', () => { - const jwk = P521Jwk.fromPublicKey(compressedPublicKey) - expect(jwk.x).toEqual(jwkJson.x) - expect(jwk.y).toEqual(jwkJson.y) - }) -}) diff --git a/packages/core/src/crypto/jose/jwk/__tests__/X25519Jwk.test.ts b/packages/core/src/crypto/jose/jwk/__tests__/X25519Jwk.test.ts deleted file mode 100644 index c582a63ca2..0000000000 --- a/packages/core/src/crypto/jose/jwk/__tests__/X25519Jwk.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { TypedArrayEncoder } from '../../../../utils' -import { KeyType } from '../../../KeyType' -import { X25519Jwk } from '../X25519Jwk' - -const jwkJson = { - kty: 'OKP', - crv: 'X25519', - x: 'W_Vcc7guviK-gPNDBmevVw-uJVamQV5rMNQGUwCqlH0', -} - -describe('X25519JWk', () => { - test('has correct properties', () => { - const jwk = new X25519Jwk({ x: jwkJson.x }) - - expect(jwk.kty).toEqual('OKP') - expect(jwk.crv).toEqual('X25519') - expect(jwk.keyType).toEqual(KeyType.X25519) - expect(jwk.publicKey).toEqual(Uint8Array.from(TypedArrayEncoder.fromBase64(jwkJson.x))) - expect(jwk.supportedEncryptionAlgorithms).toEqual(['ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW', 'ECDH-ES']) - expect(jwk.supportedSignatureAlgorithms).toEqual([]) - expect(jwk.key.keyType).toEqual(KeyType.X25519) - expect(jwk.toJson()).toEqual(jwkJson) - }) - - test('fromJson', () => { - const jwk = X25519Jwk.fromJson(jwkJson) - expect(jwk.x).toEqual(jwkJson.x) - - expect(() => X25519Jwk.fromJson({ ...jwkJson, kty: 'test' })).toThrow("Invalid 'X25519' JWK.") - }) - - test('fromPublicKey', () => { - const jwk = X25519Jwk.fromPublicKey(TypedArrayEncoder.fromBase64(jwkJson.x)) - expect(jwk.x).toEqual(jwkJson.x) - }) -}) diff --git a/packages/core/src/crypto/jose/jwk/index.ts b/packages/core/src/crypto/jose/jwk/index.ts deleted file mode 100644 index 7579a74778..0000000000 --- a/packages/core/src/crypto/jose/jwk/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './transform' -export { Ed25519Jwk } from './Ed25519Jwk' -export { X25519Jwk } from './X25519Jwk' -export { P256Jwk } from './P256Jwk' -export { P384Jwk } from './P384Jwk' -export { P521Jwk } from './P521Jwk' -export { Jwk, JwkJson } from './Jwk' diff --git a/packages/core/src/crypto/jose/jwk/transform.ts b/packages/core/src/crypto/jose/jwk/transform.ts deleted file mode 100644 index ecbbde46fe..0000000000 --- a/packages/core/src/crypto/jose/jwk/transform.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { Key } from '../../Key' -import type { JwaSignatureAlgorithm } from '../jwa' -import type { Jwk, JwkJson } from './Jwk' - -import { CredoError } from '../../../error' -import { KeyType } from '../../KeyType' -import { JwaCurve, JwaKeyType } from '../jwa' - -import { Ed25519Jwk } from './Ed25519Jwk' -import { K256Jwk } from './K256Jwk' -import { P256Jwk } from './P256Jwk' -import { P384Jwk } from './P384Jwk' -import { P521Jwk } from './P521Jwk' -import { X25519Jwk } from './X25519Jwk' -import { hasCrv } from './validate' - -const JwkClasses = [Ed25519Jwk, P256Jwk, P384Jwk, P521Jwk, X25519Jwk, K256Jwk] as const - -export function getJwkFromJson(jwkJson: JwkJson): Jwk { - if (jwkJson.kty === JwaKeyType.OKP) { - if (hasCrv(jwkJson, JwaCurve.Ed25519)) return Ed25519Jwk.fromJson(jwkJson) - if (hasCrv(jwkJson, JwaCurve.X25519)) return X25519Jwk.fromJson(jwkJson) - } - - if (jwkJson.kty === JwaKeyType.EC) { - if (hasCrv(jwkJson, JwaCurve.P256)) return P256Jwk.fromJson(jwkJson) - if (hasCrv(jwkJson, JwaCurve.P384)) return P384Jwk.fromJson(jwkJson) - if (hasCrv(jwkJson, JwaCurve.P521)) return P521Jwk.fromJson(jwkJson) - if (hasCrv(jwkJson, JwaCurve.Secp256k1)) return K256Jwk.fromJson(jwkJson) - } - - throw new Error(`Cannot create JWK from JSON. Unsupported JWK with kty '${jwkJson.kty}'.`) -} - -export function getJwkFromKey(key: Key) { - if (key.keyType === KeyType.Ed25519) return Ed25519Jwk.fromPublicKey(key.publicKey) - if (key.keyType === KeyType.X25519) return X25519Jwk.fromPublicKey(key.publicKey) - - if (key.keyType === KeyType.P256) return P256Jwk.fromPublicKey(key.publicKey) - if (key.keyType === KeyType.P384) return P384Jwk.fromPublicKey(key.publicKey) - if (key.keyType === KeyType.P521) return P521Jwk.fromPublicKey(key.publicKey) - - if (key.keyType === KeyType.K256) return K256Jwk.fromPublicKey(key.publicKey) - - throw new CredoError(`Cannot create JWK from key. Unsupported key with type '${key.keyType}'.`) -} - -export function getJwkClassFromJwaSignatureAlgorithm(alg: JwaSignatureAlgorithm | string) { - return JwkClasses.find((jwkClass) => jwkClass.supportedSignatureAlgorithms.includes(alg as JwaSignatureAlgorithm)) -} - -export function getJwkClassFromKeyType(keyType: KeyType) { - return JwkClasses.find((jwkClass) => jwkClass.keyType === keyType) -} diff --git a/packages/core/src/crypto/jose/jwk/validate.ts b/packages/core/src/crypto/jose/jwk/validate.ts deleted file mode 100644 index 507c1b91cd..0000000000 --- a/packages/core/src/crypto/jose/jwk/validate.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { JwaCurve, JwaKeyType } from '../jwa' -import type { JwkJson } from './Jwk' - -export function hasCrv(jwk: JwkJson, crv: JwaCurve): jwk is JwkJson & { crv: JwaCurve } { - return 'crv' in jwk && jwk.crv === crv -} - -export function hasKty(jwk: JwkJson, kty: JwaKeyType) { - return 'kty' in jwk && jwk.kty === kty -} - -export function hasX(jwk: JwkJson): jwk is JwkJson & { x: string } { - return 'x' in jwk && jwk.x !== undefined -} - -export function hasY(jwk: JwkJson): jwk is JwkJson & { y: string } { - return 'y' in jwk && jwk.y !== undefined -} - -export function hasValidUse( - jwk: JwkJson, - { supportsSigning, supportsEncrypting }: { supportsSigning: boolean; supportsEncrypting: boolean } -) { - return jwk.use === undefined || (supportsSigning && jwk.use === 'sig') || (supportsEncrypting && jwk.use === 'enc') -} diff --git a/packages/core/src/crypto/jose/jwt/Jwt.ts b/packages/core/src/crypto/jose/jwt/Jwt.ts index b55b77b0df..52f69f9f27 100644 --- a/packages/core/src/crypto/jose/jwt/Jwt.ts +++ b/packages/core/src/crypto/jose/jwt/Jwt.ts @@ -1,16 +1,16 @@ import type { Buffer } from '../../../utils' -import type { JwkJson } from '../jwk' import { CredoError } from '../../../error' import { JsonEncoder, TypedArrayEncoder } from '../../../utils' +import { Jwk } from '../../../modules/kms' import { JwtPayload } from './JwtPayload' // TODO: JWT Header typing interface JwtHeader { alg: string kid?: string - jwk?: JwkJson + jwk?: Jwk x5c?: string[] [key: string]: unknown } diff --git a/packages/core/src/crypto/keyUtils.ts b/packages/core/src/crypto/keyUtils.ts deleted file mode 100644 index 14b229fc8d..0000000000 --- a/packages/core/src/crypto/keyUtils.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Buffer } from '../utils' - -import { KeyType } from './KeyType' - -export function isValidSeed(seed: Buffer, keyType: KeyType): boolean { - const minimumSeedLength = { - [KeyType.Ed25519]: 32, - [KeyType.X25519]: 32, - [KeyType.Bls12381g1]: 32, - [KeyType.Bls12381g2]: 32, - [KeyType.Bls12381g1g2]: 32, - [KeyType.P256]: 64, - [KeyType.P384]: 64, - [KeyType.P521]: 64, - [KeyType.K256]: 64, - } as const - - return Buffer.isBuffer(seed) && seed.length >= minimumSeedLength[keyType] -} - -export function isValidPrivateKey(privateKey: Buffer, keyType: KeyType): boolean { - const privateKeyLength = { - [KeyType.Ed25519]: 32, - [KeyType.X25519]: 32, - [KeyType.Bls12381g1]: 32, - [KeyType.Bls12381g2]: 32, - [KeyType.Bls12381g1g2]: 32, - [KeyType.P256]: 32, - [KeyType.P384]: 48, - [KeyType.P521]: 66, - [KeyType.K256]: 32, - } as const - - return Buffer.isBuffer(privateKey) && privateKey.length === privateKeyLength[keyType] -} - -export function isSigningSupportedForKeyType(keyType: KeyType): boolean { - const keyTypeSigningSupportedMapping = { - [KeyType.Ed25519]: true, - [KeyType.X25519]: false, - [KeyType.P256]: true, - [KeyType.P384]: true, - [KeyType.P521]: true, - [KeyType.Bls12381g1]: true, - [KeyType.Bls12381g2]: true, - [KeyType.Bls12381g1g2]: true, - [KeyType.K256]: true, - } as const - - return keyTypeSigningSupportedMapping[keyType] -} - -export function isEncryptionSupportedForKeyType(keyType: KeyType): boolean { - const keyTypeEncryptionSupportedMapping = { - [KeyType.Ed25519]: false, - [KeyType.X25519]: true, - [KeyType.P256]: true, - [KeyType.P384]: true, - [KeyType.P521]: true, - [KeyType.Bls12381g1]: false, - [KeyType.Bls12381g2]: false, - [KeyType.Bls12381g1g2]: false, - [KeyType.K256]: true, - } as const - - return keyTypeEncryptionSupportedMapping[keyType] -} diff --git a/packages/core/src/crypto/multiCodecKey.ts b/packages/core/src/crypto/multiCodecKey.ts deleted file mode 100644 index 249978a4d3..0000000000 --- a/packages/core/src/crypto/multiCodecKey.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { KeyType } from './KeyType' - -// based on https://github.com/multiformats/multicodec/blob/master/table.csv -const multiCodecPrefixMap: Record = { - 234: KeyType.Bls12381g1, - 235: KeyType.Bls12381g2, - 236: KeyType.X25519, - 237: KeyType.Ed25519, - 238: KeyType.Bls12381g1g2, - 4608: KeyType.P256, - 4609: KeyType.P384, - 4610: KeyType.P521, - 231: KeyType.K256, -} - -export function getKeyTypeByMultiCodecPrefix(multiCodecPrefix: number): KeyType { - const keyType = multiCodecPrefixMap[multiCodecPrefix] - - if (!keyType) { - throw new Error(`Unsupported key type from multicodec code '${multiCodecPrefix}'`) - } - - return keyType -} - -export function getMultiCodecPrefixByKeyType(keyType: KeyType): number { - const codes = Object.keys(multiCodecPrefixMap) - const code = codes.find((key) => multiCodecPrefixMap[key] === keyType) - - if (!code) { - throw new Error(`Could not find multicodec prefix for key type '${keyType}'`) - } - - return Number(code) -} diff --git a/packages/core/src/crypto/signing-provider/SigningProvider.ts b/packages/core/src/crypto/signing-provider/SigningProvider.ts deleted file mode 100644 index 3e70d67694..0000000000 --- a/packages/core/src/crypto/signing-provider/SigningProvider.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Buffer } from '../../utils/buffer' -import type { KeyType } from '../KeyType' - -export interface KeyPair { - publicKeyBase58: string - privateKeyBase58: string - keyType: KeyType -} - -export interface SignOptions { - data: Buffer | Buffer[] - publicKeyBase58: string - privateKeyBase58: string -} - -export interface VerifyOptions { - data: Buffer | Buffer[] - publicKeyBase58: string - signature: Buffer -} - -export interface CreateKeyPairOptions { - seed?: Buffer - privateKey?: Buffer -} - -export interface SigningProvider { - readonly keyType: KeyType - - createKeyPair(options: CreateKeyPairOptions): Promise - sign(options: SignOptions): Promise - verify(options: VerifyOptions): Promise -} diff --git a/packages/core/src/crypto/signing-provider/SigningProviderError.ts b/packages/core/src/crypto/signing-provider/SigningProviderError.ts deleted file mode 100644 index bf7cae040d..0000000000 --- a/packages/core/src/crypto/signing-provider/SigningProviderError.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { CredoError } from '../../error' - -export class SigningProviderError extends CredoError {} diff --git a/packages/core/src/crypto/signing-provider/SigningProviderRegistry.ts b/packages/core/src/crypto/signing-provider/SigningProviderRegistry.ts deleted file mode 100644 index 12ae4182cb..0000000000 --- a/packages/core/src/crypto/signing-provider/SigningProviderRegistry.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { KeyType } from '../KeyType' -import type { SigningProvider } from './SigningProvider' - -import { CredoError } from '../../error' -import { injectAll, injectable } from '../../plugins' - -export const SigningProviderToken = Symbol('SigningProviderToken') - -@injectable() -export class SigningProviderRegistry { - private signingKeyProviders: SigningProvider[] - - public constructor(@injectAll(SigningProviderToken) signingKeyProviders: Array<'default' | SigningProvider>) { - // This is a really ugly hack to make tsyringe work without any SigningProviders registered - // It is currently impossible to use @injectAll if there are no instances registered for the - // token. We register a value of `default` by default and will filter that out in the registry. - // Once we have a signing provider that should always be registered we can remove this. We can make an ed25519 - // signer using the @stablelib/ed25519 library. - this.signingKeyProviders = signingKeyProviders.filter((provider) => provider !== 'default') as SigningProvider[] - } - - public hasProviderForKeyType(keyType: KeyType): boolean { - const signingKeyProvider = this.signingKeyProviders.find((x) => x.keyType === keyType) - - return signingKeyProvider !== undefined - } - - public getProviderForKeyType(keyType: KeyType): SigningProvider { - const signingKeyProvider = this.signingKeyProviders.find((x) => x.keyType === keyType) - - if (!signingKeyProvider) { - throw new CredoError(`No signing key provider for key type: ${keyType}`) - } - - return signingKeyProvider - } - - public get supportedKeyTypes(): KeyType[] { - return Array.from(new Set(this.signingKeyProviders.map((provider) => provider.keyType))) - } -} diff --git a/packages/core/src/crypto/signing-provider/__tests__/SigningProviderRegistry.test.ts b/packages/core/src/crypto/signing-provider/__tests__/SigningProviderRegistry.test.ts deleted file mode 100644 index 2054c0b93b..0000000000 --- a/packages/core/src/crypto/signing-provider/__tests__/SigningProviderRegistry.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Buffer } from '../../../utils/buffer' -import type { KeyPair, SigningProvider } from '../SigningProvider' - -import { KeyType } from '../../KeyType' -import { SigningProviderRegistry } from '../SigningProviderRegistry' - -class SigningProviderMock implements SigningProvider { - public readonly keyType = KeyType.Bls12381g2 - - public async createKeyPair(): Promise { - throw new Error('Method not implemented.') - } - public async sign(): Promise { - throw new Error('Method not implemented.') - } - public async verify(): Promise { - throw new Error('Method not implemented.') - } -} - -const signingProvider = new SigningProviderMock() -const signingProviderRegistry = new SigningProviderRegistry([signingProvider]) - -describe('SigningProviderRegistry', () => { - describe('hasProviderForKeyType', () => { - test('returns true if the key type is registered', () => { - expect(signingProviderRegistry.hasProviderForKeyType(KeyType.Bls12381g2)).toBe(true) - }) - - test('returns false if the key type is not registered', () => { - expect(signingProviderRegistry.hasProviderForKeyType(KeyType.Ed25519)).toBe(false) - }) - }) - - describe('getProviderForKeyType', () => { - test('returns the correct provider true if the key type is registered', () => { - expect(signingProviderRegistry.getProviderForKeyType(KeyType.Bls12381g2)).toBe(signingProvider) - }) - - test('throws error if the key type is not registered', () => { - expect(() => signingProviderRegistry.getProviderForKeyType(KeyType.Ed25519)).toThrowError( - 'No signing key provider for key type: ed25519' - ) - }) - }) -}) diff --git a/packages/core/src/crypto/signing-provider/index.ts b/packages/core/src/crypto/signing-provider/index.ts deleted file mode 100644 index e1ee8e8fe0..0000000000 --- a/packages/core/src/crypto/signing-provider/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './SigningProvider' -export * from './SigningProviderRegistry' -export * from './SigningProviderError' diff --git a/packages/core/src/crypto/webcrypto/CredoWalletWebCrypto.ts b/packages/core/src/crypto/webcrypto/CredoWalletWebCrypto.ts index b4c47b57c9..fa82f6d4fc 100644 --- a/packages/core/src/crypto/webcrypto/CredoWalletWebCrypto.ts +++ b/packages/core/src/crypto/webcrypto/CredoWalletWebCrypto.ts @@ -1,13 +1,13 @@ import type { AgentContext } from '../../agent' -import type { JwkJson } from '../jose' -import type { - JsonWebKey, - KeyFormat, - KeyGenAlgorithm, - KeyImportParams, - KeySignParams, - KeyUsage, - KeyVerifyParams, +import { + type JsonWebKey, + type KeyFormat, + type KeyGenAlgorithm, + type KeyImportParams, + type KeySignParams, + type KeyUsage, + type KeyVerifyParams, + keyParamsToJwaAlgorithm, } from './types' import { p384 } from '@noble/curves/p384' @@ -15,29 +15,33 @@ import { sha256, sha384 } from '@noble/hashes/sha2' import { AsnConvert, AsnParser } from '@peculiar/asn1-schema' import { SubjectPublicKeyInfo } from '@peculiar/asn1-x509' -import { Buffer } from '../../utils' -import { Key } from '../Key' -import { getJwkFromJson, getJwkFromKey } from '../jose' - import { p256 } from '@noble/curves/p256' -import { KeyType } from '../KeyType' +import { KeyManagementApi, PublicJwk } from '../../modules/kms' import { CredoWebCryptoError } from './CredoWebCryptoError' import { CredoWebCryptoKey } from './CredoWebCryptoKey' -import { credoKeyTypeIntoSpkiAlgorithm, cryptoKeyAlgorithmToCredoKeyType, spkiAlgorithmIntoCredoKeyType } from './utils' +import { cryptoKeyAlgorithmToCreateKeyOptions, publicJwkToSpki, spkiToPublicJwk } from './utils' export class CredoWalletWebCrypto { - public constructor(private agentContext: AgentContext) {} + private kms: KeyManagementApi + + public constructor(private agentContext: AgentContext) { + this.kms = agentContext.resolve(KeyManagementApi) + } public generateRandomValues(array: T): T { if (!array) return array - return this.agentContext.wallet.getRandomValues(array.byteLength) as unknown as T + return this.kms.randomBytes({ length: array.byteLength }).bytes as unknown as T } - public async sign(key: CredoWebCryptoKey, message: Uint8Array, _algorithm: KeySignParams): Promise { - const signature = await this.agentContext.wallet.sign({ - key: key.key, - data: Buffer.from(message), + public async sign(key: CredoWebCryptoKey, message: Uint8Array, algorithm: KeySignParams): Promise { + const jwaAlgorithm = keyParamsToJwaAlgorithm(algorithm, key) + + const keyId = key.publicJwk.keyId + const { signature } = await this.kms.sign({ + keyId, + data: message, + algorithm: jwaAlgorithm, }) return signature @@ -49,40 +53,45 @@ export class CredoWalletWebCrypto { message: Uint8Array, signature: Uint8Array ): Promise { + const publicKey = key.publicJwk.publicKey + + // TODO: with new KMS api we can now define custom algorithms + // such as ES256-SHA384 to support these non-standard JWA combinatiosn + // or we can do something like ES256-ph (pre-hashed for more generic) if (algorithm.name === 'ECDSA') { const hashAlg = typeof algorithm.hash === 'string' ? algorithm.hash : algorithm.hash.name - if (key.key.keyType === KeyType.P256 && hashAlg !== 'SHA-256') { + if (publicKey.kty === 'EC' && publicKey.crv === 'P-256' && hashAlg !== 'SHA-256') { if (hashAlg !== 'SHA-384') { throw new CredoWebCryptoError( - `Hash Alg: ${hashAlg} is not supported with key type ${key.key.keyType} currently` + `Hash Alg: ${hashAlg} is not supported with key type ${publicKey.crv} currently` ) } - return p256.verify(signature, sha384(message), key.key.publicKey) + return p256.verify(signature, sha384(message), publicKey.publicKey) } - if (key.key.keyType === KeyType.P384 && hashAlg !== 'SHA-384') { + if (publicKey.kty === 'EC' && publicKey.crv === 'P-384' && hashAlg !== 'SHA-384') { if (hashAlg !== 'SHA-256') { throw new CredoWebCryptoError( - `Hash Alg: ${hashAlg} is not supported with key type ${key.key.keyType} currently` + `Hash Alg: ${hashAlg} is not supported with key type ${publicKey.crv} currently` ) } - return p384.verify(signature, sha256(message), key.key.publicKey) + return p384.verify(signature, sha256(message), publicKey.publicKey) } } - const isValidSignature = await this.agentContext.wallet.verify({ - key: key.key, - signature: Buffer.from(signature), - data: Buffer.from(message), + const jwaAlgorithm = keyParamsToJwaAlgorithm(algorithm, key) + const { verified } = await this.kms.verify({ + key: key.publicJwk.toJson(), + algorithm: jwaAlgorithm, + signature, + data: message, }) - return isValidSignature + return verified } - public async generate(algorithm: KeyGenAlgorithm): Promise { - const keyType = cryptoKeyAlgorithmToCredoKeyType(algorithm) - - const key = await this.agentContext.wallet.createKey({ - keyType, + public async generate(algorithm: KeyGenAlgorithm) { + const key = await this.kms.createKey({ + type: cryptoKeyAlgorithmToCreateKeyOptions(algorithm), }) return key @@ -105,24 +114,14 @@ export class CredoWalletWebCrypto { switch (format.toLowerCase()) { case 'jwk': { - const jwk = getJwkFromJson(keyData as unknown as JwkJson) - const publicKey = Key.fromPublicKey(jwk.publicKey, jwk.keyType) - return new CredoWebCryptoKey(publicKey, algorithm as KeyGenAlgorithm, extractable, 'public', keyUsages) + const publicJwk = PublicJwk.fromUnknown(keyData) + return new CredoWebCryptoKey(publicJwk, algorithm as KeyGenAlgorithm, extractable, 'public', keyUsages) } case 'spki': { const subjectPublicKey = AsnParser.parse(keyData as Uint8Array, SubjectPublicKeyInfo) + const publicJwk = spkiToPublicJwk(subjectPublicKey) - const key = new Uint8Array(subjectPublicKey.subjectPublicKey) - - const keyType = spkiAlgorithmIntoCredoKeyType(subjectPublicKey.algorithm) - - return new CredoWebCryptoKey( - Key.fromPublicKey(key, keyType), - algorithm as KeyGenAlgorithm, - extractable, - 'public', - keyUsages - ) + return new CredoWebCryptoKey(publicJwk, algorithm as KeyGenAlgorithm, extractable, 'public', keyUsages) } default: throw new Error(`Unsupported export format: ${format}`) @@ -132,16 +131,10 @@ export class CredoWalletWebCrypto { public async exportKey(format: KeyFormat, key: CredoWebCryptoKey): Promise { switch (format.toLowerCase()) { case 'jwk': { - const jwk = getJwkFromKey(key.key) - return jwk.toJson() as unknown as JsonWebKey + return key.publicJwk.toJson() } case 'spki': { - const algorithm = credoKeyTypeIntoSpkiAlgorithm(key.key.keyType) - - const publicKeyInfo = new SubjectPublicKeyInfo({ - algorithm, - subjectPublicKey: key.key.publicKey.buffer, - }) + const publicKeyInfo = publicJwkToSpki(key.publicJwk) const derEncoded = AsnConvert.serialize(publicKeyInfo) return new Uint8Array(derEncoded) diff --git a/packages/core/src/crypto/webcrypto/CredoWebCryptoKey.ts b/packages/core/src/crypto/webcrypto/CredoWebCryptoKey.ts index a33f65cdac..fa039d7411 100644 --- a/packages/core/src/crypto/webcrypto/CredoWebCryptoKey.ts +++ b/packages/core/src/crypto/webcrypto/CredoWebCryptoKey.ts @@ -1,11 +1,11 @@ -import type { Key } from '../Key' +import { PublicJwk } from '../../modules/kms' import type { KeyGenAlgorithm, KeyType, KeyUsage } from './types' import * as core from 'webcrypto-core' export class CredoWebCryptoKey extends core.CryptoKey { public constructor( - public key: Key, + public publicJwk: PublicJwk, public override algorithm: KeyGenAlgorithm, public override extractable: boolean, public override type: KeyType, diff --git a/packages/core/src/crypto/webcrypto/__tests__/CredoWebCrypto.test.ts b/packages/core/src/crypto/webcrypto/__tests__/CredoWebCrypto.test.ts index dc0a18edc0..6344962211 100644 --- a/packages/core/src/crypto/webcrypto/__tests__/CredoWebCrypto.test.ts +++ b/packages/core/src/crypto/webcrypto/__tests__/CredoWebCrypto.test.ts @@ -1,6 +1,5 @@ import type { KeyGenAlgorithm, KeySignParams } from '../types' -import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' import { getAgentConfig, getAgentContext } from '../../../../tests' import { CredoWebCrypto } from '../CredoWebCrypto' @@ -18,12 +17,9 @@ describe('CredoWebCrypto', () => { ] beforeAll(async () => { - const agentConfig = getAgentConfig('X509Service') - const wallet = new InMemoryWallet() - const agentContext = getAgentContext({ wallet }) - - // biome-ignore lint/style/noNonNullAssertion: - await wallet.createAndOpen(agentConfig.walletConfig!) + const agentContext = getAgentContext({ + agentConfig: getAgentConfig('X509Service'), + }) webCrypto = new CredoWebCrypto(agentContext) }) diff --git a/packages/core/src/crypto/webcrypto/algorithmIdentifiers.ts b/packages/core/src/crypto/webcrypto/algorithmIdentifiers.ts index acb4a067a1..c1a1dab754 100644 --- a/packages/core/src/crypto/webcrypto/algorithmIdentifiers.ts +++ b/packages/core/src/crypto/webcrypto/algorithmIdentifiers.ts @@ -1,4 +1,5 @@ -import { id_ecPublicKey, id_secp256r1, id_secp384r1 } from '@peculiar/asn1-ecc' +import { id_ecPublicKey, id_secp256r1, id_secp384r1, id_secp521r1 } from '@peculiar/asn1-ecc' +import { id_rsaEncryption } from '@peculiar/asn1-rsa' import { AsnObjectIdentifierConverter } from '@peculiar/asn1-schema' import { AlgorithmIdentifier } from '@peculiar/asn1-x509' @@ -20,6 +21,12 @@ export const ecPublicKeyWithP256AlgorithmIdentifier = ecPublicKeyAlgorithmIdenti * */ export const ecPublicKeyWithP384AlgorithmIdentifier = ecPublicKeyAlgorithmIdentifier(id_secp384r1) +/** + * + * https://oid-rep.orange-labs.fr/get/1.3.132.0.35 + * + */ +export const ecPublicKeyWithP521AlgorithmIdentifier = ecPublicKeyAlgorithmIdentifier(id_secp521r1) /** * * https://oid-rep.orange-labs.fr/get/1.3.132.0.10 @@ -40,3 +47,14 @@ export const ed25519AlgorithmIdentifier = new AlgorithmIdentifier({ algorithm: ' * */ export const x25519AlgorithmIdentifier = new AlgorithmIdentifier({ algorithm: '1.3.101.110' }) + +/** + * + * RSA algorithm identifier + * https://oid-rep.orange-labs.fr/get/1.2.840.113549.1.1.1 + * + */ +export const rsaKeyAlgorithmIdentifier = new AlgorithmIdentifier({ + algorithm: id_rsaEncryption, + parameters: null, +}) diff --git a/packages/core/src/crypto/webcrypto/providers/CredoEcdsaProvider.ts b/packages/core/src/crypto/webcrypto/providers/CredoEcdsaProvider.ts index 044864e5e0..6b69024f41 100644 --- a/packages/core/src/crypto/webcrypto/providers/CredoEcdsaProvider.ts +++ b/packages/core/src/crypto/webcrypto/providers/CredoEcdsaProvider.ts @@ -11,6 +11,7 @@ import type { import * as core from 'webcrypto-core' +import { PublicJwk } from '../../../modules/kms' import { CredoWebCryptoKey } from '../CredoWebCryptoKey' export class CredoEcdsaProvider extends core.EcdsaProvider { @@ -37,10 +38,11 @@ export class CredoEcdsaProvider extends core.EcdsaProvider { keyUsages: KeyUsage[] ): Promise { const key = await this.walletWebCrypto.generate(algorithm) + const publicJwk = PublicJwk.fromPublicJwk(key.publicJwk) return { - publicKey: new CredoWebCryptoKey(key, algorithm, extractable, 'public', keyUsages), - privateKey: new CredoWebCryptoKey(key, algorithm, extractable, 'private', keyUsages), + publicKey: new CredoWebCryptoKey(publicJwk, algorithm, extractable, 'public', keyUsages), + privateKey: new CredoWebCryptoKey(publicJwk, algorithm, extractable, 'private', keyUsages), } } diff --git a/packages/core/src/crypto/webcrypto/providers/CredoEd25519Provider.ts b/packages/core/src/crypto/webcrypto/providers/CredoEd25519Provider.ts index 8b00a7db5c..5038fcc916 100644 --- a/packages/core/src/crypto/webcrypto/providers/CredoEd25519Provider.ts +++ b/packages/core/src/crypto/webcrypto/providers/CredoEd25519Provider.ts @@ -11,6 +11,7 @@ import type { import * as core from 'webcrypto-core' +import { PublicJwk } from '../../../modules/kms' import { CredoWebCryptoKey } from '../CredoWebCryptoKey' export class CredoEd25519Provider extends core.Ed25519Provider { @@ -37,10 +38,11 @@ export class CredoEd25519Provider extends core.Ed25519Provider { keyUsages: KeyUsage[] ): Promise { const key = await this.walletWebCrypto.generate(algorithm) + const publicJwk = PublicJwk.fromPublicJwk(key.publicJwk) return { - publicKey: new CredoWebCryptoKey(key, algorithm, extractable, 'public', keyUsages), - privateKey: new CredoWebCryptoKey(key, algorithm, extractable, 'private', keyUsages), + publicKey: new CredoWebCryptoKey(publicJwk, algorithm, extractable, 'public', keyUsages), + privateKey: new CredoWebCryptoKey(publicJwk, algorithm, extractable, 'private', keyUsages), } } diff --git a/packages/core/src/crypto/webcrypto/types.ts b/packages/core/src/crypto/webcrypto/types.ts index 6bdf5a381f..de0b46f5c1 100644 --- a/packages/core/src/crypto/webcrypto/types.ts +++ b/packages/core/src/crypto/webcrypto/types.ts @@ -3,7 +3,17 @@ * Based on: https://www.w3.org/TR/WebCryptoAPI/ */ -import type { JwkJson } from '../jose' +import { + Ed25519PublicJwk, + Jwk, + KnownJwaSignatureAlgorithm, + P256PublicJwk, + P384PublicJwk, + P521PublicJwk, + RsaPublicJwk, + Secp256k1PublicJwk, +} from '../../modules/kms' +import { CredoWebCryptoError } from './CredoWebCryptoError' import type { CredoWebCryptoKey } from './CredoWebCryptoKey' export type CredoWebCryptoKeyPair = { @@ -11,7 +21,7 @@ export type CredoWebCryptoKeyPair = { privateKey: CredoWebCryptoKey } -type HashAlgorithmIdentifier = 'SHA-256' | 'SHA-384' +type HashAlgorithmIdentifier = 'SHA-256' | 'SHA-384' | 'SHA-512' /* * @@ -26,6 +36,12 @@ export type EcdsaParams = { export type Ed25519Params = { name: 'Ed25519' } +export type RsaSsaParams = { + name: 'RSASSA-PKCS1-v1_5' | 'RSA-PSS' + hash: { name: HashAlgorithmIdentifier } | HashAlgorithmIdentifier + saltLength?: number // Only for RSA-PSS +} + /* * * Key Generation Parameters @@ -36,7 +52,14 @@ export type Ed25519KeyGenParams = { name: 'Ed25519' } export type EcKeyGenParams = { name: 'ECDSA' - namedCurve: 'P-256' | 'P-384' | 'K-256' + namedCurve: 'P-256' | 'P-384' | 'P-521' | 'K-256' +} + +export type RsaHashedKeyGenParams = { + name: 'RSASSA-PKCS1-v1_5' | 'RSA-PSS' + modulusLength: number + publicExponent: Uint8Array + hash: { name: HashAlgorithmIdentifier } } /* @@ -49,18 +72,140 @@ export type Ed25519KeyImportParams = { name: 'Ed25519' } export type EcKeyImportParams = { name: 'ECDSA' - namedCurve: 'P-256' | 'P-384' | 'K-256' + namedCurve: 'P-256' | 'P-384' | 'K-256' | 'P-521' } -export type KeyUsage = 'sign' | 'verify' +export type RsaHashedImportParams = { + name: 'RSASSA-PKCS1-v1_5' | 'RSA-PSS' + hash: { name: HashAlgorithmIdentifier } +} + +export type KeyUsage = 'sign' | 'verify' | 'encrypt' | 'decrypt' | 'wrapKey' | 'unwrapKey' | 'deriveKey' | 'deriveBits' export type KeyFormat = 'jwk' | 'pkcs8' | 'spki' | 'raw' export type KeyType = 'private' | 'public' | 'secret' -export type JsonWebKey = JwkJson +export type JsonWebKey = Jwk + +export type HashAlgorithm = { name: HashAlgorithmIdentifier } -export type HashAlgorithm = { name: 'SHA-1' } +export type KeyImportParams = EcKeyImportParams | Ed25519KeyImportParams | RsaHashedImportParams +export type KeyGenAlgorithm = EcKeyGenParams | Ed25519KeyGenParams | RsaHashedKeyGenParams +export type KeySignParams = EcdsaParams | Ed25519Params | RsaSsaParams +export type KeyVerifyParams = EcdsaParams | Ed25519Params | RsaSsaParams -export type KeyImportParams = EcKeyImportParams | Ed25519KeyImportParams -export type KeyGenAlgorithm = EcKeyGenParams | Ed25519KeyGenParams -export type KeySignParams = EcdsaParams | Ed25519Params -export type KeyVerifyParams = EcdsaParams | Ed25519Params +/** + * Derives the JWA algorithm name from KeySignParams or KeyVerifyParams + * @param params - The signing or verification parameters + * @returns The corresponding JWA algorithm string + */ +export function keyParamsToJwaAlgorithm( + params: KeySignParams | KeyVerifyParams, + key: CredoWebCryptoKey +): KnownJwaSignatureAlgorithm { + if (params.name === 'Ed25519') { + if (!key.publicJwk.is(Ed25519PublicJwk)) { + throw new CredoWebCryptoError( + `Unsupported key for algorithm for Ed25519: ${key.publicJwk.jwkTypehumanDescription}` + ) + } + + return 'EdDSA' + } + + if (params.name === 'ECDSA') { + // Normalize hash parameter + const hashName = typeof params.hash === 'string' ? params.hash : params.hash.name + + if (key.publicJwk.is(Secp256k1PublicJwk)) { + // Map ECDSA with different hash algorithms to JWA names + switch (hashName) { + case 'SHA-256': + return 'ES256K' + default: + throw new CredoWebCryptoError(`Unsupported hash algorithm for ECDSA with Secp255K1: ${hashName}`) + } + } + + // Map ECDSA with different hash algorithms to JWA names + if (key.publicJwk.is(P256PublicJwk)) { + switch (hashName) { + case 'SHA-256': + return 'ES256' + default: + throw new CredoWebCryptoError(`Unsupported hash algorithm for ECDSA with P256: ${hashName}`) + } + } + + // Map ECDSA with different hash algorithms to JWA names + if (key.publicJwk.is(P384PublicJwk)) { + switch (hashName) { + case 'SHA-384': + return 'ES384' + default: + throw new CredoWebCryptoError(`Unsupported hash algorithm for ECDSA with P384: ${hashName}`) + } + } + + // Map ECDSA with different hash algorithms to JWA names + if (key.publicJwk.is(P521PublicJwk)) { + switch (hashName) { + case 'SHA-512': + return 'ES512' + default: + throw new CredoWebCryptoError(`Unsupported hash algorithm for ECDSA with P521: ${hashName}`) + } + } + + throw new CredoWebCryptoError( + `Unsupported key ${key.publicJwk.jwkTypehumanDescription} or hash algorithm '${hashName}' for ECDSA` + ) + } + + if (params.name === 'RSASSA-PKCS1-v1_5') { + // Normalize hash parameter + const hashName = typeof params.hash === 'string' ? params.hash : params.hash.name + + if (!key.publicJwk.is(RsaPublicJwk)) { + throw new CredoWebCryptoError( + `Unsupported key for algorithm for RSASSA-PKCS1-v1_5: ${key.publicJwk.jwkTypehumanDescription}` + ) + } + + // Map RSA-PKCS1 with different hash algorithms to JWA names + switch (hashName) { + case 'SHA-256': + return 'RS256' + case 'SHA-384': + return 'RS384' + case 'SHA-512': + return 'RS512' + default: + throw new CredoWebCryptoError(`Unsupported hash algorithm for RSASSA-PKCS1-v1_5: ${hashName}`) + } + } + + if (params.name === 'RSA-PSS') { + // Normalize hash parameter + const hashName = typeof params.hash === 'string' ? params.hash : params.hash.name + + if (!key.publicJwk.is(RsaPublicJwk)) { + throw new CredoWebCryptoError( + `Unsupported key for algorithm for RSA-PSS: ${key.publicJwk.jwkTypehumanDescription}` + ) + } + + // Map RSA-PSS with different hash algorithms to JWA names + switch (hashName) { + case 'SHA-256': + return 'PS256' + case 'SHA-384': + return 'PS384' + case 'SHA-512': + return 'PS512' + default: + throw new CredoWebCryptoError(`Unsupported hash algorithm for RSA-PSS: ${hashName}`) + } + } + + throw new CredoWebCryptoError(`Unsupported algorithm: ${params.name}`) +} diff --git a/packages/core/src/crypto/webcrypto/utils/keyAlgorithmConversion.ts b/packages/core/src/crypto/webcrypto/utils/keyAlgorithmConversion.ts index d54ea3c636..8c845d713b 100644 --- a/packages/core/src/crypto/webcrypto/utils/keyAlgorithmConversion.ts +++ b/packages/core/src/crypto/webcrypto/utils/keyAlgorithmConversion.ts @@ -1,88 +1,193 @@ -import type { AlgorithmIdentifier } from '@peculiar/asn1-x509' -import type { EcKeyGenParams, KeyGenAlgorithm } from '../types' +import { RSAPublicKey } from '@peculiar/asn1-rsa' +import { AlgorithmIdentifier, SubjectPublicKeyInfo } from '@peculiar/asn1-x509' +import type { EcKeyGenParams, KeyGenAlgorithm, RsaHashedKeyGenParams } from '../types' -import { KeyType } from '../../KeyType' +import { AsnParser, AsnSerializer } from '@peculiar/asn1-schema' +import { KmsCreateKeyType, PublicJwk, getJwkHumanDescription } from '../../../modules/kms' import { CredoWebCryptoError } from '../CredoWebCryptoError' import { ecPublicKeyWithK256AlgorithmIdentifier, ecPublicKeyWithP256AlgorithmIdentifier, ecPublicKeyWithP384AlgorithmIdentifier, + ecPublicKeyWithP521AlgorithmIdentifier, ed25519AlgorithmIdentifier, + rsaKeyAlgorithmIdentifier, x25519AlgorithmIdentifier, } from '../algorithmIdentifiers' -export const credoKeyTypeIntoCryptoKeyAlgorithm = (keyType: KeyType): KeyGenAlgorithm => { - switch (keyType) { - case KeyType.Ed25519: +export const publicJwkToCryptoKeyAlgorithm = (key: PublicJwk): KeyGenAlgorithm => { + const publicJwk = key.toJson() + + if (publicJwk.kty === 'EC') { + if (publicJwk.crv === 'P-256' || publicJwk.crv === 'P-384' || publicJwk.crv === 'P-521') { + return { name: 'ECDSA', namedCurve: publicJwk.crv } + } + + if (publicJwk.crv === 'secp256k1') { + return { + name: 'ECDSA', + namedCurve: 'K-256', + } + } + } else if (publicJwk.kty === 'OKP') { + if (publicJwk.crv === 'Ed25519') { return { name: 'Ed25519' } - case KeyType.P256: - return { name: 'ECDSA', namedCurve: 'P-256' } - case KeyType.P384: - return { name: 'ECDSA', namedCurve: 'P-384' } - case KeyType.K256: - return { name: 'ECDSA', namedCurve: 'K-256' } - default: - throw new CredoWebCryptoError(`Unsupported key type: ${keyType}`) + } } + + // TODO: support RSA, but i think we need some extra params for this + throw new CredoWebCryptoError(`Unsupported ${getJwkHumanDescription(key.toJson())}`) } -export const cryptoKeyAlgorithmToCredoKeyType = (algorithm: KeyGenAlgorithm): KeyType => { +// TODO: support RSA +export const cryptoKeyAlgorithmToCreateKeyOptions = (algorithm: KeyGenAlgorithm) => { const algorithmName = algorithm.name.toUpperCase() switch (algorithmName) { case 'ED25519': - return KeyType.Ed25519 + return { + kty: 'OKP', + crv: 'Ed25519', + } satisfies KmsCreateKeyType case 'X25519': - return KeyType.X25519 - case 'ECDSA': - switch ((algorithm as EcKeyGenParams).namedCurve.toUpperCase()) { + return { + kty: 'OKP', + crv: 'X25519', + } satisfies KmsCreateKeyType + case 'ECDSA': { + const crv = (algorithm as EcKeyGenParams).namedCurve.toUpperCase() + switch (crv) { case 'P-256': - return KeyType.P256 case 'P-384': - return KeyType.P384 + case 'P-521': + return { + kty: 'EC', + crv, + } satisfies KmsCreateKeyType case 'K-256': - return KeyType.K256 + return { + kty: 'EC', + crv: 'secp256k1', + } satisfies KmsCreateKeyType default: throw new CredoWebCryptoError(`Unsupported curve for ECDSA: ${(algorithm as EcKeyGenParams).namedCurve}`) } + } + case 'RSASSA-PKCS1-V1_5': + case 'RSA-PSS': { + const rsaParams = algorithm as RsaHashedKeyGenParams + + if (rsaParams.publicExponent) { + throw new CredoWebCryptoError('Custom exponent not suported for RSA') + } + + if (rsaParams.modulusLength !== 2048 && rsaParams.modulusLength !== 3072 && rsaParams.modulusLength !== 4096) { + throw new CredoWebCryptoError( + `Unsupported modulusLength '${rsaParams.modulusLength}' for RSA key. Expected one of 2048, 3072, 4096.` + ) + } + + return { + kty: 'RSA', + modulusLength: rsaParams.modulusLength, + } satisfies KmsCreateKeyType + } } + throw new CredoWebCryptoError(`Unsupported algorithm: ${algorithmName}`) } -export const spkiAlgorithmIntoCredoKeyType = (algorithm: AlgorithmIdentifier): KeyType => { - if (algorithm.isEqual(ecPublicKeyWithP256AlgorithmIdentifier)) { - return KeyType.P256 +export const spkiToPublicJwk = (spki: SubjectPublicKeyInfo): PublicJwk => { + if (spki.algorithm.isEqual(ecPublicKeyWithP256AlgorithmIdentifier)) { + return PublicJwk.fromPublicKey({ + kty: 'EC', + crv: 'P-256', + publicKey: new Uint8Array(spki.subjectPublicKey), + }) } - if (algorithm.isEqual(ecPublicKeyWithP384AlgorithmIdentifier)) { - return KeyType.P384 + if (spki.algorithm.isEqual(ecPublicKeyWithP384AlgorithmIdentifier)) { + return PublicJwk.fromPublicKey({ + kty: 'EC', + crv: 'P-384', + publicKey: new Uint8Array(spki.subjectPublicKey), + }) } - if (algorithm.isEqual(ecPublicKeyWithK256AlgorithmIdentifier)) { - return KeyType.K256 + if (spki.algorithm.isEqual(ecPublicKeyWithP521AlgorithmIdentifier)) { + return PublicJwk.fromPublicKey({ + kty: 'EC', + crv: 'P-521', + publicKey: new Uint8Array(spki.subjectPublicKey), + }) } - if (algorithm.isEqual(ed25519AlgorithmIdentifier)) { - return KeyType.Ed25519 + if (spki.algorithm.isEqual(ecPublicKeyWithK256AlgorithmIdentifier)) { + return PublicJwk.fromPublicKey({ + kty: 'EC', + crv: 'secp256k1', + publicKey: new Uint8Array(spki.subjectPublicKey), + }) } - if (algorithm.isEqual(x25519AlgorithmIdentifier)) { - return KeyType.X25519 + if (spki.algorithm.isEqual(ed25519AlgorithmIdentifier)) { + return PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: new Uint8Array(spki.subjectPublicKey), + }) + } + if (spki.algorithm.isEqual(x25519AlgorithmIdentifier)) { + return PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'X25519', + publicKey: new Uint8Array(spki.subjectPublicKey), + }) + } + if (spki.algorithm.isEqual(rsaKeyAlgorithmIdentifier)) { + // The RSA key is another ASN.1 structure inside the subjectPublicKey bit string + // The first byte in the bit string is the number of unused bits (typically 0) + const keyWithoutUnusedBits = new Uint8Array(spki.subjectPublicKey).slice(1) + + // Parse the RSA public key structure + const rsaPublicKey = AsnParser.parse(keyWithoutUnusedBits, RSAPublicKey) + + return PublicJwk.fromPublicKey({ + kty: 'RSA', + modulus: new Uint8Array(rsaPublicKey.modulus), + exponent: new Uint8Array(rsaPublicKey.publicExponent), + }) } throw new CredoWebCryptoError( - `Unsupported algorithm: ${algorithm.algorithm}, with params: ${algorithm.parameters ? 'yes' : 'no'}` + `Unsupported algorithm: ${spki.algorithm.algorithm}, with params: ${spki.algorithm.parameters ? 'yes' : 'no'}` ) } -export const credoKeyTypeIntoSpkiAlgorithm = (keyType: KeyType): AlgorithmIdentifier => { - switch (keyType) { - case KeyType.Ed25519: - return ed25519AlgorithmIdentifier - case KeyType.X25519: - return x25519AlgorithmIdentifier - case KeyType.P256: - return ecPublicKeyWithP256AlgorithmIdentifier - case KeyType.P384: - return ecPublicKeyWithP384AlgorithmIdentifier - case KeyType.K256: - return ecPublicKeyWithK256AlgorithmIdentifier - default: - throw new CredoWebCryptoError(`Unsupported key type: ${keyType}`) +export const publicJwkToSpki = (publicJwk: PublicJwk): SubjectPublicKeyInfo => { + const publicKey = publicJwk.publicKey + + if (publicKey.kty === 'RSA') { + const rsaPublicKey = new RSAPublicKey({ + modulus: publicKey.modulus, + publicExponent: publicKey.exponent, + }) + + // 2. Encode the RSA public key to DER + const rsaPublicKeyDer = AsnSerializer.serialize(rsaPublicKey) + + return new SubjectPublicKeyInfo({ + algorithm: rsaKeyAlgorithmIdentifier, + subjectPublicKey: new Uint8Array([0, ...new Uint8Array(rsaPublicKeyDer)]), + }) } + + const crvToAlgorithm: Record<(typeof publicKey)['crv'], AlgorithmIdentifier> = { + 'P-256': ecPublicKeyWithP256AlgorithmIdentifier, + 'P-384': ecPublicKeyWithP384AlgorithmIdentifier, + 'P-521': ecPublicKeyWithP521AlgorithmIdentifier, + secp256k1: ecPublicKeyWithK256AlgorithmIdentifier, + Ed25519: ed25519AlgorithmIdentifier, + X25519: x25519AlgorithmIdentifier, + } + + return new SubjectPublicKeyInfo({ + algorithm: crvToAlgorithm[publicKey.crv], + subjectPublicKey: publicKey.publicKey, + }) } diff --git a/packages/core/src/error/ZodValidationError.ts b/packages/core/src/error/ZodValidationError.ts new file mode 100644 index 0000000000..4cf2ab729e --- /dev/null +++ b/packages/core/src/error/ZodValidationError.ts @@ -0,0 +1,14 @@ +import { z } from 'zod' + +import { formatZodError } from '../utils/zod-error' +import { CredoError } from './CredoError' + +export class ZodValidationError extends CredoError { + public constructor( + message: string, + public readonly zodError: z.ZodError + ) { + const formattedError = formatZodError(zodError) + super(`${message}\n${formattedError}`) + } +} diff --git a/packages/core/src/error/index.ts b/packages/core/src/error/index.ts index 3ed9b8a6a5..7c90ebb0f9 100644 --- a/packages/core/src/error/index.ts +++ b/packages/core/src/error/index.ts @@ -2,3 +2,4 @@ export * from './CredoError' export * from './RecordNotFoundError' export * from './RecordDuplicateError' export * from './ClassValidationError' +export * from './ZodValidationError' diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a157d6f362..666666acab 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -12,16 +12,13 @@ export type { AgentDependencies } from './agent/AgentDependencies' export type { InitConfig, - WalletConfig, JsonArray, JsonObject, JsonValue, ResolvedDidCommService, - WalletConfigRekey, - WalletExportImportConfig, - WalletStorageConfig, + XOR, } from './types' -export { KeyDerivationMethod, EncryptedMessage, PlaintextMessage } from './types' +export type { CanBePromise } from './utils/type' export type { FileSystem, DownloadToFileOptions } from './storage/FileSystem' export * from './storage/BaseRecord' export { Repository } from './storage/Repository' @@ -33,7 +30,6 @@ export { UpdateConfig, V0_1ToV0_2UpdateConfig } from './storage/migration/update export { getDirFromFilePath, joinUriParts } from './utils/path' export { InjectionSymbols } from './constants' -export * from './wallet' export { VersionString } from './utils/version' export * from './plugins' @@ -44,6 +40,7 @@ export * from './modules/cache' export * from './modules/dif-presentation-exchange' export * from './modules/sd-jwt-vc' export * from './modules/mdoc' +export * as Kms from './modules/kms' export * from './modules/dcql' export { JsonEncoder, @@ -63,17 +60,16 @@ export { } from './utils' export * from './logger' export * from './error' -export * from './wallet/error' export type { Constructor, Constructable } from './utils/mixins' export * from './agent/Events' export * from './crypto' // TODO: Clean up these exports used by DIDComm module export { - didKeyToInstanceOfKey, + didKeyToEd25519PublicJwk, didKeyToVerkey, verkeyToDidKey, - verkeyToInstanceOfKey, + verkeyToPublicJwk, isDidKey, } from './modules/dids/helpers' export { tryParseDid } from './modules/dids/domain/parse' diff --git a/packages/core/src/modules/cache/CacheModule.ts b/packages/core/src/modules/cache/CacheModule.ts index 51a2c57b1e..2224026661 100644 --- a/packages/core/src/modules/cache/CacheModule.ts +++ b/packages/core/src/modules/cache/CacheModule.ts @@ -1,31 +1,29 @@ import type { DependencyManager, Module } from '../../plugins' -import type { Optional } from '../../utils' import type { CacheModuleConfigOptions } from './CacheModuleConfig' +import { CachedStorageService } from './CachedStorageService' import { CacheModuleConfig } from './CacheModuleConfig' import { SingleContextLruCacheRepository } from './singleContextLruCache/SingleContextLruCacheRepository' import { SingleContextStorageLruCache } from './singleContextLruCache/SingleContextStorageLruCache' -// CacheModuleOptions makes the credentialProtocols property optional from the config, as it will set it when not provided. -export type CacheModuleOptions = Optional +export type CacheModuleOptions = CacheModuleConfigOptions export class CacheModule implements Module { public readonly config: CacheModuleConfig - public constructor(config?: CacheModuleOptions) { - this.config = new CacheModuleConfig({ - ...config, - cache: - config?.cache ?? - new SingleContextStorageLruCache({ - limit: 500, - }), - }) + public constructor(config: CacheModuleOptions) { + this.config = new CacheModuleConfig(config) } public register(dependencyManager: DependencyManager) { dependencyManager.registerInstance(CacheModuleConfig, this.config) + // Allows us to use the `CachedStorageService` instead of the `StorageService` + // This first checks the local cache to return a record + if (this.config.useCachedStorageService) { + dependencyManager.registerSingleton(CachedStorageService) + } + // Custom handling for when we're using the SingleContextStorageLruCache if (this.config.cache instanceof SingleContextStorageLruCache) { dependencyManager.registerSingleton(SingleContextLruCacheRepository) diff --git a/packages/core/src/modules/cache/CacheModuleConfig.ts b/packages/core/src/modules/cache/CacheModuleConfig.ts index ce5aaf99d7..314809c196 100644 --- a/packages/core/src/modules/cache/CacheModuleConfig.ts +++ b/packages/core/src/modules/cache/CacheModuleConfig.ts @@ -5,14 +5,27 @@ import type { Cache } from './Cache' */ export interface CacheModuleConfigOptions { /** + * * Implementation of the {@link Cache} interface. * - * NOTE: Starting from Credo 0.4.0 the default cache implementation will be {@link InMemoryLruCache} - * @default SingleContextStorageLruCache - with a limit of 500 + */ + cache: Cache + + /** * + * @default 60 * */ - cache: Cache + defaultExpiryInSeconds?: number + + /** + * + * Uses a caching registry before talking to the storage service when a Record has the `useCache` set to `true` + * + * @default false + * + */ + useCachedStorageService?: boolean } export class CacheModuleConfig { @@ -26,4 +39,14 @@ export class CacheModuleConfig { public get cache() { return this.options.cache } + + /** See {@link CacheModuleConfigOptions.defaultExpiryInSeconds} */ + public get defaultExpiryInSeconds() { + return this.options.defaultExpiryInSeconds ?? 60 + } + + /** See {@link CacheModuleConfigOptions.useCachedStorageService} */ + public get useCachedStorageService() { + return this.options.useCachedStorageService ?? false + } } diff --git a/packages/core/src/modules/cache/CachedStorageService.ts b/packages/core/src/modules/cache/CachedStorageService.ts new file mode 100644 index 0000000000..76e1c22394 --- /dev/null +++ b/packages/core/src/modules/cache/CachedStorageService.ts @@ -0,0 +1,77 @@ +import { AgentContext } from '../../agent' +import { BaseRecord } from '../../storage/BaseRecord' +import { BaseRecordConstructor, Query, QueryOptions, StorageService } from '../../storage/StorageService' +import { CacheModuleConfig } from './CacheModuleConfig' + +// biome-ignore lint/suspicious/noExplicitAny: +export class CachedStorageService> implements StorageService { + public constructor(private storageService: StorageService) {} + + private cache(agentContext: AgentContext) { + return agentContext.resolve(CacheModuleConfig).cache + } + + private getCacheKey(options: { type: string; id: string }) { + return `${options.type}:${options.id}` + } + + public async save(agentContext: AgentContext, record: T): Promise { + if (record.useCache) { + await this.cache(agentContext).set(agentContext, this.getCacheKey(record), record.toJSON()) + } + + return await this.storageService.save(agentContext, record) + } + + public async update(agentContext: AgentContext, record: T): Promise { + if (record.useCache) { + await this.cache(agentContext).set(agentContext, this.getCacheKey(record), record.toJSON()) + } + + return await this.storageService.update(agentContext, record) + } + + public async delete(agentContext: AgentContext, record: T): Promise { + if (record.useCache) { + await this.cache(agentContext).remove(agentContext, this.getCacheKey(record)) + } + return await this.storageService.delete(agentContext, record) + } + + public async deleteById( + agentContext: AgentContext, + recordClass: BaseRecordConstructor, + id: string + ): Promise { + if (recordClass.useCache) { + await this.cache(agentContext).remove(agentContext, this.getCacheKey({ ...recordClass, id })) + } + return await this.storageService.deleteById(agentContext, recordClass, id) + } + + public async getById(agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string): Promise { + if (recordClass.useCache) { + const cachedValue = await this.cache(agentContext).get(agentContext, `${recordClass.type}:${id}`) + + // TODO: class transform + if (cachedValue) return cachedValue + } + + return await this.storageService.getById(agentContext, recordClass, id) + } + + // TODO: not in caching interface, yet + public async getAll(agentContext: AgentContext, recordClass: BaseRecordConstructor): Promise { + return await this.storageService.getAll(agentContext, recordClass) + } + + // TODO: not in caching interface, yet + public async findByQuery( + agentContext: AgentContext, + recordClass: BaseRecordConstructor, + query: Query, + queryOptions?: QueryOptions + ): Promise { + return await this.storageService.findByQuery(agentContext, recordClass, query, queryOptions) + } +} diff --git a/packages/core/src/modules/dids/DidsApi.ts b/packages/core/src/modules/dids/DidsApi.ts index 21074ac8e0..9da68b2d3b 100644 --- a/packages/core/src/modules/dids/DidsApi.ts +++ b/packages/core/src/modules/dids/DidsApi.ts @@ -10,13 +10,15 @@ import type { } from './types' import { AgentContext } from '../../agent' -import { CredoError } from '../../error' +import { CredoError, RecordNotFoundError } from '../../error' import { injectable } from '../../plugins' -import { WalletKeyExistsError } from '../../wallet/error' +import { parseDid } from '@sphereon/ssi-types' +import { KeyManagementApi } from '../kms' import { DidsModuleConfig } from './DidsModuleConfig' +import { DidPurpose, getPublicJwkFromVerificationMethod } from './domain' import { getAlternativeDidsForPeerDid, isValidPeerDid } from './methods' -import { DidRepository } from './repository' +import { DidRecord, DidRepository } from './repository' import { DidRegistrarService, DidResolverService } from './services' @injectable() @@ -33,7 +35,8 @@ export class DidsApi { didRegistrarService: DidRegistrarService, didRepository: DidRepository, agentContext: AgentContext, - config: DidsModuleConfig + config: DidsModuleConfig, + _keyManagement: KeyManagementApi ) { this.didResolverService = didResolverService this.didRegistrarService = didRegistrarService @@ -117,7 +120,7 @@ export class DidsApi { * By default, this method will throw an error if the did already exists in the wallet. You can override this behavior by setting * the `overwrite` option to `true`. This will update the did document in the record, and allows you to update the did over time. */ - public async import({ did, didDocument, privateKeys = [], overwrite }: ImportDidOptions) { + public async import({ did, didDocument, keys = [], overwrite }: ImportDidOptions) { if (didDocument && didDocument.id !== did) { throw new CredoError(`Did document id ${didDocument.id} does not match did ${did}`) } @@ -133,29 +136,15 @@ export class DidsApi { didDocument = await this.resolveDidDocument(did) } - // Loop over all private keys and store them in the wallet. We don't check whether the keys are actually associated - // with the did document, this is up to the user. - for (const key of privateKeys) { - try { - // We can't check whether the key already exists in the wallet, but we can try to create it and catch the error - // if the key already exists. - await this.agentContext.wallet.createKey({ - keyType: key.keyType, - privateKey: key.privateKey, - }) - } catch (error) { - if (error instanceof WalletKeyExistsError) { - // If the error is a WalletKeyExistsError, we can ignore it. This means the key - // already exists in the wallet. We don't want to throw an error in this case. - } else { - throw error - } - } + for (const key of keys) { + // Make sure the keys exists in the did document + didDocument.dereferenceKey(key.didDocumentRelativeKeyId) } // Update existing did record if (existingDidRecord) { existingDidRecord.didDocument = didDocument + existingDidRecord.keys = keys existingDidRecord.setTags({ alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(did) : undefined, }) @@ -168,12 +157,57 @@ export class DidsApi { await this.didRepository.storeCreatedDid(this.agentContext, { did, didDocument, + keys, tags: { alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(did) : undefined, }, }) } + public async resolveCreatedDidRecordWithDocument(did: string) { + const [didRecord] = await this.didRepository.getCreatedDids(this.agentContext, { did }) + + if (!didRecord) { + throw new RecordNotFoundError(`Created did '${did}' not found`, { recordType: DidRecord.type }) + } + + if (didRecord.didDocument) { + return { + didRecord, + didDocument: didRecord.didDocument, + } + } + + // TODO: we should somehow store the did document on the record if the did method allows it + // E.g. for did:key we don't want to store it, but if we still have a did:indy record we do want to store it + // If the did document is not stored on the did record, we resolve it + const didDocument = await this.didResolverService.resolveDidDocument(this.agentContext, didRecord.did) + + return { + didRecord, + didDocument, + } + } + + public async resolveVerificationMethodFromCreatedDidRecord( + didUrl: string, + allowedPurposes?: Array + ) { + const parsedDid = parseDid(didUrl) + const { didDocument, didRecord } = await this.resolveCreatedDidRecordWithDocument(parsedDid.did) + + const verificationMethod = didDocument.dereferenceKey(didUrl, allowedPurposes) + const publicJwk = getPublicJwkFromVerificationMethod(verificationMethod) + publicJwk.keyId = + didRecord.keys?.find(({ didDocumentRelativeKeyId }) => verificationMethod.id.endsWith(didDocumentRelativeKeyId)) + ?.kmsKeyId ?? publicJwk.legacyKeyId + + return { + verificationMethod, + publicJwk, + } + } + public get supportedResolverMethods() { return this.didResolverService.supportedMethods } diff --git a/packages/core/src/modules/dids/DidsApiOptions.ts b/packages/core/src/modules/dids/DidsApiOptions.ts index 242227566f..f54a027ad8 100644 --- a/packages/core/src/modules/dids/DidsApiOptions.ts +++ b/packages/core/src/modules/dids/DidsApiOptions.ts @@ -1,10 +1,15 @@ -import type { KeyType } from '../../crypto' -import type { Buffer } from '../../utils' import type { DidDocument } from './domain' -interface PrivateKey { - keyType: KeyType - privateKey: Buffer +export interface DidDocumentKey { + /** + * The key id of the key in the kms associated with the + */ + kmsKeyId: string + + /** + * The key id + */ + didDocumentRelativeKeyId: string } export interface ImportDidOptions { @@ -19,9 +24,12 @@ export interface ImportDidOptions { didDocument?: DidDocument /** - * List of private keys associated with the did document that should be stored in the wallet. + * List of keys associated with the did document, that are managed by the kms of this agent. + * + * NOTE: if no keys are provided, it is not possible to sign or encrypt with keys in the imported + * did document. */ - privateKeys?: PrivateKey[] + keys?: DidDocumentKey[] /** * Whether to overwrite an existing did record if it exists. If set to false, diff --git a/packages/core/src/modules/dids/__tests__/DidsApi.test.ts b/packages/core/src/modules/dids/__tests__/DidsApi.test.ts index fd3406ed8a..632a4add74 100644 --- a/packages/core/src/modules/dids/__tests__/DidsApi.test.ts +++ b/packages/core/src/modules/dids/__tests__/DidsApi.test.ts @@ -1,17 +1,18 @@ -import { getInMemoryAgentOptions } from '../../../../tests/helpers' +import { transformPrivateKeyToPrivateJwk } from '../../../../../askar/src' +import { getAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { isLongFormDidPeer4, isShortFormDidPeer4 } from '../methods/peer/peerDidNumAlgo4' import { DidDocument, DidDocumentService, - KeyType, + PeerDidCreateOptions, PeerDidNumAlgo, TypedArrayEncoder, createPeerDidDocumentFromServices, } from '@credo-ts/core' -const agentOptions = getInMemoryAgentOptions('DidsApi') +const agentOptions = getAgentOptions('DidsApi', undefined, undefined, undefined, { requireDidcomm: true }) const agent = new Agent(agentOptions) @@ -22,33 +23,34 @@ describe('DidsApi', () => { afterAll(async () => { await agent.shutdown() - await agent.wallet.delete() }) test('import an existing did without providing a did document', async () => { - const createKeySpy = jest.spyOn(agent.context.wallet, 'createKey') - // Private key is for public key associated with did:key did - const privateKey = TypedArrayEncoder.fromString('a-sample-seed-of-32-bytes-in-tot') + const privateJwk = transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('a-sample-seed-of-32-bytes-in-tot'), + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + }).privateJwk const did = 'did:key:z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty' + const importedKey = await agent.kms.importKey({ + privateJwk, + }) expect(await agent.dids.getCreatedDids({ did })).toHaveLength(0) await agent.dids.import({ did, - privateKeys: [ + keys: [ { - privateKey, - keyType: KeyType.Ed25519, + didDocumentRelativeKeyId: '#z6MkjEayvPpjVJKFLirX8SomBTPDboHm1XSCkUev2M4siQty', + kmsKeyId: importedKey.keyId, }, ], }) - expect(createKeySpy).toHaveBeenCalledWith({ - privateKey, - keyType: KeyType.Ed25519, - }) - const createdDids = await agent.dids.getCreatedDids({ did, }) @@ -108,10 +110,6 @@ describe('DidsApi', () => { }) test('import an existing did with providing a did document', async () => { - const createKeySpy = jest.spyOn(agent.context.wallet, 'createKey') - - // Private key is for public key associated with did:key did - const privateKey = TypedArrayEncoder.fromString('a-new-sample-seed-of-32-bytes-in') const did = 'did:peer:0z6Mkhu3G8viiebsWmCiSgWiQoCZrTeuX76oLDow81YNYvJQM' expect(await agent.dids.getCreatedDids({ did })).toHaveLength(0) @@ -121,17 +119,6 @@ describe('DidsApi', () => { didDocument: new DidDocument({ id: did, }), - privateKeys: [ - { - privateKey, - keyType: KeyType.Ed25519, - }, - ], - }) - - expect(createKeySpy).toHaveBeenCalledWith({ - privateKey, - keyType: KeyType.Ed25519, }) const createdDids = await agent.dids.getCreatedDids({ @@ -179,7 +166,7 @@ describe('DidsApi', () => { did, didDocument: didDocument2, }) - ).rejects.toThrowError( + ).rejects.toThrow( "A created did did:example:123 already exists. If you want to override the existing did, set the 'overwrite' option to update the did." ) @@ -199,53 +186,26 @@ describe('DidsApi', () => { expect(createdDidsOverwrite[0].didDocument?.service).toHaveLength(1) }) - test('providing privateKeys that already exist is allowd', async () => { - const privateKey = TypedArrayEncoder.fromString('another-samples-seed-of-32-bytes') - - const did = 'did:example:456' - const didDocument = new DidDocument({ id: did }) - - await agent.dids.import({ - did, - didDocument, - privateKeys: [ - { - keyType: KeyType.Ed25519, - privateKey, - }, - ], - }) - - // Provide the same key again, should work - await agent.dids.import({ - did, - didDocument, - overwrite: true, - privateKeys: [ + test('create and resolve did:peer:4 in short and long form', async () => { + const routing = await agent.modules.mediationRecipient.getRouting({}) + const { didDocument, keys } = createPeerDidDocumentFromServices( + [ { - keyType: KeyType.Ed25519, - privateKey, + id: 'didcomm', + recipientKeys: [routing.recipientKey], + routingKeys: routing.routingKeys, + serviceEndpoint: routing.endpoints[0], }, ], - }) - }) - - test('create and resolve did:peer:4 in short and long form', async () => { - const routing = await agent.modules.mediationRecipient.getRouting({}) - const didDocument = createPeerDidDocumentFromServices([ - { - id: 'didcomm', - recipientKeys: [routing.recipientKey], - routingKeys: routing.routingKeys, - serviceEndpoint: routing.endpoints[0], - }, - ]) + true + ) - const result = await agent.dids.create({ + const result = await agent.dids.create({ method: 'peer', didDocument, options: { numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + keys, }, }) diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1.json deleted file mode 100644 index 64ea24fb7e..0000000000 --- a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/bbs/v1"], - "id": "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA", - "verificationMethod": [ - { - "id": "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA", - "type": "Bls12381G1Key2020", - "controller": "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA", - "publicKeyBase58": "6FywSzB5BPd7xehCo1G4nYHAoZPMMP3gd4PLnvgA6SsTsogtz8K7RDznqLpFPLZXAE" - } - ], - "authentication": [ - "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA" - ], - "assertionMethod": [ - "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA" - ], - "capabilityDelegation": [ - "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA" - ], - "capabilityInvocation": [ - "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA" - ] -} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1g2.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1g2.json deleted file mode 100644 index 898bf59d77..0000000000 --- a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1g2.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/bbs/v1"], - "id": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s", - "verificationMethod": [ - { - "id": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", - "type": "Bls12381G1Key2020", - "controller": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s", - "publicKeyBase58": "7BVES4h78wzabPAfMhchXyH5d8EX78S5TtzePH2YkftWcE6by9yj3NTAv9nsyCeYch" - }, - { - "id": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM", - "type": "Bls12381G2Key2020", - "controller": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s", - "publicKeyBase58": "26d2BdqELsXg7ZHCWKL2D5Y2S7mYrpkdhJemSEEvokd4qy4TULJeeU44hYPGKo4x4DbBp5ARzkv1D6xuB3bmhpdpKAXuXtode67wzh9PCtW8kTqQhH19VSiFZkLNkhe9rtf3" - } - ], - "authentication": [ - "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", - "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM" - ], - "assertionMethod": [ - "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", - "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM" - ], - "capabilityDelegation": [ - "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", - "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM" - ], - "capabilityInvocation": [ - "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", - "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM" - ] -} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g2.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g2.json deleted file mode 100644 index 29724406d1..0000000000 --- a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g2.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/bbs/v1"], - "id": "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT", - "verificationMethod": [ - { - "id": "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT", - "type": "Bls12381G2Key2020", - "controller": "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT", - "publicKeyBase58": "mxE4sHTpbPcmxNviRVR9r7D2taXcNyVJmf9TBUFS1gRt3j3Ej9Seo59GQeCzYwbQgDrfWCwEJvmBwjLvheAky5N2NqFVzk4kuq3S8g4Fmekai4P622vHqWjFrsioYYDqhf9" - } - ], - "authentication": [ - "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT" - ], - "assertionMethod": [ - "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT" - ], - "capabilityDelegation": [ - "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT" - ], - "capabilityInvocation": [ - "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT" - ] -} diff --git a/packages/core/src/modules/dids/__tests__/dids-registrar.test.ts b/packages/core/src/modules/dids/__tests__/dids-registrar.test.ts index 457cb2d73d..0a496f5e4e 100644 --- a/packages/core/src/modules/dids/__tests__/dids-registrar.test.ts +++ b/packages/core/src/modules/dids/__tests__/dids-registrar.test.ts @@ -1,14 +1,14 @@ import type { KeyDidCreateOptions } from '../methods/key/KeyDidRegistrar' import type { PeerDidNumAlgo0CreateOptions } from '../methods/peer/PeerDidRegistrar' -import { getInMemoryAgentOptions } from '../../../../tests/helpers' +import { transformPrivateKeyToPrivateJwk } from '@credo-ts/askar' +import { getAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' -import { KeyType } from '../../../crypto' import { PeerDidNumAlgo } from '../methods/peer/didPeer' import { JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' -const agentOptions = getInMemoryAgentOptions('Faber Dids Registrar') +const agentOptions = getAgentOptions('Faber Dids Registrar') describe('dids', () => { let agent: Agent @@ -20,17 +20,25 @@ describe('dids', () => { afterAll(async () => { await agent.shutdown() - await agent.wallet.delete() }) it('should create a did:key did', async () => { + const privateJwk = transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + }).privateJwk + + const { keyId } = await agent.kms.importKey({ + privateJwk, + }) + const did = await agent.dids.create({ method: 'key', options: { - keyType: KeyType.Ed25519, - }, - secret: { - privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + keyId, }, }) @@ -80,23 +88,29 @@ describe('dids', () => { ], id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, }, }) }) it('should create a did:peer did', async () => { - const privateKey = TypedArrayEncoder.fromString('e008ef10b7c163114b3857542b3736eb') + const privateJwk = transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('e008ef10b7c163114b3857542b3736eb'), + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + }).privateJwk + + const { keyId } = await agent.kms.importKey({ + privateJwk, + }) const did = await agent.dids.create({ method: 'peer', options: { - keyType: KeyType.Ed25519, + keyId, numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, }, - secret: { - privateKey, - }, }) // Same seed should resolve to same did:peer @@ -145,7 +159,6 @@ describe('dids', () => { ], id: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', }, - secret: { privateKey }, }, }) }) diff --git a/packages/core/src/modules/dids/__tests__/dids-resolver.test.ts b/packages/core/src/modules/dids/__tests__/dids-resolver.test.ts index feba6ee688..65b9e9039e 100644 --- a/packages/core/src/modules/dids/__tests__/dids-resolver.test.ts +++ b/packages/core/src/modules/dids/__tests__/dids-resolver.test.ts @@ -1,8 +1,8 @@ -import { getInMemoryAgentOptions } from '../../../../tests/helpers' +import { getAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { JsonTransformer } from '../../../utils' -const agent = new Agent(getInMemoryAgentOptions('Faber Dids')) +const agent = new Agent(getAgentOptions('Faber Dids')) describe('dids', () => { beforeAll(async () => { @@ -11,7 +11,6 @@ describe('dids', () => { afterAll(async () => { await agent.shutdown() - await agent.wallet.delete() }) it('should resolve a did:key did', async () => { diff --git a/packages/core/src/modules/dids/__tests__/keyDidDocument.test.ts b/packages/core/src/modules/dids/__tests__/keyDidDocument.test.ts index 28a3f24b2e..291c18a22b 100644 --- a/packages/core/src/modules/dids/__tests__/keyDidDocument.test.ts +++ b/packages/core/src/modules/dids/__tests__/keyDidDocument.test.ts @@ -1,54 +1,24 @@ import { JsonTransformer } from '../../../utils/JsonTransformer' -import { getDidDocumentForKey } from '../domain/keyDidDocument' +import { getDidDocumentForPublicJwk } from '../domain/keyDidDocument' import { DidKey } from '../methods/key' - -import didKeyBls12381g1Fixture from './__fixtures__/didKeyBls12381g1.json' -import didKeyBls12381g1g2Fixture from './__fixtures__/didKeyBls12381g1g2.json' -import didKeyBls12381g2Fixture from './__fixtures__/didKeyBls12381g2.json' import didKeyEd25519Fixture from './__fixtures__/didKeyEd25519.json' import didKeyX25519Fixture from './__fixtures__/didKeyX25519.json' const TEST_X25519_DID = 'did:key:z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE' const TEST_ED25519_DID = 'did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th' -const TEST_BLS12381G1_DID = 'did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA' -const TEST_BLS12381G2_DID = - 'did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT' -const TEST_BLS12381G1G2_DID = - 'did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s' -describe('getDidDocumentForKey', () => { +describe('getDidDocumentForPublicJwk', () => { it('should return a valid did:key did document for and x25519 key', () => { const didKey = DidKey.fromDid(TEST_X25519_DID) - const didDocument = getDidDocumentForKey(TEST_X25519_DID, didKey.key) + const didDocument = getDidDocumentForPublicJwk(TEST_X25519_DID, didKey.publicJwk) expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyX25519Fixture) }) it('should return a valid did:key did document for and ed25519 key', () => { const didKey = DidKey.fromDid(TEST_ED25519_DID) - const didDocument = getDidDocumentForKey(TEST_ED25519_DID, didKey.key) + const didDocument = getDidDocumentForPublicJwk(TEST_ED25519_DID, didKey.publicJwk) expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyEd25519Fixture) }) - - it('should return a valid did:key did document for and bls12381g1 key', () => { - const didKey = DidKey.fromDid(TEST_BLS12381G1_DID) - const didDocument = getDidDocumentForKey(TEST_BLS12381G1_DID, didKey.key) - - expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyBls12381g1Fixture) - }) - - it('should return a valid did:key did document for and bls12381g2 key', () => { - const didKey = DidKey.fromDid(TEST_BLS12381G2_DID) - const didDocument = getDidDocumentForKey(TEST_BLS12381G2_DID, didKey.key) - - expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyBls12381g2Fixture) - }) - - it('should return a valid did:key did document for and bls12381g1g2 key', () => { - const didKey = DidKey.fromDid(TEST_BLS12381G1G2_DID) - const didDocument = getDidDocumentForKey(TEST_BLS12381G1G2_DID, didKey.key) - - expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyBls12381g1g2Fixture) - }) }) diff --git a/packages/didcomm/src/util/__tests__/matchingEd25519Key.test.ts b/packages/core/src/modules/dids/__tests__/matchingEd25519Key.test.ts similarity index 56% rename from packages/didcomm/src/util/__tests__/matchingEd25519Key.test.ts rename to packages/core/src/modules/dids/__tests__/matchingEd25519Key.test.ts index 78a9d8e04b..8622de3471 100644 --- a/packages/didcomm/src/util/__tests__/matchingEd25519Key.test.ts +++ b/packages/core/src/modules/dids/__tests__/matchingEd25519Key.test.ts @@ -1,8 +1,7 @@ -import type { VerificationMethod } from '@credo-ts/core' - -import { DidDocument, Key, KeyType } from '@credo-ts/core' - -import { findMatchingEd25519Key } from '../matchingEd25519Key' +import { TypedArrayEncoder } from '../../../utils' +import { PublicJwk, X25519PublicJwk } from '../../kms' +import { DidDocument, VerificationMethod } from '../domain' +import { findMatchingEd25519Key } from '../findMatchingEd25519Key' describe('findMatchingEd25519Key', () => { const publicKeyBase58Ed25519 = 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8' @@ -36,19 +35,34 @@ describe('findMatchingEd25519Key', () => { }) test('returns matching Ed25519 key if corresponding X25519 key supplied', () => { - const x25519Key = Key.fromPublicKeyBase58(publicKeyBase58X25519, KeyType.X25519) + const x25519Key = PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'X25519', + publicKey: TypedArrayEncoder.fromBase58(publicKeyBase58X25519), + }) const ed25519Key = findMatchingEd25519Key(x25519Key, didDocument) - expect(ed25519Key?.publicKeyBase58).toBe(Ed25519VerificationMethod.publicKeyBase58) + // biome-ignore lint/style/noNonNullAssertion: + expect(TypedArrayEncoder.toBase58(ed25519Key?.publicJwk.publicKey.publicKey!)).toBe( + Ed25519VerificationMethod.publicKeyBase58 + ) }) test('returns undefined if non-corresponding X25519 key supplied', () => { - const differentX25519Key = Key.fromPublicKeyBase58('Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', KeyType.X25519) + const differentX25519Key = PublicJwk.fromPublicKey({ + publicKey: TypedArrayEncoder.fromBase58('Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt'), + kty: 'OKP', + crv: 'X25519', + }) expect(findMatchingEd25519Key(differentX25519Key, didDocument)).toBeUndefined() }) test('returns undefined if ed25519 key supplied', () => { - const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) - expect(findMatchingEd25519Key(ed25519Key, didDocument)).toBeUndefined() + const ed25519Key = PublicJwk.fromPublicKey({ + publicKey: TypedArrayEncoder.fromBase58(publicKeyBase58Ed25519), + kty: 'OKP', + crv: 'Ed25519', + }) + expect(findMatchingEd25519Key(ed25519Key as unknown as PublicJwk, didDocument)).toBeUndefined() }) }) @@ -66,19 +80,34 @@ describe('findMatchingEd25519Key', () => { }) test('returns matching Ed25519 key if corresponding X25519 key supplied', () => { - const x25519Key = Key.fromPublicKeyBase58(publicKeyBase58X25519, KeyType.X25519) + const x25519Key = PublicJwk.fromPublicKey({ + publicKey: TypedArrayEncoder.fromBase58(publicKeyBase58X25519), + kty: 'OKP', + crv: 'X25519', + }) const ed25519Key = findMatchingEd25519Key(x25519Key, didDocument) - expect(ed25519Key?.publicKeyBase58).toBe(Ed25519VerificationMethod.publicKeyBase58) + // biome-ignore lint/style/noNonNullAssertion: + expect(TypedArrayEncoder.toBase58(ed25519Key?.publicJwk.publicKey.publicKey!)).toBe( + Ed25519VerificationMethod.publicKeyBase58 + ) }) test('returns undefined if non-corresponding X25519 key supplied', () => { - const differentX25519Key = Key.fromPublicKeyBase58('Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', KeyType.X25519) + const differentX25519Key = PublicJwk.fromPublicKey({ + publicKey: TypedArrayEncoder.fromBase58('Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt'), + kty: 'OKP', + crv: 'X25519', + }) expect(findMatchingEd25519Key(differentX25519Key, didDocument)).toBeUndefined() }) test('returns undefined if ed25519 key supplied', () => { - const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) - expect(findMatchingEd25519Key(ed25519Key, didDocument)).toBeUndefined() + const ed25519Key = PublicJwk.fromPublicKey({ + publicKey: TypedArrayEncoder.fromBase58(publicKeyBase58Ed25519), + kty: 'OKP', + crv: 'Ed25519', + }) + expect(findMatchingEd25519Key(ed25519Key as unknown as PublicJwk, didDocument)).toBeUndefined() }) }) }) diff --git a/packages/core/src/modules/dids/__tests__/peer-did.test.ts b/packages/core/src/modules/dids/__tests__/peer-did.test.ts index cae258e7ca..c489553838 100644 --- a/packages/core/src/modules/dids/__tests__/peer-did.test.ts +++ b/packages/core/src/modules/dids/__tests__/peer-did.test.ts @@ -1,21 +1,17 @@ import type { AgentContext } from '../../../agent' -import type { Wallet } from '../../../wallet' import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' -import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' import { getAgentConfig, getAgentContext } from '../../../../tests/helpers' import { EventEmitter } from '../../../agent/EventEmitter' import { InjectionSymbols } from '../../../constants' -import { Key, KeyType } from '../../../crypto' import { JsonTransformer, TypedArrayEncoder } from '../../../utils' import { DidsModuleConfig } from '../DidsModuleConfig' import { DidCommV1Service, DidDocument, DidDocumentBuilder, - convertPublicKeyToX25519, getEd25519VerificationKey2018, getX25519KeyAgreementKey2019, } from '../domain' @@ -27,6 +23,8 @@ import { didDocumentJsonToNumAlgo1Did } from '../methods/peer/peerDidNumAlgo1' import { DidRecord, DidRepository } from '../repository' import { DidResolverService } from '../services' +import { transformPrivateKeyToPrivateJwk } from '../../../../../askar/src' +import { KeyManagementApi, PublicJwk } from '../../kms' import didPeer1zQmY from './__fixtures__/didPeer1zQmY.json' describe('peer dids', () => { @@ -34,24 +32,23 @@ describe('peer dids', () => { let didRepository: DidRepository let didResolverService: DidResolverService - let wallet: Wallet let agentContext: AgentContext let eventEmitter: EventEmitter + let kms: KeyManagementApi beforeEach(async () => { - wallet = new InMemoryWallet() const storageService = new InMemoryStorageService() eventEmitter = new EventEmitter(config.agentDependencies, new Subject()) didRepository = new DidRepository(storageService, eventEmitter) agentContext = getAgentContext({ - wallet, registerInstances: [ [DidRepository, didRepository], [InjectionSymbols.StorageService, storageService], ], + agentConfig: getAgentConfig('peer-did'), }) - await wallet.createAndOpen(config.walletConfig) + kms = agentContext.resolve(KeyManagementApi) didResolverService = new DidResolverService( config.logger, @@ -60,30 +57,39 @@ describe('peer dids', () => { ) }) - afterEach(async () => { - await wallet.delete() - }) - test('create a peer did method 1 document from ed25519 keys with a service', async () => { // The following scenario show how we could create a key and create a did document from it for DID Exchange - const ed25519Key = await wallet.createKey({ - privateKey: TypedArrayEncoder.fromString('astringoftotalin32characterslong'), - keyType: KeyType.Ed25519, + const ed25519Key = await kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('astringoftotalin32characterslong'), + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }).privateJwk, }) - const mediatorEd25519Key = await wallet.createKey({ - privateKey: TypedArrayEncoder.fromString('anotherstringof32characterslong1'), - keyType: KeyType.Ed25519, + const ed25519PublicJwk = PublicJwk.fromPublicJwk(ed25519Key.publicJwk) + + const mediatorEd25519Key = await kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('anotherstringof32characterslong1'), + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }).privateJwk, }) + const mediatorEd25519PublicJwk = PublicJwk.fromPublicJwk(mediatorEd25519Key.publicJwk) - const x25519Key = Key.fromPublicKey(convertPublicKeyToX25519(ed25519Key.publicKey), KeyType.X25519) + const x25519PublicJwk = PublicJwk.fromPublicJwk(ed25519PublicJwk.jwk.toX25519PublicJwk()) const ed25519VerificationMethod = getEd25519VerificationKey2018({ // The id can either be the first 8 characters of the key data (for ed25519 it's publicKeyBase58) // uuid is easier as it is consistent between different key types. Normally you would dynamically // generate the uuid, but static for testing purposes id: '#d0d32199-851f-48e3-b178-6122bd4216a4', - key: ed25519Key, + publicJwk: ed25519PublicJwk, // For peer dids generated with method 1, the controller MUST be #id as we don't know the did yet controller: '#id', }) @@ -92,16 +98,16 @@ describe('peer dids', () => { // uuid is easier as it is consistent between different key types. Normally you would dynamically // generate the uuid, but static for testing purposes id: '#08673492-3c44-47fe-baa4-a1780c585d75', - key: x25519Key, + publicJwk: x25519PublicJwk, // For peer dids generated with method 1, the controller MUST be #id as we don't know the did yet controller: '#id', }) - const mediatorEd25519DidKey = new DidKey(mediatorEd25519Key) - const mediatorX25519Key = Key.fromPublicKey(convertPublicKeyToX25519(mediatorEd25519Key.publicKey), KeyType.X25519) + const mediatorEd25519DidKey = new DidKey(mediatorEd25519PublicJwk) + const mediatorX25519PublicJwk = PublicJwk.fromPublicJwk(mediatorEd25519PublicJwk.jwk.toX25519PublicJwk()) // Use ed25519 did:key, which also includes the x25519 key used for didcomm - const mediatorRoutingKey = `${mediatorEd25519DidKey.did}#${mediatorX25519Key.fingerprint}` + const mediatorRoutingKey = `${mediatorEd25519DidKey.did}#${mediatorX25519PublicJwk.fingerprint}` const service = new DidCommV1Service({ id: '#service-0', diff --git a/packages/core/src/modules/dids/domain/DidDocument.ts b/packages/core/src/modules/dids/domain/DidDocument.ts index e96bd8584b..7a57b718f0 100644 --- a/packages/core/src/modules/dids/domain/DidDocument.ts +++ b/packages/core/src/modules/dids/domain/DidDocument.ts @@ -2,14 +2,14 @@ import type { DidDocumentService } from './service' import { Expose, Type } from 'class-transformer' import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator' - -import { Key } from '../../../crypto/Key' -import { KeyType } from '../../../crypto/KeyType' import { CredoError } from '../../../error' import { JsonTransformer } from '../../../utils/JsonTransformer' import { IsStringOrStringArray } from '../../../utils/transformers' -import { getKeyFromVerificationMethod } from './key-type' +import { TypedArrayEncoder } from '../../../utils' +import { Ed25519PublicJwk, PublicJwk, X25519PublicJwk } from '../../kms' +import { findMatchingEd25519Key } from '../findMatchingEd25519Key' +import { getPublicJwkFromVerificationMethod } from './key-type' import { DidCommV1Service, IndyAgentService, ServiceTransformer } from './service' import { IsStringOrVerificationMethod, VerificationMethod, VerificationMethodTransformer } from './verificationMethod' @@ -149,6 +149,30 @@ export class DidDocument { throw new CredoError(`Unable to locate verification method with id '${keyId}' in purposes ${purposes}`) } + public findVerificationMethodByPublicKey(publicJwk: PublicJwk, allowedPurposes?: DidVerificationMethods[]) { + const allPurposes: DidVerificationMethods[] = [ + 'authentication', + 'keyAgreement', + 'assertionMethod', + 'capabilityInvocation', + 'capabilityDelegation', + 'verificationMethod', + ] + + const purposes = allowedPurposes ?? allPurposes + + for (const purpose of purposes) { + for (const key of this[purpose] ?? []) { + const verificationMethod = typeof key === 'string' ? this.dereferenceVerificationMethod(key) : key + if (getPublicJwkFromVerificationMethod(verificationMethod).equals(publicJwk)) return verificationMethod + } + } + + throw new CredoError( + `Unable to locate verification method with public key ${publicJwk.jwkTypehumanDescription} in purposes ${purposes}` + ) + } + /** * Returns all of the service endpoints matching the given type. * @@ -184,31 +208,113 @@ export class DidDocument { } // TODO: it would probably be easier if we add a utility to each service so we don't have to handle logic for all service types here - public get recipientKeys(): Key[] { - let recipientKeys: Key[] = [] + public get recipientKeys(): PublicJwk[] { + return this.getRecipientKeysWithVerificationMethod({ + // False for now to avoid breaking changes + mapX25519ToEd25519: false, + }).map(({ publicJwk }) => publicJwk) + } + /** + * Returns the recipient keys with their verification method matches + * + * We should probably deprecate recipientKeys in favour of this one + */ + public getRecipientKeysWithVerificationMethod({ + mapX25519ToEd25519, + }: { mapX25519ToEd25519: MapX25519ToEd25519 }): Array<{ + verificationMethod: VerificationMethod + publicJwk: PublicJwk + }> { + const recipientKeys: Array<{ + verificationMethod: VerificationMethod + publicJwk: PublicJwk + }> = [] + + const seenVerificationMethodIds: string[] = [] for (const service of this.didCommServices) { if (service.type === IndyAgentService.type) { - recipientKeys = [ - ...recipientKeys, - ...service.recipientKeys.map((publicKeyBase58) => Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519)), - ] + for (const publicKeyBase58 of service.recipientKeys) { + const publicJwk = PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58(publicKeyBase58), + }) + const verificationMethod = [...(this.verificationMethod ?? []), ...(this.authentication ?? [])] + .map((v) => (typeof v === 'string' ? this.dereferenceVerificationMethod(v) : v)) + .find((v) => { + const vPublicJwk = getPublicJwkFromVerificationMethod(v) + return vPublicJwk.equals(publicJwk) + }) + + if (!verificationMethod) { + throw new CredoError('Could not find verification method for IndyAgentService recipient key') + } + + // Skip adding if already present + if (seenVerificationMethodIds.includes(verificationMethod.id)) { + continue + } + + recipientKeys.push({ + publicJwk, + verificationMethod, + }) + } } else if (service.type === DidCommV1Service.type) { - recipientKeys = [ - ...recipientKeys, - ...service.recipientKeys.map((recipientKey) => - getKeyFromVerificationMethod(this.dereferenceKey(recipientKey, ['authentication', 'keyAgreement'])) - ), - ] + for (const recipientKey of service.recipientKeys) { + const verificationMethod = this.dereferenceKey(recipientKey, ['authentication', 'keyAgreement']) + if (seenVerificationMethodIds.includes(verificationMethod.id)) { + // Skip adding if already present + continue + } + + const publicJwk = getPublicJwkFromVerificationMethod(verificationMethod) + + if (!publicJwk.is(Ed25519PublicJwk, X25519PublicJwk)) { + throw new CredoError( + 'Expected either Ed25519PublicJwk or X25519PublicJwk for DidcommV1Service recipient key' + ) + } + + recipientKeys.push({ + publicJwk, + verificationMethod, + }) + } } } - return recipientKeys + if (!mapX25519ToEd25519) { + return recipientKeys as Array<{ + verificationMethod: VerificationMethod + publicJwk: PublicJwk + }> + } + + return recipientKeys.map(({ publicJwk, verificationMethod }) => { + if (publicJwk.is(Ed25519PublicJwk)) return { publicJwk, verificationMethod } + + const matchingEd25519Key = findMatchingEd25519Key(publicJwk as PublicJwk, this) + + // For DIDcomm v1 if you use X25519 you MUST also include the Ed25519 key + if (!matchingEd25519Key) { + throw new CredoError( + `Unable to find matching Ed25519 key for X25519 verification method with id ${verificationMethod.id}` + ) + } + + return matchingEd25519Key + }) } public toJSON() { return JsonTransformer.toJSON(this) } + + public static fromJSON(didDocument: unknown) { + return JsonTransformer.fromJSON(didDocument, DidDocument) + } } /** diff --git a/packages/core/src/modules/dids/domain/didDocumentKey.ts b/packages/core/src/modules/dids/domain/didDocumentKey.ts new file mode 100644 index 0000000000..5d84cb381c --- /dev/null +++ b/packages/core/src/modules/dids/domain/didDocumentKey.ts @@ -0,0 +1,7 @@ +import { DidDocumentKey } from '../DidsApiOptions' +import { VerificationMethod } from './verificationMethod' + +export function getKmsKeyIdForVerifiacationMethod(verificationMethod: VerificationMethod, keys?: DidDocumentKey[]) { + return keys?.find(({ didDocumentRelativeKeyId }) => verificationMethod.id.endsWith(didDocumentRelativeKeyId)) + ?.kmsKeyId +} diff --git a/packages/core/src/modules/dids/domain/index.ts b/packages/core/src/modules/dids/domain/index.ts index 27b0ca3633..e5d601ce30 100644 --- a/packages/core/src/modules/dids/domain/index.ts +++ b/packages/core/src/modules/dids/domain/index.ts @@ -7,3 +7,4 @@ export * from './DidRegistrar' export * from './DidResolver' export * from './key-type' export { parseDid } from './parse' +export { getKmsKeyIdForVerifiacationMethod } from './didDocumentKey' diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1.test.ts deleted file mode 100644 index fc05105baa..0000000000 --- a/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { KeyType } from '../../../../../crypto' -import { Key } from '../../../../../crypto/Key' -import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' -import keyBls12381g1Fixture from '../../../__tests__/__fixtures__/didKeyBls12381g1.json' -import { VerificationMethod } from '../../verificationMethod' -import { keyDidBls12381g1 } from '../bls12381g1' - -const TEST_BLS12381G1_BASE58_KEY = '6FywSzB5BPd7xehCo1G4nYHAoZPMMP3gd4PLnvgA6SsTsogtz8K7RDznqLpFPLZXAE' -const TEST_BLS12381G1_FINGERPRINT = 'z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA' -const TEST_BLS12381G1_DID = `did:key:${TEST_BLS12381G1_FINGERPRINT}` -const TEST_BLS12381G1_PREFIX_BYTES = Buffer.concat([ - new Uint8Array([234, 1]), - TypedArrayEncoder.fromBase58(TEST_BLS12381G1_BASE58_KEY), -]) - -describe('bls12381g1', () => { - it('creates a Key instance from public key bytes and bls12381g1 key type', async () => { - const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_BLS12381G1_BASE58_KEY) - - const key = Key.fromPublicKey(publicKeyBytes, KeyType.Bls12381g1) - - expect(key.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) - }) - - it('creates a Key instance from a base58 encoded public key and bls12381g1 key type', async () => { - const key = Key.fromPublicKeyBase58(TEST_BLS12381G1_BASE58_KEY, KeyType.Bls12381g1) - - expect(key.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) - }) - - it('creates a Key instance from a fingerprint', async () => { - const key = Key.fromFingerprint(TEST_BLS12381G1_FINGERPRINT) - - expect(key.publicKeyBase58).toBe(TEST_BLS12381G1_BASE58_KEY) - }) - - it('should correctly calculate the getter properties', async () => { - const key = Key.fromFingerprint(TEST_BLS12381G1_FINGERPRINT) - - expect(key.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) - expect(key.publicKeyBase58).toBe(TEST_BLS12381G1_BASE58_KEY) - expect(key.publicKey).toEqual(Uint8Array.from(TypedArrayEncoder.fromBase58(TEST_BLS12381G1_BASE58_KEY))) - expect(key.keyType).toBe(KeyType.Bls12381g1) - expect(Buffer.from(key.prefixedPublicKey).equals(TEST_BLS12381G1_PREFIX_BYTES)).toBe(true) - }) - - it('should return a valid verification method', async () => { - const key = Key.fromFingerprint(TEST_BLS12381G1_FINGERPRINT) - const verificationMethods = keyDidBls12381g1.getVerificationMethods(TEST_BLS12381G1_DID, key) - - expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([keyBls12381g1Fixture.verificationMethod[0]]) - }) - - it('supports Bls12381G1Key2020 verification method type', () => { - expect(keyDidBls12381g1.supportedVerificationMethodTypes).toMatchObject(['Bls12381G1Key2020']) - }) - - it('returns key for Bls12381G1Key2020 verification method', () => { - const verificationMethod = JsonTransformer.fromJSON(keyBls12381g1Fixture.verificationMethod[0], VerificationMethod) - - const key = keyDidBls12381g1.getKeyFromVerificationMethod(verificationMethod) - - expect(key.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) - }) - - it('throws an error if an invalid verification method is passed', () => { - const verificationMethod = JsonTransformer.fromJSON(keyBls12381g1Fixture.verificationMethod[0], VerificationMethod) - - verificationMethod.type = 'SomeRandomType' - - expect(() => keyDidBls12381g1.getKeyFromVerificationMethod(verificationMethod)).toThrow( - "Verification method with type 'SomeRandomType' not supported for key type 'bls12381g1'" - ) - }) -}) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1g2.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1g2.test.ts deleted file mode 100644 index 85ac115900..0000000000 --- a/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1g2.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { KeyType } from '../../../../../crypto' -import { Key } from '../../../../../crypto/Key' -import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' -import keyBls12381g1g2Fixture from '../../../__tests__/__fixtures__/didKeyBls12381g1g2.json' -import { VerificationMethod } from '../../verificationMethod' -import { keyDidBls12381g1g2 } from '../bls12381g1g2' - -const TEST_BLS12381G1G2_BASE58_KEY = - 'AQ4MiG1JKHmM5N4CgkF9uQ484PHN7gXB3ctF4ayL8hT6FdD6rcfFS3ZnMNntYsyJBckfNPf3HL8VU8jzgyT3qX88Yg3TeF2NkG2aZnJDNnXH1jkJStWMxjLw22LdphqAj1rSorsDhHjE8Rtz61bD6FP9aPokQUDVpZ4zXqsXVcxJ7YEc66TTLTTPwQPS7uNM4u2Fs' -const TEST_BLS12381G1G2_FINGERPRINT = - 'z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s' -const TEST_BLS12381G1G2_DID = `did:key:${TEST_BLS12381G1G2_FINGERPRINT}` - -const TEST_BLS12381G1_BASE58_KEY = '7BVES4h78wzabPAfMhchXyH5d8EX78S5TtzePH2YkftWcE6by9yj3NTAv9nsyCeYch' -const TEST_BLS12381G1_FINGERPRINT = 'z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd' - -const TEST_BLS12381G2_BASE58_KEY = - '26d2BdqELsXg7ZHCWKL2D5Y2S7mYrpkdhJemSEEvokd4qy4TULJeeU44hYPGKo4x4DbBp5ARzkv1D6xuB3bmhpdpKAXuXtode67wzh9PCtW8kTqQhH19VSiFZkLNkhe9rtf3' -const TEST_BLS12381G2_FINGERPRINT = - 'zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM' - -const TEST_BLS12381G1G2_PREFIX_BYTES = Buffer.concat([ - new Uint8Array([238, 1]), - TypedArrayEncoder.fromBase58(TEST_BLS12381G1G2_BASE58_KEY), -]) - -describe('bls12381g1g2', () => { - it('creates a Key instance from public key bytes and bls12381g1g2 key type', async () => { - const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_BLS12381G1G2_BASE58_KEY) - - const key = Key.fromPublicKey(publicKeyBytes, KeyType.Bls12381g1g2) - - expect(key.fingerprint).toBe(TEST_BLS12381G1G2_FINGERPRINT) - }) - - it('creates a Key instance from a base58 encoded public key and bls12381g1g2 key type', async () => { - const key = Key.fromPublicKeyBase58(TEST_BLS12381G1G2_BASE58_KEY, KeyType.Bls12381g1g2) - - expect(key.fingerprint).toBe(TEST_BLS12381G1G2_FINGERPRINT) - }) - - it('creates a Key instance from a fingerprint', async () => { - const key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) - - expect(key.publicKeyBase58).toBe(TEST_BLS12381G1G2_BASE58_KEY) - }) - - it('should correctly calculate the getter properties', async () => { - const key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) - - expect(key.fingerprint).toBe(TEST_BLS12381G1G2_FINGERPRINT) - expect(key.publicKeyBase58).toBe(TEST_BLS12381G1G2_BASE58_KEY) - expect(key.publicKey).toEqual(Uint8Array.from(TypedArrayEncoder.fromBase58(TEST_BLS12381G1G2_BASE58_KEY))) - expect(key.keyType).toBe(KeyType.Bls12381g1g2) - expect(Buffer.from(key.prefixedPublicKey).equals(TEST_BLS12381G1G2_PREFIX_BYTES)).toBe(true) - }) - - it('should return a valid verification method', async () => { - const key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) - const verificationMethods = keyDidBls12381g1g2.getVerificationMethods(TEST_BLS12381G1G2_DID, key) - - expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject(keyBls12381g1g2Fixture.verificationMethod) - }) - - it('supports no verification method type', () => { - // Verification methods can be handled by g1 or g2 key types. No reason to do it in here - expect(keyDidBls12381g1g2.supportedVerificationMethodTypes).toMatchObject([]) - }) - - it('throws an error for getKeyFromVerificationMethod as it is not supported for bls12381g1g2 key types', () => { - const verificationMethod = JsonTransformer.fromJSON( - keyBls12381g1g2Fixture.verificationMethod[0], - VerificationMethod - ) - - expect(() => keyDidBls12381g1g2.getKeyFromVerificationMethod(verificationMethod)).toThrow( - 'Not supported for bls12381g1g2 key' - ) - }) - - it('should correctly go from g1g2 to g1', async () => { - const g1g2Key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) - - const g1PublicKey = g1g2Key.publicKey.slice(0, 48) - const g1DidKey = Key.fromPublicKey(g1PublicKey, KeyType.Bls12381g1) - - expect(g1DidKey.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) - expect(g1DidKey.publicKeyBase58).toBe(TEST_BLS12381G1_BASE58_KEY) - expect(g1DidKey.publicKey).toEqual(Uint8Array.from(TypedArrayEncoder.fromBase58(TEST_BLS12381G1_BASE58_KEY))) - expect(g1DidKey.keyType).toBe(KeyType.Bls12381g1) - }) - - it('should correctly go from g1g2 to g2', async () => { - const g1g2Key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) - - const g2PublicKey = g1g2Key.publicKey.slice(48) - const g2DidKey = Key.fromPublicKey(g2PublicKey, KeyType.Bls12381g2) - - expect(g2DidKey.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) - expect(g2DidKey.publicKeyBase58).toBe(TEST_BLS12381G2_BASE58_KEY) - expect(g2DidKey.publicKey).toEqual(Uint8Array.from(TypedArrayEncoder.fromBase58(TEST_BLS12381G2_BASE58_KEY))) - expect(g2DidKey.keyType).toBe(KeyType.Bls12381g2) - }) -}) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g2.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g2.test.ts deleted file mode 100644 index 254455e1ae..0000000000 --- a/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g2.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { KeyType } from '../../../../../crypto' -import { Key } from '../../../../../crypto/Key' -import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' -import keyBls12381g2Fixture from '../../../__tests__/__fixtures__/didKeyBls12381g2.json' -import { VerificationMethod } from '../../verificationMethod' -import { keyDidBls12381g2 } from '../bls12381g2' - -const TEST_BLS12381G2_BASE58_KEY = - 'mxE4sHTpbPcmxNviRVR9r7D2taXcNyVJmf9TBUFS1gRt3j3Ej9Seo59GQeCzYwbQgDrfWCwEJvmBwjLvheAky5N2NqFVzk4kuq3S8g4Fmekai4P622vHqWjFrsioYYDqhf9' -const TEST_BLS12381G2_FINGERPRINT = - 'zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT' -const TEST_BLS12381G2_DID = `did:key:${TEST_BLS12381G2_FINGERPRINT}` -const TEST_BLS12381G2_PREFIX_BYTES = Buffer.concat([ - new Uint8Array([235, 1]), - TypedArrayEncoder.fromBase58(TEST_BLS12381G2_BASE58_KEY), -]) - -describe('bls12381g2', () => { - it('creates a Key instance from public key bytes and bls12381g2 key type', async () => { - const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_BLS12381G2_BASE58_KEY) - - const key = Key.fromPublicKey(publicKeyBytes, KeyType.Bls12381g2) - - expect(key.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) - }) - - it('creates a Key instance from a base58 encoded public key and bls12381g2 key type', async () => { - const key = Key.fromPublicKeyBase58(TEST_BLS12381G2_BASE58_KEY, KeyType.Bls12381g2) - - expect(key.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) - }) - - it('creates a Key instance from a fingerprint', async () => { - const key = Key.fromFingerprint(TEST_BLS12381G2_FINGERPRINT) - - expect(key.publicKeyBase58).toBe(TEST_BLS12381G2_BASE58_KEY) - }) - - it('should correctly calculate the getter properties', async () => { - const key = Key.fromFingerprint(TEST_BLS12381G2_FINGERPRINT) - - expect(key.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) - expect(key.publicKeyBase58).toBe(TEST_BLS12381G2_BASE58_KEY) - expect(key.publicKey).toEqual(Uint8Array.from(TypedArrayEncoder.fromBase58(TEST_BLS12381G2_BASE58_KEY))) - expect(key.keyType).toBe(KeyType.Bls12381g2) - expect(Buffer.from(key.prefixedPublicKey).equals(TEST_BLS12381G2_PREFIX_BYTES)).toBe(true) - }) - - it('should return a valid verification method', async () => { - const key = Key.fromFingerprint(TEST_BLS12381G2_FINGERPRINT) - const verificationMethods = keyDidBls12381g2.getVerificationMethods(TEST_BLS12381G2_DID, key) - - expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([keyBls12381g2Fixture.verificationMethod[0]]) - }) - - it('supports Bls12381G2Key2020 verification method type', () => { - expect(keyDidBls12381g2.supportedVerificationMethodTypes).toMatchObject(['Bls12381G2Key2020']) - }) - - it('returns key for Bls12381G2Key2020 verification method', () => { - const verificationMethod = JsonTransformer.fromJSON(keyBls12381g2Fixture.verificationMethod[0], VerificationMethod) - - const key = keyDidBls12381g2.getKeyFromVerificationMethod(verificationMethod) - - expect(key.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) - }) - - it('throws an error if an invalid verification method is passed', () => { - const verificationMethod = JsonTransformer.fromJSON(keyBls12381g2Fixture.verificationMethod[0], VerificationMethod) - - verificationMethod.type = 'SomeRandomType' - - expect(() => keyDidBls12381g2.getKeyFromVerificationMethod(verificationMethod)).toThrowError( - "Verification method with type 'SomeRandomType' not supported for key type 'bls12381g2'" - ) - }) -}) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts index cf86cc533d..a78d794c05 100644 --- a/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts @@ -1,51 +1,15 @@ -import { KeyType } from '../../../../../crypto' -import { Key } from '../../../../../crypto/Key' -import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import { JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import { Ed25519PublicJwk, PublicJwk } from '../../../../kms' import didKeyEd25519Fixture from '../../../__tests__/__fixtures__//didKeyEd25519.json' import { VerificationMethod } from '../../../domain/verificationMethod' import { keyDidEd25519 } from '../ed25519' -const TEST_ED25519_BASE58_KEY = '8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K' const TEST_ED25519_FINGERPRINT = 'z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th' const TEST_ED25519_DID = `did:key:${TEST_ED25519_FINGERPRINT}` -const TEST_ED25519_PREFIX_BYTES = Buffer.concat([ - new Uint8Array([237, 1]), - TypedArrayEncoder.fromBase58(TEST_ED25519_BASE58_KEY), -]) describe('ed25519', () => { - it('creates a Key instance from public key bytes and ed25519 key type', async () => { - const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_ED25519_BASE58_KEY) - - const didKey = Key.fromPublicKey(publicKeyBytes, KeyType.Ed25519) - - expect(didKey.fingerprint).toBe(TEST_ED25519_FINGERPRINT) - }) - - it('creates a Key instance from a base58 encoded public key and ed25519 key type', async () => { - const didKey = Key.fromPublicKeyBase58(TEST_ED25519_BASE58_KEY, KeyType.Ed25519) - - expect(didKey.fingerprint).toBe(TEST_ED25519_FINGERPRINT) - }) - - it('creates a Key instance from a fingerprint', async () => { - const didKey = Key.fromFingerprint(TEST_ED25519_FINGERPRINT) - - expect(didKey.fingerprint).toBe(TEST_ED25519_FINGERPRINT) - }) - - it('should correctly calculate the getter properties', async () => { - const didKey = Key.fromFingerprint(TEST_ED25519_FINGERPRINT) - - expect(didKey.fingerprint).toBe(TEST_ED25519_FINGERPRINT) - expect(didKey.publicKeyBase58).toBe(TEST_ED25519_BASE58_KEY) - expect(didKey.publicKey).toEqual(Uint8Array.from(TypedArrayEncoder.fromBase58(TEST_ED25519_BASE58_KEY))) - expect(didKey.keyType).toBe(KeyType.Ed25519) - expect(Buffer.from(didKey.prefixedPublicKey).equals(TEST_ED25519_PREFIX_BYTES)).toBe(true) - }) - it('should return a valid verification method', async () => { - const key = Key.fromFingerprint(TEST_ED25519_FINGERPRINT) + const key = PublicJwk.fromFingerprint(TEST_ED25519_FINGERPRINT) as PublicJwk const verificationMethods = keyDidEd25519.getVerificationMethods(TEST_ED25519_DID, key) expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([didKeyEd25519Fixture.verificationMethod[0]]) @@ -63,7 +27,7 @@ describe('ed25519', () => { it('returns key for Ed25519VerificationKey2018 verification method', () => { const verificationMethod = JsonTransformer.fromJSON(didKeyEd25519Fixture.verificationMethod[0], VerificationMethod) - const key = keyDidEd25519.getKeyFromVerificationMethod(verificationMethod) + const key = keyDidEd25519.getPublicJwkFromVerificationMethod(verificationMethod) expect(key.fingerprint).toBe(TEST_ED25519_FINGERPRINT) }) @@ -79,9 +43,9 @@ describe('ed25519', () => { VerificationMethod ) - const key = keyDidEd25519.getKeyFromVerificationMethod(verificationMethod) + const key = keyDidEd25519.getPublicJwkFromVerificationMethod(verificationMethod) as PublicJwk - expect(key.publicKeyBase58).toBe('6jFdQvXwdR2FicGycegT2F9GYX2djeoGQVoXtPWr6enL') + expect(TypedArrayEncoder.toBase58(key.publicKey.publicKey)).toBe('6jFdQvXwdR2FicGycegT2F9GYX2djeoGQVoXtPWr6enL') }) it('throws an error if an invalid verification method is passed', () => { @@ -89,8 +53,8 @@ describe('ed25519', () => { verificationMethod.type = 'SomeRandomType' - expect(() => keyDidEd25519.getKeyFromVerificationMethod(verificationMethod)).toThrow( - "Verification method with type 'SomeRandomType' not supported for key type 'ed25519'" + expect(() => keyDidEd25519.getPublicJwkFromVerificationMethod(verificationMethod)).toThrow( + "Verification method with type 'SomeRandomType' not supported for key type Ed25519" ) }) }) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/jwk.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/jwk.test.ts index 9e529c26b8..4698f104cf 100644 --- a/packages/core/src/modules/dids/domain/key-type/__tests__/jwk.test.ts +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/jwk.test.ts @@ -1,5 +1,5 @@ -import { Key } from '../../../../../crypto/Key' import { JsonTransformer } from '../../../../../utils' +import { P256PublicJwk, PublicJwk } from '../../../../kms' import didKeyP256Fixture from '../../../__tests__/__fixtures__/didKeyP256.json' import { VerificationMethod } from '../../verificationMethod' import { VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 } from '../../verificationMethod/JsonWebKey2020' @@ -10,7 +10,7 @@ const TEST_P256_DID = `did:key:${TEST_P256_FINGERPRINT}` describe('keyDidJsonWebKey', () => { it('should return a valid verification method', async () => { - const key = Key.fromFingerprint(TEST_P256_FINGERPRINT) + const key = PublicJwk.fromFingerprint(TEST_P256_FINGERPRINT) as PublicJwk const verificationMethods = keyDidJsonWebKey.getVerificationMethods(TEST_P256_DID, key) expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([didKeyP256Fixture.verificationMethod[0]]) @@ -22,21 +22,13 @@ describe('keyDidJsonWebKey', () => { ]) }) - it('returns key for JsonWebKey2020 verification method', () => { - const verificationMethod = JsonTransformer.fromJSON(didKeyP256Fixture.verificationMethod[0], VerificationMethod) - - const key = keyDidJsonWebKey.getKeyFromVerificationMethod(verificationMethod) - - expect(key.fingerprint).toBe(TEST_P256_FINGERPRINT) - }) - it('throws an error if an invalid verification method is passed', () => { const verificationMethod = JsonTransformer.fromJSON(didKeyP256Fixture.verificationMethod[0], VerificationMethod) verificationMethod.type = 'SomeRandomType' - expect(() => keyDidJsonWebKey.getKeyFromVerificationMethod(verificationMethod)).toThrow( - 'Invalid verification method passed' + expect(() => keyDidJsonWebKey.getPublicJwkFromVerificationMethod(verificationMethod)).toThrow( + 'Not supported for key did json web key' ) }) }) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/x25519.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/x25519.test.ts index 7055e34b32..fb21e4bd12 100644 --- a/packages/core/src/modules/dids/domain/key-type/__tests__/x25519.test.ts +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/x25519.test.ts @@ -1,51 +1,15 @@ -import { KeyType } from '../../../../../crypto' -import { Key } from '../../../../../crypto/Key' -import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import { JsonTransformer } from '../../../../../utils' +import { PublicJwk, X25519PublicJwk } from '../../../../kms' import didKeyX25519Fixture from '../../../__tests__/__fixtures__/didKeyX25519.json' import { VerificationMethod } from '../../verificationMethod' import { keyDidX25519 } from '../x25519' -const TEST_X25519_BASE58_KEY = '6fUMuABnqSDsaGKojbUF3P7ZkEL3wi2njsDdUWZGNgCU' const TEST_X25519_FINGERPRINT = 'z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE' const TEST_X25519_DID = `did:key:${TEST_X25519_FINGERPRINT}` -const TEST_X25519_PREFIX_BYTES = Buffer.concat([ - new Uint8Array([236, 1]), - TypedArrayEncoder.fromBase58(TEST_X25519_BASE58_KEY), -]) describe('x25519', () => { - it('creates a Key instance from public key bytes and x25519 key type', async () => { - const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_X25519_BASE58_KEY) - - const didKey = Key.fromPublicKey(publicKeyBytes, KeyType.X25519) - - expect(didKey.fingerprint).toBe(TEST_X25519_FINGERPRINT) - }) - - it('creates a Key instance from a base58 encoded public key and x25519 key type', async () => { - const didKey = Key.fromPublicKeyBase58(TEST_X25519_BASE58_KEY, KeyType.X25519) - - expect(didKey.fingerprint).toBe(TEST_X25519_FINGERPRINT) - }) - - it('creates a Key instance from a fingerprint', async () => { - const didKey = Key.fromFingerprint(TEST_X25519_FINGERPRINT) - - expect(didKey.fingerprint).toBe(TEST_X25519_FINGERPRINT) - }) - - it('should correctly calculate the getter properties', async () => { - const didKey = Key.fromFingerprint(TEST_X25519_FINGERPRINT) - - expect(didKey.fingerprint).toBe(TEST_X25519_FINGERPRINT) - expect(didKey.publicKeyBase58).toBe(TEST_X25519_BASE58_KEY) - expect(didKey.publicKey).toEqual(Uint8Array.from(TypedArrayEncoder.fromBase58(TEST_X25519_BASE58_KEY))) - expect(didKey.keyType).toBe(KeyType.X25519) - expect(Buffer.from(didKey.prefixedPublicKey).equals(TEST_X25519_PREFIX_BYTES)).toBe(true) - }) - it('should return a valid verification method', async () => { - const key = Key.fromFingerprint(TEST_X25519_FINGERPRINT) + const key = PublicJwk.fromFingerprint(TEST_X25519_FINGERPRINT) as PublicJwk const verificationMethods = keyDidX25519.getVerificationMethods(TEST_X25519_DID, key) expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([didKeyX25519Fixture.keyAgreement[0]]) @@ -62,7 +26,7 @@ describe('x25519', () => { it('returns key for X25519KeyAgreementKey2019 verification method', () => { const verificationMethod = JsonTransformer.fromJSON(didKeyX25519Fixture.keyAgreement[0], VerificationMethod) - const key = keyDidX25519.getKeyFromVerificationMethod(verificationMethod) + const key = keyDidX25519.getPublicJwkFromVerificationMethod(verificationMethod) expect(key.fingerprint).toBe(TEST_X25519_FINGERPRINT) }) @@ -72,8 +36,8 @@ describe('x25519', () => { verificationMethod.type = 'SomeRandomType' - expect(() => keyDidX25519.getKeyFromVerificationMethod(verificationMethod)).toThrowError( - `Verification method with type 'SomeRandomType' not supported for key type 'x25519'` + expect(() => keyDidX25519.getPublicJwkFromVerificationMethod(verificationMethod)).toThrow( + `Verification method with type 'SomeRandomType' not supported for key type X25519` ) }) }) diff --git a/packages/core/src/modules/dids/domain/key-type/bls12381g1.ts b/packages/core/src/modules/dids/domain/key-type/bls12381g1.ts deleted file mode 100644 index ab520ae91c..0000000000 --- a/packages/core/src/modules/dids/domain/key-type/bls12381g1.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { VerificationMethod } from '../verificationMethod' -import type { KeyDidMapping } from './keyDidMapping' - -import { KeyType } from '../../../../crypto/KeyType' -import { CredoError } from '../../../../error' -import { - VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020, - getBls12381G1Key2020, - getKeyFromBls12381G1Key2020, - isBls12381G1Key2020, -} from '../verificationMethod' - -export const keyDidBls12381g1: KeyDidMapping = { - supportedVerificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020], - - getVerificationMethods: (did, key) => [ - getBls12381G1Key2020({ id: `${did}#${key.fingerprint}`, key, controller: did }), - ], - getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { - if (isBls12381G1Key2020(verificationMethod)) { - return getKeyFromBls12381G1Key2020(verificationMethod) - } - - throw new CredoError( - `Verification method with type '${verificationMethod.type}' not supported for key type '${KeyType.Bls12381g1}'` - ) - }, -} diff --git a/packages/core/src/modules/dids/domain/key-type/bls12381g1g2.ts b/packages/core/src/modules/dids/domain/key-type/bls12381g1g2.ts deleted file mode 100644 index e5d402de4c..0000000000 --- a/packages/core/src/modules/dids/domain/key-type/bls12381g1g2.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { KeyDidMapping } from './keyDidMapping' - -import { Key } from '../../../../crypto/Key' -import { KeyType } from '../../../../crypto/KeyType' -import { CredoError } from '../../../../error' -import { getBls12381G1Key2020, getBls12381G2Key2020 } from '../verificationMethod' - -export function getBls12381g1g2VerificationMethod(did: string, key: Key) { - const g1PublicKey = key.publicKey.slice(0, 48) - const g2PublicKey = key.publicKey.slice(48) - - const bls12381g1Key = Key.fromPublicKey(g1PublicKey, KeyType.Bls12381g1) - const bls12381g2Key = Key.fromPublicKey(g2PublicKey, KeyType.Bls12381g2) - - const bls12381g1VerificationMethod = getBls12381G1Key2020({ - id: `${did}#${bls12381g1Key.fingerprint}`, - key: bls12381g1Key, - controller: did, - }) - const bls12381g2VerificationMethod = getBls12381G2Key2020({ - id: `${did}#${bls12381g2Key.fingerprint}`, - key: bls12381g2Key, - controller: did, - }) - - return [bls12381g1VerificationMethod, bls12381g2VerificationMethod] -} - -export const keyDidBls12381g1g2: KeyDidMapping = { - supportedVerificationMethodTypes: [], - // For a G1G2 key, we return two verification methods - getVerificationMethods: getBls12381g1g2VerificationMethod, - getKeyFromVerificationMethod: () => { - throw new CredoError('Not supported for bls12381g1g2 key') - }, -} diff --git a/packages/core/src/modules/dids/domain/key-type/bls12381g2.ts b/packages/core/src/modules/dids/domain/key-type/bls12381g2.ts deleted file mode 100644 index b207f82309..0000000000 --- a/packages/core/src/modules/dids/domain/key-type/bls12381g2.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { VerificationMethod } from '../verificationMethod' -import type { KeyDidMapping } from './keyDidMapping' - -import { KeyType } from '../../../../crypto/KeyType' -import { CredoError } from '../../../../error' -import { - VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020, - getBls12381G2Key2020, - getKeyFromBls12381G2Key2020, - isBls12381G2Key2020, -} from '../verificationMethod' - -export const keyDidBls12381g2: KeyDidMapping = { - supportedVerificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], - - getVerificationMethods: (did, key) => [ - getBls12381G2Key2020({ id: `${did}#${key.fingerprint}`, key, controller: did }), - ], - - getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { - if (isBls12381G2Key2020(verificationMethod)) { - return getKeyFromBls12381G2Key2020(verificationMethod) - } - - throw new CredoError( - `Verification method with type '${verificationMethod.type}' not supported for key type '${KeyType.Bls12381g2}'` - ) - }, -} diff --git a/packages/core/src/modules/dids/domain/key-type/ed25519.ts b/packages/core/src/modules/dids/domain/key-type/ed25519.ts index 9331d14f96..5d2eaf669f 100644 --- a/packages/core/src/modules/dids/domain/key-type/ed25519.ts +++ b/packages/core/src/modules/dids/domain/key-type/ed25519.ts @@ -1,55 +1,44 @@ -import type { VerificationMethod } from '../verificationMethod' -import type { KeyDidMapping } from './keyDidMapping' - -import { KeyType } from '../../../../crypto/KeyType' import { CredoError } from '../../../../error' +import { Ed25519PublicJwk } from '../../../kms' +import type { VerificationMethod } from '../verificationMethod' import { VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, VERIFICATION_METHOD_TYPE_MULTIKEY, getEd25519VerificationKey2018, - getKeyFromEd25519VerificationKey2018, - getKeyFromEd25519VerificationKey2020, - getKeyFromJsonWebKey2020, - getKeyFromMultikey, + getPublicJwkFromEd25519VerificationKey2018, + getPublicJwkFromEd25519VerificationKey2020, isEd25519VerificationKey2018, isEd25519VerificationKey2020, - isJsonWebKey2020, - isMultikey, } from '../verificationMethod' +import type { KeyDidMapping } from './keyDidMapping' export { convertPublicKeyToX25519 } from '@stablelib/ed25519' -export const keyDidEd25519: KeyDidMapping = { +export const keyDidEd25519: KeyDidMapping = { + PublicJwkTypes: [Ed25519PublicJwk], supportedVerificationMethodTypes: [ VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, VERIFICATION_METHOD_TYPE_MULTIKEY, ], - getVerificationMethods: (did, key) => [ - getEd25519VerificationKey2018({ id: `${did}#${key.fingerprint}`, key, controller: did }), + getVerificationMethods: (did, publicJwk) => [ + getEd25519VerificationKey2018({ id: `${did}#${publicJwk.fingerprint}`, publicJwk, controller: did }), ], - getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + + getPublicJwkFromVerificationMethod: (verificationMethod: VerificationMethod) => { if (isEd25519VerificationKey2018(verificationMethod)) { - return getKeyFromEd25519VerificationKey2018(verificationMethod) + return getPublicJwkFromEd25519VerificationKey2018(verificationMethod) } if (isEd25519VerificationKey2020(verificationMethod)) { - return getKeyFromEd25519VerificationKey2020(verificationMethod) - } - - if (isJsonWebKey2020(verificationMethod)) { - return getKeyFromJsonWebKey2020(verificationMethod) - } - - if (isMultikey(verificationMethod)) { - return getKeyFromMultikey(verificationMethod) + return getPublicJwkFromEd25519VerificationKey2020(verificationMethod) } throw new CredoError( - `Verification method with type '${verificationMethod.type}' not supported for key type '${KeyType.Ed25519}'` + `Verification method with type '${verificationMethod.type}' not supported for key type Ed25519` ) }, } diff --git a/packages/core/src/modules/dids/domain/key-type/index.ts b/packages/core/src/modules/dids/domain/key-type/index.ts index be3f4beda4..faebcf401e 100644 --- a/packages/core/src/modules/dids/domain/key-type/index.ts +++ b/packages/core/src/modules/dids/domain/key-type/index.ts @@ -1,11 +1,4 @@ -export { - getKeyDidMappingByKeyType, - getKeyFromVerificationMethod, - getSupportedVerificationMethodTypesFromKeyType, -} from './keyDidMapping' +export { getPublicJwkFromVerificationMethod } from './keyDidMapping' -export * from './bls12381g2' -export * from './bls12381g1' -export * from './bls12381g1g2' export * from './ed25519' export * from './x25519' diff --git a/packages/core/src/modules/dids/domain/key-type/keyDidJsonWebKey.ts b/packages/core/src/modules/dids/domain/key-type/keyDidJsonWebKey.ts index cd30e94bea..77ec80f243 100644 --- a/packages/core/src/modules/dids/domain/key-type/keyDidJsonWebKey.ts +++ b/packages/core/src/modules/dids/domain/key-type/keyDidJsonWebKey.ts @@ -1,20 +1,17 @@ -import type { VerificationMethod } from '../verificationMethod' import type { KeyDidMapping } from './keyDidMapping' -import { getJwkFromJson } from '../../../../crypto/jose/jwk' import { CredoError } from '../../../../error' +import { P256PublicJwk, P384PublicJwk, P521PublicJwk } from '../../../kms' import { getJsonWebKey2020 } from '../verificationMethod' -import { VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, isJsonWebKey2020 } from '../verificationMethod/JsonWebKey2020' +import { VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 } from '../verificationMethod/JsonWebKey2020' -export const keyDidJsonWebKey: KeyDidMapping = { +export const keyDidJsonWebKey: KeyDidMapping = { + PublicJwkTypes: [P256PublicJwk, P384PublicJwk, P521PublicJwk], supportedVerificationMethodTypes: [VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020], - getVerificationMethods: (did, key) => [getJsonWebKey2020({ did, key })], + getVerificationMethods: (did, publicJwk) => [getJsonWebKey2020({ did, publicJwk })], - getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { - if (!isJsonWebKey2020(verificationMethod) || !verificationMethod.publicKeyJwk) { - throw new CredoError('Invalid verification method passed') - } - - return getJwkFromJson(verificationMethod.publicKeyJwk).key + getPublicJwkFromVerificationMethod: () => { + // This is handled on a higher level + throw new CredoError('Not supported for key did json web key') }, } diff --git a/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts b/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts index da76edba3e..d1a2fd1446 100644 --- a/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts +++ b/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts @@ -1,114 +1,76 @@ -import type { Key } from '../../../../crypto/Key' -import type { VerificationMethod } from '../verificationMethod' - -import { KeyType } from '../../../../crypto/KeyType' -import { getJwkFromJson } from '../../../../crypto/jose/jwk' import { CredoError } from '../../../../error' -import { VERIFICATION_METHOD_TYPE_MULTIKEY, getKeyFromMultikey, isMultikey } from '../verificationMethod' -import { VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, isJsonWebKey2020 } from '../verificationMethod/JsonWebKey2020' +import type { VerificationMethod } from '../verificationMethod' +import { getPublicJwkFromMultikey, isMultikey } from '../verificationMethod' +import { getPublicJwkFromJsonWebKey2020, isJsonWebKey2020 } from '../verificationMethod/JsonWebKey2020' -import { keyDidBls12381g1 } from './bls12381g1' -import { keyDidBls12381g1g2 } from './bls12381g1g2' -import { keyDidBls12381g2 } from './bls12381g2' +import { Constructor } from '../../../../utils/mixins' +import { PublicJwk, getJwkHumanDescription } from '../../../kms' +import { SupportedPublicJwk, SupportedPublicJwkClass } from '../../../kms/jwk/PublicJwk' import { keyDidEd25519 } from './ed25519' import { keyDidJsonWebKey } from './keyDidJsonWebKey' import { keyDidSecp256k1 } from './secp256k1' import { keyDidX25519 } from './x25519' -export interface KeyDidMapping { - getVerificationMethods: (did: string, key: Key) => VerificationMethod[] - getKeyFromVerificationMethod(verificationMethod: VerificationMethod): Key +export interface KeyDidMapping< + PublicJwkType extends InstanceType = InstanceType, +> { + PublicJwkTypes: Array> + getVerificationMethods: (did: string, publicJwk: PublicJwk) => VerificationMethod[] + getPublicJwkFromVerificationMethod(verificationMethod: VerificationMethod): PublicJwk supportedVerificationMethodTypes: string[] } -// TODO: Maybe we should make this dynamically? -const keyDidMapping: Record = { - [KeyType.Ed25519]: keyDidEd25519, - [KeyType.X25519]: keyDidX25519, - [KeyType.Bls12381g1]: keyDidBls12381g1, - [KeyType.Bls12381g2]: keyDidBls12381g2, - [KeyType.Bls12381g1g2]: keyDidBls12381g1g2, - [KeyType.P256]: keyDidJsonWebKey, - [KeyType.P384]: keyDidJsonWebKey, - [KeyType.P521]: keyDidJsonWebKey, - [KeyType.K256]: keyDidSecp256k1, -} +const supportedKeyDids = [keyDidEd25519, keyDidX25519, keyDidJsonWebKey, keyDidSecp256k1] -/** - * Dynamically creates a mapping from verification method key type to the key Did interface - * for all key types. - * - * { - * "Ed25519VerificationKey2018": KeyDidMapping - * } - */ -const verificationMethodKeyDidMapping = Object.values(KeyType).reduce>( - (mapping, keyType) => { - const supported = keyDidMapping[keyType].supportedVerificationMethodTypes.reduce>( - (accumulator, vMethodKeyType) => ({ - // biome-ignore lint/performance/noAccumulatingSpread: - ...accumulator, - [vMethodKeyType]: keyDidMapping[keyType], - }), - {} - ) +// TODO: at some point we should update all usages to Jwk / Multikey methods +// so we don't need key type specific verification methods anymore +export function getVerificationMethodsForPublicJwk(publicJwk: PublicJwk, did: string) { + const { getVerificationMethods } = getKeyDidMappingByPublicJwk(publicJwk) - return { - // biome-ignore lint/performance/noAccumulatingSpread: - ...mapping, - ...supported, - } - }, - {} -) + return getVerificationMethods(did, publicJwk) +} -export function getKeyDidMappingByKeyType(keyType: KeyType) { - const keyDid = keyDidMapping[keyType] +export function getSupportedVerificationMethodTypesForPublicJwk( + publicJwk: SupportedPublicJwk | PublicJwk | SupportedPublicJwkClass +): string[] { + const { supportedVerificationMethodTypes } = getKeyDidMappingByPublicJwk(publicJwk) - if (!keyDid) { - throw new CredoError(`Unsupported key did from key type '${keyType}'`) - } - - return keyDid + return supportedVerificationMethodTypes } -export function getKeyFromVerificationMethod(verificationMethod: VerificationMethod): Key { +export function getPublicJwkFromVerificationMethod(verificationMethod: VerificationMethod): PublicJwk { // This is a special verification method, as it supports basically all key types. if (isJsonWebKey2020(verificationMethod)) { - // TODO: move this validation to another place - if (!verificationMethod.publicKeyJwk) { - throw new CredoError( - `Missing publicKeyJwk on verification method with type ${VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020}` - ) - } - - return getJwkFromJson(verificationMethod.publicKeyJwk).key + return getPublicJwkFromJsonWebKey2020(verificationMethod) } if (isMultikey(verificationMethod)) { - if (!verificationMethod.publicKeyMultibase) { - throw new CredoError( - `Missing publicKeyMultibase on verification method with type ${VERIFICATION_METHOD_TYPE_MULTIKEY}` - ) - } - - return getKeyFromMultikey(verificationMethod) + return getPublicJwkFromMultikey(verificationMethod) } - const keyDid = verificationMethodKeyDidMapping[verificationMethod.type] + const keyDid = supportedKeyDids.find((keyDid) => + keyDid.supportedVerificationMethodTypes.includes(verificationMethod.type) + ) if (!keyDid) { throw new CredoError(`Unsupported key did from verification method type '${verificationMethod.type}'`) } - return keyDid.getKeyFromVerificationMethod(verificationMethod) + return keyDid.getPublicJwkFromVerificationMethod(verificationMethod) } -export function getSupportedVerificationMethodTypesFromKeyType(keyType: KeyType) { - const keyDid = keyDidMapping[keyType] +function getKeyDidMappingByPublicJwk(jwk: SupportedPublicJwk | PublicJwk | SupportedPublicJwkClass): KeyDidMapping { + const jwkTypeClass = jwk instanceof PublicJwk ? jwk.jwk.constructor : 'publicKey' in jwk ? jwk.constructor : jwk + + const keyDid = supportedKeyDids.find((supportedKeyDid) => + // biome-ignore lint/suspicious/noExplicitAny: + supportedKeyDid.PublicJwkTypes.includes(jwkTypeClass as any) + ) if (!keyDid) { - throw new CredoError(`Unsupported key did from key type '${keyType}'`) + throw new CredoError( + `Unsupported did mapping for jwk '${jwk instanceof PublicJwk ? jwk.jwkTypehumanDescription : 'publicKey' in jwk ? getJwkHumanDescription(jwk.jwk) : jwk.name}'` + ) } - return keyDid.supportedVerificationMethodTypes + return keyDid as KeyDidMapping } diff --git a/packages/core/src/modules/dids/domain/key-type/secp256k1.ts b/packages/core/src/modules/dids/domain/key-type/secp256k1.ts index 0fc25edc98..c1f13b74de 100644 --- a/packages/core/src/modules/dids/domain/key-type/secp256k1.ts +++ b/packages/core/src/modules/dids/domain/key-type/secp256k1.ts @@ -1,35 +1,31 @@ -import type { VerificationMethod } from '../verificationMethod' -import type { KeyDidMapping } from './keyDidMapping' - -import { KeyType } from '../../../../crypto/KeyType' import { CredoError } from '../../../../error' +import { Secp256k1PublicJwk } from '../../../kms' +import type { VerificationMethod } from '../verificationMethod' import { VERIFICATION_METHOD_TYPE_ECDSA_SECP256K1_VERIFICATION_KEY_2019, VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, + VERIFICATION_METHOD_TYPE_MULTIKEY, getJsonWebKey2020, - getKeyFromEcdsaSecp256k1VerificationKey2019, - getKeyFromJsonWebKey2020, + getPublicJwkFromEcdsaSecp256k1VerificationKey2019, isEcdsaSecp256k1VerificationKey2019, - isJsonWebKey2020, } from '../verificationMethod' +import type { KeyDidMapping } from './keyDidMapping' -export const keyDidSecp256k1: KeyDidMapping = { +export const keyDidSecp256k1: KeyDidMapping = { + PublicJwkTypes: [Secp256k1PublicJwk], supportedVerificationMethodTypes: [ VERIFICATION_METHOD_TYPE_ECDSA_SECP256K1_VERIFICATION_KEY_2019, VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, + VERIFICATION_METHOD_TYPE_MULTIKEY, ], - getVerificationMethods: (did, key) => [getJsonWebKey2020({ did, key })], - getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + getVerificationMethods: (did, publicJwk) => [getJsonWebKey2020({ did, publicJwk })], + getPublicJwkFromVerificationMethod: (verificationMethod: VerificationMethod) => { if (isEcdsaSecp256k1VerificationKey2019(verificationMethod)) { - return getKeyFromEcdsaSecp256k1VerificationKey2019(verificationMethod) - } - - if (isJsonWebKey2020(verificationMethod)) { - return getKeyFromJsonWebKey2020(verificationMethod) + return getPublicJwkFromEcdsaSecp256k1VerificationKey2019(verificationMethod) } throw new CredoError( - `Verification method with type '${verificationMethod.type}' not supported for key type '${KeyType.K256}'` + `Verification method with type '${verificationMethod.type}' not supported for key type Secp256K1` ) }, } diff --git a/packages/core/src/modules/dids/domain/key-type/x25519.ts b/packages/core/src/modules/dids/domain/key-type/x25519.ts index a476990770..443244a598 100644 --- a/packages/core/src/modules/dids/domain/key-type/x25519.ts +++ b/packages/core/src/modules/dids/domain/key-type/x25519.ts @@ -1,45 +1,32 @@ -import type { VerificationMethod } from '../verificationMethod' -import type { KeyDidMapping } from './keyDidMapping' - -import { KeyType } from '../../../../crypto/KeyType' import { CredoError } from '../../../../error' +import { X25519PublicJwk } from '../../../kms' +import type { VerificationMethod } from '../verificationMethod' import { VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, VERIFICATION_METHOD_TYPE_MULTIKEY, VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019, - getKeyFromJsonWebKey2020, - getKeyFromMultikey, - getKeyFromX25519KeyAgreementKey2019, + getPublicJwkFrommX25519KeyAgreementKey2019, getX25519KeyAgreementKey2019, - isJsonWebKey2020, - isMultikey, isX25519KeyAgreementKey2019, } from '../verificationMethod' +import type { KeyDidMapping } from './keyDidMapping' -export const keyDidX25519: KeyDidMapping = { +export const keyDidX25519: KeyDidMapping = { + PublicJwkTypes: [X25519PublicJwk], supportedVerificationMethodTypes: [ VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019, VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, VERIFICATION_METHOD_TYPE_MULTIKEY, ], - getVerificationMethods: (did, key) => [ - getX25519KeyAgreementKey2019({ id: `${did}#${key.fingerprint}`, key, controller: did }), + getVerificationMethods: (did, publicJwk) => [ + getX25519KeyAgreementKey2019({ id: `${did}#${publicJwk.fingerprint}`, publicJwk, controller: did }), ], - getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { - if (isJsonWebKey2020(verificationMethod)) { - return getKeyFromJsonWebKey2020(verificationMethod) - } + getPublicJwkFromVerificationMethod: (verificationMethod: VerificationMethod) => { if (isX25519KeyAgreementKey2019(verificationMethod)) { - return getKeyFromX25519KeyAgreementKey2019(verificationMethod) - } - - if (isMultikey(verificationMethod)) { - return getKeyFromMultikey(verificationMethod) + return getPublicJwkFrommX25519KeyAgreementKey2019(verificationMethod) } - throw new CredoError( - `Verification method with type '${verificationMethod.type}' not supported for key type '${KeyType.X25519}'` - ) + throw new CredoError(`Verification method with type '${verificationMethod.type}' not supported for key type X25519`) }, } diff --git a/packages/core/src/modules/dids/domain/keyDidDocument.ts b/packages/core/src/modules/dids/domain/keyDidDocument.ts index 1078b7548e..00220f22eb 100644 --- a/packages/core/src/modules/dids/domain/keyDidDocument.ts +++ b/packages/core/src/modules/dids/domain/keyDidDocument.ts @@ -1,81 +1,55 @@ -import type { DidDocument } from './DidDocument' -import type { VerificationMethod } from './verificationMethod/VerificationMethod' - -import { Key } from '../../../crypto/Key' -import { KeyType } from '../../../crypto/KeyType' import { CredoError } from '../../../error' -import { SECURITY_CONTEXT_BBS_URL, SECURITY_JWS_CONTEXT_URL, SECURITY_X25519_CONTEXT_URL } from '../../vc/constants' +import { SECURITY_JWS_CONTEXT_URL, SECURITY_X25519_CONTEXT_URL } from '../../vc/constants' import { ED25519_SUITE_CONTEXT_URL_2018 } from '../../vc/data-integrity/signature-suites/ed25519/constants' +import type { VerificationMethod } from './verificationMethod/VerificationMethod' +import { + Ed25519PublicJwk, + P256PublicJwk, + P384PublicJwk, + P521PublicJwk, + Secp256k1PublicJwk, + X25519PublicJwk, + getJwkHumanDescription, +} from '../../kms' +import { PublicJwk } from '../../kms/jwk/PublicJwk' import { DidDocumentBuilder } from './DidDocumentBuilder' -import { getBls12381g1g2VerificationMethod } from './key-type' import { convertPublicKeyToX25519 } from './key-type/ed25519' -import { - getBls12381G1Key2020, - getBls12381G2Key2020, - getEd25519VerificationKey2018, - getJsonWebKey2020, - getX25519KeyAgreementKey2019, -} from './verificationMethod' - -const didDocumentKeyTypeMapping: Record DidDocument> = { - [KeyType.Ed25519]: getEd25519DidDoc, - [KeyType.X25519]: getX25519DidDoc, - [KeyType.Bls12381g1]: getBls12381g1DidDoc, - [KeyType.Bls12381g2]: getBls12381g2DidDoc, - [KeyType.Bls12381g1g2]: getBls12381g1g2DidDoc, - [KeyType.P256]: getJsonWebKey2020DidDocument, - [KeyType.P384]: getJsonWebKey2020DidDocument, - [KeyType.P521]: getJsonWebKey2020DidDocument, - [KeyType.K256]: getJsonWebKey2020DidDocument, -} - -export function getDidDocumentForKey(did: string, key: Key) { - const getDidDocument = didDocumentKeyTypeMapping[key.keyType] - - return getDidDocument(did, key) -} +import { getEd25519VerificationKey2018, getJsonWebKey2020, getX25519KeyAgreementKey2019 } from './verificationMethod' -function getBls12381g1DidDoc(did: string, key: Key) { - const verificationMethod = getBls12381G1Key2020({ id: `${did}#${key.fingerprint}`, key, controller: did }) - - return getSignatureKeyBase({ - did, - key, - verificationMethod, - }) - .addContext(SECURITY_CONTEXT_BBS_URL) - .build() -} - -function getBls12381g1g2DidDoc(did: string, key: Key) { - const verificationMethods = getBls12381g1g2VerificationMethod(did, key) - - const didDocumentBuilder = new DidDocumentBuilder(did) - - for (const verificationMethod of verificationMethods) { - didDocumentBuilder - .addVerificationMethod(verificationMethod) - .addAuthentication(verificationMethod.id) - .addAssertionMethod(verificationMethod.id) - .addCapabilityDelegation(verificationMethod.id) - .addCapabilityInvocation(verificationMethod.id) +export function getDidDocumentForPublicJwk(did: string, publicJwk: PublicJwk) { + if (publicJwk.jwk instanceof Ed25519PublicJwk) { + return getEd25519DidDoc(did, publicJwk as PublicJwk) + } + if (publicJwk.jwk instanceof X25519PublicJwk) { + return getX25519DidDoc(did, publicJwk as PublicJwk) + } + if ( + publicJwk.jwk instanceof P256PublicJwk || + publicJwk.jwk instanceof P384PublicJwk || + publicJwk.jwk instanceof P521PublicJwk || + publicJwk.jwk instanceof Secp256k1PublicJwk + ) { + return getJsonWebKey2020DidDocument(did, publicJwk) } - return didDocumentBuilder.addContext(SECURITY_CONTEXT_BBS_URL).build() + throw new CredoError(`Unsupported public key type for did document: ${getJwkHumanDescription(publicJwk.toJson())}`) } -export function getJsonWebKey2020DidDocument(did: string, key: Key) { - const verificationMethod = getJsonWebKey2020({ did, key }) +export function getJsonWebKey2020DidDocument(did: string, publicJwk: PublicJwk) { + const verificationMethod = getJsonWebKey2020({ did, publicJwk }) const didDocumentBuilder = new DidDocumentBuilder(did) didDocumentBuilder.addContext(SECURITY_JWS_CONTEXT_URL).addVerificationMethod(verificationMethod) - if (!key.supportsEncrypting && !key.supportsSigning) { + if ( + publicJwk.supportedSignatureAlgorithms.length === 0 && + publicJwk.supportdEncryptionKeyAgreementAlgorithms.length === 0 + ) { throw new CredoError('Key must support at least signing or encrypting') } - if (key.supportsSigning) { + if (publicJwk.supportedSignatureAlgorithms.length > 0) { didDocumentBuilder .addAuthentication(verificationMethod.id) .addAssertionMethod(verificationMethod.id) @@ -83,25 +57,35 @@ export function getJsonWebKey2020DidDocument(did: string, key: Key) { .addCapabilityInvocation(verificationMethod.id) } - if (key.supportsEncrypting) { + if (publicJwk.supportdEncryptionKeyAgreementAlgorithms.length > 0) { didDocumentBuilder.addKeyAgreement(verificationMethod.id) } return didDocumentBuilder.build() } -function getEd25519DidDoc(did: string, key: Key) { - const verificationMethod = getEd25519VerificationKey2018({ id: `${did}#${key.fingerprint}`, key, controller: did }) +function getEd25519DidDoc(did: string, publicJwk: PublicJwk) { + const verificationMethod = getEd25519VerificationKey2018({ + id: `${did}#${publicJwk.fingerprint}`, + publicJwk, + controller: did, + }) + + const publicKeyX25519 = convertPublicKeyToX25519(publicJwk.publicKey.publicKey) + + const publicJwkX25519 = PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'X25519', + publicKey: publicKeyX25519, + }) - const publicKeyX25519 = convertPublicKeyToX25519(key.publicKey) - const didKeyX25519 = Key.fromPublicKey(publicKeyX25519, KeyType.X25519) const x25519VerificationMethod = getX25519KeyAgreementKey2019({ - id: `${did}#${didKeyX25519.fingerprint}`, - key: didKeyX25519, + id: `${did}#${publicJwkX25519.fingerprint}`, + publicJwk: publicJwkX25519, controller: did, }) - const didDocBuilder = getSignatureKeyBase({ did, key, verificationMethod }) + const didDocBuilder = getSignatureKeyBase({ did, publicJwk, verificationMethod }) didDocBuilder .addContext(ED25519_SUITE_CONTEXT_URL_2018) @@ -111,8 +95,12 @@ function getEd25519DidDoc(did: string, key: Key) { return didDocBuilder.build() } -function getX25519DidDoc(did: string, key: Key) { - const verificationMethod = getX25519KeyAgreementKey2019({ id: `${did}#${key.fingerprint}`, key, controller: did }) +function getX25519DidDoc(did: string, publicJwk: PublicJwk) { + const verificationMethod = getX25519KeyAgreementKey2019({ + id: `${did}#${publicJwk.fingerprint}`, + publicJwk, + controller: did, + }) const document = new DidDocumentBuilder(did) .addKeyAgreement(verificationMethod) @@ -122,28 +110,16 @@ function getX25519DidDoc(did: string, key: Key) { return document } -function getBls12381g2DidDoc(did: string, key: Key) { - const verificationMethod = getBls12381G2Key2020({ id: `${did}#${key.fingerprint}`, key, controller: did }) - - return getSignatureKeyBase({ - did, - key, - verificationMethod, - }) - .addContext(SECURITY_CONTEXT_BBS_URL) - .build() -} - function getSignatureKeyBase({ did, - key, + publicJwk, verificationMethod, }: { did: string - key: Key + publicJwk: PublicJwk verificationMethod: VerificationMethod }) { - const keyId = `${did}#${key.fingerprint}` + const keyId = `${did}#${publicJwk.fingerprint}` return new DidDocumentBuilder(did) .addVerificationMethod(verificationMethod) diff --git a/packages/core/src/modules/dids/domain/verificationMethod/Bls12381G1Key2020.ts b/packages/core/src/modules/dids/domain/verificationMethod/Bls12381G1Key2020.ts deleted file mode 100644 index 224a407856..0000000000 --- a/packages/core/src/modules/dids/domain/verificationMethod/Bls12381G1Key2020.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { KeyType } from '../../../../crypto' -import { Key } from '../../../../crypto/Key' -import { CredoError } from '../../../../error' - -import { VerificationMethod } from './VerificationMethod' - -export const VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020 = 'Bls12381G1Key2020' -type Bls12381G1Key2020 = VerificationMethod & { - type: typeof VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020 -} - -/** - * Get a Bls12381G1Key2020 verification method. - */ -export function getBls12381G1Key2020({ key, id, controller }: { id: string; key: Key; controller: string }) { - return new VerificationMethod({ - id, - type: VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020, - controller, - publicKeyBase58: key.publicKeyBase58, - }) -} - -/** - * Check whether a verification method is a Bls12381G1Key2020 verification method. - */ -export function isBls12381G1Key2020(verificationMethod: VerificationMethod): verificationMethod is Bls12381G1Key2020 { - return verificationMethod.type === VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020 -} - -/** - * Get a key from a Bls12381G1Key2020 verification method. - */ -export function getKeyFromBls12381G1Key2020(verificationMethod: Bls12381G1Key2020) { - if (!verificationMethod.publicKeyBase58) { - throw new CredoError('verification method is missing publicKeyBase58') - } - - return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.Bls12381g1) -} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/Bls12381G2Key2020.ts b/packages/core/src/modules/dids/domain/verificationMethod/Bls12381G2Key2020.ts deleted file mode 100644 index dc2c7bd6d7..0000000000 --- a/packages/core/src/modules/dids/domain/verificationMethod/Bls12381G2Key2020.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { KeyType } from '../../../../crypto' -import { Key } from '../../../../crypto/Key' -import { CredoError } from '../../../../error' - -import { VerificationMethod } from './VerificationMethod' - -export const VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020 = 'Bls12381G2Key2020' -type Bls12381G2Key2020 = VerificationMethod & { - type: typeof VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020 -} - -/** - * Get a Bls12381G2Key2020 verification method. - */ -export function getBls12381G2Key2020({ key, id, controller }: { id: string; key: Key; controller: string }) { - return new VerificationMethod({ - id, - type: VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020, - controller, - publicKeyBase58: key.publicKeyBase58, - }) -} - -/** - * Check whether a verification method is a Bls12381G2Key2020 verification method. - */ -export function isBls12381G2Key2020(verificationMethod: VerificationMethod): verificationMethod is Bls12381G2Key2020 { - return verificationMethod.type === VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020 -} - -/** - * Get a key from a Bls12381G2Key2020 verification method. - */ -export function getKeyFromBls12381G2Key2020(verificationMethod: Bls12381G2Key2020) { - if (!verificationMethod.publicKeyBase58) { - throw new CredoError('verification method is missing publicKeyBase58') - } - - return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.Bls12381g2) -} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/EcdsaSecp256k1VerificationKey2019.ts b/packages/core/src/modules/dids/domain/verificationMethod/EcdsaSecp256k1VerificationKey2019.ts index 8de9e649ad..21db14f525 100644 --- a/packages/core/src/modules/dids/domain/verificationMethod/EcdsaSecp256k1VerificationKey2019.ts +++ b/packages/core/src/modules/dids/domain/verificationMethod/EcdsaSecp256k1VerificationKey2019.ts @@ -1,6 +1,6 @@ -import { KeyType } from '../../../../crypto' -import { Key } from '../../../../crypto/Key' import { CredoError } from '../../../../error' +import { TypedArrayEncoder } from '../../../../utils' +import { PublicJwk, Secp256k1PublicJwk } from '../../../kms' import { VerificationMethod } from './VerificationMethod' @@ -14,19 +14,19 @@ type EcdsaSecp256k1VerificationKey2019 = VerificationMethod & { * Get a EcdsaSecp256k1VerificationKey2019 verification method. */ export function getEcdsaSecp256k1VerificationKey2019({ - key, + publicJwk, id, controller, }: { id: string - key: Key + publicJwk: PublicJwk controller: string }) { return new VerificationMethod({ id, type: VERIFICATION_METHOD_TYPE_ECDSA_SECP256K1_VERIFICATION_KEY_2019, controller, - publicKeyBase58: key.publicKeyBase58, + publicKeyBase58: TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey), }) } @@ -40,12 +40,18 @@ export function isEcdsaSecp256k1VerificationKey2019( } /** - * Get a key from a EcdsaSecp256k1VerificationKey2019 verification method. + * Get a public jwk from a EcdsaSecp256k1VerificationKey2019 verification method. */ -export function getKeyFromEcdsaSecp256k1VerificationKey2019(verificationMethod: EcdsaSecp256k1VerificationKey2019) { +export function getPublicJwkFromEcdsaSecp256k1VerificationKey2019( + verificationMethod: EcdsaSecp256k1VerificationKey2019 +) { if (!verificationMethod.publicKeyBase58) { throw new CredoError('verification method is missing publicKeyBase58') } - return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.K256) + return PublicJwk.fromPublicKey({ + kty: 'EC', + crv: 'secp256k1', + publicKey: TypedArrayEncoder.fromBase58(verificationMethod.publicKeyBase58), + }) } diff --git a/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2018.ts b/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2018.ts index 3851d70b16..0ba9c43e85 100644 --- a/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2018.ts +++ b/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2018.ts @@ -1,6 +1,6 @@ -import { KeyType } from '../../../../crypto' -import { Key } from '../../../../crypto/Key' import { CredoError } from '../../../../error' +import { TypedArrayEncoder } from '../../../../utils' +import { Ed25519PublicJwk, PublicJwk } from '../../../kms' import { VerificationMethod } from './VerificationMethod' @@ -12,12 +12,16 @@ type Ed25519VerificationKey2018 = VerificationMethod & { /** * Get a Ed25519VerificationKey2018 verification method. */ -export function getEd25519VerificationKey2018({ key, id, controller }: { id: string; key: Key; controller: string }) { +export function getEd25519VerificationKey2018({ + publicJwk, + id, + controller, +}: { id: string; publicJwk: PublicJwk; controller: string }) { return new VerificationMethod({ id, type: VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, controller, - publicKeyBase58: key.publicKeyBase58, + publicKeyBase58: TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey), }) } @@ -33,10 +37,18 @@ export function isEd25519VerificationKey2018( /** * Get a key from a Ed25519VerificationKey2018 verification method. */ -export function getKeyFromEd25519VerificationKey2018(verificationMethod: Ed25519VerificationKey2018) { + +/** + * Get a public jwk from a Ed25519VerificationKey2018 verification method. + */ +export function getPublicJwkFromEd25519VerificationKey2018(verificationMethod: Ed25519VerificationKey2018) { if (!verificationMethod.publicKeyBase58) { throw new CredoError('verification method is missing publicKeyBase58') } - return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.Ed25519) + return PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58(verificationMethod.publicKeyBase58), + }) } diff --git a/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2020.ts b/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2020.ts index 607b47b717..3acce186e3 100644 --- a/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2020.ts +++ b/packages/core/src/modules/dids/domain/verificationMethod/Ed25519VerificationKey2020.ts @@ -1,6 +1,5 @@ -import { KeyType } from '../../../../crypto' -import { Key } from '../../../../crypto/Key' import { CredoError } from '../../../../error' +import { Ed25519PublicJwk, PublicJwk, getJwkHumanDescription } from '../../../kms' import { VerificationMethod } from './VerificationMethod' @@ -12,12 +11,16 @@ type Ed25519VerificationKey2020 = VerificationMethod & { /** * Get a Ed25519VerificationKey2020 verification method. */ -export function getEd25519VerificationKey2020({ key, id, controller }: { id: string; key: Key; controller: string }) { +export function getEd25519VerificationKey2020({ + publicJwk, + id, + controller, +}: { id: string; publicJwk: PublicJwk; controller: string }) { return new VerificationMethod({ id, type: VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, controller, - publicKeyMultibase: key.fingerprint, + publicKeyMultibase: publicJwk.fingerprint, }) } @@ -33,15 +36,19 @@ export function isEd25519VerificationKey2020( /** * Get a key from a Ed25519VerificationKey2020 verification method. */ -export function getKeyFromEd25519VerificationKey2020(verificationMethod: Ed25519VerificationKey2020) { +export function getPublicJwkFromEd25519VerificationKey2020(verificationMethod: Ed25519VerificationKey2020) { if (!verificationMethod.publicKeyMultibase) { throw new CredoError('verification method is missing publicKeyMultibase') } - const key = Key.fromFingerprint(verificationMethod.publicKeyMultibase) - if (key.keyType !== KeyType.Ed25519) { - throw new CredoError(`Verification method publicKeyMultibase is for unexpected key type ${key.keyType}`) + const publicJwk = PublicJwk.fromFingerprint(verificationMethod.publicKeyMultibase) + const publicKey = publicJwk.publicKey + + if (publicKey.kty !== 'OKP' || publicKey.crv !== 'Ed25519') { + throw new CredoError( + `Verification method ${verificationMethod.type} is for unexpected ${getJwkHumanDescription(publicJwk.toJson())}.` + ) } - return key + return publicJwk } diff --git a/packages/core/src/modules/dids/domain/verificationMethod/JsonWebKey2020.ts b/packages/core/src/modules/dids/domain/verificationMethod/JsonWebKey2020.ts index 068db6211e..c0d1a82e82 100644 --- a/packages/core/src/modules/dids/domain/verificationMethod/JsonWebKey2020.ts +++ b/packages/core/src/modules/dids/domain/verificationMethod/JsonWebKey2020.ts @@ -1,31 +1,28 @@ -import type { Key } from '../../../../crypto/Key' -import type { JwkJson } from '../../../../crypto/jose/jwk/Jwk' import type { VerificationMethod } from './VerificationMethod' -import { getJwkFromJson, getJwkFromKey } from '../../../../crypto/jose/jwk' import { CredoError } from '../../../../error' +import { PublicJwk } from '../../../kms' export const VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 = 'JsonWebKey2020' -type JwkOrKey = { jwk: JwkJson; key?: never } | { key: Key; jwk?: never } type GetJsonWebKey2020Options = { did: string verificationMethodId?: string -} & JwkOrKey + publicJwk: PublicJwk +} /** * Get a JsonWebKey2020 verification method. */ export function getJsonWebKey2020(options: GetJsonWebKey2020Options) { - const jwk = options.jwk ? getJwkFromJson(options.jwk) : getJwkFromKey(options.key) - const verificationMethodId = options.verificationMethodId ?? `${options.did}#${jwk.key.fingerprint}` + const verificationMethodId = options.verificationMethodId ?? `${options.did}#${options.publicJwk.fingerprint}` return { id: verificationMethodId, type: VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, controller: options.did, - publicKeyJwk: options.jwk ?? jwk.toJson(), + publicKeyJwk: options.publicJwk.toJson(), } } @@ -41,12 +38,12 @@ export function isJsonWebKey2020( /** * Get a key from a JsonWebKey2020 verification method. */ -export function getKeyFromJsonWebKey2020(verificationMethod: VerificationMethod & { type: 'JsonWebKey2020' }) { +export function getPublicJwkFromJsonWebKey2020(verificationMethod: VerificationMethod & { type: 'JsonWebKey2020' }) { if (!verificationMethod.publicKeyJwk) { throw new CredoError( `Missing publicKeyJwk on verification method with type ${VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020}` ) } - return getJwkFromJson(verificationMethod.publicKeyJwk).key + return PublicJwk.fromUnknown(verificationMethod.publicKeyJwk) } diff --git a/packages/core/src/modules/dids/domain/verificationMethod/Multikey.ts b/packages/core/src/modules/dids/domain/verificationMethod/Multikey.ts index e201f969ae..5cd192f0c6 100644 --- a/packages/core/src/modules/dids/domain/verificationMethod/Multikey.ts +++ b/packages/core/src/modules/dids/domain/verificationMethod/Multikey.ts @@ -1,29 +1,29 @@ import type { VerificationMethod } from './VerificationMethod' -import { Key } from '../../../../crypto/Key' import { CredoError } from '../../../../error' +import { PublicJwk } from '../../../kms' export const VERIFICATION_METHOD_TYPE_MULTIKEY = 'Multikey' type GetMultikeyOptions = { did: string - key: Key + publicJwk: PublicJwk verificationMethodId?: string } /** * Get a Multikey verification method. */ -export function getMultikey({ did, key, verificationMethodId }: GetMultikeyOptions) { +export function getMultikey({ did, publicJwk, verificationMethodId }: GetMultikeyOptions) { if (!verificationMethodId) { - verificationMethodId = `${did}#${key.fingerprint}` + verificationMethodId = `${did}#${publicJwk.fingerprint}` } return { id: verificationMethodId, type: VERIFICATION_METHOD_TYPE_MULTIKEY, controller: did, - publicKeyMultibase: key.fingerprint, + publicKeyMultibase: publicJwk.fingerprint, } } @@ -37,14 +37,14 @@ export function isMultikey( } /** - * Get a key from a Multikey verification method. + * Get a public jwk from a Multikey verification method. */ -export function getKeyFromMultikey(verificationMethod: VerificationMethod & { type: 'Multikey' }) { +export function getPublicJwkFromMultikey(verificationMethod: VerificationMethod & { type: 'Multikey' }) { if (!verificationMethod.publicKeyMultibase) { throw new CredoError( `Missing publicKeyMultibase on verification method with type ${VERIFICATION_METHOD_TYPE_MULTIKEY}` ) } - return Key.fromFingerprint(verificationMethod.publicKeyMultibase) + return PublicJwk.fromFingerprint(verificationMethod.publicKeyMultibase) } diff --git a/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts b/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts index fab4b76c83..013ea560b1 100644 --- a/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts +++ b/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts @@ -1,6 +1,5 @@ -import type { JwkJson } from '../../../../crypto/jose/jwk/Jwk' - import { IsOptional, IsString } from 'class-validator' +import { Jwk } from '../../../kms' export interface VerificationMethodOptions { id: string @@ -8,7 +7,7 @@ export interface VerificationMethodOptions { controller: string publicKeyBase58?: string publicKeyBase64?: string - publicKeyJwk?: JwkJson + publicKeyJwk?: Jwk publicKeyHex?: string publicKeyMultibase?: string publicKeyPem?: string @@ -51,7 +50,7 @@ export class VerificationMethod { public publicKeyBase64?: string // TODO: validation of JWK - public publicKeyJwk?: JwkJson + public publicKeyJwk?: Jwk @IsOptional() @IsString() diff --git a/packages/core/src/modules/dids/domain/verificationMethod/X25519KeyAgreementKey2019.ts b/packages/core/src/modules/dids/domain/verificationMethod/X25519KeyAgreementKey2019.ts index 7df0c332f5..402f27c2c3 100644 --- a/packages/core/src/modules/dids/domain/verificationMethod/X25519KeyAgreementKey2019.ts +++ b/packages/core/src/modules/dids/domain/verificationMethod/X25519KeyAgreementKey2019.ts @@ -1,6 +1,6 @@ -import { KeyType } from '../../../../crypto' -import { Key } from '../../../../crypto/Key' import { CredoError } from '../../../../error' +import { TypedArrayEncoder } from '../../../../utils' +import { PublicJwk, X25519PublicJwk } from '../../../kms' import { VerificationMethod } from './VerificationMethod' @@ -12,12 +12,16 @@ type X25519KeyAgreementKey2019 = VerificationMethod & { /** * Get a X25519KeyAgreementKey2019 verification method. */ -export function getX25519KeyAgreementKey2019({ key, id, controller }: { id: string; key: Key; controller: string }) { +export function getX25519KeyAgreementKey2019({ + publicJwk, + id, + controller, +}: { id: string; publicJwk: PublicJwk; controller: string }) { return new VerificationMethod({ id, type: VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019, controller, - publicKeyBase58: key.publicKeyBase58, + publicKeyBase58: TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey), }) } @@ -33,10 +37,14 @@ export function isX25519KeyAgreementKey2019( /** * Get a key from a X25519KeyAgreementKey2019 verification method. */ -export function getKeyFromX25519KeyAgreementKey2019(verificationMethod: X25519KeyAgreementKey2019) { +export function getPublicJwkFrommX25519KeyAgreementKey2019(verificationMethod: X25519KeyAgreementKey2019) { if (!verificationMethod.publicKeyBase58) { throw new CredoError('verification method is missing publicKeyBase58') } - return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.X25519) + return PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'X25519', + publicKey: TypedArrayEncoder.fromBase58(verificationMethod.publicKeyBase58), + }) } diff --git a/packages/core/src/modules/dids/domain/verificationMethod/index.ts b/packages/core/src/modules/dids/domain/verificationMethod/index.ts index 53809e4d6f..3e06ec0770 100644 --- a/packages/core/src/modules/dids/domain/verificationMethod/index.ts +++ b/packages/core/src/modules/dids/domain/verificationMethod/index.ts @@ -1,8 +1,6 @@ export { VerificationMethod } from './VerificationMethod' export { VerificationMethodTransformer, IsStringOrVerificationMethod } from './VerificationMethodTransformer' -export * from './Bls12381G1Key2020' -export * from './Bls12381G2Key2020' export * from './Ed25519VerificationKey2018' export * from './Ed25519VerificationKey2020' export * from './JsonWebKey2020' diff --git a/packages/core/src/modules/dids/findMatchingEd25519Key.ts b/packages/core/src/modules/dids/findMatchingEd25519Key.ts new file mode 100644 index 0000000000..d6b395481b --- /dev/null +++ b/packages/core/src/modules/dids/findMatchingEd25519Key.ts @@ -0,0 +1,40 @@ +import { Ed25519PublicJwk, PublicJwk, X25519PublicJwk, assymetricPublicJwkMatches } from '../kms' +import { DidDocument } from './domain/DidDocument' +import { getPublicJwkFromVerificationMethod } from './domain/key-type/keyDidMapping' +import { VerificationMethod } from './domain/verificationMethod' + +/** + * Tries to find a matching Ed25519 key to the supplied X25519 key + * @param x25519Key X25519 key + * @param didDocument Did document containing all the keys + * @returns a matching Ed25519 key or `undefined` (if no matching key found) + */ +export function findMatchingEd25519Key( + x25519Key: PublicJwk, + didDocument: DidDocument +): { publicJwk: PublicJwk; verificationMethod: VerificationMethod } | undefined { + const verificationMethods = didDocument.verificationMethod ?? [] + const keyAgreements = didDocument.keyAgreement ?? [] + const authentications = didDocument.authentication ?? [] + const allKeyReferences: VerificationMethod[] = [ + ...verificationMethods, + ...authentications.filter((keyAgreement): keyAgreement is VerificationMethod => typeof keyAgreement !== 'string'), + ...keyAgreements.filter((keyAgreement): keyAgreement is VerificationMethod => typeof keyAgreement !== 'string'), + ] + + return allKeyReferences + .map((keyReference) => { + const verificationMethod = didDocument.dereferenceKey(keyReference.id) + return { + publicJwk: getPublicJwkFromVerificationMethod(verificationMethod), + verificationMethod, + } + }) + + .find((v): v is typeof v & { publicJwk: PublicJwk } => { + if (!v.publicJwk.is(Ed25519PublicJwk)) return false + + const keyX25519 = PublicJwk.fromPublicJwk(v.publicJwk.jwk.toX25519PublicJwk()) + return assymetricPublicJwkMatches(keyX25519.toJson(), x25519Key.toJson()) + }) +} diff --git a/packages/core/src/modules/dids/helpers.ts b/packages/core/src/modules/dids/helpers.ts index a4f68eead9..a0e6736689 100644 --- a/packages/core/src/modules/dids/helpers.ts +++ b/packages/core/src/modules/dids/helpers.ts @@ -1,7 +1,8 @@ -import { Key, KeyType } from '../../crypto' -import { isDid } from '../../utils' +import { CredoError } from '../../error' +import { TypedArrayEncoder, isDid } from '../../utils' +import { Ed25519PublicJwk, PublicJwk } from '../kms' -import { DidKey } from './methods/key' +import { DidKey } from './methods/key/DidKey' export function isDidKey(key: string) { return isDid(key, 'key') @@ -9,26 +10,42 @@ export function isDidKey(key: string) { export function didKeyToVerkey(key: string) { if (isDidKey(key)) { - const publicKeyBase58 = DidKey.fromDid(key).key.publicKeyBase58 + const publicKey = DidKey.fromDid(key).publicJwk.publicKey + if (publicKey.kty !== 'OKP' || publicKey.crv !== 'Ed25519') { + throw new CredoError('Expected OKP key with crv Ed25519') + } + + const publicKeyBase58 = TypedArrayEncoder.toBase58(publicKey.publicKey) return publicKeyBase58 } + return key } -export function verkeyToDidKey(key: string) { - if (isDidKey(key)) return key - const publicKeyBase58 = key - const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519) +export function verkeyToDidKey(verkey: string) { + if (isDidKey(verkey)) return verkey + + const ed25519Key = verkeyToPublicJwk(verkey) const didKey = new DidKey(ed25519Key) return didKey.did } -export function didKeyToInstanceOfKey(key: string) { +export function didKeyToEd25519PublicJwk(key: string) { const didKey = DidKey.fromDid(key) - return didKey.key + if (didKey.publicJwk.jwk instanceof Ed25519PublicJwk) { + return didKey.publicJwk as PublicJwk + } + + throw new CredoError( + `Expected public jwk to have kty OKP with crv Ed25519, found ${didKey.publicJwk.jwkTypehumanDescription}` + ) } -export function verkeyToInstanceOfKey(verkey: string) { - const ed25519Key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) +export function verkeyToPublicJwk(verkey: string) { + const ed25519Key = PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58(verkey), + }) as PublicJwk return ed25519Key } diff --git a/packages/core/src/modules/dids/index.ts b/packages/core/src/modules/dids/index.ts index 5f1677eb88..98cbc46e04 100644 --- a/packages/core/src/modules/dids/index.ts +++ b/packages/core/src/modules/dids/index.ts @@ -7,3 +7,4 @@ export * from './services' export * from './DidsModule' export * from './methods' export * from './DidsModuleConfig' +export { findMatchingEd25519Key } from './findMatchingEd25519Key' diff --git a/packages/core/src/modules/dids/methods/jwk/DidJwk.ts b/packages/core/src/modules/dids/methods/jwk/DidJwk.ts index 81366791e0..3f4cef7082 100644 --- a/packages/core/src/modules/dids/methods/jwk/DidJwk.ts +++ b/packages/core/src/modules/dids/methods/jwk/DidJwk.ts @@ -1,33 +1,31 @@ -import type { Jwk } from '../../../../crypto' - -import { getJwkFromJson } from '../../../../crypto/jose/jwk' import { JsonEncoder } from '../../../../utils' +import { PublicJwk } from '../../../kms' import { parseDid } from '../../domain/parse' import { getDidJwkDocument } from './didJwkDidDocument' export class DidJwk { - public readonly did: string - - private constructor(did: string) { - this.did = did - } + private constructor( + public readonly did: string, + public readonly publicJwk: PublicJwk + ) {} public get allowsEncrypting() { - return this.jwk.use === 'enc' || this.key.supportsEncrypting + return this.publicJwk.toJson().use === 'enc' || this.publicJwk.supportdEncryptionKeyAgreementAlgorithms.length > 0 } public get allowsSigning() { - return this.jwk.use === 'sig' || this.key.supportsSigning + return this.publicJwk.toJson().use === 'sig' || this.publicJwk.supportedSignatureAlgorithms.length > 0 } public static fromDid(did: string) { const parsed = parseDid(did) const jwkJson = JsonEncoder.fromBase64(parsed.id) + // This validates the jwk - getJwkFromJson(jwkJson) + const publicJwk = PublicJwk.fromUnknown(jwkJson) - return new DidJwk(did) + return new DidJwk(did, publicJwk) } /** @@ -38,27 +36,14 @@ export class DidJwk { return `${this.did}#0` } - public static fromJwk(jwk: Jwk) { - const did = `did:jwk:${JsonEncoder.toBase64URL(jwk.toJson())}` - - return new DidJwk(did) - } + public static fromPublicJwk(publicJwk: PublicJwk) { + const did = `did:jwk:${JsonEncoder.toBase64URL(publicJwk.toJson({ includeKid: false }))}` - public get key() { - return this.jwk.key - } - - public get jwk() { - const jwk = getJwkFromJson(this.jwkJson) - - return jwk + return new DidJwk(did, publicJwk) } public get jwkJson() { - const parsed = parseDid(this.did) - const jwkJson = JsonEncoder.fromBase64(parsed.id) - - return jwkJson + return this.publicJwk.toJson() } public get didDocument() { diff --git a/packages/core/src/modules/dids/methods/jwk/JwkDidRegistrar.ts b/packages/core/src/modules/dids/methods/jwk/JwkDidRegistrar.ts index 187d72d2ef..7aa04837a6 100644 --- a/packages/core/src/modules/dids/methods/jwk/JwkDidRegistrar.ts +++ b/packages/core/src/modules/dids/methods/jwk/JwkDidRegistrar.ts @@ -1,13 +1,18 @@ import type { AgentContext } from '../../../../agent' -import type { Key, KeyType } from '../../../../crypto' -import type { Buffer } from '../../../../utils' import type { DidRegistrar } from '../../domain/DidRegistrar' import type { DidCreateOptions, DidCreateResult, DidDeactivateResult, DidUpdateResult } from '../../types' -import { getJwkFromKey } from '../../../../crypto/jose/jwk' import { DidDocumentRole } from '../../domain/DidDocumentRole' import { DidRecord, DidRepository } from '../../repository' +import { XOR } from '../../../../types' +import { + KeyManagementApi, + KmsCreateKeyOptions, + KmsCreateKeyTypeAssymetric, + KmsJwkPublicAsymmetric, + PublicJwk, +} from '../../../kms' import { DidJwk } from './DidJwk' export class JwkDidRegistrar implements DidRegistrar { @@ -16,50 +21,64 @@ export class JwkDidRegistrar implements DidRegistrar { public async create(agentContext: AgentContext, options: JwkDidCreateOptions): Promise { const didRepository = agentContext.dependencyManager.resolve(DidRepository) - const keyType = options.options.keyType - const seed = options.secret?.seed - const privateKey = options.secret?.privateKey - try { - let key = options.options.key - - if (key && (keyType || seed || privateKey)) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Key instance cannot be combined with key type, seed or private key', - }, + let publicJwk: KmsJwkPublicAsymmetric + let keyId: string + const kms = agentContext.dependencyManager.resolve(KeyManagementApi) + + if (options.options.createKey) { + const createKeyResult = await kms.createKey(options.options.createKey) + publicJwk = createKeyResult.publicJwk + keyId = createKeyResult.keyId + } else if (options.options.keyId) { + const _publicJwk = await kms.getPublicKey({ keyId: options.options.keyId }) + keyId = options.options.keyId + if (!_publicJwk) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notFound: key with key id '${options.options.keyId}' not found`, + }, + } } - } - if (keyType) { - key = await agentContext.wallet.createKey({ - keyType, - seed, - privateKey, - }) - } + if (_publicJwk.kty === 'oct') { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notFound: key with key id '${options.options.keyId}' uses unsupported kty 'oct' for did:jwk`, + }, + } + } - if (!key) { + publicJwk = _publicJwk + } else { return { didDocumentMetadata: {}, didRegistrationMetadata: {}, didState: { state: 'failed', - reason: 'Missing key type or key instance', + reason: 'Missing keyId or createKey', }, } } - const jwk = getJwkFromKey(key) - const didJwk = DidJwk.fromJwk(jwk) + const didJwk = DidJwk.fromPublicJwk(PublicJwk.fromPublicJwk(publicJwk)) // Save the did so we know we created it and can issue with it const didRecord = new DidRecord({ did: didJwk.did, role: DidDocumentRole.Created, + keys: [ + { + didDocumentRelativeKeyId: '#0', + kmsKeyId: keyId, + }, + ], }) await didRepository.save(agentContext, didRecord) @@ -70,15 +89,6 @@ export class JwkDidRegistrar implements DidRegistrar { state: 'finished', did: didJwk.did, didDocument: didJwk.didDocument, - secret: { - // FIXME: the uni-registrar creates the seed in the registrar method - // if it doesn't exist so the seed can always be returned. Currently - // we can only return it if the seed was passed in by the user. Once - // we have a secure method for generating seeds we should use the same - // approach - seed: options.secret?.seed, - privateKey: options.secret?.privateKey, - }, }, } } catch (error) { @@ -121,14 +131,13 @@ export interface JwkDidCreateOptions extends DidCreateOptions { // For now we don't support creating a did:jwk with a did or did document did?: never didDocument?: never - options: { - keyType?: KeyType - key?: Key - } - secret?: { - seed?: Buffer - privateKey?: Buffer - } + secret?: never + + /** + * You can create a did:jwk based on an existing `keyId`, or provide `createKey` options + * to create a new key. + */ + options: XOR<{ createKey: KmsCreateKeyOptions }, { keyId: string }> } // Update and Deactivate not supported for did:jwk diff --git a/packages/core/src/modules/dids/methods/jwk/__tests__/DidJwk.test.ts b/packages/core/src/modules/dids/methods/jwk/__tests__/DidJwk.test.ts index 036e0c940d..807d27b2dd 100644 --- a/packages/core/src/modules/dids/methods/jwk/__tests__/DidJwk.test.ts +++ b/packages/core/src/modules/dids/methods/jwk/__tests__/DidJwk.test.ts @@ -1,4 +1,4 @@ -import { getJwkFromJson } from '../../../../../crypto/jose/jwk' +import { PublicJwk } from '../../../../kms' import { DidJwk } from '../DidJwk' import { p256DidJwkEyJjcnYi0iFixture } from './__fixtures__/p256DidJwkEyJjcnYi0i' @@ -16,7 +16,9 @@ describe('DidJwk', () => { }) it('creates a DidJwk instance from a jwk instance', async () => { - const didJwk = DidJwk.fromJwk(getJwkFromJson(p256DidJwkEyJjcnYi0iFixture.verificationMethod[0].publicKeyJwk)) + const didJwk = DidJwk.fromPublicJwk( + PublicJwk.fromUnknown(p256DidJwkEyJjcnYi0iFixture.verificationMethod[0].publicKeyJwk) + ) expect(didJwk.did).toBe(p256DidJwkEyJjcnYi0iFixture.id) expect(didJwk.didDocument.toJSON()).toMatchObject(p256DidJwkEyJjcnYi0iFixture) diff --git a/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidRegistrar.test.ts b/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidRegistrar.test.ts index 0783ca7bf0..b787837426 100644 --- a/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidRegistrar.test.ts +++ b/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidRegistrar.test.ts @@ -1,11 +1,8 @@ -import type { Wallet } from '../../../../../wallet' - -import { getAgentContext, mockFunction } from '../../../../../../tests/helpers' -import { KeyType } from '../../../../../crypto' -import { getJwkFromJson } from '../../../../../crypto/jose/jwk' +import { transformPrivateKeyToPrivateJwk } from '../../../../../../../askar/src/utils' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' import { TypedArrayEncoder } from '../../../../../utils' import { JsonTransformer } from '../../../../../utils/JsonTransformer' -import { WalletError } from '../../../../../wallet/error' +import { KeyManagementApi } from '../../../../kms' import { DidDocumentRole } from '../../../domain/DidDocumentRole' import { DidRepository } from '../../../repository/DidRepository' import { JwkDidRegistrar } from '../JwkDidRegistrar' @@ -13,24 +10,16 @@ import { JwkDidRegistrar } from '../JwkDidRegistrar' jest.mock('../../../repository/DidRepository') const DidRepositoryMock = DidRepository as jest.Mock -const jwk = getJwkFromJson({ - crv: 'P-256', - kty: 'EC', - x: 'acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0', - y: '_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE', -}) -const walletMock = { - createKey: jest.fn(() => jwk.key), -} as unknown as Wallet - const didRepositoryMock = new DidRepositoryMock() const jwkDidRegistrar = new JwkDidRegistrar() const agentContext = getAgentContext({ - wallet: walletMock, registerInstances: [[DidRepository, didRepositoryMock]], + agentConfig: getAgentConfig('JwkDidRegistrar'), }) +const kms = agentContext.dependencyManager.resolve(KeyManagementApi) + describe('DidRegistrar', () => { afterEach(() => { jest.clearAllMocks() @@ -39,14 +28,22 @@ describe('DidRegistrar', () => { describe('JwkDidRegistrar', () => { it('should correctly create a did:jwk document using P256 key type', async () => { const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + const { privateJwk } = transformPrivateKeyToPrivateJwk({ + type: { + kty: 'EC', + crv: 'P-256', + }, + privateKey, + }) + + const { keyId } = await kms.importKey({ + privateJwk, + }) const result = await jwkDidRegistrar.create(agentContext, { method: 'jwk', options: { - keyType: KeyType.P256, - }, - secret: { - privateKey, + keyId, }, }) @@ -55,68 +52,40 @@ describe('DidRegistrar', () => { didRegistrationMetadata: {}, didState: { state: 'finished', - did: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + did: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InRlQTNYV1pQTXUyYVRtelB1aVM1eVRkeUhUY3JGNWJJUG4yTlNYS0gwLVEiLCJ5IjoiX3QybE01dGNGOFV2dDZ0QlFZRTVlOHVweGtlbGtEZ3QtWFc0aXhyQUlKayJ9', didDocument: { '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], - id: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + id: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InRlQTNYV1pQTXUyYVRtelB1aVM1eVRkeUhUY3JGNWJJUG4yTlNYS0gwLVEiLCJ5IjoiX3QybE01dGNGOFV2dDZ0QlFZRTVlOHVweGtlbGtEZ3QtWFc0aXhyQUlKayJ9', verificationMethod: [ { - id: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + id: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InRlQTNYV1pQTXUyYVRtelB1aVM1eVRkeUhUY3JGNWJJUG4yTlNYS0gwLVEiLCJ5IjoiX3QybE01dGNGOFV2dDZ0QlFZRTVlOHVweGtlbGtEZ3QtWFc0aXhyQUlKayJ9#0', type: 'JsonWebKey2020', controller: - 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InRlQTNYV1pQTXUyYVRtelB1aVM1eVRkeUhUY3JGNWJJUG4yTlNYS0gwLVEiLCJ5IjoiX3QybE01dGNGOFV2dDZ0QlFZRTVlOHVweGtlbGtEZ3QtWFc0aXhyQUlKayJ9', publicKeyJwk: { crv: 'P-256', kty: 'EC', - x: 'acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0', - y: '_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE', + x: 'teA3XWZPMu2aTmzPuiS5yTdyHTcrF5bIPn2NSXKH0-Q', + y: '_t2lM5tcF8Uvt6tBQYE5e8upxkelkDgt-XW4ixrAIJk', }, }, ], assertionMethod: [ - 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InRlQTNYV1pQTXUyYVRtelB1aVM1eVRkeUhUY3JGNWJJUG4yTlNYS0gwLVEiLCJ5IjoiX3QybE01dGNGOFV2dDZ0QlFZRTVlOHVweGtlbGtEZ3QtWFc0aXhyQUlKayJ9#0', ], authentication: [ - 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InRlQTNYV1pQTXUyYVRtelB1aVM1eVRkeUhUY3JGNWJJUG4yTlNYS0gwLVEiLCJ5IjoiX3QybE01dGNGOFV2dDZ0QlFZRTVlOHVweGtlbGtEZ3QtWFc0aXhyQUlKayJ9#0', ], capabilityInvocation: [ - 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InRlQTNYV1pQTXUyYVRtelB1aVM1eVRkeUhUY3JGNWJJUG4yTlNYS0gwLVEiLCJ5IjoiX3QybE01dGNGOFV2dDZ0QlFZRTVlOHVweGtlbGtEZ3QtWFc0aXhyQUlKayJ9#0', ], capabilityDelegation: [ - 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InRlQTNYV1pQTXUyYVRtelB1aVM1eVRkeUhUY3JGNWJJUG4yTlNYS0gwLVEiLCJ5IjoiX3QybE01dGNGOFV2dDZ0QlFZRTVlOHVweGtlbGtEZ3QtWFc0aXhyQUlKayJ9#0', ], keyAgreement: [ - 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InRlQTNYV1pQTXUyYVRtelB1aVM1eVRkeUhUY3JGNWJJUG4yTlNYS0gwLVEiLCJ5IjoiX3QybE01dGNGOFV2dDZ0QlFZRTVlOHVweGtlbGtEZ3QtWFc0aXhyQUlKayJ9#0', ], }, - secret: { - privateKey, - }, - }, - }) - - expect(walletMock.createKey).toHaveBeenCalledWith({ keyType: KeyType.P256, privateKey }) - }) - - it('should return an error state if a key instance and key type are both provided', async () => { - const key = await agentContext.wallet.createKey({ - keyType: KeyType.P256, - }) - - const result = await jwkDidRegistrar.create(agentContext, { - method: 'jwk', - options: { - key, - keyType: KeyType.P256, - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Key instance cannot be combined with key type, seed or private key', }, }) }) @@ -124,6 +93,7 @@ describe('DidRegistrar', () => { it('should return an error state if no key or key type is provided', async () => { const result = await jwkDidRegistrar.create(agentContext, { method: 'jwk', + // @ts-ignore options: {}, }) @@ -132,47 +102,30 @@ describe('DidRegistrar', () => { didRegistrationMetadata: {}, didState: { state: 'failed', - reason: 'Missing key type or key instance', - }, - }) - }) - - it('should return an error state if a key creation error is thrown', async () => { - mockFunction(walletMock.createKey).mockRejectedValueOnce(new WalletError('Invalid private key provided')) - const result = await jwkDidRegistrar.create(agentContext, { - method: 'jwk', - options: { - keyType: KeyType.P256, - }, - secret: { - privateKey: TypedArrayEncoder.fromString('invalid'), - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: expect.stringContaining('Invalid private key provided'), + reason: 'Missing keyId or createKey', }, }) }) it('should store the did document', async () => { const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + const { privateJwk } = transformPrivateKeyToPrivateJwk({ + type: { + crv: 'P-256', + kty: 'EC', + }, + privateKey, + }) const did = - 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9' + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InRlQTNYV1pQTXUyYVRtelB1aVM1eVRkeUhUY3JGNWJJUG4yTlNYS0gwLVEiLCJ5IjoiX3QybE01dGNGOFV2dDZ0QlFZRTVlOHVweGtlbGtEZ3QtWFc0aXhyQUlKayJ9' + const key = await kms.importKey({ + privateJwk, + }) await jwkDidRegistrar.create(agentContext, { method: 'jwk', - options: { - keyType: KeyType.P256, - }, - secret: { - privateKey, - }, + options: { keyId: key.keyId }, }) expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) diff --git a/packages/core/src/modules/dids/methods/jwk/didJwkDidDocument.ts b/packages/core/src/modules/dids/methods/jwk/didJwkDidDocument.ts index b183950a31..40ed8dfef4 100644 --- a/packages/core/src/modules/dids/methods/jwk/didJwkDidDocument.ts +++ b/packages/core/src/modules/dids/methods/jwk/didJwkDidDocument.ts @@ -1,22 +1,17 @@ import type { DidJwk } from './DidJwk' import { CredoError } from '../../../../error' -import { JsonEncoder } from '../../../../utils' import { SECURITY_JWS_CONTEXT_URL } from '../../../vc/constants' import { DidDocumentBuilder, getJsonWebKey2020 } from '../../domain' -import { parseDid } from '../../domain/parse' export function getDidJwkDocument(didJwk: DidJwk) { if (!didJwk.allowsEncrypting && !didJwk.allowsSigning) { throw new CredoError('At least one of allowsSigning or allowsEncrypting must be enabled') } - const parsed = parseDid(didJwk.did) - const jwkJson = JsonEncoder.fromBase64(parsed.id) - const verificationMethod = getJsonWebKey2020({ did: didJwk.did, - jwk: jwkJson, + publicJwk: didJwk.publicJwk, verificationMethodId: didJwk.verificationMethodId, }) diff --git a/packages/core/src/modules/dids/methods/key/DidKey.ts b/packages/core/src/modules/dids/methods/key/DidKey.ts index fb377d63c0..357ed6b673 100644 --- a/packages/core/src/modules/dids/methods/key/DidKey.ts +++ b/packages/core/src/modules/dids/methods/key/DidKey.ts @@ -1,26 +1,26 @@ -import { Key } from '../../../../crypto/Key' -import { getDidDocumentForKey } from '../../domain/keyDidDocument' +import { PublicJwk } from '../../../kms' +import { getDidDocumentForPublicJwk } from '../../domain/keyDidDocument' import { parseDid } from '../../domain/parse' export class DidKey { - public readonly key: Key + public readonly publicJwk: PublicJwk - public constructor(key: Key) { - this.key = key + public constructor(publicJwk: PublicJwk) { + this.publicJwk = publicJwk } public static fromDid(did: string) { const parsed = parseDid(did) - const key = Key.fromFingerprint(parsed.id) - return new DidKey(key) + const publicJwk = PublicJwk.fromFingerprint(parsed.id) + return new DidKey(publicJwk) } public get did() { - return `did:key:${this.key.fingerprint}` + return `did:key:${this.publicJwk.fingerprint}` } public get didDocument() { - return getDidDocumentForKey(this.did, this.key) + return getDidDocumentForPublicJwk(this.did, this.publicJwk) } } diff --git a/packages/core/src/modules/dids/methods/key/KeyDidRegistrar.ts b/packages/core/src/modules/dids/methods/key/KeyDidRegistrar.ts index 65e3d931f1..ccf3952315 100644 --- a/packages/core/src/modules/dids/methods/key/KeyDidRegistrar.ts +++ b/packages/core/src/modules/dids/methods/key/KeyDidRegistrar.ts @@ -1,12 +1,17 @@ import type { AgentContext } from '../../../../agent' -import type { Key, KeyType } from '../../../../crypto' -import type { Buffer } from '../../../../utils' import type { DidRegistrar } from '../../domain/DidRegistrar' import type { DidCreateOptions, DidCreateResult, DidDeactivateResult, DidUpdateResult } from '../../types' +import { XOR } from '../../../../types' +import { + KeyManagementApi, + KmsCreateKeyOptions, + KmsCreateKeyTypeAssymetric, + KmsJwkPublicAsymmetric, + PublicJwk, +} from '../../../kms' import { DidDocumentRole } from '../../domain/DidDocumentRole' import { DidRecord, DidRepository } from '../../repository' - import { DidKey } from './DidKey' export class KeyDidRegistrar implements DidRegistrar { @@ -15,49 +20,59 @@ export class KeyDidRegistrar implements DidRegistrar { public async create(agentContext: AgentContext, options: KeyDidCreateOptions): Promise { const didRepository = agentContext.dependencyManager.resolve(DidRepository) - const keyType = options.options.keyType - const seed = options.secret?.seed - const privateKey = options.secret?.privateKey - try { - let key = options.options.key - - if (key && (keyType || seed || privateKey)) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Key instance cannot be combined with key type, seed or private key', - }, - } - } - - if (keyType) { - key = await agentContext.wallet.createKey({ - keyType, - seed, - privateKey, + let publicJwk: KmsJwkPublicAsymmetric + let keyId: string + const kms = agentContext.dependencyManager.resolve(KeyManagementApi) + + if (options.options.createKey) { + const createKeyResult = await kms.createKey(options.options.createKey) + publicJwk = createKeyResult.publicJwk + keyId = createKeyResult.keyId + } else { + const _publicJwk = await kms.getPublicKey({ + keyId: options.options.keyId, }) - } + keyId = options.options.keyId + if (!_publicJwk) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notFound: key with key id '${options.options.keyId}' not found`, + }, + } + } - if (!key) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Missing key type or key instance', - }, + if (_publicJwk.kty === 'oct') { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notFound: key with key id '${options.options.keyId}' uses unsupported kty 'oct' for did:key`, + }, + } } + + publicJwk = _publicJwk } - const didKey = new DidKey(key) + const jwk = PublicJwk.fromPublicJwk(publicJwk) + const didKey = new DidKey(jwk) // Save the did so we know we created it and can issue with it const didRecord = new DidRecord({ did: didKey.did, role: DidDocumentRole.Created, + + keys: [ + { + didDocumentRelativeKeyId: `#${didKey.publicJwk.fingerprint}`, + kmsKeyId: keyId, + }, + ], }) await didRepository.save(agentContext, didRecord) @@ -68,15 +83,7 @@ export class KeyDidRegistrar implements DidRegistrar { state: 'finished', did: didKey.did, didDocument: didKey.didDocument, - secret: { - // FIXME: the uni-registrar creates the seed in the registrar method - // if it doesn't exist so the seed can always be returned. Currently - // we can only return it if the seed was passed in by the user. Once - // we have a secure method for generating seeds we should use the same - // approach - seed: options.secret?.seed, - privateKey: options.secret?.privateKey, - }, + secret: {}, }, } } catch (error) { @@ -119,14 +126,13 @@ export interface KeyDidCreateOptions extends DidCreateOptions { // For now we don't support creating a did:key with a did or did document did?: never didDocument?: never - options: { - keyType?: KeyType - key?: Key - } - secret?: { - seed?: Buffer - privateKey?: Buffer - } + secret?: never + + /** + * You can create a did:key based on an existing `keyId`, or provide `createKey` options + * to create a new key. + */ + options: XOR<{ createKey: KmsCreateKeyOptions }, { keyId: string }> } // Update and Deactivate not supported for did:key diff --git a/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts b/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts index 5994e3baeb..c2f1ce069b 100644 --- a/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts +++ b/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts @@ -1,8 +1,6 @@ -import { KeyType } from '../../../../../crypto' -import { Key } from '../../../../../crypto/Key' -import didKeyBls12381g1 from '../../../__tests__/__fixtures__/didKeyBls12381g1.json' -import didKeyBls12381g1g2 from '../../../__tests__/__fixtures__/didKeyBls12381g1g2.json' -import didKeyBls12381g2 from '../../../__tests__/__fixtures__/didKeyBls12381g2.json' +import { TypedArrayEncoder } from '../../../../../utils' +import { PublicJwk } from '../../../../kms' + import didKeyEd25519 from '../../../__tests__/__fixtures__/didKeyEd25519.json' import didKeyK256 from '../../../__tests__/__fixtures__/didKeyK256.json' import didKeyP256 from '../../../__tests__/__fixtures__/didKeyP256.json' @@ -13,17 +11,7 @@ import { DidKey } from '../DidKey' describe('DidKey', () => { it('creates a DidKey instance from a did', async () => { - const documentTypes = [ - didKeyX25519, - didKeyEd25519, - didKeyBls12381g1, - didKeyBls12381g2, - didKeyBls12381g1g2, - didKeyP256, - didKeyP384, - didKeyP521, - didKeyK256, - ] + const documentTypes = [didKeyX25519, didKeyEd25519, didKeyP256, didKeyP384, didKeyP521, didKeyK256] for (const documentType of documentTypes) { const didKey = DidKey.fromDid(documentType.id) @@ -33,7 +21,11 @@ describe('DidKey', () => { }) it('creates a DidKey instance from a key instance', async () => { - const key = Key.fromPublicKeyBase58(didKeyX25519.keyAgreement[0].publicKeyBase58, KeyType.X25519) + const key = PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'X25519', + publicKey: TypedArrayEncoder.fromBase58(didKeyX25519.keyAgreement[0].publicKeyBase58), + }) const didKey = new DidKey(key) expect(didKey.did).toBe(didKeyX25519.id) diff --git a/packages/core/src/modules/dids/methods/key/__tests__/KeyDidRegistrar.test.ts b/packages/core/src/modules/dids/methods/key/__tests__/KeyDidRegistrar.test.ts index a5fff9ccc1..bf3055a010 100644 --- a/packages/core/src/modules/dids/methods/key/__tests__/KeyDidRegistrar.test.ts +++ b/packages/core/src/modules/dids/methods/key/__tests__/KeyDidRegistrar.test.ts @@ -1,11 +1,8 @@ -import type { Wallet } from '../../../../../wallet' - -import { getAgentContext, mockFunction } from '../../../../../../tests/helpers' -import { KeyType } from '../../../../../crypto' -import { Key } from '../../../../../crypto/Key' +import { transformPrivateKeyToPrivateJwk } from '../../../../../../../askar/src' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' import { TypedArrayEncoder } from '../../../../../utils' import { JsonTransformer } from '../../../../../utils/JsonTransformer' -import { WalletError } from '../../../../../wallet/error' +import { KeyManagementApi } from '../../../../kms' import { DidDocumentRole } from '../../../domain/DidDocumentRole' import { DidRepository } from '../../../repository/DidRepository' import { KeyDidRegistrar } from '../KeyDidRegistrar' @@ -15,17 +12,14 @@ import didKeyz6MksLeFixture from './__fixtures__/didKeyz6MksLe.json' jest.mock('../../../repository/DidRepository') const DidRepositoryMock = DidRepository as jest.Mock -const walletMock = { - createKey: jest.fn(() => Key.fromFingerprint('z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU')), -} as unknown as Wallet - const didRepositoryMock = new DidRepositoryMock() const keyDidRegistrar = new KeyDidRegistrar() const agentContext = getAgentContext({ - wallet: walletMock, registerInstances: [[DidRepository, didRepositoryMock]], + agentConfig: getAgentConfig('KeyDidRegistrar'), }) +const kms = agentContext.resolve(KeyManagementApi) describe('DidRegistrar', () => { afterEach(() => { @@ -34,44 +28,22 @@ describe('DidRegistrar', () => { describe('KeyDidRegistrar', () => { it('should correctly create a did:key document using Ed25519 key type', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - - const result = await keyDidRegistrar.create(agentContext, { - method: 'key', - options: { - keyType: KeyType.Ed25519, - }, - secret: { - privateKey, - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'finished', - did: 'did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU', - didDocument: didKeyz6MksLeFixture, - secret: { - privateKey, - }, + const privateJwk = transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e'), + type: { + kty: 'OKP', + crv: 'Ed25519', }, - }) - - expect(walletMock.createKey).toHaveBeenCalledWith({ keyType: KeyType.Ed25519, privateKey }) - }) + }).privateJwk - it('should return an error state if a key instance and key type are both provided', async () => { - const key = await agentContext.wallet.createKey({ - keyType: KeyType.P256, + const { keyId } = await kms.importKey({ + privateJwk, }) const result = await keyDidRegistrar.create(agentContext, { method: 'key', options: { - key, - keyType: KeyType.P256, + keyId, }, }) @@ -79,8 +51,9 @@ describe('DidRegistrar', () => { didDocumentMetadata: {}, didRegistrationMetadata: {}, didState: { - state: 'failed', - reason: 'Key instance cannot be combined with key type, seed or private key', + state: 'finished', + did: 'did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU', + didDocument: didKeyz6MksLeFixture, }, }) }) @@ -88,6 +61,7 @@ describe('DidRegistrar', () => { it('should return an error state if no key or key type is provided', async () => { const result = await keyDidRegistrar.create(agentContext, { method: 'key', + // @ts-ignore options: {}, }) @@ -96,45 +70,32 @@ describe('DidRegistrar', () => { didRegistrationMetadata: {}, didState: { state: 'failed', - reason: 'Missing key type or key instance', + reason: 'unknownError: Invalid options provided to getPublicKey method\n\t- Required at "keyId"', }, }) }) - it('should return an error state if a key creation error is thrown', async () => { - mockFunction(walletMock.createKey).mockRejectedValueOnce(new WalletError('Invalid private key provided')) - const result = await keyDidRegistrar.create(agentContext, { - method: 'key', - options: { - keyType: KeyType.Ed25519, - }, - secret: { - privateKey: TypedArrayEncoder.fromString('invalid'), - }, - }) + it('should store the did document', async () => { + const _privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + const did = 'did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU' - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: expect.stringContaining('Invalid private key provided'), + const privateJwk = transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e'), + type: { + kty: 'OKP', + crv: 'Ed25519', }, - }) - }) + }).privateJwk - it('should store the did document', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - const did = 'did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU' + const { keyId } = await kms.importKey({ + privateJwk, + }) await keyDidRegistrar.create(agentContext, { method: 'key', options: { - keyType: KeyType.Ed25519, - }, - secret: { - privateKey, + keyId, }, }) diff --git a/packages/core/src/modules/dids/methods/key/__tests__/KeyDidResolver.test.ts b/packages/core/src/modules/dids/methods/key/__tests__/KeyDidResolver.test.ts index 08157cbdcb..f4a3cbcb31 100644 --- a/packages/core/src/modules/dids/methods/key/__tests__/KeyDidResolver.test.ts +++ b/packages/core/src/modules/dids/methods/key/__tests__/KeyDidResolver.test.ts @@ -60,7 +60,7 @@ describe('DidResolver', () => { didDocumentMetadata: {}, didResolutionMetadata: { error: 'notFound', - message: `resolver_error: Unable to resolve did 'did:key:z6MkmjYasdfasfd8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th': Error: Unsupported key type from multicodec code '107'`, + message: `resolver_error: Unable to resolve did 'did:key:z6MkmjYasdfasfd8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th': KeyManagementError: Unsupported multicodec public key with prefix '107'`, }, }) }) diff --git a/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts b/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts index 82c6e18f2f..b38b7b92e2 100644 --- a/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts +++ b/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts @@ -1,6 +1,5 @@ import type { AgentContext } from '../../../../agent' -import type { Key, KeyType } from '../../../../crypto' -import type { Buffer } from '../../../../utils' + import type { DidRegistrar } from '../../domain/DidRegistrar' import type { DidCreateOptions, DidCreateResult, DidDeactivateResult, DidUpdateResult } from '../../types' @@ -9,8 +8,11 @@ import { DidDocument } from '../../domain' import { DidDocumentRole } from '../../domain/DidDocumentRole' import { DidRecord, DidRepository } from '../../repository' +import { XOR } from '../../../../types' +import { KeyManagementApi, KmsCreateKeyOptions, KmsCreateKeyTypeAssymetric, PublicJwk } from '../../../kms' +import { DidDocumentKey } from '../../DidsApiOptions' import { PeerDidNumAlgo, getAlternativeDidsForPeerDid } from './didPeer' -import { keyToNumAlgo0DidDocument } from './peerDidNumAlgo0' +import { publicJwkToNumAlgo0DidDocument } from './peerDidNumAlgo0' import { didDocumentJsonToNumAlgo1Did } from './peerDidNumAlgo1' import { didDocumentToNumAlgo2Did } from './peerDidNumAlgo2' import { didDocumentToNumAlgo4Did } from './peerDidNumAlgo4' @@ -26,65 +28,80 @@ export class PeerDidRegistrar implements DidRegistrar { | PeerDidNumAlgo2CreateOptions | PeerDidNumAlgo4CreateOptions ): Promise { + const kms = agentContext.dependencyManager.resolve(KeyManagementApi) const didRepository = agentContext.dependencyManager.resolve(DidRepository) let did: string let didDocument: DidDocument + let keys: DidDocumentKey[] + try { if (isPeerDidNumAlgo0CreateOptions(options)) { - const keyType = options.options.keyType - const seed = options.secret?.seed - const privateKey = options.secret?.privateKey - - let key = options.options.key - - if (key && (keyType || seed || privateKey)) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Key instance cannot be combined with key type, seed or private key', + let publicJwk: PublicJwk + + if (options.options.createKey) { + const createKeyResult = await kms.createKey(options.options.createKey) + publicJwk = PublicJwk.fromPublicJwk(createKeyResult.publicJwk) + keys = [ + { + didDocumentRelativeKeyId: `#${publicJwk.fingerprint}`, + kmsKeyId: createKeyResult.keyId, }, + ] + } else { + const _publicJwk = await kms.getPublicKey({ + keyId: options.options.keyId, + }) + + if (!_publicJwk) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notFound: key with key id '${options.options.keyId}' not found`, + }, + } } - } - if (keyType) { - key = await agentContext.wallet.createKey({ - keyType, - seed, - privateKey, - }) - } + if (_publicJwk.kty === 'oct') { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notFound: key with key id '${options.options.keyId}' uses unsupported kty 'oct' for did:key`, + }, + } + } - if (!key) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Missing key type or key instance', + publicJwk = PublicJwk.fromPublicJwk(_publicJwk) + keys = [ + { + didDocumentRelativeKeyId: `#${publicJwk.fingerprint}`, + kmsKeyId: options.options.keyId, }, - } + ] } - // TODO: validate did:peer document - - didDocument = keyToNumAlgo0DidDocument(key) + didDocument = publicJwkToNumAlgo0DidDocument(publicJwk) did = didDocument.id } else if (isPeerDidNumAlgo1CreateOptions(options)) { const didDocumentJson = options.didDocument.toJSON() did = didDocumentJsonToNumAlgo1Did(didDocumentJson) + keys = options.options.keys didDocument = JsonTransformer.fromJSON({ ...didDocumentJson, id: did }, DidDocument) } else if (isPeerDidNumAlgo2CreateOptions(options)) { const didDocumentJson = options.didDocument.toJSON() did = didDocumentToNumAlgo2Did(options.didDocument) + keys = options.options.keys didDocument = JsonTransformer.fromJSON({ ...didDocumentJson, id: did }, DidDocument) } else if (isPeerDidNumAlgo4CreateOptions(options)) { const didDocumentJson = options.didDocument.toJSON() + keys = options.options.keys const { longFormDid, shortFormDid } = didDocumentToNumAlgo4Did(options.didDocument) @@ -104,11 +121,23 @@ export class PeerDidRegistrar implements DidRegistrar { } } + if (!keys || keys.length === 0) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `Missing required 'keys' linking did document verification method id to the kms key id. Provide at least one key in the create options`, + }, + } + } + // Save the did so we know we created it and can use it for didcomm const didRecord = new DidRecord({ did, role: DidDocumentRole.Created, didDocument: isPeerDidNumAlgo1CreateOptions(options) ? didDocument : undefined, + keys, tags: { // We need to save the recipientKeys, so we can find the associated did // of a key when we receive a message from another connection. @@ -125,15 +154,6 @@ export class PeerDidRegistrar implements DidRegistrar { state: 'finished', did: didDocument.id, didDocument, - secret: { - // FIXME: the uni-registrar creates the seed in the registrar method - // if it doesn't exist so the seed can always be returned. Currently - // we can only return it if the seed was passed in by the user. Once - // we have a secure method for generating seeds we should use the same - // approach - seed: options.secret?.seed, - privateKey: options.secret?.privateKey, - }, }, } } catch (error) { @@ -142,7 +162,7 @@ export class PeerDidRegistrar implements DidRegistrar { didRegistrationMetadata: {}, didState: { state: 'failed', - reason: `unknown error: ${error.message}`, + reason: `unknownError: ${error.message}`, }, } } @@ -198,14 +218,9 @@ export interface PeerDidNumAlgo0CreateOptions extends DidCreateOptions { did?: never didDocument?: never options: { - keyType?: KeyType - key?: Key numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc - } - secret?: { - seed?: Buffer - privateKey?: Buffer - } + } & XOR<{ createKey: KmsCreateKeyOptions }, { keyId: string }> + secret?: never } export interface PeerDidNumAlgo1CreateOptions extends DidCreateOptions { @@ -214,8 +229,16 @@ export interface PeerDidNumAlgo1CreateOptions extends DidCreateOptions { didDocument: DidDocument options: { numAlgo: PeerDidNumAlgo.GenesisDoc + + /** + * The linking between the did document keys and the kms keys. If you want to use + * the DID within Credo you MUST add the key here. All keys must be present in the did + * document, but not all did document keys must be present in this array, to allow for keys + * that are not controleld by this agent. + */ + keys: DidDocumentKey[] } - secret?: undefined + secret?: never } export interface PeerDidNumAlgo2CreateOptions extends DidCreateOptions { @@ -224,8 +247,16 @@ export interface PeerDidNumAlgo2CreateOptions extends DidCreateOptions { didDocument: DidDocument options: { numAlgo: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc + + /** + * The linking between the did document keys and the kms keys. If you want to use + * the DID within Credo you MUST add the key here. All keys must be present in the did + * document, but not all did document keys must be present in this array, to allow for keys + * that are not controleld by this agent. + */ + keys: DidDocumentKey[] } - secret?: undefined + secret?: never } export interface PeerDidNumAlgo4CreateOptions extends DidCreateOptions { @@ -234,8 +265,16 @@ export interface PeerDidNumAlgo4CreateOptions extends DidCreateOptions { didDocument: DidDocument options: { numAlgo: PeerDidNumAlgo.ShortFormAndLongForm + + /** + * The linking between the did document keys and the kms keys. If you want to use + * the DID within Credo you MUST add the key here. All keys must be present in the did + * document, but not all did document keys must be present in this array, to allow for keys + * that are not controleld by this agent. + */ + keys: DidDocumentKey[] } - secret?: undefined + secret?: never } // Update and Deactivate not supported for did:peer diff --git a/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts index 523b126611..217679e8cd 100644 --- a/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts +++ b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts @@ -83,6 +83,10 @@ export class PeerDidResolver implements DidResolver { didResolutionMetadata: { contentType: 'application/did+ld+json' }, } } catch (error) { + agentContext.config.logger.error(`Error resolving did '${did}'`, { + error, + }) + return { didDocument: null, didDocumentMetadata, diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts index 1496c339a9..9237f3eeed 100644 --- a/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts +++ b/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts @@ -1,29 +1,27 @@ -import type { Wallet } from '../../../../../wallet' - -import { getAgentContext, mockFunction } from '../../../../../../tests/helpers' -import { KeyType } from '../../../../../crypto' -import { Key } from '../../../../../crypto/Key' -import { TypedArrayEncoder } from '../../../../../utils' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' import { JsonTransformer } from '../../../../../utils/JsonTransformer' -import { WalletError } from '../../../../../wallet/error' import { DidCommV1Service, DidDocumentBuilder, getEd25519VerificationKey2018 } from '../../../domain' import { DidDocumentRole } from '../../../domain/DidDocumentRole' import { DidRepository } from '../../../repository/DidRepository' import { PeerDidRegistrar } from '../PeerDidRegistrar' import { PeerDidNumAlgo } from '../didPeer' +import { transformPrivateKeyToPrivateJwk } from '../../../../../../../askar/src' +import { TypedArrayEncoder } from '../../../../../utils' +import { Ed25519PublicJwk, KeyManagementApi, PublicJwk } from '../../../../kms' import didPeer0z6MksLeFixture from './__fixtures__/didPeer0z6MksLe.json' jest.mock('../../../repository/DidRepository') const DidRepositoryMock = DidRepository as jest.Mock -const walletMock = { - createKey: jest.fn(() => Key.fromFingerprint('z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU')), -} as unknown as Wallet const didRepositoryMock = new DidRepositoryMock() -const agentContext = getAgentContext({ wallet: walletMock, registerInstances: [[DidRepository, didRepositoryMock]] }) +const agentContext = getAgentContext({ + registerInstances: [[DidRepository, didRepositoryMock]], + agentConfig: getAgentConfig('PeerDidRegistrar'), +}) const peerDidRegistrar = new PeerDidRegistrar() +const kms = agentContext.resolve(KeyManagementApi) describe('DidRegistrar', () => { afterEach(() => { @@ -33,44 +31,23 @@ describe('DidRegistrar', () => { describe('PeerDidRegistrar', () => { describe('did:peer:0', () => { it('should correctly create a did:peer:0 document using Ed25519 key type', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - - const result = await peerDidRegistrar.create(agentContext, { - method: 'peer', - options: { - keyType: KeyType.Ed25519, - numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, - }, - secret: { - privateKey, + const privateJwk = transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e'), + type: { + kty: 'OKP', + crv: 'Ed25519', }, - }) + }).privateJwk - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'finished', - did: 'did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU', - didDocument: didPeer0z6MksLeFixture, - secret: { - privateKey, - }, - }, - }) - }) - - it('should return an error state if a key instance and key type are both provided', async () => { - const key = await agentContext.wallet.createKey({ - keyType: KeyType.P256, + const { keyId } = await kms.importKey({ + privateJwk, }) const result = await peerDidRegistrar.create(agentContext, { method: 'peer', options: { + keyId, numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, - key, - keyType: KeyType.P256, }, }) @@ -78,8 +55,9 @@ describe('DidRegistrar', () => { didDocumentMetadata: {}, didRegistrationMetadata: {}, didState: { - state: 'failed', - reason: 'Key instance cannot be combined with key type, seed or private key', + state: 'finished', + did: 'did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU', + didDocument: didPeer0z6MksLeFixture, }, }) }) @@ -87,6 +65,7 @@ describe('DidRegistrar', () => { it('should return an error state if no key or key type is provided', async () => { const result = await peerDidRegistrar.create(agentContext, { method: 'peer', + // @ts-ignore options: { numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, }, @@ -97,47 +76,28 @@ describe('DidRegistrar', () => { didRegistrationMetadata: {}, didState: { state: 'failed', - reason: 'Missing key type or key instance', - }, - }) - }) - - it('should return an error state if a key creation error is thrown', async () => { - mockFunction(walletMock.createKey).mockRejectedValueOnce(new WalletError('Invalid private key provided')) - - const result = await peerDidRegistrar.create(agentContext, { - method: 'peer', - options: { - keyType: KeyType.Ed25519, - numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, - }, - secret: { - privateKey: TypedArrayEncoder.fromString('invalid'), - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: expect.stringContaining('Invalid private key provided'), + reason: 'unknownError: Invalid options provided to getPublicKey method\n\t- Required at "keyId"', }, }) }) it('should store the did without the did document', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + const { keyId } = await kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e'), + }).privateJwk, + }) const did = 'did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU' await peerDidRegistrar.create(agentContext, { method: 'peer', options: { - keyType: KeyType.Ed25519, numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, - }, - secret: { - privateKey, + keyId, }, }) @@ -157,7 +117,9 @@ describe('DidRegistrar', () => { describe('did:peer:1', () => { const verificationMethod = getEd25519VerificationKey2018({ - key: Key.fromFingerprint('z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz'), + publicJwk: PublicJwk.fromFingerprint( + 'z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz' + ) as PublicJwk, // controller in method 1 did should be #id controller: '#id', id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', @@ -182,6 +144,12 @@ describe('DidRegistrar', () => { didDocument: didDocument, options: { numAlgo: PeerDidNumAlgo.GenesisDoc, + keys: [ + { + didDocumentRelativeKeyId: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + kmsKeyId: 'some-key-id', + }, + ], }, }) @@ -232,6 +200,12 @@ describe('DidRegistrar', () => { didDocument, options: { numAlgo: PeerDidNumAlgo.GenesisDoc, + keys: [ + { + didDocumentRelativeKeyId: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + kmsKeyId: 'test', + }, + ], }, }) @@ -250,9 +224,11 @@ describe('DidRegistrar', () => { }) describe('did:peer:2', () => { - const key = Key.fromFingerprint('z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz') + const publicJwk = PublicJwk.fromFingerprint( + 'z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz' + ) as PublicJwk const verificationMethod = getEd25519VerificationKey2018({ - key, + publicJwk, // controller in method 1 did should be #id controller: '#id', // Use relative id for peer dids with pattern 'key-N' @@ -278,6 +254,12 @@ describe('DidRegistrar', () => { didDocument: didDocument, options: { numAlgo: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + keys: [ + { + didDocumentRelativeKeyId: '#key-1', + kmsKeyId: 'test', + }, + ], }, }) @@ -310,7 +292,6 @@ describe('DidRegistrar', () => { ], authentication: ['#key-1'], }, - secret: {}, }, }) }) @@ -324,6 +305,13 @@ describe('DidRegistrar', () => { didDocument, options: { numAlgo: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + // FIXME: it should check that the key id exists and starts with `#` + keys: [ + { + didDocumentRelativeKeyId: '#a', + kmsKeyId: 'test', + }, + ], }, }) @@ -342,9 +330,11 @@ describe('DidRegistrar', () => { }) describe('did:peer:4', () => { - const key = Key.fromFingerprint('z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz') + const publicJwk = PublicJwk.fromFingerprint( + 'z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz' + ) as PublicJwk const verificationMethod = getEd25519VerificationKey2018({ - key, + publicJwk, controller: '#id', // Use relative id for peer dids id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', @@ -369,6 +359,13 @@ describe('DidRegistrar', () => { didDocument: didDocument, options: { numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + // FIXME: it should check that the key id exists and starts with `#` + keys: [ + { + didDocumentRelativeKeyId: '#a', + kmsKeyId: 'test', + }, + ], }, }) @@ -405,7 +402,6 @@ describe('DidRegistrar', () => { ], authentication: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], }, - secret: {}, }, }) }) @@ -419,6 +415,14 @@ describe('DidRegistrar', () => { didDocument, options: { numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + + // FIXME: it should check that the key id exists and starts with `#` + keys: [ + { + didDocumentRelativeKeyId: '#a', + kmsKeyId: 'test', + }, + ], }, }) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo0.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo0.test.ts index f2cd9f0ea0..83f9da237b 100644 --- a/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo0.test.ts +++ b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo0.test.ts @@ -1,20 +1,17 @@ -import { Key } from '../../../../../crypto' -import didKeyBls12381g1 from '../../../__tests__/__fixtures__/didKeyBls12381g1.json' -import didKeyBls12381g1g2 from '../../../__tests__/__fixtures__/didKeyBls12381g1g2.json' -import didKeyBls12381g2 from '../../../__tests__/__fixtures__/didKeyBls12381g2.json' +import { PublicJwk } from '../../../../kms' import didKeyEd25519 from '../../../__tests__/__fixtures__/didKeyEd25519.json' import didKeyX25519 from '../../../__tests__/__fixtures__/didKeyX25519.json' -import { didToNumAlgo0DidDocument, keyToNumAlgo0DidDocument } from '../peerDidNumAlgo0' +import { didToNumAlgo0DidDocument, publicJwkToNumAlgo0DidDocument } from '../peerDidNumAlgo0' describe('peerDidNumAlgo0', () => { describe('keyToNumAlgo0DidDocument', () => { test('transforms a key correctly into a peer did method 0 did document', async () => { - const didDocuments = [didKeyEd25519, didKeyBls12381g1, didKeyX25519, didKeyBls12381g1g2, didKeyBls12381g2] + const didDocuments = [didKeyEd25519, didKeyX25519] for (const didDocument of didDocuments) { - const key = Key.fromFingerprint(didDocument.id.split(':')[2]) + const key = PublicJwk.fromFingerprint(didDocument.id.split(':')[2]) - const didPeerDocument = keyToNumAlgo0DidDocument(key) + const didPeerDocument = publicJwkToNumAlgo0DidDocument(key) const expectedDidPeerDocument = JSON.parse(JSON.stringify(didDocument).replace(/did:key:/g, 'did:peer:0')) expect(didPeerDocument.toJSON()).toMatchObject(expectedDidPeerDocument) @@ -24,7 +21,7 @@ describe('peerDidNumAlgo0', () => { describe('didToNumAlgo0DidDocument', () => { test('transforms a method 0 did correctly into a did document', () => { - const didDocuments = [didKeyEd25519, didKeyBls12381g1, didKeyX25519, didKeyBls12381g1g2, didKeyBls12381g2] + const didDocuments = [didKeyEd25519, didKeyX25519] for (const didDocument of didDocuments) { const didPeer = didToNumAlgo0DidDocument(didDocument.id.replace('did:key:', 'did:peer:0')) diff --git a/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts b/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts index 0499d841db..7b85f1d269 100644 --- a/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts +++ b/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts @@ -2,20 +2,24 @@ import type { ResolvedDidCommService } from '../../../../types' import { convertPublicKeyToX25519 } from '@stablelib/ed25519' -import { Key } from '../../../../crypto/Key' -import { KeyType } from '../../../../crypto/KeyType' -import { CredoError } from '../../../../error' -import { getEd25519VerificationKey2018, getX25519KeyAgreementKey2019 } from '../../domain' +import { PublicJwk } from '../../../kms' +import { DidDocumentKey } from '../../DidsApiOptions' +import { DidDocument, getEd25519VerificationKey2018, getX25519KeyAgreementKey2019 } from '../../domain' import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' import { DidCommV1Service } from '../../domain/service/DidCommV1Service' import { DidKey } from '../key' -export function createPeerDidDocumentFromServices(services: ResolvedDidCommService[]) { +export function createPeerDidDocumentFromServices( + services: ResolvedDidCommService[], + withKeys: WithKeys +): { didDocument: DidDocument; keys: WithKeys extends true ? DidDocumentKey[] : undefined } { const didDocumentBuilder = new DidDocumentBuilder('') // Keep track of all added key id based on the fingerprint so we can add them to the recipientKeys as references const recipientKeyIdMapping: { [fingerprint: string]: string } = {} + const keys: DidDocumentKey[] = [] + let keyIndex = 1 services.forEach((service, index) => { // Get the local key reference for each of the recipient keys @@ -23,27 +27,42 @@ export function createPeerDidDocumentFromServices(services: ResolvedDidCommServi // Key already added to the did document if (recipientKeyIdMapping[recipientKey.fingerprint]) return recipientKeyIdMapping[recipientKey.fingerprint] - if (recipientKey.keyType !== KeyType.Ed25519) { - throw new CredoError( - `Unable to create did document from services. recipient key type ${recipientKey.keyType} is not supported. Supported key types are ${KeyType.Ed25519}` - ) - } - const x25519Key = Key.fromPublicKey(convertPublicKeyToX25519(recipientKey.publicKey), KeyType.X25519) + const x25519Key = PublicJwk.fromPublicKey({ + crv: 'X25519', + kty: 'OKP', + publicKey: convertPublicKeyToX25519(recipientKey.publicKey.publicKey), + }) // key ids follow the #key-N pattern to comply with did:peer:2 spec + const ed25519RelativeVerificationMethodId = `#key-${keyIndex++}` const ed25519VerificationMethod = getEd25519VerificationKey2018({ - id: `#key-${keyIndex++}`, - key: recipientKey, + id: ed25519RelativeVerificationMethodId, + publicJwk: recipientKey, controller: '#id', }) + const x25519RelativeVerificationMethodId = `#key-${keyIndex++}` const x25519VerificationMethod = getX25519KeyAgreementKey2019({ - id: `#key-${keyIndex++}`, - key: x25519Key, + id: x25519RelativeVerificationMethodId, + publicJwk: x25519Key, controller: '#id', }) recipientKeyIdMapping[recipientKey.fingerprint] = ed25519VerificationMethod.id + // NOTE: both use the same key id as the x25519 key is derived from the ed25519 key + // This is special for DIDComm v1 and any kms that wants to support DIDComm v1 will have + // to support both Ed25519 and X25519 operations on a Ed25519 key + if (withKeys) { + keys.push({ + didDocumentRelativeKeyId: ed25519RelativeVerificationMethodId, + kmsKeyId: recipientKey.keyId, + }) + keys.push({ + didDocumentRelativeKeyId: x25519RelativeVerificationMethodId, + kmsKeyId: recipientKey.keyId, + }) + } + // We should not add duplicated keys for services didDocumentBuilder.addAuthentication(ed25519VerificationMethod).addKeyAgreement(x25519VerificationMethod) @@ -68,5 +87,8 @@ export function createPeerDidDocumentFromServices(services: ResolvedDidCommServi ) }) - return didDocumentBuilder.build() + return { + didDocument: didDocumentBuilder.build(), + keys: (withKeys ? keys : undefined) as WithKeys extends true ? DidDocumentKey[] : undefined, + } } diff --git a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo0.ts b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo0.ts index d1e28f4103..8f278e516a 100644 --- a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo0.ts +++ b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo0.ts @@ -1,14 +1,14 @@ -import { Key } from '../../../../crypto/Key' import { CredoError } from '../../../../error' -import { getDidDocumentForKey } from '../../domain/keyDidDocument' +import { PublicJwk } from '../../../kms' +import { getDidDocumentForPublicJwk } from '../../domain/keyDidDocument' import { parseDid } from '../../domain/parse' import { PeerDidNumAlgo, getNumAlgoFromPeerDid, isValidPeerDid } from './didPeer' -export function keyToNumAlgo0DidDocument(key: Key) { - const did = `did:peer:0${key.fingerprint}` +export function publicJwkToNumAlgo0DidDocument(publicJwk: PublicJwk) { + const did = `did:peer:0${publicJwk.fingerprint}` - return getDidDocumentForKey(did, key) + return getDidDocumentForPublicJwk(did, publicJwk) } export function didToNumAlgo0DidDocument(did: string) { @@ -23,7 +23,7 @@ export function didToNumAlgo0DidDocument(did: string) { throw new CredoError(`Invalid numAlgo ${numAlgo}, expected ${PeerDidNumAlgo.InceptionKeyWithoutDoc}`) } - const key = Key.fromFingerprint(parsed.id.substring(1)) + const publicJwk = PublicJwk.fromFingerprint(parsed.id.substring(1)) - return getDidDocumentForKey(did, key) + return getDidDocumentForPublicJwk(did, publicJwk) } diff --git a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo2.ts b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo2.ts index d7fc584d90..58a2a8dee8 100644 --- a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo2.ts +++ b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo2.ts @@ -1,12 +1,14 @@ -import type { JsonObject } from '../../../../types' -import type { DidDocument, VerificationMethod } from '../../domain' - -import { Key } from '../../../../crypto/Key' import { CredoError } from '../../../../error' +import type { JsonObject } from '../../../../types' import { JsonEncoder, JsonTransformer } from '../../../../utils' +import { PublicJwk } from '../../../kms' +import type { DidDocument, VerificationMethod } from '../../domain' import { DidDocumentService } from '../../domain' import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' -import { getKeyDidMappingByKeyType, getKeyFromVerificationMethod } from '../../domain/key-type' +import { + getPublicJwkFromVerificationMethod, + getVerificationMethodsForPublicJwk, +} from '../../domain/key-type/keyDidMapping' import { parseDid } from '../../domain/parse' enum DidPeerPurpose { @@ -73,9 +75,8 @@ export function didToNumAlgo2DidDocument(did: string) { // Otherwise we can be sure it is a key else { // Decode the fingerprint, and extract the verification method(s) - const key = Key.fromFingerprint(entryContent) - const { getVerificationMethods } = getKeyDidMappingByKeyType(key.keyType) - const verificationMethods = getVerificationMethods(did, key) + const publicJwk = PublicJwk.fromFingerprint(entryContent) + const verificationMethods = getVerificationMethodsForPublicJwk(publicJwk, did) // Add all verification methods to the did document for (const verificationMethod of verificationMethods) { @@ -115,7 +116,7 @@ export function didDocumentToNumAlgo2Did(didDocument: DidDocument) { // Transform all verification methods into a fingerprint (multibase, multicodec) for (const entry of dereferenced) { - const key = getKeyFromVerificationMethod(entry) + const key = getPublicJwkFromVerificationMethod(entry) // Encode as '.PurposeFingerprint' const encoded = `.${purpose}${key.fingerprint}` diff --git a/packages/core/src/modules/dids/repository/DidRecord.ts b/packages/core/src/modules/dids/repository/DidRecord.ts index 2ad9df2f01..ba7bab4252 100644 --- a/packages/core/src/modules/dids/repository/DidRecord.ts +++ b/packages/core/src/modules/dids/repository/DidRecord.ts @@ -10,6 +10,7 @@ import { DidDocument } from '../domain' import { DidDocumentRole } from '../domain/DidDocumentRole' import { parseDid } from '../domain/parse' +import { DidDocumentKey } from '../DidsApiOptions' import { DidRecordMetadataKeys } from './didRecordMetadataTypes' export interface DidRecordProps { @@ -19,6 +20,12 @@ export interface DidRecordProps { didDocument?: DidDocument createdAt?: Date tags?: CustomDidTags + + /** + * The kms key ids associated with the did record. Should only be used + * when role is {@link DidDocumentRole.Created} + */ + keys?: DidDocumentKey[] } export interface CustomDidTags extends TagsBase { @@ -55,6 +62,12 @@ export class DidRecord extends BaseRecord { * Finds a {@link DidRecord}, containing the specified recipientKey that was received by this agent. * To find a {@link DidRecord} that was created by this agent, use {@link DidRepository.findCreatedDidByRecipientKey}. */ - public findReceivedDidByRecipientKey(agentContext: AgentContext, recipientKey: Key) { + public findReceivedDidByRecipientKey(agentContext: AgentContext, recipientKey: PublicJwk) { return this.findSingleByQuery(agentContext, { recipientKeyFingerprints: [recipientKey.fingerprint], role: DidDocumentRole.Received, @@ -36,14 +37,14 @@ export class DidRepository extends Repository { * Finds a {@link DidRecord}, containing the specified recipientKey that was created by this agent. * To find a {@link DidRecord} that was received by this agent, use {@link DidRepository.findReceivedDidByRecipientKey}. */ - public findCreatedDidByRecipientKey(agentContext: AgentContext, recipientKey: Key) { + public findCreatedDidByRecipientKey(agentContext: AgentContext, recipientKey: PublicJwk) { return this.findSingleByQuery(agentContext, { recipientKeyFingerprints: [recipientKey.fingerprint], role: DidDocumentRole.Created, }) } - public findAllByRecipientKey(agentContext: AgentContext, recipientKey: Key) { + public findAllByRecipientKey(agentContext: AgentContext, recipientKey: PublicJwk) { return this.findByQuery(agentContext, { recipientKeyFingerprints: [recipientKey.fingerprint] }) } @@ -73,12 +74,16 @@ export class DidRepository extends Repository { }) } - public async storeCreatedDid(agentContext: AgentContext, { did, didDocument, tags }: StoreDidOptions) { + public async storeCreatedDid( + agentContext: AgentContext, + { did, didDocument, tags, keys }: StoreDidOptions & { keys?: DidDocumentKey[] } + ) { const didRecord = new DidRecord({ did, didDocument, role: DidDocumentRole.Created, tags, + keys, }) await this.save(agentContext, didRecord) @@ -104,4 +109,5 @@ interface StoreDidOptions { did: string didDocument?: DidDocument tags?: CustomDidTags + keys?: DidDocumentKey[] } diff --git a/packages/core/src/modules/dids/services/DidResolverService.ts b/packages/core/src/modules/dids/services/DidResolverService.ts index 17447cf514..266519ec5a 100644 --- a/packages/core/src/modules/dids/services/DidResolverService.ts +++ b/packages/core/src/modules/dids/services/DidResolverService.ts @@ -97,6 +97,7 @@ export class DidResolverService { } } + // TODO: we should store the document for future reference if (resolver.allowsLocalDidRecord && useLocalCreatedDidRecord) { // TODO: did should have tag whether a did document is present in the did record const [didRecord] = await this.didRepository.getCreatedDids(agentContext, { diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index 6d06d7ca0f..7ba58b43e7 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -1,4 +1,5 @@ import type { Checked, PresentationSignCallBackParams, Validated, VerifiablePresentationResult } from '@animo-id/pex' +import { PEX, Status } from '@animo-id/pex' import type { InputDescriptorV2 } from '@sphereon/pex-models' import type { SdJwtDecodedVerifiableCredential, @@ -22,14 +23,11 @@ import type { } from './models' import type { PresentationToCreate } from './utils' -import { PEVersion, PEX, Status } from '@animo-id/pex' -import { PartialSdJwtDecodedVerifiableCredential } from '@animo-id/pex/dist/main/lib' import { injectable } from 'tsyringe' -import { getJwkFromKey } from '../../crypto' import { CredoError } from '../../error' import { JsonTransformer } from '../../utils' -import { DidsApi, getKeyFromVerificationMethod } from '../dids' +import { DidsApi, getPublicJwkFromVerificationMethod } from '../dids' import { Mdoc, MdocApi, @@ -51,6 +49,8 @@ import { AnonCredsDataIntegrityServiceSymbol, } from '../vc/data-integrity/models/IAnonCredsDataIntegrityService' +import { PEVersion, PartialSdJwtDecodedVerifiableCredential } from '@animo-id/pex/dist/main/lib' +import { getJwkHumanDescription } from '../kms' import { DifPresentationExchangeError } from './DifPresentationExchangeError' import { DifPresentationExchangeSubmissionLocation } from './models' import { @@ -134,7 +134,7 @@ export class DifPresentationExchangeService { ? presentations.map(getSphereonOriginalVerifiablePresentation) : getSphereonOriginalVerifiablePresentation(presentations), { - limitDisclosureSignatureSuites: ['BbsBlsSignatureProof2020', 'DataIntegrityProof.anoncreds-2023'], + limitDisclosureSignatureSuites: ['DataIntegrityProof.anoncreds-2023'], presentationSubmission, } ) @@ -321,25 +321,26 @@ export class DifPresentationExchangeService { verificationMethod: VerificationMethod, suitableAlgorithms?: Array ) { - const key = getKeyFromVerificationMethod(verificationMethod) - const jwk = getJwkFromKey(key) + const publicJwk = getPublicJwkFromVerificationMethod(verificationMethod) if (suitableAlgorithms) { - const possibleAlgorithms = jwk.supportedSignatureAlgorithms.filter((alg) => suitableAlgorithms?.includes(alg)) + const possibleAlgorithms = publicJwk.supportedSignatureAlgorithms.filter((alg) => + suitableAlgorithms?.includes(alg) + ) if (!possibleAlgorithms || possibleAlgorithms.length === 0) { throw new DifPresentationExchangeError( [ 'Found no suitable signing algorithm.', - `Algorithms supported by Verification method: ${jwk.supportedSignatureAlgorithms.join(', ')}`, + `Algorithms supported by Verification method: ${publicJwk.supportedSignatureAlgorithms.join(', ')}`, `Suitable algorithms: ${suitableAlgorithms.join(', ')}`, ].join('\n') ) } + + return possibleAlgorithms[0] } - const alg = jwk.supportedSignatureAlgorithms[0] - if (!alg) throw new DifPresentationExchangeError(`No supported algs for key type: ${key.keyType}`) - return alg + return publicJwk.signatureAlgorithm } private getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( @@ -420,11 +421,11 @@ export class DifPresentationExchangeService { // For each of the supported algs, find the key types, then find the proof types const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) - const key = getKeyFromVerificationMethod(verificationMethod) - const supportedSignatureSuites = signatureSuiteRegistry.getAllByKeyType(key.keyType) + const publicJwk = getPublicJwkFromVerificationMethod(verificationMethod) + const supportedSignatureSuites = signatureSuiteRegistry.getAllByPublicJwkType(publicJwk.jwk) if (supportedSignatureSuites.length === 0) { throw new DifPresentationExchangeError( - `Couldn't find a supported signature suite for the given key type '${key.keyType}'` + `Couldn't find a supported signature suite for the given jwk ${getJwkHumanDescription(publicJwk.toJson())}` ) } @@ -438,7 +439,7 @@ export class DifPresentationExchangeService { [ 'No possible signature suite found for the given verification method.', `Verification method type: ${verificationMethod.type}`, - `Key type: ${key.keyType}`, + `jwk type: ${getJwkHumanDescription(publicJwk.toJson())}`, `SupportedSignatureSuites: '${supportedSignatureSuites.map((s) => s.proofType).join(', ')}'`, `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, ].join('\n') diff --git a/packages/core/src/modules/dif-presentation-exchange/__tests__/DifPresentationExchangeService.test.ts b/packages/core/src/modules/dif-presentation-exchange/__tests__/DifPresentationExchangeService.test.ts index 828644ee93..26de391652 100644 --- a/packages/core/src/modules/dif-presentation-exchange/__tests__/DifPresentationExchangeService.test.ts +++ b/packages/core/src/modules/dif-presentation-exchange/__tests__/DifPresentationExchangeService.test.ts @@ -1,11 +1,10 @@ import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' -import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' import { agentDependencies, getAgentContext } from '../../../../tests' -import { AgentContext } from '../../../agent' import { InjectionSymbols } from '../../../constants' -import { Buffer, JsonTransformer } from '../../../utils' +import { JsonTransformer } from '../../../utils' +import { KeyManagementApi } from '../../kms' import { Mdoc, MdocDeviceResponse, MdocRecord, MdocRepository } from '../../mdoc' import { sprindFunkeTestVectorBase64Url } from '../../mdoc/__tests__/mdoc.fixtures' import { SdJwtVcRecord, SdJwtVcRepository } from '../../sd-jwt-vc' @@ -18,7 +17,6 @@ import { import { DifPresentationExchangeService } from '../DifPresentationExchangeService' import { type DifPresentationExchangeDefinitionV2, DifPresentationExchangeSubmissionLocation } from '../models' -const wallet = new InMemoryWallet() const agentContext = getAgentContext({ registerInstances: [ [InjectionSymbols.StorageService, new InMemoryStorageService()], @@ -27,9 +25,10 @@ const agentContext = getAgentContext({ [SignatureSuiteToken, 'default'], [W3cCredentialsModuleConfig, new W3cCredentialsModuleConfig()], ], - wallet, }) -agentContext.dependencyManager.registerInstance(AgentContext, agentContext) + +const kms = agentContext.resolve(KeyManagementApi) + const sdJwtVcRecord = new SdJwtVcRecord({ compactSdJwtVc: 'eyJ4NWMiOlsiTUlJQ2REQ0NBaHVnQXdJQkFnSUJBakFLQmdncWhrak9QUVFEQWpDQmlERUxNQWtHQTFVRUJoTUNSRVV4RHpBTkJnTlZCQWNNQmtKbGNteHBiakVkTUJzR0ExVUVDZ3dVUW5WdVpHVnpaSEoxWTJ0bGNtVnBJRWR0WWtneEVUQVBCZ05WQkFzTUNGUWdRMU1nU1VSRk1UWXdOQVlEVlFRRERDMVRVRkpKVGtRZ1JuVnVhMlVnUlZWRVNTQlhZV3hzWlhRZ1VISnZkRzkwZVhCbElFbHpjM1ZwYm1jZ1EwRXdIaGNOTWpRd05UTXhNRGd4TXpFM1doY05NalV3TnpBMU1EZ3hNekUzV2pCc01Rc3dDUVlEVlFRR0V3SkVSVEVkTUJzR0ExVUVDZ3dVUW5WdVpHVnpaSEoxWTJ0bGNtVnBJRWR0WWtneENqQUlCZ05WQkFzTUFVa3hNakF3QmdOVkJBTU1LVk5RVWtsT1JDQkdkVzVyWlNCRlZVUkpJRmRoYkd4bGRDQlFjbTkwYjNSNWNHVWdTWE56ZFdWeU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRU9GQnE0WU1LZzR3NWZUaWZzeXR3QnVKZi83RTdWaFJQWGlObTUyUzNxMUVUSWdCZFh5REsza1Z4R3hnZUhQaXZMUDN1dU12UzZpREVjN3FNeG12ZHVLT0JrRENCalRBZEJnTlZIUTRFRmdRVWlQaENrTEVyRFhQTFcyL0owV1ZlZ2h5dyttSXdEQVlEVlIwVEFRSC9CQUl3QURBT0JnTlZIUThCQWY4RUJBTUNCNEF3TFFZRFZSMFJCQ1l3SklJaVpHVnRieTV3YVdRdGFYTnpkV1Z5TG1KMWJtUmxjMlJ5ZFdOclpYSmxhUzVrWlRBZkJnTlZIU01FR0RBV2dCVFVWaGpBaVRqb0RsaUVHTWwyWXIrcnU4V1F2akFLQmdncWhrak9QUVFEQWdOSEFEQkVBaUFiZjVUemtjUXpoZldvSW95aTFWTjdkOEk5QnNGS20xTVdsdVJwaDJieUdRSWdLWWtkck5mMnhYUGpWU2JqVy9VLzVTNXZBRUM1WHhjT2FudXNPQnJvQmJVPSIsIk1JSUNlVENDQWlDZ0F3SUJBZ0lVQjVFOVFWWnRtVVljRHRDaktCL0gzVlF2NzJnd0NnWUlLb1pJemowRUF3SXdnWWd4Q3pBSkJnTlZCQVlUQWtSRk1ROHdEUVlEVlFRSERBWkNaWEpzYVc0eEhUQWJCZ05WQkFvTUZFSjFibVJsYzJSeWRXTnJaWEpsYVNCSGJXSklNUkV3RHdZRFZRUUxEQWhVSUVOVElFbEVSVEUyTURRR0ExVUVBd3d0VTFCU1NVNUVJRVoxYm10bElFVlZSRWtnVjJGc2JHVjBJRkJ5YjNSdmRIbHdaU0JKYzNOMWFXNW5JRU5CTUI0WERUSTBNRFV6TVRBMk5EZ3dPVm9YRFRNME1EVXlPVEEyTkRnd09Wb3dnWWd4Q3pBSkJnTlZCQVlUQWtSRk1ROHdEUVlEVlFRSERBWkNaWEpzYVc0eEhUQWJCZ05WQkFvTUZFSjFibVJsYzJSeWRXTnJaWEpsYVNCSGJXSklNUkV3RHdZRFZRUUxEQWhVSUVOVElFbEVSVEUyTURRR0ExVUVBd3d0VTFCU1NVNUVJRVoxYm10bElFVlZSRWtnVjJGc2JHVjBJRkJ5YjNSdmRIbHdaU0JKYzNOMWFXNW5JRU5CTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFWUd6ZHdGRG5jNytLbjVpYkF2Q09NOGtlNzdWUXhxZk1jd1pMOElhSUErV0NST2NDZm1ZL2dpSDkycU1ydTVwL2t5T2l2RTBSQy9JYmRNT052RG9VeWFObU1HUXdIUVlEVlIwT0JCWUVGTlJXR01DSk9PZ09XSVFZeVhaaXY2dTd4WkMrTUI4R0ExVWRJd1FZTUJhQUZOUldHTUNKT09nT1dJUVl5WFppdjZ1N3haQytNQklHQTFVZEV3RUIvd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnR0dNQW9HQ0NxR1NNNDlCQU1DQTBjQU1FUUNJR0VtN3drWktIdC9hdGI0TWRGblhXNnlybndNVVQydTEzNmdkdGwxMFk2aEFpQnVURnF2Vll0aDFyYnh6Q1AweFdaSG1RSzlrVnl4bjhHUGZYMjdFSXp6c3c9PSJdLCJraWQiOiJNSUdVTUlHT3BJR0xNSUdJTVFzd0NRWURWUVFHRXdKRVJURVBNQTBHQTFVRUJ3d0dRbVZ5YkdsdU1SMHdHd1lEVlFRS0RCUkNkVzVrWlhOa2NuVmphMlZ5WldrZ1IyMWlTREVSTUE4R0ExVUVDd3dJVkNCRFV5QkpSRVV4TmpBMEJnTlZCQU1NTFZOUVVrbE9SQ0JHZFc1clpTQkZWVVJKSUZkaGJHeGxkQ0JRY205MGIzUjVjR1VnU1hOemRXbHVaeUJEUVFJQkFnPT0iLCJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJwbGFjZV9vZl9iaXJ0aCI6eyJfc2QiOlsiVS01ZlVXLU5EM1laajZTcUdyQXV4NXJWYWZOalhqZ2hvMmRUUmpQX3hOTSJdfSwiX3NkIjpbIjlFaUpQNEw2NDI0bEtTVGs5NHpIOWhaWVc5UjNuS1R3V0V5TVBJN2dvWHciLCJHVlhRWEtFMmpWR1d0VEF6T1d5ck85TTZySW1qYkZJWGFnRkMyWElMbGhJIiwiUUV2bHpNd0ozZS1tOEtpWEk5bGx2bnVQblh5UHRXN2VCSF9GcXFVTnk3WSIsImljWkpTRkFqLVg3T29Sam5vRFRReXFwU1dNQUVuaTcydWZDZmFFWC1uQkUiLCJsUHJqb3BqbEN5bFdHWVo0cmh4S1RUTUsxS3p1Sm5ISUtybzNwUUhlUXF3IiwicjJORHZtRFY3QmU3TlptVFR0VE9fekdZX3RTdWdYVXoxeDJBXzZuOFhvdyIsInJPbjFJUkpUQWtEV1pSTGc3MUYzaDVsbFpPc1ZPMl9aemlOUy1majNEUFUiXSwiYWRkcmVzcyI6eyJfc2QiOlsiQnI1aVZtZnZlaTloQ01mMktVOGRFVjFER2hrdUtsQ1pUeGFEQ0FMb3NJbyIsIkx6czJpR09SNHF0clhhYmdwMzFfcjFFUFNmazlaUDJQRElJUTRQaHlPT00iLCJadUV5cG41Y0s0WVpWdHdkeGFoWXJqMjZ1MFI2UmxpOVVJWlNjUGhoWTB3Iiwidi1rMzl2VGI5NFI5a25VWTZtbzlXUVdEQkNJS3lya0J4bExTQVl3T2MyNCJdfSwiaXNzdWluZ19jb3VudHJ5IjoiREUiLCJ2Y3QiOiJodHRwczovL2V4YW1wbGUuYm1pLmJ1bmQuZGUvY3JlZGVudGlhbC9waWQvMS4wIiwiaXNzdWluZ19hdXRob3JpdHkiOiJERSIsIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiaHR0cHM6Ly9kZW1vLnBpZC1pc3N1ZXIuYnVuZGVzZHJ1Y2tlcmVpLmRlL2MxIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkhzS194Tl95SVU4eWlqdW9BWlhsbndFRU00ZlhZenVNRmd5TTE5SmRYMUkiLCJ5IjoiQUR2NnplVDl3YmgxU0ZxMG14TkcxMUZueC05eFdSRFcwR18xN1dSRXpRSSJ9fSwiZXhwIjoxNzMzNTcxMzI3LCJpYXQiOjE3MzIzNjE3MjcsImFnZV9lcXVhbF9vcl9vdmVyIjp7Il9zZCI6WyJLRDF0U0hnYWotZi1qbkZURkRDMW1sZ0RwNzhMZE1KcHlqWnRRU0k4a1ZnIiwiTDRjTTMtZU1mRHg0Znc2UEw3OVRTVFBnM042VXdzOGNPc3JOYmNqaEEtYyIsImRYUFBQX2lmNFM3XzBzcXZXNTBwZEdlMWszbS1wMnM3M1JicDlncThGaDAiLCJtYnllcU05YUkzRkVvWmFoODA5eTN0dlRCV1NvZTBMSlRUYTlONGNjdmlZIiwicm1zd0dEZnhvS0ZFYlFsNzZ4S1ZVT0hrX0MyQlVpVnQ5RDlvMTFrMmZNSSIsInZsY2Y4WTNhQnNTeEZBeVZfYk9NTndvX3FTT1pHc3ViSVZiY0FVSWVBSGMiXX19.gruqjNOuJBgHXEnG9e60wOoqiyEaL1K9pdL215a0ffZCjtIZ_kICDrO5vBiTrEmvjjd6w_N_thEYLhzob77Epg~WyJWRXlWQWF0LXoyNU8tbkQ0MVBaOGdnIiwiZmFtaWx5X25hbWUiLCJNVVNURVJNQU5OIl0~WyJLcnRPei1lRk9hMU9JYmpmUHUxcHRBIiwiZ2l2ZW5fbmFtZSIsIkVSSUtBIl0~WyJQQUVjSHp0NWk5bFFzNUZlRmFGUS1RIiwiYmlydGhkYXRlIiwiMTk2NC0wOC0xMiJd~', @@ -484,8 +483,7 @@ describe('DifPresentationExchangeService', () => { const selectedCredentials = pexService.selectCredentialsForRequest(credentialsForRequest) - jest.spyOn(wallet, 'sign').mockImplementation(async () => Buffer.from('signed')) - + jest.spyOn(kms, 'sign').mockResolvedValue({ signature: new Uint8Array([]) }) const presentation = await pexService.createPresentation(agentContext, { credentialsForInputDescriptor: selectedCredentials, challenge: 'something', @@ -616,7 +614,7 @@ describe('DifPresentationExchangeService', () => { const selectedCredentials = pexService.selectCredentialsForRequest(credentialsForRequest) - jest.spyOn(wallet, 'sign').mockImplementation(async () => Buffer.from('signed')) + jest.spyOn(kms, 'sign').mockResolvedValue({ signature: new Uint8Array([]) }) const presentation = await pexService.createPresentation(agentContext, { credentialsForInputDescriptor: selectedCredentials, @@ -757,7 +755,7 @@ describe('DifPresentationExchangeService', () => { const selectedCredentials = pexService.selectCredentialsForRequest(credentialsForRequest) - jest.spyOn(wallet, 'sign').mockImplementation(async () => Buffer.from('signed')) + jest.spyOn(kms, 'sign').mockResolvedValue({ signature: new Uint8Array([]) }) const presentation = await pexService.createPresentation(agentContext, { credentialsForInputDescriptor: selectedCredentials, diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts index 8f252c81b8..cfcdc7ed98 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -1,4 +1,10 @@ -import type { IPresentationDefinition, PEX, SelectResults, SubmissionRequirementMatch } from '@animo-id/pex' +import { + type IPresentationDefinition, + type PEX, + type SelectResults, + Status, + type SubmissionRequirementMatch, +} from '@animo-id/pex' import type { SubmissionRequirementMatchFrom, SubmissionRequirementMatchInputDescriptor, @@ -11,7 +17,7 @@ import type { SubmissionEntryCredential, } from '../models' -import { Status } from '@animo-id/pex' +// import { Status } from '@animo-id/pex' import { SubmissionRequirementMatchType } from '@animo-id/pex/dist/main/lib/evaluation/core' import { JSONPath } from '@astronautlabs/jsonpath' import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode' diff --git a/packages/core/src/modules/kms/KeyManagementApi.ts b/packages/core/src/modules/kms/KeyManagementApi.ts new file mode 100644 index 0000000000..81d87a8360 --- /dev/null +++ b/packages/core/src/modules/kms/KeyManagementApi.ts @@ -0,0 +1,371 @@ +import { injectable } from 'tsyringe' + +import { AgentContext } from '../../agent' +import { parseWithErrorHandling } from '../../utils/zod' + +import { KeyManagementModuleConfig } from './KeyManagementModuleConfig' +import { KeyManagementError } from './error/KeyManagementError' +import { KeyManagementKeyNotFoundError } from './error/KeyManagementKeyNotFoundError' +import { KmsJwkPrivate, getJwkHumanDescription } from './jwk' +import { createKeyTypeForSigningAlgorithm } from './jwk/alg/signing' +import { + KmsDecryptOptions, + KmsDeleteKeyOptions, + KmsGetPublicKeyOptions, + KmsImportKeyOptions, + KmsOperation, + KmsRandomBytesOptions, + getKmsOperationHumanDescription, +} from './options' +import { + KmsCreateKeyForSignatureAlgorithmOptions, + KmsCreateKeyOptions, + KmsCreateKeyReturn, + KmsCreateKeyType, + KmsCreateKeyTypeAssymetric, + zKmsCreateKeyForSignatureAlgorithmOptions, + zKmsCreateKeyOptions, +} from './options/KmsCreateKeyOptions' +import { zKmsDecryptOptions } from './options/KmsDecryptOptions' +import { zKmsDeleteKeyOptions } from './options/KmsDeleteKeyOptions' +import { KmsEncryptOptions, zKmsEncryptOptions } from './options/KmsEncryptOptions' +import { zKmsGetPublicKeyOptions } from './options/KmsGetPublicKeyOptions' +import { KmsImportKeyReturn, zKmsImportKeyOptions } from './options/KmsImportKeyOptions' +import { zKmsRandomBytesOptions } from './options/KmsRandomBytesOptions' +import { KmsSignOptions, zKmsSignOptions } from './options/KmsSignOptions' +import { KmsVerifyOptions, zKmsVerifyOptions } from './options/KmsVerifyOptions' +import { WithBackend, zWithBackend } from './options/backend' + +@injectable() +export class KeyManagementApi { + public constructor( + private keyManagementConfig: KeyManagementModuleConfig, + private agentContext: AgentContext + ) {} + + /** + * Whether whether an operation is supported. + * + * @returns a list of backends that support the operation. In case + * no backends are supported it returns an empty array + */ + public supportedBackendsForOperation(operation: KmsOperation): string[] { + const supportedBackends: string[] = [] + + for (const kms of this.keyManagementConfig.backends) { + const isOperationSupported = kms.isOperationSupported(this.agentContext, operation) + if (isOperationSupported) { + supportedBackends.push(kms.backend) + } + } + + return supportedBackends + } + + /** + * Create a key. + */ + public async createKey( + options: WithBackend> + ): Promise> { + const { backend, ...kmsOptions } = parseWithErrorHandling( + zWithBackend(zKmsCreateKeyOptions), + options, + 'Invalid options provided to createKey method' + ) + + const kms = this.getKms(this.agentContext, backend, { + operation: 'createKey', + type: options.type, + }) + + const key = await kms.createKey(this.agentContext, kmsOptions) + key.publicJwk.kid = key.keyId + + this.agentContext.config.logger.debug( + `Created key ${getJwkHumanDescription(key.publicJwk)} with key id '${key.keyId}'` + ) + + return key + } + + /** + * Create a key. + */ + public async createKeyForSignatureAlgorithm( + options: WithBackend + ): Promise> { + const { backend, algorithm, ...kmsOptions } = parseWithErrorHandling( + zWithBackend(zKmsCreateKeyForSignatureAlgorithmOptions), + options, + 'Invalid options provided to createKeyForSignatureAlgorithm method' + ) + + const type = createKeyTypeForSigningAlgorithm(options.algorithm) + const kms = this.getKms(this.agentContext, backend, { + operation: 'createKey', + type, + }) + + // FIXME: do we want this? + // Ensure the kid is set to the keyId + const key = await kms.createKey(this.agentContext, { + ...kmsOptions, + type, + }) + key.publicJwk.kid = key.keyId + + return key + } + + /** + * Sign using a key. + */ + public async sign(options: WithBackend) { + const { backend, ...kmsOptions } = parseWithErrorHandling( + zWithBackend(zKmsSignOptions), + options, + 'Invalid options provided to sign method' + ) + + const operation = { + operation: 'sign', + algorithm: options.algorithm, + } as const + + const kms = backend + ? this.getKms(this.agentContext, backend, operation) + : (await this.getKmsForOperationAndKeyId(this.agentContext, options.keyId, operation)).kms + return await kms.sign(this.agentContext, kmsOptions) + } + + /** + * Verify using a key. + */ + public async verify(options: WithBackend) { + const { backend, ...kmsOptions } = parseWithErrorHandling( + zWithBackend(zKmsVerifyOptions), + options, + 'Invalid options provided to verify method' + ) + + const operation = { operation: 'verify', algorithm: options.algorithm } as const + const kms = + backend || typeof options.key !== 'string' + ? this.getKms(this.agentContext, backend, operation) + : (await this.getKmsForOperationAndKeyId(this.agentContext, options.key, operation)).kms + + return await kms.verify(this.agentContext, kmsOptions) + } + + /** + * Encrypt. + */ + public async encrypt(options: WithBackend) { + const { backend, ...kmsOptions } = parseWithErrorHandling( + zWithBackend(zKmsEncryptOptions), + options, + 'Invalid options provided to encrypt method' + ) + + const operation = { + operation: 'encrypt', + encryption: options.encryption, + keyAgreement: typeof options.key === 'object' && 'algorithm' in options.key ? options.key : undefined, + } as const + const kms = + backend || typeof options.key !== 'string' + ? this.getKms(this.agentContext, backend, operation) + : (await this.getKmsForOperationAndKeyId(this.agentContext, options.key, operation)).kms + + return await kms.encrypt(this.agentContext, kmsOptions) + } + + /** + * Decrypt. + */ + public async decrypt(options: WithBackend) { + const { backend, ...kmsOptions } = parseWithErrorHandling( + zWithBackend(zKmsDecryptOptions), + options, + 'Invalid options provided to decrypt method' + ) + + const operation = { + operation: 'decrypt', + decryption: options.decryption, + keyAgreement: typeof options.key === 'object' && 'algorithm' in options.key ? options.key : undefined, + } as const + const kms = + backend || typeof options.key !== 'string' + ? this.getKms( + this.agentContext, + + backend, + operation + ) + : (await this.getKmsForOperationAndKeyId(this.agentContext, options.key, operation)).kms + + return await kms.decrypt(this.agentContext, kmsOptions) + } + + /** + * Import a key. + */ + public async importKey( + options: WithBackend> + ): Promise> { + const { backend, ...kmsOptions } = parseWithErrorHandling( + zWithBackend(zKmsImportKeyOptions), + options, + 'Invalid options provided to importKey method' + ) + + const operation = { + operation: 'importKey', + privateJwk: options.privateJwk, + } as const + const kms = this.getKms(this.agentContext, backend, operation) + + const key = await kms.importKey(this.agentContext, kmsOptions) + + this.agentContext.config.logger.trace( + `Imported key ${getJwkHumanDescription(key.publicJwk)} with key id '${key.keyId}'` + ) + + return key + } + + /** + * Get a public key. + */ + public async getPublicKey(options: WithBackend) { + const { backend, keyId } = parseWithErrorHandling( + zWithBackend(zKmsGetPublicKeyOptions), + options, + 'Invalid options provided to getPublicKey method' + ) + + if (backend) { + const kms = this.getKms(this.agentContext, backend) + const publicKey = await kms.getPublicKey(this.agentContext, keyId) + + if (!publicKey) { + throw new KeyManagementKeyNotFoundError(keyId, backend) + } + } + + const { publicKey } = await this.getKmsForOperationAndKeyId(this.agentContext, options.keyId) + return publicKey + } + + /** + * Delete a key. + */ + public async deleteKey(options: WithBackend) { + const { backend, ...kmsOptions } = parseWithErrorHandling( + zWithBackend(zKmsDeleteKeyOptions), + options, + 'Invalid options provided to deleteKey method' + ) + + const operation = { + operation: 'deleteKey', + } as const + const kms = this.getKms(this.agentContext, backend, operation) + return await kms.deleteKey(this.agentContext, kmsOptions) + } + + /** + * Generate random bytes + */ + public randomBytes(options: WithBackend) { + const { backend, ...kmsOptions } = parseWithErrorHandling( + zWithBackend(zKmsRandomBytesOptions), + options, + 'Invalid options provided to randomBytes method' + ) + + const operation = { + operation: 'randomBytes', + } as const + const kms = this.getKms(this.agentContext, backend, operation) + return kms.randomBytes(this.agentContext, kmsOptions) + } + + /** + * Get the kms associated with a specific `keyId`. + * + * This uses a naive approach of fetching the key for each configured kms + * until it finds the registered key. + * + * In the future this approach might be optimized based on: + * - caching + * - keeping a registry + * - backend specific key prefixes + */ + private async getKmsForOperationAndKeyId(agentContext: AgentContext, keyId: string, operation?: KmsOperation) { + for (const kms of this.keyManagementConfig.backends) { + const isOperationSupported = operation ? kms.isOperationSupported(agentContext, operation) : true + if (!isOperationSupported) continue + + const publicKey = await kms.getPublicKey(this.agentContext, keyId) + if (publicKey) + return { + publicKey, + kms, + } + } + + if (operation) { + throw new KeyManagementError( + `No key management service supports ${getKmsOperationHumanDescription(operation)} that has a key with keyId '${keyId}'` + ) + } + + throw new KeyManagementError(`No key management service has a key with keyId '${keyId}'`) + } + + /** + * Get the kms backend for a specific operation. + * + * If a backend is provided, it will be checked if the backend supports + * the operation. Otherwise the first backend that supports the operation + * will be used. + */ + private getKms(agentContext: AgentContext, backend?: string, operation?: KmsOperation) { + if (backend) { + const kms = this.keyManagementConfig.backends.find((kms) => kms.backend === backend) + if (!kms) { + const availableBackends = this.keyManagementConfig.backends.map((kms) => `'${kms.backend}'`) + throw new KeyManagementError( + `No key management service is configured for backend '${backend}'. Available backends are ${availableBackends.join( + ', ' + )}` + ) + } + + const isOperationSupported = operation ? kms.isOperationSupported(agentContext, operation) : true + if (!isOperationSupported && operation) { + throw new KeyManagementError( + `Key management service backend '${backend}' does not support ${getKmsOperationHumanDescription(operation)}` + ) + } + + return kms + } + + for (const kms of this.keyManagementConfig.backends) { + const isOperationSupported = operation ? kms.isOperationSupported(agentContext, operation) : true + if (isOperationSupported) return kms + } + + if (operation) { + throw new KeyManagementError( + `No key management service backend found that supports ${getKmsOperationHumanDescription(operation)}` + ) + } + + throw new KeyManagementError('No key management service backend found.') + } +} diff --git a/packages/core/src/modules/kms/KeyManagementModule.ts b/packages/core/src/modules/kms/KeyManagementModule.ts new file mode 100644 index 0000000000..53a8b08de4 --- /dev/null +++ b/packages/core/src/modules/kms/KeyManagementModule.ts @@ -0,0 +1,22 @@ +import type { DependencyManager, Module } from '../../plugins' +import type { KeyManagementModuleConfigOptions } from './KeyManagementModuleConfig' + +import { KeyManagementApi } from './KeyManagementApi' +import { KeyManagementModuleConfig } from './KeyManagementModuleConfig' + +export class KeyManagementModule implements Module { + public readonly api = KeyManagementApi + public readonly config: KeyManagementModuleConfig + + public constructor(config: KeyManagementModuleConfigOptions) { + this.config = new KeyManagementModuleConfig(config) + } + + /** + * Registers the dependencies of the key management module. + */ + public register(dependencyManager: DependencyManager) { + // Config + dependencyManager.registerInstance(KeyManagementModuleConfig, this.config) + } +} diff --git a/packages/core/src/modules/kms/KeyManagementModuleConfig.ts b/packages/core/src/modules/kms/KeyManagementModuleConfig.ts new file mode 100644 index 0000000000..450620b99a --- /dev/null +++ b/packages/core/src/modules/kms/KeyManagementModuleConfig.ts @@ -0,0 +1,64 @@ +import type { KeyManagementService } from './KeyManagementService' + +import { KeyManagementError } from './error/KeyManagementError' + +export interface KeyManagementModuleConfigOptions { + /** + * The backends to use for key management and cryptographic operations. + */ + backends?: KeyManagementService[] + + /** + * The default backend to use, indicated by the `backend` property + * on the `KeyManagementService` instance. + * + * If provided and it doesn't match an entry in the `backends` array + * an error will be thrown. + * + * If not provided, the first backend from the `backends` array will be used. + */ + defaultBackend?: string +} + +export class KeyManagementModuleConfig { + #defaultBackend?: string + #backends: KeyManagementService[] + + public constructor(options: KeyManagementModuleConfigOptions) { + this.#backends = options.backends ? [...options.backends] : [] + + if (options.defaultBackend) { + const defaultBackend = this.#backends.find((kms) => kms.backend === options.defaultBackend) + if (!defaultBackend) { + throw new KeyManagementError( + `Default backend '${options.defaultBackend}' provided in KeyManagementModuleConfig, but not found in 'backends'. Make sure the backend identifier matches with a registered backend.` + ) + } + this.#defaultBackend = options.defaultBackend + } + } + + public get backends() { + return this.#backends + } + + public registerBackend(backend: KeyManagementService) { + this.backends.push(backend) + } + + public get defaultBackend() { + const backend = this.backends.find((kms) => !this.#defaultBackend || this.#defaultBackend === kms.backend) + if (!backend) { + throw new KeyManagementError('Unable to determine default backend. ') + } + + return backend + } + + private toJSON() { + return { + defaultBackend: this.#defaultBackend, + backends: this.backends.map((backend) => backend.backend), + } + } +} diff --git a/packages/core/src/modules/kms/KeyManagementService.ts b/packages/core/src/modules/kms/KeyManagementService.ts new file mode 100644 index 0000000000..bdf59a0803 --- /dev/null +++ b/packages/core/src/modules/kms/KeyManagementService.ts @@ -0,0 +1,82 @@ +import type { AgentContext } from '../../agent' +import type { KmsJwkPrivate, KmsJwkPublic } from './jwk/knownJwk' +import type { KmsDecryptOptions, KmsDecryptReturn, KmsRandomBytesOptions, KmsRandomBytesReturn } from './options' +import type { KmsCreateKeyOptions, KmsCreateKeyReturn, KmsCreateKeyType } from './options/KmsCreateKeyOptions' +import type { KmsDeleteKeyOptions } from './options/KmsDeleteKeyOptions' +import type { KmsEncryptOptions, KmsEncryptReturn } from './options/KmsEncryptOptions' +import type { KmsImportKeyOptions, KmsImportKeyReturn } from './options/KmsImportKeyOptions' +import { KmsOperation } from './options/KmsOperation' +import type { KmsSignOptions, KmsSignReturn } from './options/KmsSignOptions' +import type { KmsVerifyOptions, KmsVerifyReturn } from './options/KmsVerifyOptions' + +export interface KeyManagementService { + /** + * The 'backend' name of this key management service + */ + readonly backend: string + + /** + * Whether this backend supports an operation. Generally if no backend is provided + * for an operation the first supported backend will be chosen. For operations based on + * a key id, the first supported backed will be checked whether it can handle that specific + * key id. + */ + isOperationSupported(agentContext: AgentContext, operation: KmsOperation): boolean + + /** + * Get the public representation of a key. + * + * In case of a symmetric key the returned JWK won't include + * any cryptographic key material itself, but will include + * all the key related metadata. + */ + getPublicKey(agentContext: AgentContext, keyId: string): Promise + + /** + * Create a key + */ + createKey( + agentContext: AgentContext, + options: KmsCreateKeyOptions + ): Promise> + + /** + * Import a key + */ + importKey( + agentContext: AgentContext, + options: KmsImportKeyOptions + ): Promise> + + /** + * Delete a key. + * + * @returns boolean whether the key was removed. + */ + deleteKey(agentContext: AgentContext, options: KmsDeleteKeyOptions): Promise + + /** + * Sign with a specific key + */ + sign(agentContext: AgentContext, options: KmsSignOptions): Promise + + /** + * Verify with a specific key + */ + verify(agentContext: AgentContext, options: KmsVerifyOptions): Promise + + /** + * Encrypt data + */ + encrypt(agentContext: AgentContext, options: KmsEncryptOptions): Promise + + /** + * Decrypt data + */ + decrypt(agentContext: AgentContext, options: KmsDecryptOptions): Promise + + /** + * Generate secure random bytes + */ + randomBytes(agentContext: AgentContext, options: KmsRandomBytesOptions): KmsRandomBytesReturn +} diff --git a/packages/core/src/modules/kms/__tests__/CreateKeyOptions.test.ts b/packages/core/src/modules/kms/__tests__/CreateKeyOptions.test.ts new file mode 100644 index 0000000000..6b03f6ab20 --- /dev/null +++ b/packages/core/src/modules/kms/__tests__/CreateKeyOptions.test.ts @@ -0,0 +1,44 @@ +import { parseWithErrorHandling } from '../../../utils/zod' +import { zKmsCreateKeyType } from '../options/KmsCreateKeyOptions' + +describe('CreateKeyOptions', () => { + test('should throw error for invalid create key type', async () => { + expect(() => + parseWithErrorHandling(zKmsCreateKeyType, { + kty: 'oct', + algorithm: 'AES', + }) + ).toThrow('Error validating schema with data {"kty":"oct","algorithm":"AES"}') + }) + + test('should correctly parse create key type', async () => { + expect(() => + zKmsCreateKeyType.parse({ + kty: 'oct', + algorithm: 'aes', + length: 128, + }) + ).not.toThrow() + + expect(() => + zKmsCreateKeyType.parse({ + kty: 'RSA', + modulusLength: 4096, + }) + ).not.toThrow() + + expect(() => + zKmsCreateKeyType.parse({ + kty: 'EC', + crv: 'P-256', + }) + ).not.toThrow() + + expect(() => + zKmsCreateKeyType.parse({ + kty: 'OKP', + crv: 'Ed25519', + }) + ).not.toThrow() + }) +}) diff --git a/packages/core/src/modules/kms/__tests__/KeyManagementApi.test.ts b/packages/core/src/modules/kms/__tests__/KeyManagementApi.test.ts new file mode 100644 index 0000000000..8d6f7a71fe --- /dev/null +++ b/packages/core/src/modules/kms/__tests__/KeyManagementApi.test.ts @@ -0,0 +1,146 @@ +import { getAgentOptions } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { ZodValidationError } from '../../../error/ZodValidationError' +import { KeyManagementError } from '../error/KeyManagementError' + +const agentOptions = getAgentOptions('KeyManagementApi') +const agent = new Agent(agentOptions) + +describe('KeyManagementApi', () => { + beforeAll(async () => { + await agent.initialize() + }) + + afterAll(async () => { + await agent.shutdown() + }) + + test('throws error if invalid backend provided', async () => { + await expect( + agent.kms.getPublicKey({ + keyId: 'hello', + backend: 'non-existing', + }) + ).rejects.toThrow( + new KeyManagementError( + `No key management service is configured for backend 'non-existing'. Available backends are 'node'` + ) + ) + }) + + test('successfully create, get and delete a key', async () => { + const result = await agent.kms.createKey({ + keyId: 'hello', + type: { + kty: 'EC', + crv: 'P-256', + }, + }) + + expect(result).toEqual({ + keyId: 'hello', + publicJwk: { + kid: 'hello', + kty: 'EC', + crv: 'P-256', + x: expect.any(String), + y: expect.any(String), + }, + }) + + const publicJwk = await agent.kms.getPublicKey({ + keyId: 'hello', + }) + expect(publicJwk).toEqual(result.publicJwk) + + const deleted = await agent.kms.deleteKey({ + keyId: 'hello', + }) + expect(deleted).toEqual(true) + + const deleted2 = await agent.kms.deleteKey({ + keyId: 'hello', + }) + expect(deleted2).toEqual(false) + }) + + test('throws error on invalid input for createKey', async () => { + await expect( + agent.kms.createKey({ + keyId: 'hello', + type: { + kty: 'EC', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + crv: 'P-something', + }, + }) + ).rejects.toThrow(ZodValidationError) + }) + + test('throws error on invalid input for getPublicKey', async () => { + await expect( + agent.kms.getPublicKey({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + keyId: undefined, + }) + ).rejects.toThrow(ZodValidationError) + }) + + test('throws error on invalid input for deleteKey', async () => { + await expect( + agent.kms.getPublicKey({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + keyId: undefined, + }) + ).rejects.toThrow(ZodValidationError) + }) + + test('successfully sign and verify with key', async () => { + const { keyId, publicJwk } = await agent.kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, + }) + + const { signature } = await agent.kms.sign({ + keyId, + algorithm: 'ES256', + data: new Uint8Array([1, 2, 3]), + }) + + const verifyResult = await agent.kms.verify({ + key: keyId, + algorithm: 'ES256', + signature, + data: new Uint8Array([1, 2, 3]), + }) + expect(verifyResult).toEqual({ + verified: true, + publicJwk, + }) + }) + + test('throws error on invalid input to sign', async () => { + await expect( + agent.kms.sign({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + keyId: undefined, + }) + ).rejects.toThrow(ZodValidationError) + }) + + test('throws error on invalid input to verify', async () => { + await expect( + agent.kms.verify({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + key: undefined, + }) + ).rejects.toThrow(ZodValidationError) + }) +}) diff --git a/packages/core/src/modules/kms/error/KeyManagementAlgorithmNotSupportedError.ts b/packages/core/src/modules/kms/error/KeyManagementAlgorithmNotSupportedError.ts new file mode 100644 index 0000000000..abee5556c0 --- /dev/null +++ b/packages/core/src/modules/kms/error/KeyManagementAlgorithmNotSupportedError.ts @@ -0,0 +1,10 @@ +import { KeyManagementError } from './KeyManagementError' + +export class KeyManagementAlgorithmNotSupportedError extends KeyManagementError { + public constructor( + notSupported: string, + public backend: string + ) { + super(`${backend} backend does not support ${notSupported}.`) + } +} diff --git a/packages/core/src/modules/kms/error/KeyManagementError.ts b/packages/core/src/modules/kms/error/KeyManagementError.ts new file mode 100644 index 0000000000..b62855e3c0 --- /dev/null +++ b/packages/core/src/modules/kms/error/KeyManagementError.ts @@ -0,0 +1,3 @@ +import { CredoError } from '../../../error' + +export class KeyManagementError extends CredoError {} diff --git a/packages/core/src/modules/kms/error/KeyManagementKeyExistsError.ts b/packages/core/src/modules/kms/error/KeyManagementKeyExistsError.ts new file mode 100644 index 0000000000..b68a22b74f --- /dev/null +++ b/packages/core/src/modules/kms/error/KeyManagementKeyExistsError.ts @@ -0,0 +1,7 @@ +import { KeyManagementError } from './KeyManagementError' + +export class KeyManagementKeyExistsError extends KeyManagementError { + public constructor(keyId: string, backend: string) { + super(`A key with key id '${keyId}' already exists in backend '${backend}'`) + } +} diff --git a/packages/core/src/modules/kms/error/KeyManagementKeyNotFoundError.ts b/packages/core/src/modules/kms/error/KeyManagementKeyNotFoundError.ts new file mode 100644 index 0000000000..947777c95d --- /dev/null +++ b/packages/core/src/modules/kms/error/KeyManagementKeyNotFoundError.ts @@ -0,0 +1,7 @@ +import { KeyManagementError } from './KeyManagementError' + +export class KeyManagementKeyNotFoundError extends KeyManagementError { + public constructor(keyId: string, backend: string) { + super(`Key with key id '${keyId}' not found in backend '${backend}'`) + } +} diff --git a/packages/core/src/modules/kms/index.ts b/packages/core/src/modules/kms/index.ts new file mode 100644 index 0000000000..7c151b5516 --- /dev/null +++ b/packages/core/src/modules/kms/index.ts @@ -0,0 +1,14 @@ +export * from './KeyManagementApi' +export * from './KeyManagementModule' +export * from './KeyManagementModuleConfig' +export * from './KeyManagementService' + +export * from './options' + +export * from './error/KeyManagementError' +export * from './error/KeyManagementKeyExistsError' +export * from './error/KeyManagementKeyNotFoundError' +export * from './error/KeyManagementAlgorithmNotSupportedError' + +export * from './jwk' +export { legacyKeyIdFromPublicJwk } from './legacy' diff --git a/packages/core/src/modules/kms/jwk/PublicJwk.ts b/packages/core/src/modules/kms/jwk/PublicJwk.ts new file mode 100644 index 0000000000..426d05bcf2 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/PublicJwk.ts @@ -0,0 +1,294 @@ +import { CredoError } from '../../../error' +import { MultiBaseEncoder, TypedArrayEncoder, VarintEncoder } from '../../../utils' +import { Constructor } from '../../../utils/mixins' +import { parseWithErrorHandling } from '../../../utils/zod' +import { KeyManagementError } from '../error/KeyManagementError' +import { legacyKeyIdFromPublicJwk } from '../legacy' +import { assymetricPublicJwkMatches } from './equals' +import { getJwkHumanDescription } from './humanDescription' +import { KnownJwaKeyAgreementAlgorithm, KnownJwaSignatureAlgorithm } from './jwa' +import { KmsJwkPublicAsymmetric, assertJwkAsymmetric, publicJwkFromPrivateJwk, zKmsJwkPublic } from './knownJwk' + +import { + Ed25519PublicJwk, + P256PublicJwk, + P384PublicJwk, + P521PublicJwk, + RsaPublicJwk, + Secp256k1PublicJwk, + X25519PublicJwk, +} from './kty' + +export const SupportedPublicJwks = [ + Ed25519PublicJwk, + P256PublicJwk, + P384PublicJwk, + P521PublicJwk, + RsaPublicJwk, + Secp256k1PublicJwk, + X25519PublicJwk, +] +export type SupportedPublicJwkClass = (typeof SupportedPublicJwks)[number] +export type SupportedPublicJwk = + | Ed25519PublicJwk + | P256PublicJwk + | P384PublicJwk + | P521PublicJwk + | RsaPublicJwk + | Secp256k1PublicJwk + | X25519PublicJwk + +type ExtractByJwk = T extends { jwk: infer J } ? (K extends J ? T : never) : never + +type ExtractByPublicKey = T extends { publicKey: infer J } ? (K extends J ? T : never) : never + +export class PublicJwk { + private constructor(public readonly jwk: Jwk) {} + + public static fromUnknown(jwkJson: unknown) { + // We remove any private properties if they are present + const publicJwk = publicJwkFromPrivateJwk(parseWithErrorHandling(zKmsJwkPublic, jwkJson, 'jwk is not a valid jwk')) + assertJwkAsymmetric(publicJwk) + + let jwkInstance: SupportedPublicJwk + if (publicJwk.kty === 'RSA') { + jwkInstance = new RsaPublicJwk(publicJwk) + } else if (publicJwk.kty === 'EC') { + if (publicJwk.crv === 'P-256') { + jwkInstance = new P256PublicJwk({ + ...publicJwk, + crv: publicJwk.crv, + }) + } else if (publicJwk.crv === 'P-384') { + jwkInstance = new P384PublicJwk({ + ...publicJwk, + crv: publicJwk.crv, + }) + } else if (publicJwk.crv === 'P-521') { + jwkInstance = new P521PublicJwk({ + ...publicJwk, + crv: publicJwk.crv, + }) + } else if (publicJwk.crv === 'secp256k1') { + jwkInstance = new Secp256k1PublicJwk({ + ...publicJwk, + crv: publicJwk.crv, + }) + } else { + throw new KeyManagementError( + `Unsupported kty '${publicJwk.kty}' with crv '${publicJwk.crv}' for creating jwk instance` + ) + } + } else if (publicJwk.crv === 'Ed25519') { + jwkInstance = new Ed25519PublicJwk({ + ...publicJwk, + crv: publicJwk.crv, + }) + } else if (publicJwk.crv === 'X25519') { + jwkInstance = new X25519PublicJwk({ + ...publicJwk, + crv: publicJwk.crv, + }) + } else { + throw new KeyManagementError(`Unsupported kty '${publicJwk.kty}' for creating jwk instance`) + } + + return new PublicJwk(jwkInstance) + } + + // FIXME: all Jwk combinations should be separate types. + // so not kty: EC, and crv: P-256 | P-384 + // but: kty: EC, and crv: P-256 | kty: EC, and crv: P-384 + // As the first appraoch messes with TypeScript's type inference + public static fromPublicJwk(jwk: Jwk) { + return PublicJwk.fromUnknown(jwk) as PublicJwk< + ExtractByJwk extends never ? SupportedPublicJwk : ExtractByJwk + > + } + + public toJson({ includeKid = true }: { includeKid?: boolean } = {}): Jwk['jwk'] { + const jwk = { ...this.jwk.jwk } + + // biome-ignore lint/performance/noDelete: + if (!includeKid) delete jwk.kid + + return jwk + } + + public get supportedSignatureAlgorithms(): KnownJwaSignatureAlgorithm[] { + return this.jwk.supportedSignatureAlgorithms ?? [] + } + + public get supportdEncryptionKeyAgreementAlgorithms(): KnownJwaKeyAgreementAlgorithm[] { + return this.jwk.supportdEncryptionKeyAgreementAlgorithms ?? [] + } + + /** + * key type as defined in [JWA Specification](https://tools.ietf.org/html/rfc7518#section-6.1) + */ + public get kty(): Jwk['jwk']['kty'] { + return this.jwk.jwk.kty + } + + /** + * Get the key id for a public jwk. If the public jwk does not have + */ + public get keyId(): string { + if (this.jwk.jwk.kid) return this.jwk.jwk.kid + + throw new KeyManagementError('Unable to determine keyId for jwk') + } + + public get hasKeyId(): boolean { + return this.jwk.jwk.kid !== undefined + } + + public set keyId(keyId: string) { + this.jwk.jwk.kid = keyId + } + + public get legacyKeyId() { + return legacyKeyIdFromPublicJwk(this) + } + + public get publicKey(): Jwk['publicKey'] { + return this.jwk.publicKey + } + + /** + * Get the signature algorithm to use with this jwk. If the jwk has an `alg` field defined + * it will use that alg, and otherwise fall back to the first supported signature algorithm. + * + * If no algorithm is supported it will throw an error + */ + public get signatureAlgorithm() { + if (this.jwk.jwk.alg) { + if (!this.supportedSignatureAlgorithms.includes(this.jwk.jwk.alg as KnownJwaSignatureAlgorithm)) { + throw new KeyManagementError( + `${getJwkHumanDescription(this.jwk.jwk)} defines alg '${this.jwk.jwk.alg}' but this alg is not supported.` + ) + } + + return this.jwk.jwk.alg as this['supportedSignatureAlgorithms'][number] + } + + const alg = this.supportedSignatureAlgorithms[0] + if (!alg) { + throw new KeyManagementError(`${getJwkHumanDescription(this.jwk.jwk)} has no supported signature algorithms`) + } + + return alg as this['supportedSignatureAlgorithms'][number] + } + + public static fromPublicKey(publicKey: Supported) { + let jwkInstance: SupportedPublicJwk + + if (publicKey.kty === 'RSA') { + jwkInstance = RsaPublicJwk.fromPublicKey(publicKey) + } else if (publicKey.kty === 'EC') { + if (publicKey.crv === 'P-256') { + jwkInstance = P256PublicJwk.fromPublicKey(publicKey.publicKey) + } else if (publicKey.crv === 'P-384') { + jwkInstance = P384PublicJwk.fromPublicKey(publicKey.publicKey) + } else if (publicKey.crv === 'P-521') { + jwkInstance = P521PublicJwk.fromPublicKey(publicKey.publicKey) + } else if (publicKey.crv === 'secp256k1') { + jwkInstance = Secp256k1PublicJwk.fromPublicKey(publicKey.publicKey) + } else { + throw new KeyManagementError( + // @ts-expect-error + `Unsupported kty '${publicKey.kty}' with crv '${publicKey.crv}' for creating jwk instance based on public key bytes` + ) + } + } else if (publicKey.crv === 'X25519') { + jwkInstance = X25519PublicJwk.fromPublicKey(publicKey.publicKey) + } else if (publicKey.crv === 'Ed25519') { + jwkInstance = Ed25519PublicJwk.fromPublicKey(publicKey.publicKey) + } else { + throw new KeyManagementError( + // @ts-expect-error + `Unsupported kty '${publicKey.kty}' for creating jwk instance based on public key bytes` + ) + } + + return new PublicJwk(jwkInstance) as PublicJwk> + } + + /** + * Returns the jwk encoded a Base58 multibase encoded multicodec key + */ + public get fingerprint() { + const prefixBytes = VarintEncoder.encode(this.jwk.multicodecPrefix) + const prefixedPublicKey = new Uint8Array([...prefixBytes, ...this.jwk.multicodec]) + + return `z${TypedArrayEncoder.toBase58(prefixedPublicKey)}` + } + + /** + * Create a jwk instance based on a Base58 multibase encoded multicodec key + */ + public static fromFingerprint(fingerprint: string) { + const { data } = MultiBaseEncoder.decode(fingerprint) + const [code, byteLength] = VarintEncoder.decode(data) + const publicKey = data.slice(byteLength) + + const PublicJwkClass = SupportedPublicJwks.find((JwkClass) => JwkClass.multicodecPrefix === code) + if (!PublicJwkClass) { + throw new KeyManagementError(`Unsupported multicodec public key with prefix '${code}'`) + } + + const jwk = PublicJwkClass.fromMulticodec(publicKey) + return new PublicJwk(jwk) + } + + /** + * Check whether this PublicJwk instance is of a specific type + */ + public is< + Jwk1 extends SupportedPublicJwk, + Jwk2 extends SupportedPublicJwk = Jwk1, + Jwk3 extends SupportedPublicJwk = Jwk1, + >( + jwkType1: Constructor, + jwkType2?: Constructor, + jwkType3?: Constructor + ): this is PublicJwk | PublicJwk | PublicJwk { + const types = [jwkType1, jwkType2, jwkType3].filter(Boolean) as Constructor[] + return types.some((type) => this.jwk.constructor === type) + } + + /** + * Check whether this jwk instance is the same as another jwk instance. + * It does this by comparing the key types and public keys, not other fields + * of the JWK such as keyId, use, etc.. + */ + public equals(other: PublicJwk) { + return assymetricPublicJwkMatches(this.toJson(), other.toJson()) + } + + private toJSON() { + return { + jwk: this.jwk, + } + } + + /** + * Get human description of a jwk type. This does + * not include the (public) key material + */ + public get jwkTypehumanDescription() { + return getJwkHumanDescription(this.toJson()) + } + + public static supportedPublicJwkClassForSignatureAlgorithm(alg: KnownJwaSignatureAlgorithm): SupportedPublicJwkClass { + const supportedPublicJwkClass = SupportedPublicJwks.find((JwkClass) => + JwkClass.supportedSignatureAlgorithms.includes(alg) + ) + + if (!supportedPublicJwkClass) { + throw new CredoError(`Could not determine supported public jwk class for alg '${alg}'`) + } + + return supportedPublicJwkClass + } +} diff --git a/packages/core/src/modules/kms/jwk/alg/encryption.ts b/packages/core/src/modules/kms/jwk/alg/encryption.ts new file mode 100644 index 0000000000..9e2257d400 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/alg/encryption.ts @@ -0,0 +1,66 @@ +import type { KnownJwaContentEncryptionAlgorithm, KnownJwaKeyEncryptionAlgorithm } from '../jwa' +import type { KmsJwkPrivate, KmsJwkPublic } from '../knownJwk' +import type { KmsJwkPublicOct } from '../kty/oct/octJwk' + +import { TypedArrayEncoder } from '../../../../utils' +import { KeyManagementError } from '../../error/KeyManagementError' +import { getJwkHumanDescription } from '../humanDescription' + +export function supportedEncryptionAlgsForKey(jwk: KmsJwkPrivate | Exclude) { + const algs: Array = [] + + // Only symmetric (oct) keys can be used directly for content encryption + if (jwk.kty === 'oct') { + const keyBits = TypedArrayEncoder.fromBase64(jwk.k).length * 8 + + // For CBC-HMAC composite algorithms we need exact key sizes + if (keyBits === 256) algs.push('A128CBC-HS256') + if (keyBits === 384) algs.push('A192CBC-HS384') + if (keyBits === 512) algs.push('A256CBC-HS512') + + // For GCM/CBC we just need the exact AES key size + if (keyBits === 128) algs.push('A128GCM', 'A128CBC', 'A128KW') + if (keyBits === 192) algs.push('A192GCM', 'A192KW') + if (keyBits === 256) algs.push('A256GCM', 'A256CBC', 'A256KW', 'C20P', 'XC20P') + } + + return algs +} + +/** + * Get the allowed content encryption algs for a key. If takes all the known supported + * algs and will filter these based on the optional `alg` key in the JWK. + * + * This does not handle the intended key `use` and `key_ops`. + */ +export function allowedEncryptionAlgsForKey( + jwk: KmsJwkPrivate | Exclude +): Array { + const supportedAlgs = supportedEncryptionAlgsForKey(jwk) + const allowedAlg = jwk.alg + + return !allowedAlg + ? // If no `alg` specified on jwk, return all supported algs + supportedAlgs + : // If `alg` is specified and supported, return the allowed alg + allowedAlg && supportedAlgs.includes(allowedAlg as KnownJwaContentEncryptionAlgorithm) + ? [allowedAlg as KnownJwaContentEncryptionAlgorithm | KnownJwaKeyEncryptionAlgorithm] + : // Otherwise nothing is allowed (`alg` is specified but not supported) + [] +} + +export function assertAllowedEncryptionAlgForKey( + jwk: KmsJwkPrivate | Exclude, + algorithm: KnownJwaContentEncryptionAlgorithm | KnownJwaKeyEncryptionAlgorithm +) { + const allowedAlgs = allowedEncryptionAlgsForKey(jwk) + if (!allowedAlgs.includes(algorithm)) { + const allowedAlgsText = + allowedAlgs.length > 0 ? ` Allowed algs are ${allowedAlgs.map((alg) => `'${alg}'`).join(', ')}` : '' + throw new KeyManagementError( + `${getJwkHumanDescription( + jwk + )} cannot be used with algorithm '${algorithm}' for content encryption or decryption.${allowedAlgsText}` + ) + } +} diff --git a/packages/core/src/modules/kms/jwk/alg/index.ts b/packages/core/src/modules/kms/jwk/alg/index.ts new file mode 100644 index 0000000000..30af7a6b9d --- /dev/null +++ b/packages/core/src/modules/kms/jwk/alg/index.ts @@ -0,0 +1,11 @@ +export { + allowedEncryptionAlgsForKey, + assertAllowedEncryptionAlgForKey, + supportedEncryptionAlgsForKey, +} from './encryption' +export { allowedSigningAlgsForSigningKey, assertAllowedSigningAlgForKey, supportedSigningAlgsForKey } from './signing' +export { + allowedKeyDerivationAlgsForKey, + assertAllowedKeyDerivationAlgForKey, + supportedKeyDerivationAlgsForKey, +} from './keyDerivation' diff --git a/packages/core/src/modules/kms/jwk/alg/keyDerivation.ts b/packages/core/src/modules/kms/jwk/alg/keyDerivation.ts new file mode 100644 index 0000000000..b135380d5f --- /dev/null +++ b/packages/core/src/modules/kms/jwk/alg/keyDerivation.ts @@ -0,0 +1,70 @@ +import type { KnownJwaKeyAgreementAlgorithm } from '../jwa' +import type { KmsJwkPrivate, KmsJwkPublic, KmsJwkPublicCrv } from '../knownJwk' +import type { KmsJwkPrivateOct, KmsJwkPublicOct } from '../kty/oct/octJwk' +import type { KmsJwkPrivateRsa, KmsJwkPublicRsa } from '../kty/rsa/rsaJwk' + +import { KeyManagementError } from '../../error/KeyManagementError' +import { getJwkHumanDescription } from '../humanDescription' + +function isCrvJwk( + jwk: Jwk +): jwk is Exclude { + return jwk.kty === 'EC' || jwk.kty === 'OKP' +} + +export function supportedKeyDerivationAlgsForKey( + jwk: KmsJwkPrivate | Exclude +): KnownJwaKeyAgreementAlgorithm[] { + const algs: KnownJwaKeyAgreementAlgorithm[] = [] + + const allowedCurves: KmsJwkPublicCrv['crv'][] = ['P-256', 'P-384', 'P-521', 'X25519', 'secp256k1'] + if (isCrvJwk(jwk) && allowedCurves.includes(jwk.crv)) { + algs.push('ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW') + } + + // Special case where we allow Ed25519 for X25519 based operation, since that is + // how DIDComm v1 works. + if (jwk.kty === 'OKP' && (jwk.crv === 'X25519' || jwk.crv === 'Ed25519')) { + algs.push('ECDH-HSALSA20') + } + + return algs +} + +/** + * Get the allowed key derivation algs for a key. If takes all the known supported + * algs and will filter these based on the optional `alg` key in the JWK. + * + * This does not handle the intended key `use` and `key_ops`. + */ +export function allowedKeyDerivationAlgsForKey( + jwk: KmsJwkPrivate | Exclude +): KnownJwaKeyAgreementAlgorithm[] { + const supportedAlgs = supportedKeyDerivationAlgsForKey(jwk) + const allowedAlg = jwk.alg + + return !allowedAlg + ? // If no `alg` specified on jwk, return all supported algs + supportedAlgs + : // If `alg` is specified and supported, return the allowed alg + allowedAlg && supportedAlgs.includes(allowedAlg as KnownJwaKeyAgreementAlgorithm) + ? [allowedAlg as KnownJwaKeyAgreementAlgorithm] + : // Otherwise nothing is allowed (`alg` is specified but not supported) + [] +} + +export function assertAllowedKeyDerivationAlgForKey( + jwk: KmsJwkPrivate | Exclude, + algorithm: KnownJwaKeyAgreementAlgorithm +) { + const allowedAlgs = allowedKeyDerivationAlgsForKey(jwk) + if (!allowedAlgs.includes(algorithm)) { + const allowedAlgsText = + allowedAlgs.length > 0 ? ` Allowed algs are ${allowedAlgs.map((alg) => `'${alg}'`).join(', ')}` : '' + throw new KeyManagementError( + `${getJwkHumanDescription( + jwk + )} cannot be used with algorithm '${algorithm}' for key derivation.${allowedAlgsText}` + ) + } +} diff --git a/packages/core/src/modules/kms/jwk/alg/signing.ts b/packages/core/src/modules/kms/jwk/alg/signing.ts new file mode 100644 index 0000000000..ec52ec4798 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/alg/signing.ts @@ -0,0 +1,181 @@ +import type { KnownJwaSignatureAlgorithm } from '../jwa' +import type { KmsJwkPrivate, KmsJwkPublic } from '../knownJwk' +import type { KmsJwkPublicOct } from '../kty/oct/octJwk' + +import { TypedArrayEncoder } from '../../../../utils' +import { KeyManagementError } from '../../error/KeyManagementError' +import { KmsCreateKeyType } from '../../options' +import { getJwkHumanDescription } from '../humanDescription' + +/** + * Get the allowed algs for a signing key. If takes all the known supported + * algs and will filter these based on the optional `alg` key in the JWK. + * + * This does not handle the intended key `use` and `key_ops`. + */ +export function allowedSigningAlgsForSigningKey( + jwk: KmsJwkPrivate | Exclude +): KnownJwaSignatureAlgorithm[] { + const supportedAlgs = supportedSigningAlgsForKey(jwk) + const allowedAlg = jwk.alg + + return !allowedAlg + ? // If no `alg` specified on jwk, return all supported algs + supportedAlgs + : // If `alg` is specified and supported, return the allowed alg + allowedAlg && supportedAlgs.includes(allowedAlg as KnownJwaSignatureAlgorithm) + ? [allowedAlg as KnownJwaSignatureAlgorithm] + : // Otherwise nothing is allowed (`alg` is specified but not supported) + [] +} + +export function assertAllowedSigningAlgForKey( + jwk: KmsJwkPrivate | Exclude, + algorithm: KnownJwaSignatureAlgorithm +) { + const allowedAlgs = allowedSigningAlgsForSigningKey(jwk) + if (!allowedAlgs.includes(algorithm)) { + const allowedAlgsText = + allowedAlgs.length > 0 ? ` Allowed algs are ${allowedAlgs.map((alg) => `'${alg}'`).join(', ')}` : '' + throw new KeyManagementError( + `${getJwkHumanDescription( + jwk + )} cannot be used with algorithm '${algorithm}' for signature creation or verification.${allowedAlgsText}` + ) + } +} + +// NOTE: this should be replaced by the PublicJwk class +// but it woun't work for oct keys +export function supportedSigningAlgsForKey( + jwk: KmsJwkPrivate | Exclude +): KnownJwaSignatureAlgorithm[] { + if (jwk.kty === 'EC' || jwk.kty === 'OKP') { + switch (jwk.crv) { + case 'secp256k1': + return ['ES256K'] + case 'P-256': + return ['ES256'] + case 'P-384': + return ['ES384'] + case 'P-521': + return ['ES512'] + case 'Ed25519': + return ['EdDSA'] + + // X25519 + default: + return [] + } + } + + if (jwk.kty === 'RSA') { + const keyBits = TypedArrayEncoder.fromBase64(jwk.n).length * 8 + + // RSA needs minimum bit lengths for each algorithm + const minBits2048: KnownJwaSignatureAlgorithm[] = ['PS256', 'RS256'] + const minBits3072: KnownJwaSignatureAlgorithm[] = [...minBits2048, 'RS384', 'PS384'] + const minBits4096: KnownJwaSignatureAlgorithm[] = [...minBits3072, 'RS512', 'PS512'] + + return keyBits >= 4096 ? minBits4096 : keyBits >= 3072 ? minBits3072 : keyBits >= 2048 ? minBits2048 : [] + } + + // On other layers we need to filter for alg types, as you don't want any `oct` key with enough length to used for hmac purposes + if (jwk.kty === 'oct') { + const keyBits = TypedArrayEncoder.fromBase64(jwk.k).length * 8 + + // hmac needs minimum bit lengths for each algorithm + const minBits256: KnownJwaSignatureAlgorithm[] = ['HS256'] + const minBits384: KnownJwaSignatureAlgorithm[] = [...minBits256, 'HS384'] + const minBits512: KnownJwaSignatureAlgorithm[] = [...minBits384, 'HS512'] + return keyBits >= 512 ? minBits512 : keyBits >= 384 ? minBits384 : keyBits >= 256 ? minBits256 : [] + } + + return [] +} + +// Can we move this to the JWK classes? +export function createKeyTypeForSigningAlgorithm(algorithm: KnownJwaSignatureAlgorithm): KmsCreateKeyType { + // On JWK class we can have + if (algorithm === 'ES256') { + return { + kty: 'EC', + crv: 'P-256', + } + } + + if (algorithm === 'ES384') { + return { + kty: 'EC', + crv: 'P-384', + } + } + + if (algorithm === 'ES512') { + return { + kty: 'EC', + crv: 'P-521', + } + } + + if (algorithm === 'ES256K') { + return { + kty: 'EC', + crv: 'secp256k1', + } + } + + if (algorithm === 'EdDSA') { + return { + kty: 'OKP', + crv: 'Ed25519', + } + } + + if (algorithm === 'HS256') { + return { + kty: 'oct', + algorithm: 'hmac', + length: 256, + } + } + + if (algorithm === 'HS384') { + return { + kty: 'oct', + algorithm: 'hmac', + length: 384, + } + } + + if (algorithm === 'HS512') { + return { + kty: 'oct', + algorithm: 'hmac', + length: 512, + } + } + + if (algorithm === 'PS256' || algorithm === 'RS256') { + return { + kty: 'RSA', + modulusLength: 2048, + } + } + + if (algorithm === 'PS384' || algorithm === 'RS384') { + return { + kty: 'RSA', + modulusLength: 3072, + } + } + + if (algorithm === 'PS512' || algorithm === 'RS512') { + return { + kty: 'RSA', + modulusLength: 4096, + } + } + + throw new KeyManagementError(`unknown signature algorithm '${algorithm}' for creating key `) +} diff --git a/packages/core/src/modules/kms/jwk/assertSupported.ts b/packages/core/src/modules/kms/jwk/assertSupported.ts new file mode 100644 index 0000000000..1810e4ccbf --- /dev/null +++ b/packages/core/src/modules/kms/jwk/assertSupported.ts @@ -0,0 +1,41 @@ +import { KeyManagementAlgorithmNotSupportedError } from '../error/KeyManagementAlgorithmNotSupportedError' +import { + KmsDecryptDataDecryption, + KmsEncryptDataEncryption, + KmsKeyAgreementDecryptOptions, + KmsKeyAgreementEncryptOptions, +} from '../options' +import { + KnownJwaContentEncryptionAlgorithm, + KnownJwaKeyAgreementAlgorithm, + KnownJwaKeyEncryptionAlgorithm, +} from './jwa' + +export function assertSupportedKeyAgreementAlgorithm< + KeyAgreement extends KmsKeyAgreementEncryptOptions | KmsKeyAgreementDecryptOptions, + SupportedAlgorithms extends KnownJwaKeyAgreementAlgorithm[], +>( + keyAgreement: KeyAgreement, + supportedAlgorithms: SupportedAlgorithms, + backend: string +): asserts keyAgreement is KeyAgreement & { algorithm: SupportedAlgorithms[number] } { + if (!supportedAlgorithms.includes(keyAgreement.algorithm as (typeof supportedAlgorithms)[number])) { + throw new KeyManagementAlgorithmNotSupportedError( + `JWA key agreement algorithm '${keyAgreement.algorithm}'`, + backend + ) + } +} + +export function assertSupportedEncryptionAlgorithm< + Encryption extends KmsEncryptDataEncryption | KmsDecryptDataDecryption, + SupportedAlgorithms extends Array, +>( + encryption: Encryption, + supportedAlgorithms: SupportedAlgorithms, + backend: string +): asserts encryption is Encryption & { algorithm: SupportedAlgorithms[number] } { + if (!supportedAlgorithms.includes(encryption.algorithm as (typeof supportedAlgorithms)[number])) { + throw new KeyManagementAlgorithmNotSupportedError(`JWA encryption algorithm '${encryption.algorithm}'`, backend) + } +} diff --git a/packages/core/src/modules/kms/jwk/equals.ts b/packages/core/src/modules/kms/jwk/equals.ts new file mode 100644 index 0000000000..95d4be3eda --- /dev/null +++ b/packages/core/src/modules/kms/jwk/equals.ts @@ -0,0 +1,74 @@ +import { KeyManagementError } from '../error/KeyManagementError' +import { getJwkHumanDescription } from './humanDescription' +import { KmsJwkPrivateAsymmetric, KmsJwkPublicAsymmetric } from './knownJwk' + +/** + * Checks if two JWK public keys have matching key types + * Supports EC, OKP, and RSA key types + */ +export function assymetricJwkKeyTypeMatches( + first: KmsJwkPublicAsymmetric | KmsJwkPrivateAsymmetric, + second: KmsJwkPublicAsymmetric | KmsJwkPrivateAsymmetric +): boolean { + if (first.kty !== second.kty) return false + + if (first.kty === 'EC' && second.kty === 'EC') { + return first.crv === second.crv + } + + if (first.kty === 'OKP' && second.kty === 'OKP') { + return first.crv === second.crv + } + + if (first.kty === 'RSA' && second.kty === 'RSA') { + // RSA doesn't have curve parameter, so key type match is sufficient + return true + } + + // Unknown key type + return false +} + +/** + * Checks if two JWK public keys have matching key types + * Supports EC, OKP, and RSA key types + */ +export function assertAsymmetricJwkKeyTypeMatches( + first: KmsJwkPublicAsymmetric | KmsJwkPrivateAsymmetric, + second: KmsJwkPublicAsymmetric | KmsJwkPrivateAsymmetric +): asserts first is typeof second { + if (!assymetricJwkKeyTypeMatches(first, second)) { + throw new KeyManagementError( + `Expected jwk types to match, but found ${getJwkHumanDescription(first)} and ${getJwkHumanDescription(second)}` + ) + } +} + +/** + * Checks if two JWK public keys have matching key material + * Supports EC, OKP, and RSA key types + */ +export function assymetricPublicJwkMatches(first: KmsJwkPublicAsymmetric, second: KmsJwkPublicAsymmetric): boolean { + // First check that types match + if (!assymetricJwkKeyTypeMatches(first, second)) { + return false + } + + // For EC keys, compare x and y coordinates + if (first.kty === 'EC' && second.kty === 'EC') { + return first.x === second.x && first.y === second.y + } + + // For OKP keys, compare x coordinate (Ed25519, X25519, etc.) + if (first.kty === 'OKP' && second.kty === 'OKP') { + return first.x === second.x + } + + // For RSA keys, compare modulus (n) and exponent (e) + if (first.kty === 'RSA' && second.kty === 'RSA') { + return first.n === second.n && first.e === second.e + } + + // Unknown key type + return false +} diff --git a/packages/core/src/modules/kms/jwk/humanDescription.ts b/packages/core/src/modules/kms/jwk/humanDescription.ts new file mode 100644 index 0000000000..901cbe8d01 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/humanDescription.ts @@ -0,0 +1,31 @@ +import type { KmsJwkPrivate, KmsJwkPublic } from './knownJwk' + +import { TypedArrayEncoder } from '../../../utils' + +/** + * Gets text description of a key. + * + * - `EC key with crv ''` + * - `RSA key with bith length + * - `oct key` + * - `'' key` + */ +export function getJwkHumanDescription(jwk: KmsJwkPrivate | KmsJwkPublic) { + if (jwk.kty === 'EC' || jwk.kty === 'OKP') { + return `${jwk.kty} key with crv '${jwk.crv}'` + } + + if (jwk.kty === 'RSA') { + // n is the modulus, base64url encoded. Decode to get bit length + const nBytes = TypedArrayEncoder.fromBase64(jwk.n).length + const bitLength = nBytes * 8 + return `RSA key with bit length ${bitLength}` + } + if (jwk.kty === 'oct') { + return 'oct key' + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return `'${jwk.kty}' key'` +} diff --git a/packages/core/src/modules/kms/jwk/index.ts b/packages/core/src/modules/kms/jwk/index.ts new file mode 100644 index 0000000000..24b57d26cb --- /dev/null +++ b/packages/core/src/modules/kms/jwk/index.ts @@ -0,0 +1,65 @@ +export type { + KnownJwaSignatureAlgorithm, + KnownJwaContentEncryptionAlgorithm, + KnownJwaKeyEncryptionAlgorithm, + KnownJwaKeyAgreementAlgorithm, +} from './jwa' +export { + KnownJwaKeyAgreementAlgorithms, + KnownJwaContentEncryptionAlgorithms, + KnownJwaKeyEncryptionAlgorithms, + KnownJwaSignatureAlgorithms, +} from './jwa' +export { + type KmsJwkPrivate, + type KmsJwkPublic, + publicJwkFromPrivateJwk, + type KmsJwkPublicAsymmetric, + assertJwkAsymmetric, + isJwkAsymmetric, + type KmsJwkPrivateAsymmetric, + type KmsJwkPublicFromCreateType, + type KmsJwkPrivateFromKmsJwkPublic, + type KmsJwkPublicFromKmsJwkPrivate, +} from './knownJwk' + +export { assertSupportedKeyAgreementAlgorithm, assertSupportedEncryptionAlgorithm } from './assertSupported' +export type { + KmsJwkPrivateEc, + KmsJwkPublicEc, + KmsJwkPrivateOct, + KmsJwkPublicOct, + KmsJwkPrivateOkp, + KmsJwkPublicOkp, + KmsJwkPrivateRsa, + KmsJwkPublicRsa, +} from './kty' + +export { + Ed25519PublicJwk, + P256PublicJwk, + P384PublicJwk, + P521PublicJwk, + RsaPublicJwk, + X25519PublicJwk, + Secp256k1PublicJwk, + derEcSignatureToRaw, + rawEcSignatureToDer, +} from './kty' + +export { Jwk, JwkCommon } from './jwk' +export { + keyAllowsSign, + keyAllowsVerify, + assertKeyAllowsSign, + assertKeyAllowsVerify, + keyAllowsEncrypt, + assertKeyAllowsEncrypt, + keyAllowsDecrypt, + assertKeyAllowsDecrypt, + assertKeyAllowsDerive, +} from './keyOps' +export * from './alg' +export { getJwkHumanDescription } from './humanDescription' +export { assymetricJwkKeyTypeMatches, assymetricPublicJwkMatches, assertAsymmetricJwkKeyTypeMatches } from './equals' +export { PublicJwk } from './PublicJwk' diff --git a/packages/core/src/modules/kms/jwk/jwa.ts b/packages/core/src/modules/kms/jwk/jwa.ts new file mode 100644 index 0000000000..4fcb1c1782 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/jwa.ts @@ -0,0 +1,82 @@ +import * as z from '../../../utils/zod' + +function recordToUnion(record: Record) { + return Object.values(record) as [ + (typeof record)[keyof typeof record], + (typeof record)[keyof typeof record], + ...(typeof record)[keyof typeof record][], + ] +} + +export const KnownJwaSignatureAlgorithms = { + HS256: 'HS256', + HS384: 'HS384', + HS512: 'HS512', + RS256: 'RS256', + RS384: 'RS384', + RS512: 'RS512', + ES256: 'ES256', + ES384: 'ES384', + ES512: 'ES512', + PS256: 'PS256', + PS384: 'PS384', + PS512: 'PS512', + EdDSA: 'EdDSA', + ES256K: 'ES256K', +} as const + +export const zKnownJwaSignatureAlgorithm = z.enum(recordToUnion(KnownJwaSignatureAlgorithms)) +export type KnownJwaSignatureAlgorithm = z.output + +export function isKnownJwaSignatureAlgorithm(alg: string): alg is KnownJwaSignatureAlgorithm { + return Object.values(KnownJwaSignatureAlgorithms).includes(alg as keyof typeof KnownJwaSignatureAlgorithms) +} + +// Content encryption algorithms ("enc" parameter) +export const KnownJwaContentEncryptionAlgorithms = { + // AES-GCM Content Encryption + A128GCM: 'A128GCM', + A192GCM: 'A192GCM', + A256GCM: 'A256GCM', + + // AES-CBC Content Encryption + A128CBC: 'A128CBC', + A256CBC: 'A256CBC', + + // (X)ChaCha20-Poly1305 + C20P: 'C20P', + XC20P: 'XC20P', + + /** + * As is used in DIDComm v1 + */ + 'XSALSA20-POLY1305': 'XSALSA20-POLY1305', + + A128CBC_HS256: 'A128CBC-HS256', + A192CBC_HS384: 'A192CBC-HS384', + A256CBC_HS512: 'A256CBC-HS512', +} as const +export const zKnownJwaContentEncryptionAlgorithm = z.enum(recordToUnion(KnownJwaContentEncryptionAlgorithms)) +export type KnownJwaContentEncryptionAlgorithm = z.output + +export const KnownJwaKeyEncryptionAlgorithms = { + // AES Key Wrapping + A128KW: 'A128KW', + A192KW: 'A192KW', + A256KW: 'A256KW', +} as const +const zKnownJwaKeyEncryptionAlgorithm = z.enum(recordToUnion(KnownJwaKeyEncryptionAlgorithms)) +export type KnownJwaKeyEncryptionAlgorithm = z.output + +// Key derivation / wrapping algorithms ("alg" parameter) +export const KnownJwaKeyAgreementAlgorithms = { + // ECDH-ES with P-256/P-384/P-521 + ECDH_ES: 'ECDH-ES', + ECDH_ES_A128KW: 'ECDH-ES+A128KW', + ECDH_ES_A192KW: 'ECDH-ES+A192KW', + ECDH_ES_A256KW: 'ECDH-ES+A256KW', + + ECDH_HSALSA20: 'ECDH-HSALSA20', +} as const +const zKnownJwaKeyAgreementAlgorithm = z.enum(recordToUnion(KnownJwaKeyAgreementAlgorithms)) +export type KnownJwaKeyAgreementAlgorithm = z.output diff --git a/packages/core/src/modules/kms/jwk/jwk.ts b/packages/core/src/modules/kms/jwk/jwk.ts new file mode 100644 index 0000000000..8eb2e44005 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/jwk.ts @@ -0,0 +1,63 @@ +import * as z from '../../../utils/zod' + +import { zJwkKeyOps, zJwkUse } from './keyOps' + +export const vJwkCommon = z + .object({ + kty: z.string(), + kid: z.optional(z.string()), + alg: z.optional(z.string()), + + key_ops: z.optional(zJwkKeyOps), + use: z.optional(zJwkUse), + + ext: z.optional(z.boolean()), + + x5c: z.optional(z.array(z.string())), + x5t: z.optional(z.string()), + 'x5t#S256': z.optional(z.string()), + x5u: z.optional(z.string()), + }) + .passthrough() +export type JwkCommon = z.output + +// This can be used to verify the general structure matches +// without verifying any key type specific combinations (just +// that if e.g. x is present it should be a string) +export const vJwk = z + .object({ + ...vJwkCommon.shape, + + // EC/OKP + crv: z.optional(z.string()), + x: z.optional(z.string()), + d: z.optional(z.string()), + + // EC + y: z.optional(z.string()), + + // oct + k: z.optional(z.string()), + + // RSA + e: z.optional(z.string()), + n: z.optional(z.string()), + dp: z.optional(z.string()), + dq: z.optional(z.string()), + oth: z.optional( + z.array( + z + .object({ + d: z.optional(z.string()), + r: z.optional(z.string()), + t: z.optional(z.string()), + }) + .passthrough() + ) + ), + p: z.optional(z.string()), + q: z.optional(z.string()), + qi: z.optional(z.string()), + }) + .passthrough() +export type Jwk = z.output diff --git a/packages/core/src/modules/kms/jwk/keyOps.ts b/packages/core/src/modules/kms/jwk/keyOps.ts new file mode 100644 index 0000000000..41ec8b3d71 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/keyOps.ts @@ -0,0 +1,112 @@ +import type { KmsJwkPrivate, KmsJwkPublic } from './knownJwk' + +import * as z from '../../../utils/zod' +import { KeyManagementError } from '../error/KeyManagementError' + +import { getJwkHumanDescription } from './humanDescription' + +export const zKnownJwkUse = z.union([z.literal('sig').describe('signature'), z.literal('enc').describe('encryption')]) +export type KnownJwkUse = z.output + +export const zJwkUse = z.union([zKnownJwkUse, z.string()]) +export type JwkUse = z.output + +export const zKnownJwkKeyOps = z.union([ + z.literal('sign').describe('compute digital signature or MAC'), + z.literal('verify').describe('verify digital signature or MAC'), + z.literal('encrypt').describe('encrypt content'), + z.literal('decrypt').describe('decrypt content and validate decryption, if applicable'), + z.literal('wrapKey').describe('encrypt key'), + z.literal('unwrapKey').describe('decrypt key and validate decryption, if applicable'), + z.literal('deriveKey').describe('derive key'), + z.literal('deriveBits').describe('derive bits not to be used as a key'), +]) +export type KnownJwkKeyOps = z.output + +export const zJwkKeyOps = z.uniqueArray(z.union([zKnownJwkKeyOps, z.string()])) +export type JwkKeyOps = z.output + +export function keyAllowsDerive(key: KmsJwkPublic | KmsJwkPrivate): boolean { + // Check if key has use/key_ops restrictions + if (key.use && key.use !== 'enc') { + return false + } + if (key.key_ops && !key.key_ops.includes('deriveKey')) { + return false + } + return true +} + +export function assertKeyAllowsDerive(jwk: KmsJwkPrivate | KmsJwkPublic) { + if (!keyAllowsDerive(jwk)) { + throw new KeyManagementError(`${getJwkHumanDescription(jwk)} usage does not allow key derivation operations`) + } +} + +export function keyAllowsVerify(key: KmsJwkPublic | KmsJwkPrivate): boolean { + // Check if key has use/key_ops restrictions + if (key.use && key.use !== 'sig') { + return false + } + if (key.key_ops && !key.key_ops.includes('verify')) { + return false + } + return true +} + +export function assertKeyAllowsVerify(jwk: KmsJwkPrivate | KmsJwkPublic) { + if (!keyAllowsVerify(jwk)) { + throw new KeyManagementError(`${getJwkHumanDescription(jwk)} usage does not allow verification operations`) + } +} + +export function keyAllowsSign(key: KmsJwkPrivate | KmsJwkPublic): boolean { + // Check if key has use/key_ops restrictions + if (key.use && key.use !== 'sig') { + return false + } + if (key.key_ops && !key.key_ops.includes('sign')) { + return false + } + return true +} + +export function assertKeyAllowsSign(jwk: KmsJwkPrivate | KmsJwkPublic) { + if (!keyAllowsSign(jwk)) { + throw new KeyManagementError(`${getJwkHumanDescription(jwk)} usage does not allow signing operations`) + } +} + +export function keyAllowsEncrypt(key: KmsJwkPublic | KmsJwkPrivate): boolean { + // Check if key has use/key_ops restrictions + if (key.use && key.use !== 'enc') { + return false + } + if (key.key_ops && !key.key_ops.includes('encrypt')) { + return false + } + return true +} + +export function assertKeyAllowsEncrypt(jwk: KmsJwkPrivate | KmsJwkPublic) { + if (!keyAllowsEncrypt(jwk)) { + throw new KeyManagementError(`${getJwkHumanDescription(jwk)} usage does not allow encryption operations`) + } +} + +export function keyAllowsDecrypt(key: KmsJwkPublic | KmsJwkPrivate): boolean { + // Check if key has use/key_ops restrictions + if (key.use && key.use !== 'enc') { + return false + } + if (key.key_ops && !key.key_ops.includes('decrypt')) { + return false + } + return true +} + +export function assertKeyAllowsDecrypt(jwk: KmsJwkPrivate | KmsJwkPublic) { + if (!keyAllowsDecrypt(jwk)) { + throw new KeyManagementError(`${getJwkHumanDescription(jwk)} usage does not allow decryption operations`) + } +} diff --git a/packages/core/src/modules/kms/jwk/knownJwk.ts b/packages/core/src/modules/kms/jwk/knownJwk.ts new file mode 100644 index 0000000000..a523791ba0 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/knownJwk.ts @@ -0,0 +1,141 @@ +import * as z from '../../../utils/zod' +import { KeyManagementError } from '../error/KeyManagementError' +import { + KmsCreateKeyType, + KmsCreateKeyTypeEc, + KmsCreateKeyTypeOct, + KmsCreateKeyTypeOkp, + KmsCreateKeyTypeRsa, +} from '../options' + +import { + KmsJwkPrivateEc, + KmsJwkPublicEc, + zKmsJwkPrivateEc, + zKmsJwkPrivateToPublicEc, + zKmsJwkPublicEc, +} from './kty/ec/ecJwk' +import { + KmsJwkPrivateOct, + KmsJwkPublicOct, + zKmsJwkPrivateOct, + zKmsJwkPrivateToPublicOct, + zKmsJwkPublicOct, +} from './kty/oct/octJwk' +import { + KmsJwkPrivateOkp, + KmsJwkPublicOkp, + zKmsJwkPrivateOkp, + zKmsJwkPrivateToPublicOkp, + zKmsJwkPublicOkp, +} from './kty/okp/okpJwk' +import { + KmsJwkPrivateRsa, + KmsJwkPublicRsa, + zKmsJwkPrivateRsa, + zKmsJwkPrivateToPublicRsa, + zKmsJwkPublicRsa, +} from './kty/rsa/rsaJwk' + +export const zKmsJwkPublicAsymmetric = z.discriminatedUnion('kty', [ + zKmsJwkPublicEc, + zKmsJwkPublicRsa, + zKmsJwkPublicOkp, +]) +export type KmsJwkPublicAsymmetric = z.output + +export function isJwkAsymmetric( + jwk: KmsJwkPublic | KmsJwkPrivate +): jwk is KmsJwkPrivateAsymmetric | KmsJwkPublicAsymmetric { + return jwk.kty !== 'oct' +} + +export function assertJwkAsymmetric( + jwk: KmsJwkPublic | KmsJwkPrivate, + keyId?: string +): asserts jwk is KmsJwkPublicAsymmetric | KmsJwkPrivateAsymmetric { + if (!isJwkAsymmetric(jwk)) { + if (keyId) { + throw new KeyManagementError(`Expected jwk with keyId ${keyId} to be an assymetric jwk, but found kty 'oct'`) + } + throw new KeyManagementError("Expected jwk to be an assymetric jwk, but found kty 'oct'") + } +} + +export const zKmsJwkPublicCrv = z.discriminatedUnion('kty', [zKmsJwkPublicEc, zKmsJwkPublicOkp]) +export type KmsJwkPublicCrv = z.output + +export const zKmsJwkPublic = z.discriminatedUnion('kty', [ + zKmsJwkPublicEc, + zKmsJwkPublicRsa, + zKmsJwkPublicOct, + zKmsJwkPublicOkp, +]) +export type KmsJwkPublic = z.output + +const zKmsJwkPrivateToPublic = z + .discriminatedUnion('kty', [ + zKmsJwkPrivateToPublicEc, + zKmsJwkPrivateToPublicRsa, + zKmsJwkPrivateToPublicOct, + zKmsJwkPrivateToPublicOkp, + ]) + // Mdoc library does not work well with undefined values. It should not be needed + // but for now it's the easiest approach + .transform( + (jwk): KmsJwkPublic => + Object.fromEntries(Object.entries(jwk).filter(([, value]) => value !== undefined)) as KmsJwkPublic + ) + +export const zKmsJwkPrivateCrv = z.discriminatedUnion('kty', [zKmsJwkPrivateEc, zKmsJwkPrivateOkp]) +export type KmsJwkPrivateCrv = z.output + +export const zKmsJwkPrivate = z.discriminatedUnion('kty', [ + zKmsJwkPrivateEc, + zKmsJwkPrivateRsa, + zKmsJwkPrivateOct, + zKmsJwkPrivateOkp, +]) +export type KmsJwkPrivate = z.output + +export const zKmsJwkPrivateAsymmetric = z.discriminatedUnion('kty', [ + zKmsJwkPrivateEc, + zKmsJwkPrivateRsa, + zKmsJwkPrivateOkp, +]) +export type KmsJwkPrivateAsymmetric = z.output + +export function publicJwkFromPrivateJwk(privateJwk: KmsJwkPrivate | KmsJwkPublic): KmsJwkPublic { + // This will remove any private properties + return z.parseWithErrorHandling(zKmsJwkPrivateToPublic, privateJwk) +} + +export type KmsJwkPrivateFromKmsJwkPublic = Type extends KmsCreateKeyTypeRsa + ? KmsJwkPrivateRsa + : Type extends KmsCreateKeyTypeOct + ? KmsJwkPrivateOct + : Type extends KmsCreateKeyTypeOkp + ? KmsJwkPrivateOkp & { crv: Type['crv'] } + : Type extends KmsCreateKeyTypeEc + ? KmsJwkPrivateEc & { crv: Type['crv'] } + : KmsJwkPrivate + +export type KmsJwkPublicFromKmsJwkPrivate = Jwk extends KmsJwkPrivateRsa + ? KmsJwkPublicRsa + : Jwk extends KmsJwkPrivateOct + ? KmsJwkPublicOct + : Jwk extends KmsJwkPrivateOkp + ? KmsJwkPublicOkp & { crv: Jwk['crv'] } + : Jwk extends KmsJwkPrivateEc + ? KmsJwkPublicEc & { crv: Jwk['crv'] } + : KmsJwkPublic + +export type KmsJwkPublicFromCreateType = Type extends KmsCreateKeyTypeRsa + ? KmsJwkPublicRsa + : Type extends KmsCreateKeyTypeOct + ? KmsJwkPublicOct + : Type extends KmsCreateKeyTypeOkp + ? KmsJwkPublicOkp & { crv: Type['crv'] } + : Type extends KmsCreateKeyTypeEc + ? KmsJwkPublicEc & { crv: Type['crv'] } + : KmsJwkPublic diff --git a/packages/core/src/modules/kms/jwk/kty/PublicJwk.ts b/packages/core/src/modules/kms/jwk/kty/PublicJwk.ts new file mode 100644 index 0000000000..3f4c510741 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/PublicJwk.ts @@ -0,0 +1,11 @@ +import { KnownJwaKeyAgreementAlgorithm, KnownJwaSignatureAlgorithm } from '../jwa' +import { KmsJwkPublicAsymmetric } from '../knownJwk' + +export interface PublicJwkType { + readonly jwk: Jwk + + supportedSignatureAlgorithms: KnownJwaSignatureAlgorithm[] | undefined + supportdEncryptionKeyAgreementAlgorithms: KnownJwaKeyAgreementAlgorithm[] | undefined + + multicodec: Uint8Array +} diff --git a/packages/core/src/modules/kms/jwk/kty/ec/P256PublicJwk.ts b/packages/core/src/modules/kms/jwk/kty/ec/P256PublicJwk.ts new file mode 100644 index 0000000000..4fef86d537 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/ec/P256PublicJwk.ts @@ -0,0 +1,40 @@ +import { KnownJwaKeyAgreementAlgorithms, KnownJwaSignatureAlgorithm, KnownJwaSignatureAlgorithms } from '../../jwa' +import { PublicJwkType } from '../PublicJwk' +import { KmsJwkPublicEc } from './ecJwk' +import { ecPublicJwkToPublicKey, ecPublicKeyToPublicJwk } from './ecPublicKey' + +type Jwk = KmsJwkPublicEc & { crv: 'P-256' } + +export class P256PublicJwk implements PublicJwkType { + public static supportedSignatureAlgorithms: KnownJwaSignatureAlgorithm[] = [KnownJwaSignatureAlgorithms.ES256] + public static supportdEncryptionKeyAgreementAlgorithms = [KnownJwaKeyAgreementAlgorithms.ECDH_ES] + public static multicodecPrefix = 4608 + + public supportdEncryptionKeyAgreementAlgorithms = P256PublicJwk.supportdEncryptionKeyAgreementAlgorithms + public supportedSignatureAlgorithms = P256PublicJwk.supportedSignatureAlgorithms + public multicodecPrefix = P256PublicJwk.multicodecPrefix + + public constructor(public readonly jwk: Jwk) {} + + public get publicKey() { + return { + crv: this.jwk.crv, + kty: this.jwk.kty, + publicKey: ecPublicJwkToPublicKey(this.jwk), + } + } + + public get multicodec() { + return ecPublicJwkToPublicKey(this.jwk, { compressed: true }) + } + + public static fromPublicKey(publicKey: Uint8Array) { + const jwk = ecPublicKeyToPublicJwk(publicKey, 'P-256') + return new P256PublicJwk(jwk) + } + + public static fromMulticodec(multicodec: Uint8Array) { + const jwk = ecPublicKeyToPublicJwk(multicodec, 'P-256') + return new P256PublicJwk(jwk) + } +} diff --git a/packages/core/src/modules/kms/jwk/kty/ec/P384PublicJwk.ts b/packages/core/src/modules/kms/jwk/kty/ec/P384PublicJwk.ts new file mode 100644 index 0000000000..027bb2884c --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/ec/P384PublicJwk.ts @@ -0,0 +1,40 @@ +import { KnownJwaKeyAgreementAlgorithms, KnownJwaSignatureAlgorithm, KnownJwaSignatureAlgorithms } from '../../jwa' +import { PublicJwkType } from '../PublicJwk' +import { KmsJwkPublicEc } from './ecJwk' +import { ecPublicJwkToPublicKey, ecPublicKeyToPublicJwk } from './ecPublicKey' + +type Jwk = KmsJwkPublicEc & { crv: 'P-384' } + +export class P384PublicJwk implements PublicJwkType { + public static supportedSignatureAlgorithms: KnownJwaSignatureAlgorithm[] = [KnownJwaSignatureAlgorithms.ES384] + public static supportdEncryptionKeyAgreementAlgorithms = [KnownJwaKeyAgreementAlgorithms.ECDH_ES] + public static multicodecPrefix = 4609 + + public supportdEncryptionKeyAgreementAlgorithms = P384PublicJwk.supportdEncryptionKeyAgreementAlgorithms + public supportedSignatureAlgorithms = P384PublicJwk.supportedSignatureAlgorithms + public multicodecPrefix = P384PublicJwk.multicodecPrefix + + public constructor(public readonly jwk: Jwk) {} + + public get publicKey() { + return { + crv: this.jwk.crv, + kty: this.jwk.kty, + publicKey: ecPublicJwkToPublicKey(this.jwk), + } + } + + public get multicodec() { + return ecPublicJwkToPublicKey(this.jwk, { compressed: true }) + } + + public static fromPublicKey(publicKey: Uint8Array) { + const jwk = ecPublicKeyToPublicJwk(publicKey, 'P-384') + return new P384PublicJwk(jwk) + } + + public static fromMulticodec(multicodec: Uint8Array) { + const jwk = ecPublicKeyToPublicJwk(multicodec, 'P-384') + return new P384PublicJwk(jwk) + } +} diff --git a/packages/core/src/modules/kms/jwk/kty/ec/P521PublicJwk.ts b/packages/core/src/modules/kms/jwk/kty/ec/P521PublicJwk.ts new file mode 100644 index 0000000000..f4a8f00228 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/ec/P521PublicJwk.ts @@ -0,0 +1,40 @@ +import { KnownJwaKeyAgreementAlgorithms, KnownJwaSignatureAlgorithm, KnownJwaSignatureAlgorithms } from '../../jwa' +import { PublicJwkType } from '../PublicJwk' +import { KmsJwkPublicEc } from './ecJwk' +import { ecPublicJwkToPublicKey, ecPublicKeyToPublicJwk } from './ecPublicKey' + +type Jwk = KmsJwkPublicEc & { crv: 'P-521' } + +export class P521PublicJwk implements PublicJwkType { + public static supportedSignatureAlgorithms: KnownJwaSignatureAlgorithm[] = [KnownJwaSignatureAlgorithms.ES512] + public static supportdEncryptionKeyAgreementAlgorithms = [KnownJwaKeyAgreementAlgorithms.ECDH_ES] + public static multicodecPrefix = 4610 + + public supportdEncryptionKeyAgreementAlgorithms = P521PublicJwk.supportdEncryptionKeyAgreementAlgorithms + public supportedSignatureAlgorithms = P521PublicJwk.supportedSignatureAlgorithms + public multicodecPrefix = P521PublicJwk.multicodecPrefix + + public constructor(public readonly jwk: Jwk) {} + + public get publicKey() { + return { + crv: this.jwk.crv, + kty: this.jwk.kty, + publicKey: ecPublicJwkToPublicKey(this.jwk), + } + } + + public get multicodec() { + return ecPublicJwkToPublicKey(this.jwk, { compressed: true }) + } + + public static fromPublicKey(publicKey: Uint8Array) { + const jwk = ecPublicKeyToPublicJwk(publicKey, 'P-521') + return new P521PublicJwk(jwk) + } + + public static fromMulticodec(multicodec: Uint8Array) { + const jwk = ecPublicKeyToPublicJwk(multicodec, 'P-521') + return new P521PublicJwk(jwk) + } +} diff --git a/packages/core/src/modules/kms/jwk/kty/ec/Secp256k1PublicJwk.ts b/packages/core/src/modules/kms/jwk/kty/ec/Secp256k1PublicJwk.ts new file mode 100644 index 0000000000..8b6469d7c2 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/ec/Secp256k1PublicJwk.ts @@ -0,0 +1,40 @@ +import { KnownJwaKeyAgreementAlgorithms, KnownJwaSignatureAlgorithm, KnownJwaSignatureAlgorithms } from '../../jwa' +import { PublicJwkType } from '../PublicJwk' +import { KmsJwkPublicEc } from './ecJwk' +import { ecPublicJwkToPublicKey, ecPublicKeyToPublicJwk } from './ecPublicKey' + +type Jwk = KmsJwkPublicEc & { crv: 'secp256k1' } + +export class Secp256k1PublicJwk implements PublicJwkType { + public static supportedSignatureAlgorithms: KnownJwaSignatureAlgorithm[] = [KnownJwaSignatureAlgorithms.ES256K] + public static supportdEncryptionKeyAgreementAlgorithms = [KnownJwaKeyAgreementAlgorithms.ECDH_ES] + public static multicodecPrefix = 231 + + public supportdEncryptionKeyAgreementAlgorithms = Secp256k1PublicJwk.supportdEncryptionKeyAgreementAlgorithms + public supportedSignatureAlgorithms = Secp256k1PublicJwk.supportedSignatureAlgorithms + public multicodecPrefix = Secp256k1PublicJwk.multicodecPrefix + + public constructor(public readonly jwk: Jwk) {} + + public get publicKey() { + return { + crv: this.jwk.crv, + kty: this.jwk.kty, + publicKey: ecPublicJwkToPublicKey(this.jwk), + } + } + + public get multicodec() { + return ecPublicJwkToPublicKey(this.jwk, { compressed: true }) + } + + public static fromPublicKey(publicKey: Uint8Array) { + const jwk = ecPublicKeyToPublicJwk(publicKey, 'secp256k1') + return new Secp256k1PublicJwk(jwk) + } + + public static fromMulticodec(multicodec: Uint8Array) { + const jwk = ecPublicKeyToPublicJwk(multicodec, 'secp256k1') + return new Secp256k1PublicJwk(jwk) + } +} diff --git a/packages/core/src/modules/kms/jwk/kty/ec/ecJwk.ts b/packages/core/src/modules/kms/jwk/kty/ec/ecJwk.ts new file mode 100644 index 0000000000..6057789a50 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/ec/ecJwk.ts @@ -0,0 +1,29 @@ +import * as z from '../../../../../utils/zod' +import { vJwkCommon } from '../../jwk' + +export const zKmsJwkPublicEc = z.object({ + ...vJwkCommon.shape, + kty: z.literal('EC'), + crv: z.enum(['P-256', 'P-384', 'P-521', 'secp256k1']), + + // Public + x: z.base64Url, // Public key x-coordinate + y: z.base64Url, // Public key y-coordinate + + // Private + d: z.optional(z.undefined()), +}) +export type KmsJwkPublicEc = z.output + +export const zKmsJwkPrivateToPublicEc = z.object({ + ...zKmsJwkPublicEc.shape, + d: z.optionalToUndefined(z.base64Url), +}) + +export const zKmsJwkPrivateEc = z.object({ + ...zKmsJwkPublicEc.shape, + + // Private + d: z.base64Url, +}) +export type KmsJwkPrivateEc = z.output diff --git a/packages/core/src/modules/kms/jwk/kty/ec/ecPublicKey.ts b/packages/core/src/modules/kms/jwk/kty/ec/ecPublicKey.ts new file mode 100644 index 0000000000..6d85c5ecf3 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/ec/ecPublicKey.ts @@ -0,0 +1,62 @@ +import { + AffinePoint, + CurveParams, + Secp256k1, + Secp256r1, + Secp384r1, + Secp521r1, + isValidCompressedPublicKeyFormat, + isValidDecompressedPublicKeyFormat, +} from 'ec-compression' +import { TypedArrayEncoder } from '../../../../../utils' +import { KeyManagementError } from '../../../error/KeyManagementError' +import { KmsJwkPublicEc } from './ecJwk' + +// CurveParams for ec-compression lib +export const ecCrvToCurveParams: Record = { + 'P-256': Secp256r1, + 'P-384': Secp384r1, + 'P-521': Secp521r1, + secp256k1: Secp256k1, +} + +export function ecPublicJwkToPublicKey( + publicJwk: KmsJwkPublicEc, + { compressed = false }: { compressed?: boolean } = {} +): Uint8Array { + const xAsBytes = Uint8Array.from(TypedArrayEncoder.fromBase64(publicJwk.x)) + const yAsBytes = Uint8Array.from(TypedArrayEncoder.fromBase64(publicJwk.y)) + + const affinePoint = new AffinePoint(xAsBytes, yAsBytes) + + return compressed ? affinePoint.compressedForm : affinePoint.decompressedForm +} + +export function ecPublicKeyToPublicJwk(publicKey: Uint8Array, crv: Crv) { + const curveParams = ecCrvToCurveParams[crv] + + if (!curveParams) { + throw new KeyManagementError(`kty EC with crv '${crv}' is not supported for creating jwk based on public key bytes`) + } + + let affinePoint: AffinePoint + + if (isValidCompressedPublicKeyFormat(publicKey, curveParams)) { + affinePoint = AffinePoint.fromCompressedPoint(publicKey, curveParams) + } else if (isValidDecompressedPublicKeyFormat(publicKey, curveParams)) { + affinePoint = AffinePoint.fromDecompressedPoint(publicKey, curveParams) + } else { + throw new KeyManagementError( + `public key for kty EC with crv '${crv}' is neither a valid compressed or uncompressed key. Key prefix '${publicKey[0]}', key length '${publicKey.length}'` + ) + } + + const jwk = { + kty: 'EC', + crv, + x: TypedArrayEncoder.toBase64URL(affinePoint.xBytes), + y: TypedArrayEncoder.toBase64URL(affinePoint.yBytes), + } satisfies KmsJwkPublicEc & { crv: Crv } + + return jwk +} diff --git a/packages/core/src/modules/kms/jwk/kty/ec/ecSignature.ts b/packages/core/src/modules/kms/jwk/kty/ec/ecSignature.ts new file mode 100644 index 0000000000..d92922c289 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/ec/ecSignature.ts @@ -0,0 +1,126 @@ +import { ECDSASigValue } from '@peculiar/asn1-ecc' +import { AsnConvert } from '@peculiar/asn1-schema' +import { KeyManagementError } from '../../../error/KeyManagementError' +import { KmsJwkPublicEc } from './ecJwk' +import { ecCrvToCurveParams } from './ecPublicKey' + +/** + * Converts a RAW EC signature to DER format + * + * @param rawSignature - Raw signature as r || s concatenated values + * @param crv - The EC crv of the key used for the signature + * @returns DER encoded signature + */ +export function rawEcSignatureToDer(rawSignature: Uint8Array, crv: KmsJwkPublicEc['crv']): Uint8Array { + const pointBitLength = ecCrvToCurveParams[crv].pointBitLength + const pointByteLength = Math.ceil(pointBitLength / 8) + + if (rawSignature.length !== pointByteLength * 2) { + throw new KeyManagementError( + `Invalid raw signature length for EC signature conversion. Expected ${pointByteLength * 2} bytes for crv ${crv}` + ) + } + + // Extract r and s values from the raw signature + const r = rawSignature.slice(0, pointByteLength) + const s = rawSignature.slice(pointByteLength) + + // Remove leading zeros that aren't necessary for ASN.1 encoding + const rValue = removeLeadingZeros(r) + const sValue = removeLeadingZeros(s) + + // Create the EcDsaSignature object + const signature = new ECDSASigValue() + signature.r = new Uint8Array(ensurePositive(rValue)) + signature.s = new Uint8Array(ensurePositive(sValue)) + + // Convert to DER + return new Uint8Array(AsnConvert.serialize(signature)) +} + +/** + * Converts a DER encoded EC signature to RAW format + * + * @param derSignature - DER encoded signature + * @param crv - The EC crv of the key used for the signature + * @returns Raw signature as r || s concatenated values + */ +export function derEcSignatureToRaw(derSignature: Uint8Array, crv: KmsJwkPublicEc['crv']): Uint8Array { + // Parse DER signature + const asn = AsnConvert.parse(derSignature, ECDSASigValue) + + const pointBitLength = ecCrvToCurveParams[crv].pointBitLength + const pointByteLength = Math.ceil(pointBitLength / 8) + + // Ensure r and s are padded to the correct point size + const rPadded = padToLength(new Uint8Array(asn.r), pointByteLength) + const sPadded = padToLength(new Uint8Array(asn.s), pointByteLength) + + // Concatenate to form raw signature + const rawSignature = new Uint8Array(pointByteLength * 2) + rawSignature.set(rPadded, 0) + rawSignature.set(sPadded, pointByteLength) + + return rawSignature +} + +/** + * Helper function to remove unnecessary leading zeros from an integer representation + * + * @param data - The integer bytes + * @returns - Data with leading zeros removed + */ +function removeLeadingZeros(data: Uint8Array): Uint8Array { + let startIndex = 0 + while (startIndex < data.length - 1 && data[startIndex] === 0) { + startIndex++ + } + + return data.slice(startIndex) +} + +/** + * Ensures an integer value is represented as positive in ASN.1 by + * adding a leading zero if the high bit is set + * + * @param data - The integer bytes + * @returns Data ensuring positive integer representation + */ +function ensurePositive(data: Uint8Array): Uint8Array { + // If high bit is set, prepend a zero byte to ensure it's treated as positive + if (data.length > 0 && (data[0] & 0x80) !== 0) { + const result = new Uint8Array(data.length + 1) + result.set(data, 1) + return result + } + return data +} + +/** + * Pads an integer value to the specified length + * + * @param data - The integer bytes + * @param targetLength - The desired length + * @returns Padded data + */ +function padToLength(data: Uint8Array, targetLength: number) { + if (data.length === targetLength) { + return data + } + + if (data.length > targetLength) { + // If the value is larger, ensure we're not losing significant bytes + const significantStart = data.length - targetLength + for (let i = 0; i < significantStart; i++) { + if (data[i] !== 0) { + throw new KeyManagementError('Value too large for the specified point size') + } + } + return data.slice(significantStart) + } + + // Pad with leading zeros + const result = new Uint8Array(targetLength) + result.set(data, targetLength - data.length) + return result +} diff --git a/packages/core/src/modules/kms/jwk/kty/index.ts b/packages/core/src/modules/kms/jwk/kty/index.ts new file mode 100644 index 0000000000..39b494d160 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/index.ts @@ -0,0 +1,42 @@ +export { + zKmsJwkPrivateEc, + zKmsJwkPrivateToPublicEc, + zKmsJwkPublicEc, + type KmsJwkPrivateEc, + type KmsJwkPublicEc, +} from './ec/ecJwk' +export { ecPublicJwkToPublicKey } from './ec/ecPublicKey' +export { P256PublicJwk } from './ec/P256PublicJwk' +export { P384PublicJwk } from './ec/P384PublicJwk' +export { P521PublicJwk } from './ec/P521PublicJwk' +export { Secp256k1PublicJwk } from './ec/Secp256k1PublicJwk' +export { derEcSignatureToRaw, rawEcSignatureToDer } from './ec/ecSignature' + +export { + zKmsJwkPrivateOct, + zKmsJwkPrivateToPublicOct, + zKmsJwkPublicOct, + type KmsJwkPrivateOct, + type KmsJwkPublicOct, +} from './oct/octJwk' + +export { + zKmsJwkPrivateOkp, + zKmsJwkPrivateToPublicOkp, + zKmsJwkPublicOkp, + type KmsJwkPrivateOkp, + type KmsJwkPublicOkp, +} from './okp/okpJwk' +export { okpPublicJwkToPublicKey } from './okp/okpPublicKey' +export { Ed25519PublicJwk } from './okp/Ed25519PublicJwk' +export { X25519PublicJwk } from './okp/X25519PublicJwk' + +export { + zKmsJwkPrivateRsa, + zKmsJwkPrivateToPublicRsa, + zKmsJwkPublicRsa, + type KmsJwkPrivateRsa, + type KmsJwkPublicRsa, +} from './rsa/rsaJwk' +export { rsaPublicJwkToPublicKey } from './rsa/rsaPublicKey' +export { RsaPublicJwk } from './rsa/RsaPublicJwk' diff --git a/packages/core/src/modules/kms/jwk/kty/oct/octJwk.ts b/packages/core/src/modules/kms/jwk/kty/oct/octJwk.ts new file mode 100644 index 0000000000..a92988e563 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/oct/octJwk.ts @@ -0,0 +1,25 @@ +import * as z from '../../../../../utils/zod' +import { vJwkCommon } from '../../jwk' + +export const zKmsJwkPublicOct = z.object({ + ...vJwkCommon.shape, + kty: z.literal('oct'), + + // Private + k: z.optional(z.undefined()), // Key +}) +export type KmsJwkPublicOct = z.output + +export const zKmsJwkPrivateToPublicOct = z.object({ + ...zKmsJwkPublicOct.shape, + + k: z.optionalToUndefined(z.base64Url), // Key +}) + +export const zKmsJwkPrivateOct = z.object({ + ...zKmsJwkPublicOct.shape, + + // Private + k: z.base64Url, // Key +}) +export type KmsJwkPrivateOct = z.output diff --git a/packages/core/src/modules/kms/jwk/kty/okp/Ed25519PublicJwk.ts b/packages/core/src/modules/kms/jwk/kty/okp/Ed25519PublicJwk.ts new file mode 100644 index 0000000000..35180cf3fd --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/okp/Ed25519PublicJwk.ts @@ -0,0 +1,46 @@ +import { convertPublicKeyToX25519 } from '@stablelib/ed25519' +import { KnownJwaKeyAgreementAlgorithm, KnownJwaSignatureAlgorithm, KnownJwaSignatureAlgorithms } from '../../jwa' +import { PublicJwkType } from '../PublicJwk' +import { X25519PublicJwk } from './X25519PublicJwk' +import { KmsJwkPublicOkp } from './okpJwk' +import { okpPublicJwkToPublicKey, okpPublicKeyToPublicJwk } from './okpPublicKey' + +type Jwk = KmsJwkPublicOkp & { crv: 'Ed25519' } + +export class Ed25519PublicJwk implements PublicJwkType { + public static supportedSignatureAlgorithms: KnownJwaSignatureAlgorithm[] = [KnownJwaSignatureAlgorithms.EdDSA] + public static supportdEncryptionKeyAgreementAlgorithms: KnownJwaKeyAgreementAlgorithm[] = [] + public static multicodecPrefix = 237 + + public supportdEncryptionKeyAgreementAlgorithms = Ed25519PublicJwk.supportdEncryptionKeyAgreementAlgorithms + public supportedSignatureAlgorithms = Ed25519PublicJwk.supportedSignatureAlgorithms + public multicodecPrefix = Ed25519PublicJwk.multicodecPrefix + + public constructor(public readonly jwk: Jwk) {} + + public get publicKey() { + return { + crv: this.jwk.crv, + kty: this.jwk.kty, + publicKey: okpPublicJwkToPublicKey(this.jwk), + } + } + + public get multicodec() { + return okpPublicJwkToPublicKey(this.jwk) + } + + public static fromPublicKey(publicKey: Uint8Array) { + const jwk = okpPublicKeyToPublicJwk(publicKey, 'Ed25519') + return new Ed25519PublicJwk(jwk) + } + + public static fromMulticodec(multicodec: Uint8Array) { + const jwk = okpPublicKeyToPublicJwk(multicodec, 'Ed25519') + return new Ed25519PublicJwk(jwk) + } + + public toX25519PublicJwk() { + return X25519PublicJwk.fromPublicKey(convertPublicKeyToX25519(this.publicKey.publicKey)).jwk + } +} diff --git a/packages/core/src/modules/kms/jwk/kty/okp/X25519PublicJwk.ts b/packages/core/src/modules/kms/jwk/kty/okp/X25519PublicJwk.ts new file mode 100644 index 0000000000..855e66c7d4 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/okp/X25519PublicJwk.ts @@ -0,0 +1,40 @@ +import { KnownJwaKeyAgreementAlgorithms, KnownJwaSignatureAlgorithm } from '../../jwa' +import { PublicJwkType } from '../PublicJwk' +import { KmsJwkPublicOkp } from './okpJwk' +import { okpPublicJwkToPublicKey, okpPublicKeyToPublicJwk } from './okpPublicKey' + +type Jwk = KmsJwkPublicOkp & { crv: 'X25519' } + +export class X25519PublicJwk implements PublicJwkType { + public static supportdEncryptionKeyAgreementAlgorithms = [KnownJwaKeyAgreementAlgorithms.ECDH_HSALSA20] + public static supportedSignatureAlgorithms: KnownJwaSignatureAlgorithm[] = [] + public static multicodecPrefix = 236 + + public supportdEncryptionKeyAgreementAlgorithms = X25519PublicJwk.supportdEncryptionKeyAgreementAlgorithms + public supportedSignatureAlgorithms = X25519PublicJwk.supportedSignatureAlgorithms + public multicodecPrefix = X25519PublicJwk.multicodecPrefix + + public constructor(public readonly jwk: Jwk) {} + + public get publicKey() { + return { + crv: this.jwk.crv, + kty: this.jwk.kty, + publicKey: okpPublicJwkToPublicKey(this.jwk), + } + } + + public get multicodec() { + return okpPublicJwkToPublicKey(this.jwk) + } + + public static fromPublicKey(publicKey: Uint8Array) { + const jwk = okpPublicKeyToPublicJwk(publicKey, 'X25519') + return new X25519PublicJwk(jwk) + } + + public static fromMulticodec(multicodec: Uint8Array) { + const jwk = okpPublicKeyToPublicJwk(multicodec, 'X25519') + return new X25519PublicJwk(jwk) + } +} diff --git a/packages/core/src/modules/kms/jwk/kty/okp/__tests__/Ed25519PublicJwk.test.ts b/packages/core/src/modules/kms/jwk/kty/okp/__tests__/Ed25519PublicJwk.test.ts new file mode 100644 index 0000000000..c121c99c2e --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/okp/__tests__/Ed25519PublicJwk.test.ts @@ -0,0 +1,40 @@ +import { TypedArrayEncoder } from '@credo-ts/core' +import { PublicJwk } from '../../../PublicJwk' +import { Ed25519PublicJwk } from '../Ed25519PublicJwk' + +const TEST_ED25519_BASE58_KEY = '8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K' +const TEST_ED25519_FINGERPRINT = 'z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th' + +describe('Ed25519PublicJwk', () => { + it('creates an Ed25519PublicJwk instance from public key bytes and ed25519 key type', async () => { + const publicJwk = PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58(TEST_ED25519_BASE58_KEY), + }) + + expect(publicJwk.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + }) + + it('creates a Ed25519PublicJwk instance from a fingerprint', async () => { + const publicJwk = PublicJwk.fromFingerprint(TEST_ED25519_FINGERPRINT) + + expect(publicJwk.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + }) + + it('should correctly calculate the getter properties', async () => { + const publicJwk = PublicJwk.fromFingerprint(TEST_ED25519_FINGERPRINT) as PublicJwk + + expect(publicJwk.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + expect(publicJwk.publicKey).toEqual({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: Uint8Array.from(TypedArrayEncoder.fromBase58(TEST_ED25519_BASE58_KEY)), + }) + expect(publicJwk.toJson()).toEqual({ + kty: 'OKP', + crv: 'Ed25519', + x: TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromBase58(TEST_ED25519_BASE58_KEY)), + }) + }) +}) diff --git a/packages/core/src/modules/kms/jwk/kty/okp/__tests__/X25519PublicJwk.test.ts b/packages/core/src/modules/kms/jwk/kty/okp/__tests__/X25519PublicJwk.test.ts new file mode 100644 index 0000000000..6296700d7f --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/okp/__tests__/X25519PublicJwk.test.ts @@ -0,0 +1,40 @@ +import { TypedArrayEncoder } from '@credo-ts/core' +import { PublicJwk } from '../../../PublicJwk' +import { X25519PublicJwk } from '../X25519PublicJwk' + +const TEST_X25519_BASE58_KEY = '6fUMuABnqSDsaGKojbUF3P7ZkEL3wi2njsDdUWZGNgCU' +const TEST_X25519_FINGERPRINT = 'z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE' + +describe('X25519PublicJwk', () => { + it('creates an X25519PublicJwk instance from public key bytes and x25519 key type', async () => { + const publicJwk = PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'X25519', + publicKey: TypedArrayEncoder.fromBase58(TEST_X25519_BASE58_KEY), + }) + + expect(publicJwk.fingerprint).toBe(TEST_X25519_FINGERPRINT) + }) + + it('creates a X25519PublicJwk instance from a fingerprint', async () => { + const publicJwk = PublicJwk.fromFingerprint(TEST_X25519_FINGERPRINT) + + expect(publicJwk.fingerprint).toBe(TEST_X25519_FINGERPRINT) + }) + + it('should correctly calculate the getter properties', async () => { + const publicJwk = PublicJwk.fromFingerprint(TEST_X25519_FINGERPRINT) as PublicJwk + + expect(publicJwk.fingerprint).toBe(TEST_X25519_FINGERPRINT) + expect(publicJwk.publicKey).toEqual({ + kty: 'OKP', + crv: 'X25519', + publicKey: Uint8Array.from(TypedArrayEncoder.fromBase58(TEST_X25519_BASE58_KEY)), + }) + expect(publicJwk.toJson()).toEqual({ + kty: 'OKP', + crv: 'X25519', + x: TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromBase58(TEST_X25519_BASE58_KEY)), + }) + }) +}) diff --git a/packages/core/src/modules/kms/jwk/kty/okp/okpJwk.ts b/packages/core/src/modules/kms/jwk/kty/okp/okpJwk.ts new file mode 100644 index 0000000000..ba2fd8aab5 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/okp/okpJwk.ts @@ -0,0 +1,30 @@ +import * as z from '../../../../../utils/zod' +import { vJwkCommon } from '../../jwk' + +// TODO: we should probably create a separate Jwk type for each crv, so we +// can use the type in Credo if we need a specific key +export const zKmsJwkPublicOkp = z.object({ + ...vJwkCommon.shape, + kty: z.literal('OKP'), + crv: z.enum(['X25519', 'Ed25519']), + + // Public + x: z.base64Url, + + // Private + d: z.optional(z.base64Url), +}) +export type KmsJwkPublicOkp = z.output + +export const zKmsJwkPrivateToPublicOkp = z.object({ + ...zKmsJwkPublicOkp.shape, + d: z.optionalToUndefined(z.base64Url), +}) + +export const zKmsJwkPrivateOkp = z.object({ + ...zKmsJwkPublicOkp.shape, + + // Private + d: z.base64Url, +}) +export type KmsJwkPrivateOkp = z.output diff --git a/packages/core/src/modules/kms/jwk/kty/okp/okpPublicKey.ts b/packages/core/src/modules/kms/jwk/kty/okp/okpPublicKey.ts new file mode 100644 index 0000000000..0dc27dd562 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/okp/okpPublicKey.ts @@ -0,0 +1,18 @@ +import { TypedArrayEncoder } from '../../../../../utils' +import { KmsJwkPublicOkp } from './okpJwk' + +export function okpPublicJwkToPublicKey(publicJwk: KmsJwkPublicOkp): Uint8Array { + const publicKey = Uint8Array.from(TypedArrayEncoder.fromBase64(publicJwk.x)) + + return publicKey +} + +export function okpPublicKeyToPublicJwk(publicKey: Uint8Array, crv: Curve) { + const jwk = { + kty: 'OKP', + crv, + x: TypedArrayEncoder.toBase64URL(publicKey), + } satisfies KmsJwkPublicOkp & { crv: Curve } + + return jwk +} diff --git a/packages/core/src/modules/kms/jwk/kty/rsa/RsaPublicJwk.ts b/packages/core/src/modules/kms/jwk/kty/rsa/RsaPublicJwk.ts new file mode 100644 index 0000000000..94e58a9372 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/rsa/RsaPublicJwk.ts @@ -0,0 +1,54 @@ +import { TypedArrayEncoder } from '../../../../../utils' +import { KeyManagementError } from '../../../error/KeyManagementError' +import { KnownJwaKeyAgreementAlgorithm, KnownJwaSignatureAlgorithm } from '../../jwa' +import { PublicJwkType } from '../PublicJwk' +import { KmsJwkPublicRsa } from './rsaJwk' +import { rsaPublicJwkToPublicKey, rsaPublicKeyToPublicJwk } from './rsaPublicKey' + +export class RsaPublicJwk implements PublicJwkType { + public static supportdEncryptionKeyAgreementAlgorithms: KnownJwaKeyAgreementAlgorithm[] = [] + public static supportedSignatureAlgorithms: KnownJwaSignatureAlgorithm[] = [ + 'PS256', + 'RS256', + 'RS384', + 'PS384', + 'RS512', + 'PS512', + ] + public static multicodecPrefix = 4613 + + public multicodecPrefix = RsaPublicJwk.multicodecPrefix + public supportdEncryptionKeyAgreementAlgorithms = RsaPublicJwk.supportdEncryptionKeyAgreementAlgorithms + + public get supportedSignatureAlgorithms() { + const keyBits = TypedArrayEncoder.fromBase64(this.jwk.n).length * 8 + + // RSA needs minimum bit lengths for each algorithm + const minBits2048: KnownJwaSignatureAlgorithm[] = ['PS256', 'RS256'] + const minBits3072: KnownJwaSignatureAlgorithm[] = [...minBits2048, 'RS384', 'PS384'] + const minBits4096: KnownJwaSignatureAlgorithm[] = [...minBits3072, 'RS512', 'PS512'] + + return keyBits >= 4096 ? minBits4096 : keyBits >= 3072 ? minBits3072 : keyBits >= 2048 ? minBits2048 : [] + } + + public constructor(public readonly jwk: KmsJwkPublicRsa) {} + + public get publicKey() { + return { + kty: this.jwk.kty, + ...rsaPublicJwkToPublicKey(this.jwk), + } + } + + public get multicodec(): Uint8Array { + throw new KeyManagementError('multicodec not supported for RsaPublicJwk') + } + + public static fromPublicKey(publicKey: { modulus: Uint8Array; exponent: Uint8Array }) { + return new RsaPublicJwk(rsaPublicKeyToPublicJwk(publicKey)) + } + + public static fromMulticodec(_multicodec: Uint8Array): RsaPublicJwk { + throw new KeyManagementError('fromMulticodec not supported for RsaPublicJwk') + } +} diff --git a/packages/core/src/modules/kms/jwk/kty/rsa/rsaJwk.ts b/packages/core/src/modules/kms/jwk/kty/rsa/rsaJwk.ts new file mode 100644 index 0000000000..b462f61773 --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/rsa/rsaJwk.ts @@ -0,0 +1,57 @@ +import * as z from '../../../../../utils/zod' +import { vJwkCommon } from '../../jwk' + +const zKmsJwkPrivateRsaOth = z.array( + z + .object({ + d: z.optional(z.base64Url), + r: z.optional(z.base64Url), + t: z.optional(z.base64Url), + }) + .passthrough() +) + +export const zKmsJwkPublicRsa = z.object({ + ...vJwkCommon.shape, + kty: z.literal('RSA'), + + // Public + n: z.base64Url, // Modulus + e: z.base64Url, // Public exponent + + // Private + d: z.optional(z.undefined()), // Private exponent + p: z.optional(z.undefined()), // First prime factor + q: z.optional(z.undefined()), // Second prime factor + dp: z.optional(z.undefined()), // First factor CRT exponent + dq: z.optional(z.undefined()), // Second factor CRT exponent + qi: z.optional(z.undefined()), // First CRT coefficient + oth: z.optional(z.undefined()), +}) +export type KmsJwkPublicRsa = z.output + +export const zKmsJwkPrivateToPublicRsa = z.object({ + ...zKmsJwkPublicRsa.shape, + + d: z.optionalToUndefined(z.base64Url), // Private exponent + p: z.optionalToUndefined(z.base64Url), // First prime factor + q: z.optionalToUndefined(z.base64Url), // Second prime factor + dp: z.optionalToUndefined(z.base64Url), // First factor CRT exponent + dq: z.optionalToUndefined(z.base64Url), // Second factor CRT exponent + qi: z.optionalToUndefined(z.base64Url), // First CRT coefficient + oth: z.optionalToUndefined(zKmsJwkPrivateRsaOth), +}) + +export const zKmsJwkPrivateRsa = z.object({ + ...zKmsJwkPublicRsa.shape, + + // Private + d: z.base64Url, // Private exponent + p: z.base64Url, // First prime factor + q: z.base64Url, // Second prime factor + dp: z.base64Url, // First factor CRT exponent + dq: z.base64Url, // Second factor CRT exponent + qi: z.base64Url, // First CRT coefficient + oth: z.optional(zKmsJwkPrivateRsaOth), +}) +export type KmsJwkPrivateRsa = z.output diff --git a/packages/core/src/modules/kms/jwk/kty/rsa/rsaPublicKey.ts b/packages/core/src/modules/kms/jwk/kty/rsa/rsaPublicKey.ts new file mode 100644 index 0000000000..b232f32dab --- /dev/null +++ b/packages/core/src/modules/kms/jwk/kty/rsa/rsaPublicKey.ts @@ -0,0 +1,25 @@ +import { TypedArrayEncoder } from '../../../../../utils' +import { KmsJwkPublicRsa } from './rsaJwk' + +export function rsaPublicJwkToPublicKey(publicJwk: KmsJwkPublicRsa) { + const modulus = Uint8Array.from(TypedArrayEncoder.fromBase64(publicJwk.n)) + const exponent = Uint8Array.from(TypedArrayEncoder.fromBase64(publicJwk.e)) + + return { + modulus, + exponent, + } +} + +export function rsaPublicKeyToPublicJwk(options: { + modulus: Uint8Array + exponent: Uint8Array +}): KmsJwkPublicRsa { + const jwk: KmsJwkPublicRsa = { + kty: 'RSA', + n: TypedArrayEncoder.toBase64URL(options.modulus), + e: TypedArrayEncoder.toBase64URL(options.exponent), + } + + return jwk +} diff --git a/packages/core/src/modules/kms/legacy.ts b/packages/core/src/modules/kms/legacy.ts new file mode 100644 index 0000000000..b37acbdb1e --- /dev/null +++ b/packages/core/src/modules/kms/legacy.ts @@ -0,0 +1,19 @@ +import { TypedArrayEncoder } from '../../utils' +import { KeyManagementError } from './error/KeyManagementError' +import { PublicJwk } from './jwk' + +/** + * Returns the legacy key id based on the public key encoded as base58 + * + * This is what was has been used by askar + */ +export function legacyKeyIdFromPublicJwk(publicJwk: PublicJwk) { + const publicKey = publicJwk.publicKey + if (publicKey.kty === 'RSA') { + throw new KeyManagementError( + 'Unable to derive legacy key id from RSA key. Support for RSA keys was only added after explit key ids were added.' + ) + } + + return TypedArrayEncoder.toBase58(publicKey.publicKey) +} diff --git a/packages/core/src/modules/kms/options/KmsCreateKeyOptions.ts b/packages/core/src/modules/kms/options/KmsCreateKeyOptions.ts new file mode 100644 index 0000000000..1093e59b30 --- /dev/null +++ b/packages/core/src/modules/kms/options/KmsCreateKeyOptions.ts @@ -0,0 +1,119 @@ +import type { KmsJwkPublicFromCreateType } from '../jwk/knownJwk' + +import * as z from '../../../utils/zod' +import { KnownJwaSignatureAlgorithm } from '../jwk' +import { zKnownJwaSignatureAlgorithm } from '../jwk/jwa' +import { zKmsJwkPublicEc } from '../jwk/kty/ec/ecJwk' +import { zKmsJwkPublicOct } from '../jwk/kty/oct/octJwk' +import { zKmsJwkPublicOkp } from '../jwk/kty/okp/okpJwk' +import { zKmsJwkPublicRsa } from '../jwk/kty/rsa/rsaJwk' +import { zKmsKeyId } from './common' + +const zKmsCreateKeyTypeEc = zKmsJwkPublicEc.pick({ kty: true, crv: true }) +export type KmsCreateKeyTypeEc = z.output + +/** + * Octer key pair, commonly used for Ed25519 and X25519 key types + */ +const zKmsCreateKeyTypeOkp = zKmsJwkPublicOkp.pick({ kty: true, crv: true }) +export type KmsCreateKeyTypeOkp = z.output + +/** + * RSA key pair. + */ +const zKmsCreateKeyTypeRsa = zKmsJwkPublicRsa.pick({ kty: true }).extend({ + modulusLength: z.union([z.literal(2048), z.literal(3072), z.literal(4096)]), +}) +export type KmsCreateKeyTypeRsa = z.output + +/** + * Represents an octect sequence for symmetric keys + */ +export const zKmsCreateKeyTypeOct = z.discriminatedUnion('algorithm', [ + z.object({ + kty: zKmsJwkPublicOct.shape.kty, + algorithm: z.literal('aes'), + length: z.union([ + z.literal(128), + z.literal(192), + z.literal(256), + z + .number() + .int() + .refine((length) => length % 8 === 0, 'aes key length must be multiple of 8'), + ]), + }), + z.object({ + kty: zKmsJwkPublicOct.shape.kty, + algorithm: z.literal('hmac').describe('For usage with HS256, HS384 and HS512'), + length: z.union([z.literal(256), z.literal(384), z.literal(512)]), + }), + z.object({ + kty: zKmsJwkPublicOct.shape.kty, + + /** + * For usage with ChaCha20-Poly1305 and XChaCha20-Poly1305 + */ + algorithm: z.literal('C20P').describe('For usage with ChaCha20-Poly1305 and XChaCha20-Poly1305'), + }), +]) +export type KmsCreateKeyTypeOct = z.output + +export const zKmsCreateKeyTypeAssymetric = z.union([zKmsCreateKeyTypeEc, zKmsCreateKeyTypeOkp, zKmsCreateKeyTypeRsa]) +export type KmsCreateKeyTypeAssymetric = z.output + +// TOOD: see if we can use nested discriminated union with zod? +export const zKmsCreateKeyType = z.union([ + zKmsCreateKeyTypeEc, + zKmsCreateKeyTypeOkp, + zKmsCreateKeyTypeRsa, + zKmsCreateKeyTypeOct, +]) +export type KmsCreateKeyType = z.output + +export const zKmsCreateKeyOptions = z.object({ + keyId: z.optional(zKmsKeyId), + type: zKmsCreateKeyType, +}) + +export interface KmsCreateKeyOptions { + /** + * The `kid` for the key. + */ + keyId?: string + + /** + * The type of key to generate + */ + type: Type +} + +export const zKmsCreateKeyForSignatureAlgorithmOptions = z.object({ + keyId: z.optional(zKmsKeyId), + algorithm: zKnownJwaSignatureAlgorithm, +}) + +export interface KmsCreateKeyForSignatureAlgorithmOptions { + /** + * The `kid` for the key. + */ + keyId?: string + + /** + * The JWA signature algorithm to create the key for. + */ + algorithm: KnownJwaSignatureAlgorithm +} + +export interface KmsCreateKeyReturn { + keyId: string + + /** + * The public JWK representation of the created key. `kid` will always + * be defined. + * + * In case of a symmetric (oct) key this won't include any key material, but + * will include additional JWK claims such as `use`, `kty`, and `kid` + */ + publicJwk: KmsJwkPublicFromCreateType & { kid: string } +} diff --git a/packages/core/src/modules/kms/options/KmsDecryptOptions.ts b/packages/core/src/modules/kms/options/KmsDecryptOptions.ts new file mode 100644 index 0000000000..4414bbe0e4 --- /dev/null +++ b/packages/core/src/modules/kms/options/KmsDecryptOptions.ts @@ -0,0 +1,104 @@ +import * as z from '../../../utils/zod' +import { KnownJwaContentEncryptionAlgorithms } from '../jwk/jwa' +import { zKmsJwkPrivateOct } from '../jwk/kty/oct/octJwk' +import { zKmsKeyAgreementDecryptOptions } from './KmsKeyAgreementDecryptOptions' +import { zKmsKeyId } from './common' + +const zKmsDecryptDataDecryptionAesGcm = z.object({ + // AES-GCM Content Decryption + algorithm: z.enum([ + KnownJwaContentEncryptionAlgorithms.A128GCM, + KnownJwaContentEncryptionAlgorithms.A192GCM, + KnownJwaContentEncryptionAlgorithms.A256GCM, + ]), + + iv: z.instanceof(Uint8Array).refine((iv) => iv.length === 12, 'iv must be 12 bytes for AES GCM'), + aad: z.optional(z.instanceof(Uint8Array)), + tag: z.instanceof(Uint8Array), +}) +export type KmsDecryptDataDecryptionAesGcm = z.output + +// AES-CBC Content Decryption +const zKmsDecryptDataDecryptionAesCbc = z.object({ + algorithm: z.enum([KnownJwaContentEncryptionAlgorithms.A128CBC, KnownJwaContentEncryptionAlgorithms.A256CBC]), + iv: z.instanceof(Uint8Array).refine((iv) => iv.length === 16, 'iv must be 16 bytes for AES CBC'), +}) +export type KmsDecryptDataDecryptionAesCbc = z.output + +// AES-CBC Content Decryption +const zKmsDecryptDataDecryptionAesCbcHmac = z.object({ + algorithm: z.enum([ + KnownJwaContentEncryptionAlgorithms.A128CBC_HS256, + KnownJwaContentEncryptionAlgorithms.A192CBC_HS384, + KnownJwaContentEncryptionAlgorithms.A256CBC_HS512, + ]), + iv: z.instanceof(Uint8Array).refine((iv) => iv.length === 16, 'iv must be 16 bytes for AES CBC with HMAC'), + aad: z.optional(z.instanceof(Uint8Array)), + tag: z.instanceof(Uint8Array), +}) +export type KmsDecryptDataDecryptionAesCbcHmac = z.output + +// XSalsa20-Poly1305 Content Decryption +const zKmsDecryptDataDecryptionSalsa = z.object({ + algorithm: z.enum([KnownJwaContentEncryptionAlgorithms['XSALSA20-POLY1305']]), + iv: z.instanceof(Uint8Array).optional(), +}) + +// ChaCha20-Poly1305 Content Decryption +const zKmsDecryptDataDecryptionC20p = z.object({ + algorithm: z.enum([KnownJwaContentEncryptionAlgorithms.C20P, KnownJwaContentEncryptionAlgorithms.XC20P]), + iv: z.instanceof(Uint8Array), + aad: z.optional(z.instanceof(Uint8Array)), + tag: z.instanceof(Uint8Array), +}) +// FIXME: see how we can do refine with the discriminated union +// .refine( +// ({ iv, algorithm }) => iv.length === (algorithm === 'C20P' ? 12 : 24), +// `iv must be 12 bytes for C20P (ChaCha20-Poly1305) or 24 bytes for XC20P (XChaCha20-Poly1305)` +// ) +export type KmsDecryptDataDecryptionC20p = z.output + +const zKmsDecryptDataDecryption = z.discriminatedUnion('algorithm', [ + zKmsDecryptDataDecryptionAesCbc, + zKmsDecryptDataDecryptionAesCbcHmac, + zKmsDecryptDataDecryptionAesGcm, + zKmsDecryptDataDecryptionC20p, + zKmsDecryptDataDecryptionSalsa, +]) +export type KmsDecryptDataDecryption = z.output + +export const zKmsDecryptOptions = z.object({ + /** + * The key to use for decrypting. There are three possible formats: + * - a key id, pointing to a symmetric (oct) jwk that can be used directly for decryption + * - a private symmetric (oct) jwk object that can be used directly for decryption + * - an object configuring key agreement, based on an existing assymetric key + */ + key: z.union([ + zKmsKeyId, + zKmsJwkPrivateOct.describe('A private oct (symmetric) jwk'), + zKmsKeyAgreementDecryptOptions, + ]), + + /** + * The decryption algorithm used to decrypt the data/content. + * In JWE this parameter is referred to as "enc". + */ + decryption: zKmsDecryptDataDecryption.describe( + 'Options related to the decryption algorithm to use for decrypting the data' + ), + + /** + * The encrypted data to decrypt + */ + encrypted: z.instanceof(Uint8Array).describe('The encrypted data to decrypt'), +}) + +export type KmsDecryptOptions = z.output + +export interface KmsDecryptReturn { + /** + * The decrypted data + */ + data: Uint8Array +} diff --git a/packages/core/src/modules/kms/options/KmsDeleteKeyOptions.ts b/packages/core/src/modules/kms/options/KmsDeleteKeyOptions.ts new file mode 100644 index 0000000000..fe645ae68b --- /dev/null +++ b/packages/core/src/modules/kms/options/KmsDeleteKeyOptions.ts @@ -0,0 +1,11 @@ +import * as z from '../../../utils/zod' +import { zKmsKeyId } from './common' + +export const zKmsDeleteKeyOptions = z.object({ + /** + * The `kid` for the key. + */ + keyId: zKmsKeyId, +}) + +export type KmsDeleteKeyOptions = z.output diff --git a/packages/core/src/modules/kms/options/KmsEncryptOptions.ts b/packages/core/src/modules/kms/options/KmsEncryptOptions.ts new file mode 100644 index 0000000000..020c492453 --- /dev/null +++ b/packages/core/src/modules/kms/options/KmsEncryptOptions.ts @@ -0,0 +1,141 @@ +import * as z from '../../../utils/zod' +import { KnownJwaContentEncryptionAlgorithms } from '../jwk/jwa' +import { zKmsJwkPrivateOct } from '../jwk/kty/oct/octJwk' +import { zKmsKeyAgreementEncryptOptions } from './KmsKeyAgreementEncryptOptions' +import { zKmsKeyId } from './common' + +const zKmsEncryptDataEncryptionAesGcm = z.object({ + // AES-GCM Content Encryption + algorithm: z.enum([ + KnownJwaContentEncryptionAlgorithms.A128GCM, + KnownJwaContentEncryptionAlgorithms.A192GCM, + KnownJwaContentEncryptionAlgorithms.A256GCM, + ]), + + iv: z.optional(z.instanceof(Uint8Array).refine((iv) => iv.length === 12, 'iv must be 12 bytes for AES GCM')), + aad: z.optional(z.instanceof(Uint8Array)), +}) +export type KmsEncryptDataEncryptionAesGcm = z.output + +// AES-CBC Content Encryption +const zKmsEncryptDataEncryptionAesCbc = z.object({ + algorithm: z.enum([KnownJwaContentEncryptionAlgorithms.A128CBC, KnownJwaContentEncryptionAlgorithms.A256CBC]), + iv: z.optional(z.instanceof(Uint8Array).refine((iv) => iv.length === 16, 'iv must be 16 bytes for AES CBC')), +}) +export type KmsEncryptDataEncryptionAesCbc = z.output + +// AES-CBC with HMAC-SHA2 Content Encryption +const zKmsEncryptDataEncryptionAesCbcHmac = z.object({ + algorithm: z.enum([ + KnownJwaContentEncryptionAlgorithms.A128CBC_HS256, + KnownJwaContentEncryptionAlgorithms.A192CBC_HS384, + KnownJwaContentEncryptionAlgorithms.A256CBC_HS512, + ]), + iv: z.optional( + z.instanceof(Uint8Array).refine((iv) => iv.length === 16, 'iv must be 16 bytes for AES CBC with HMAC') + ), + aad: z.optional(z.instanceof(Uint8Array)), +}) +export type KmsEncryptDataEncryptionAesCbcHmac = z.output + +// XSalsa-Poly1305 Content Encryption +const zKmsDecryptDataEncryptionSalsa = z.object({ + algorithm: z.enum([KnownJwaContentEncryptionAlgorithms['XSALSA20-POLY1305']]), + iv: z.instanceof(Uint8Array).optional(), +}) + +// ChaCha20-Poly130 Content Encryption +const zKmsEncryptDataEncryptionC20p = z.object({ + algorithm: z.enum([KnownJwaContentEncryptionAlgorithms.C20P, KnownJwaContentEncryptionAlgorithms.XC20P]), + iv: z.optional(z.instanceof(Uint8Array)), + aad: z.optional(z.instanceof(Uint8Array)), +}) +// FIXME: if we use refine, we can't use discriminated union. and that makes the error handlnig shitty +// .refine( +// ({ iv, algorithm }) => !iv || iv.length === (algorithm === 'C20P' ? 12 : 24), +// `iv must be 12 bytes for C20P (ChaCha20-Poly1305) or 24 bytes for XC20P (XChaCha20-Poly1305)` +// ) + +export type KmsEncryptDataEncryptionX20c = z.output + +export const zKmsEncryptDataEncryption = z.discriminatedUnion('algorithm', [ + zKmsEncryptDataEncryptionAesCbc, + zKmsEncryptDataEncryptionAesCbcHmac, + zKmsEncryptDataEncryptionAesGcm, + zKmsEncryptDataEncryptionC20p, + zKmsDecryptDataEncryptionSalsa, +]) +export type KmsEncryptDataEncryption = z.output + +export const zKmsEncryptOptions = z.object({ + /** + * The key to use for encrypting. There are three possible formats: + * - a key id, pointing to a symmetric (oct) jwk that can be used directly for encryption + * - a private symmetric (oct) jwk object that can be used directly for encryption + * - an object configuring key agreement, based on an existing assymetric key + */ + key: z.union([ + zKmsKeyId, + zKmsJwkPrivateOct.describe('A private oct (symmetric) jwk'), + zKmsKeyAgreementEncryptOptions, + ]), + + /** + * The encryption algorithm used to encrypt the data/content. + * In JWE this parameter is referred to as "enc". + */ + encryption: zKmsEncryptDataEncryption.describe( + 'Options related to the encryption algorithm to use for encrypting the data' + ), + + /** + * The data to encrypt + */ + data: z.instanceof(Uint8Array).describe('The data to encrypt'), +}) + +export type KmsEncryptOptions = z.output +export interface KmsEncryptReturn { + /** + * The encrypted data, also known as "ciphertext" in JWE + */ + encrypted: Uint8Array + + /** + * Optional authentication tag + */ + tag?: Uint8Array + + /** + * The initialization vector. For algorithms where the iv is required + * and not provided, this will contain the auto-generated value. + */ + iv?: Uint8Array + + /** + * The encrypted content encryption key, if key wrapping was used + */ + encryptedKey?: KmsEncryptedKey +} + +export const zKmsEncryptedKey = z.object({ + /** + * Optional authentication tag + */ + tag: z.instanceof(Uint8Array).optional(), + + /** + * The initialization vector. + */ + iv: z.instanceof(Uint8Array).optional(), + + /** + * The encrypted key + */ + encrypted: z.instanceof(Uint8Array), +}) + +/** + * An encrypted content encryption key (CEK). + */ +export type KmsEncryptedKey = z.infer diff --git a/packages/core/src/modules/kms/options/KmsGetPublicKeyOptions.ts b/packages/core/src/modules/kms/options/KmsGetPublicKeyOptions.ts new file mode 100644 index 0000000000..0813d410db --- /dev/null +++ b/packages/core/src/modules/kms/options/KmsGetPublicKeyOptions.ts @@ -0,0 +1,11 @@ +import * as z from '../../../utils/zod' +import { zKmsKeyId } from './common' + +export const zKmsGetPublicKeyOptions = z.object({ + /** + * The key id of the key to get the public bytes for. + */ + keyId: zKmsKeyId, +}) + +export type KmsGetPublicKeyOptions = z.output diff --git a/packages/core/src/modules/kms/options/KmsImportKeyOptions.ts b/packages/core/src/modules/kms/options/KmsImportKeyOptions.ts new file mode 100644 index 0000000000..137f5dc7b4 --- /dev/null +++ b/packages/core/src/modules/kms/options/KmsImportKeyOptions.ts @@ -0,0 +1,31 @@ +import * as z from '../../../utils/zod' +import { KmsJwkPrivate, KmsJwkPublicFromKmsJwkPrivate, zKmsJwkPrivate } from '../jwk/knownJwk' + +export const zKmsImportKeyOptions = z.object({ + /** + * The private jwk to import. If the key needs to use a specific keyId, make sure to set + * the `kid` property on the JWK. If no kid is provided a key id will be generated. + */ + privateJwk: zKmsJwkPrivate, +}) + +export interface KmsImportKeyOptions { + /** + * The private jwk to import. If the key needs to use a specific keyId, make sure to set + * the `kid` property on the JWK. If no kid is provided a key id will be generated. + */ + privateJwk: Jwk +} + +export interface KmsImportKeyReturn { + keyId: string + + /** + * The public JWK representation of the imported key. `kid` will always + * be defined. + * + * In case of a symmetric (oct) key this won't include any key material, but + * will include additional JWK claims such as `use`, `kty`, and `kid` + */ + publicJwk: KmsJwkPublicFromKmsJwkPrivate & { kid: string } +} diff --git a/packages/core/src/modules/kms/options/KmsKeyAgreementDecryptOptions.ts b/packages/core/src/modules/kms/options/KmsKeyAgreementDecryptOptions.ts new file mode 100644 index 0000000000..ffe32a6bad --- /dev/null +++ b/packages/core/src/modules/kms/options/KmsKeyAgreementDecryptOptions.ts @@ -0,0 +1,56 @@ +import * as z from '../../../utils/zod' +import { zKmsJwkPublicOkp } from '../jwk/kty/okp/okpJwk' +import { zKmsEncryptedKey } from './KmsEncryptOptions' +import { zKmsJwkPublicEcdh, zKmsKeyAgreementEcdhEs } from './KmsKeyAgreementEncryptOptions' +import { zKmsKeyId } from './common' + +const zKmsKeyAgreementDecryptEcdhEsKw = z.object({ + /** + * The key id pointing to the ephemeral public key. + * + * The key type MUST match with the externalPublicJwk + */ + keyId: zKmsKeyId, + + algorithm: z.enum(['ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']), + + externalPublicJwk: zKmsJwkPublicEcdh, + + /** + * The encrypted content encryption key (cek) + */ + encryptedKey: zKmsEncryptedKey, + + apu: z.optional(z.instanceof(Uint8Array)), + apv: z.optional(z.instanceof(Uint8Array)), +}) +export type KmsKeyAgreementDecryptEcdhEsKw = z.output + +const zKmsKeyAgreementDecryptEcdhHsalsa20 = z.object({ + /** + * The key id to use for decrypting the content encryption key. + */ + keyId: zKmsKeyId, + + /** + * Perform key agreement based on the HSALSA20 as used in Libsodium's + * Cryptobox. This is not based on an official JWA algorithm, but is + * used primarily for DIDComm v1 messaging. + */ + algorithm: z.literal('ECDH-HSALSA20'), + + /** + * Can be undefined for anonymous encryption + */ + externalPublicJwk: zKmsJwkPublicOkp.extend({ crv: zKmsJwkPublicOkp.shape.crv.extract(['X25519']) }).optional(), +}) +export type KmsKeyAgreementDecryptEcdhHsalsa20 = z.output + +export const zKmsKeyAgreementDecryptOptions = z + .discriminatedUnion('algorithm', [ + zKmsKeyAgreementEcdhEs, + zKmsKeyAgreementDecryptEcdhEsKw, + zKmsKeyAgreementDecryptEcdhHsalsa20, + ]) + .describe('Options for key agreement based on an assymetric key.') +export type KmsKeyAgreementDecryptOptions = z.output diff --git a/packages/core/src/modules/kms/options/KmsKeyAgreementEncryptOptions.ts b/packages/core/src/modules/kms/options/KmsKeyAgreementEncryptOptions.ts new file mode 100644 index 0000000000..67dcd9c2e0 --- /dev/null +++ b/packages/core/src/modules/kms/options/KmsKeyAgreementEncryptOptions.ts @@ -0,0 +1,72 @@ +import * as z from '../../../utils/zod' +import { zKmsJwkPublicEc } from '../jwk/kty/ec/ecJwk' +import { zKmsJwkPublicOkp } from '../jwk/kty/okp/okpJwk' +import { zKmsKeyId } from './common' + +export const zKmsJwkPublicEcdh = z.union([ + zKmsJwkPublicOkp.extend({ crv: zKmsJwkPublicOkp.shape.crv.extract(['X25519']) }), + zKmsJwkPublicEc.extend({ crv: zKmsJwkPublicEc.shape.crv.extract(['P-256', 'P-384', 'P-521', 'secp256k1']) }), +]) + +export type KmsJwkPublicEcdh = z.infer + +export const zKmsKeyAgreementEcdhEs = z.object({ + /** + * The key id pointing to the ephemeral public key. + * + * The key type MUST match with the externalPublicJwk + */ + keyId: zKmsKeyId, + + algorithm: z.literal('ECDH-ES'), + + externalPublicJwk: zKmsJwkPublicEcdh, + + apu: z.optional(z.instanceof(Uint8Array)), + apv: z.optional(z.instanceof(Uint8Array)), +}) +export type KmsKeyAgreementEcdhEs = z.output + +const zKmsKeyAgreementEncryptEcdhEsKw = z.object({ + /** + * The key id pointing to the ephemeral public key. + * + * The key type MUST match with the externalPublicJwk + */ + keyId: zKmsKeyId, + + algorithm: z.enum(['ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']), + + externalPublicJwk: zKmsJwkPublicEcdh, + + apu: z.optional(z.instanceof(Uint8Array)), + apv: z.optional(z.instanceof(Uint8Array)), +}) +export type KmsKeyAgreementEncryptEcdhEsKw = z.output + +const zKmsKeyAgreementEncryptEcdhHsalsa20 = z.object({ + /** + * The key id to use for encrypting the content encryption key. + * If no key id is provided, anonymous encryption is used. + */ + keyId: zKmsKeyId.optional(), + + /** + * Perform key agreement based on the HSALSA20 as used in Libsodium's + * Cryptobox. This is not based on an official JWA algorithm, but is + * used primarily for DIDComm v1 messaging. + */ + algorithm: z.literal('ECDH-HSALSA20'), + + externalPublicJwk: zKmsJwkPublicOkp.extend({ crv: zKmsJwkPublicOkp.shape.crv.extract(['X25519']) }), +}) +export type KmsKeyAgreementEncryptEcdhHsalsa20 = z.output + +export const zKmsKeyAgreementEncryptOptions = z + .discriminatedUnion('algorithm', [ + zKmsKeyAgreementEcdhEs, + zKmsKeyAgreementEncryptEcdhEsKw, + zKmsKeyAgreementEncryptEcdhHsalsa20, + ]) + .describe('Options for key agreement based on an assymetric key.') +export type KmsKeyAgreementEncryptOptions = z.output diff --git a/packages/core/src/modules/kms/options/KmsOperation.ts b/packages/core/src/modules/kms/options/KmsOperation.ts new file mode 100644 index 0000000000..a506eeebfa --- /dev/null +++ b/packages/core/src/modules/kms/options/KmsOperation.ts @@ -0,0 +1,111 @@ +import { KeyManagementError } from '../error/KeyManagementError' +import { KmsJwkPrivate, KnownJwaSignatureAlgorithm, getJwkHumanDescription } from '../jwk' +import { KmsCreateKeyType } from './KmsCreateKeyOptions' +import { KmsDecryptDataDecryption } from './KmsDecryptOptions' +import { KmsEncryptDataEncryption } from './KmsEncryptOptions' +import { KmsKeyAgreementDecryptOptions } from './KmsKeyAgreementDecryptOptions' +import { KmsKeyAgreementEncryptOptions } from './KmsKeyAgreementEncryptOptions' + +export type KmsOperationCreateKey = { + operation: 'createKey' + type: KmsCreateKeyType +} + +export type KmsOperationImportKey = { + operation: 'importKey' + privateJwk: KmsJwkPrivate +} + +export type KmsOperationDeleteKey = { + operation: 'deleteKey' +} + +export type KmsOperationSign = { + operation: 'sign' + algorithm: KnownJwaSignatureAlgorithm +} + +export type KmsOperationVerify = { + operation: 'verify' + algorithm: KnownJwaSignatureAlgorithm +} + +export type KmsOperationEncrypt = { + operation: 'encrypt' + encryption: KmsEncryptDataEncryption + keyAgreement?: KmsKeyAgreementEncryptOptions +} + +export type KmsOperationDecrypt = { + operation: 'decrypt' + decryption: KmsDecryptDataDecryption + keyAgreement?: KmsKeyAgreementDecryptOptions +} + +export type KmsOperationRandomBytes = { + operation: 'randomBytes' +} + +export type KmsOperation = + | KmsOperationCreateKey + | KmsOperationImportKey + | KmsOperationDeleteKey + | KmsOperationSign + | KmsOperationVerify + | KmsOperationEncrypt + | KmsOperationDecrypt + | KmsOperationRandomBytes + +export function getKmsOperationHumanDescription(operation: KmsOperation) { + if (operation.operation === 'deleteKey') { + return "'deleteKey' operation" + } + + if (operation.operation === 'createKey') { + let base = `'createKey' operation with kty '${operation.type.kty}'` + + if (operation.type.kty === 'EC' || operation.type.kty === 'OKP') { + base += ` and crv '${operation.type.crv}'` + } else if (operation.type.kty === 'RSA') { + base += ` and bit length '${operation.type.modulusLength}'` + } else if (operation.type.kty === 'oct') { + base += ` and algorithm '${operation.type.algorithm}'` + + if (operation.type.algorithm === 'aes' || operation.type.algorithm === 'hmac') { + base += ` with key length '${operation.type.length}'` + } + } + + return base + } + + if (operation.operation === 'importKey') { + return `'importKey' operation with ${getJwkHumanDescription(operation.privateJwk)}` + } + + if (operation.operation === 'sign' || operation.operation === 'verify') { + return `'${operation.operation}' operation with algorithm '${operation.algorithm}'` + } + + if (operation.operation === 'encrypt') { + let message = `'encrypt' operation with encryption algorithm '${operation.encryption.algorithm}'` + if (operation.keyAgreement) { + message += `and key agreement algorithm '${operation.keyAgreement.algorithm}'` + } + return message + } + + if (operation.operation === 'decrypt') { + let message = `'decrypt' operation with encryption algorithm '${operation.decryption.algorithm}'` + if (operation.keyAgreement) { + message += `and key agreement algorithm '${operation.keyAgreement.algorithm}'` + } + return message + } + + if (operation.operation === 'randomBytes') { + return `'randomBytes' operation` + } + + throw new KeyManagementError('Unsupported operation') +} diff --git a/packages/core/src/modules/kms/options/KmsRandomBytesOptions.ts b/packages/core/src/modules/kms/options/KmsRandomBytesOptions.ts new file mode 100644 index 0000000000..b85d6f8b0b --- /dev/null +++ b/packages/core/src/modules/kms/options/KmsRandomBytesOptions.ts @@ -0,0 +1,17 @@ +import * as z from '../../../utils/zod' + +export const zKmsRandomBytesOptions = z.object({ + /** + * The number of random bytes to genreate + */ + length: z.number().positive(), +}) + +export type KmsRandomBytesOptions = z.output + +export interface KmsRandomBytesReturn { + /** + * The generated random bytes + */ + bytes: Uint8Array +} diff --git a/packages/core/src/modules/kms/options/KmsSignOptions.ts b/packages/core/src/modules/kms/options/KmsSignOptions.ts new file mode 100644 index 0000000000..23e58d4cde --- /dev/null +++ b/packages/core/src/modules/kms/options/KmsSignOptions.ts @@ -0,0 +1,25 @@ +import * as z from '../../../utils/zod' +import { zKnownJwaSignatureAlgorithm } from '../jwk/jwa' +import { zKmsKeyId } from './common' + +export const zKmsSignOptions = z.object({ + /** + * The key to use for signing + */ + keyId: zKmsKeyId, + + /** + * The JWA signature algorithm to use for signing + */ + algorithm: zKnownJwaSignatureAlgorithm.describe('The JWA signature algorithm to use for signing'), + + /** + * The data to sign + */ + data: z.instanceof(Uint8Array).describe('The data to sign'), +}) + +export type KmsSignOptions = z.output +export interface KmsSignReturn { + signature: Uint8Array +} diff --git a/packages/core/src/modules/kms/options/KmsVerifyOptions.ts b/packages/core/src/modules/kms/options/KmsVerifyOptions.ts new file mode 100644 index 0000000000..003ed18686 --- /dev/null +++ b/packages/core/src/modules/kms/options/KmsVerifyOptions.ts @@ -0,0 +1,41 @@ +import type { KmsJwkPublic } from '../jwk/knownJwk' + +import * as z from '../../../utils/zod' +import { zKnownJwaSignatureAlgorithm } from '../jwk/jwa' +import { zKmsJwkPublicAsymmetric } from '../jwk/knownJwk' +import { zKmsKeyId } from './common' + +export const zKmsVerifyOptions = z.object({ + /** + * The key to verify with. Either a string referring to a keyId, or a `KmsJwkPublicAssymetric` for verifying with a + * public asymmetric JWK. + * + * It is currently not possible to verify a signature with symmetric a + * key that is not already present in the KMS. + */ + key: z.union([zKmsKeyId, zKmsJwkPublicAsymmetric]), + + /** + * The JWA signature algorithm to use for verification + */ + algorithm: zKnownJwaSignatureAlgorithm.describe('The JWA signature algorithm to use for verification'), + + /** + * The data to verify + */ + data: z.instanceof(Uint8Array).describe('The data to verify'), + + /** + * The signature to verify the data against + */ + signature: z.instanceof(Uint8Array).describe('The signature on the data to verify'), +}) + +export type KmsVerifyOptions = z.output + +export type KmsVerifyReturn = + | { + verified: true + publicJwk: KmsJwkPublic + } + | { verified: false } diff --git a/packages/core/src/modules/kms/options/backend.ts b/packages/core/src/modules/kms/options/backend.ts new file mode 100644 index 0000000000..1e0b05c646 --- /dev/null +++ b/packages/core/src/modules/kms/options/backend.ts @@ -0,0 +1,13 @@ +import * as z from '../../../utils/zod' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const zWithBackend = (schema: Schema) => + schema.and(z.object({ backend: z.optional(z.string()) })) + +export type WithBackend = T & { + /** + * The backend to use for creating the key. If not provided the + * default backend for key operations will be used. + */ + backend?: string +} diff --git a/packages/core/src/modules/kms/options/common.ts b/packages/core/src/modules/kms/options/common.ts new file mode 100644 index 0000000000..244f20c1e4 --- /dev/null +++ b/packages/core/src/modules/kms/options/common.ts @@ -0,0 +1,3 @@ +import { z } from '../../../utils/zod' + +export const zKmsKeyId = z.string().describe('A reference to a key in the KMS') diff --git a/packages/core/src/modules/kms/options/index.ts b/packages/core/src/modules/kms/options/index.ts new file mode 100644 index 0000000000..aa124484fb --- /dev/null +++ b/packages/core/src/modules/kms/options/index.ts @@ -0,0 +1,58 @@ +export type { + KmsCreateKeyOptions, + KmsCreateKeyReturn, + KmsCreateKeyType, + KmsCreateKeyTypeEc, + KmsCreateKeyTypeOct, + KmsCreateKeyTypeOkp, + KmsCreateKeyTypeRsa, + KmsCreateKeyTypeAssymetric, + KmsCreateKeyForSignatureAlgorithmOptions, +} from './KmsCreateKeyOptions' + +export type { KmsDeleteKeyOptions } from './KmsDeleteKeyOptions' +export type { KmsRandomBytesOptions, KmsRandomBytesReturn } from './KmsRandomBytesOptions' +export type { KmsSignOptions, KmsSignReturn } from './KmsSignOptions' +export type { KmsVerifyOptions, KmsVerifyReturn } from './KmsVerifyOptions' +export type { KmsImportKeyOptions, KmsImportKeyReturn } from './KmsImportKeyOptions' +export type { KmsGetPublicKeyOptions } from './KmsGetPublicKeyOptions' +export type { + KmsEncryptDataEncryption, + KmsEncryptOptions, + KmsEncryptReturn, + KmsEncryptDataEncryptionAesCbc, + KmsEncryptDataEncryptionAesGcm, + KmsEncryptDataEncryptionX20c, + KmsEncryptedKey, +} from './KmsEncryptOptions' +export { + KmsDecryptDataDecryption, + KmsDecryptDataDecryptionAesCbc, + KmsDecryptDataDecryptionAesGcm, + KmsDecryptDataDecryptionC20p, + KmsDecryptOptions, + KmsDecryptReturn, +} from './KmsDecryptOptions' +export { + KmsKeyAgreementEcdhEs, + KmsKeyAgreementEncryptEcdhEsKw, + KmsKeyAgreementEncryptEcdhHsalsa20, + KmsKeyAgreementEncryptOptions, + KmsJwkPublicEcdh, +} from './KmsKeyAgreementEncryptOptions' +export { + KmsKeyAgreementDecryptOptions, + KmsKeyAgreementDecryptEcdhHsalsa20, + KmsKeyAgreementDecryptEcdhEsKw, +} from './KmsKeyAgreementDecryptOptions' +export { + KmsOperation, + KmsOperationCreateKey, + KmsOperationDecrypt, + KmsOperationDeleteKey, + KmsOperationEncrypt, + KmsOperationImportKey, + KmsOperationSign, + KmsOperationVerify, + getKmsOperationHumanDescription, +} from './KmsOperation' diff --git a/packages/core/src/modules/mdoc/Mdoc.ts b/packages/core/src/modules/mdoc/Mdoc.ts index 306653ebe4..1a677b7667 100644 --- a/packages/core/src/modules/mdoc/Mdoc.ts +++ b/packages/core/src/modules/mdoc/Mdoc.ts @@ -1,6 +1,5 @@ import type { IssuerSignedDocument } from '@animo-id/mdoc' import type { AgentContext } from '../../agent' -import type { Key } from '../../crypto' import type { MdocNameSpaces, MdocSignOptions, MdocVerifyOptions } from './MdocOptions' import { @@ -12,10 +11,11 @@ import { parseDeviceSigned, parseIssuerSigned, } from '@animo-id/mdoc' -import { JwaSignatureAlgorithm, JwkJson, getJwkFromJson, getJwkFromKey } from '../../crypto' import { ClaimFormat } from '../vc/index' import { X509Certificate, X509ModuleConfig } from '../x509' +import { KnownJwaSignatureAlgorithm, PublicJwk } from '../kms' +import { isKnownJwaSignatureAlgorithm } from '../kms/jwk/jwa' import { TypedArrayEncoder } from './../../utils' import { getMdocContext } from './MdocContext' import { MdocError } from './MdocError' @@ -50,11 +50,11 @@ export class Mdoc { /** * Get the device key to which the mdoc is bound */ - public get deviceKey(): Key | null { + public get deviceKey(): PublicJwk | null { const deviceKeyRaw = this.issuerSignedDocument.issuerSigned.issuerAuth.decodedPayload.deviceKeyInfo?.deviceKey if (!deviceKeyRaw) return null - return getJwkFromJson(COSEKey.import(deviceKeyRaw).toJWK() as JwkJson).key + return PublicJwk.fromUnknown(COSEKey.import(deviceKeyRaw).toJWK()) } public static fromBase64Url(mdocBase64Url: string, expectedDocType?: string): Mdoc { @@ -84,14 +84,15 @@ export class Mdoc { return this.issuerSignedDocument.docType } - public get alg(): JwaSignatureAlgorithm { + public get alg(): KnownJwaSignatureAlgorithm { const algName = this.issuerSignedDocument.issuerSigned.issuerAuth.algName if (!algName) { throw new MdocError('Cannot extract the signature algorithm from the mdoc.') } - if (Object.values(JwaSignatureAlgorithm).includes(algName as JwaSignatureAlgorithm)) { - return algName as JwaSignatureAlgorithm + if (isKnownJwaSignatureAlgorithm(algName)) { + return algName } + throw new MdocError(`Cannot parse mdoc. The signature algorithm '${algName}' is not supported.`) } @@ -125,38 +126,25 @@ export class Mdoc { ) } - public get deviceKeyJwk() { - const deviceKey = this.issuerSignedDocument.issuerSigned.issuerAuth.decodedPayload.deviceKeyInfo?.deviceKey - if (!deviceKey) return null - - const publicDeviceJwk = COSEKey.import(deviceKey).toJWK() - const jwkInstance = getJwkFromJson(publicDeviceJwk as JwkJson) - - return jwkInstance - } - public static async sign(agentContext: AgentContext, options: MdocSignOptions) { const { docType, validityInfo, namespaces, holderKey, issuerCertificate } = options const mdocContext = getMdocContext(agentContext) - const holderPublicJwk = getJwkFromKey(holderKey) const document = new Document(docType, mdocContext) .useDigestAlgorithm('SHA-256') .addValidityInfo(validityInfo) - .addDeviceKeyInfo({ deviceKey: holderPublicJwk.toJson() }) + .addDeviceKeyInfo({ deviceKey: holderKey.toJson() }) for (const [namespace, namespaceRecord] of Object.entries(namespaces)) { document.addIssuerNameSpace(namespace, namespaceRecord) } - const cert = X509Certificate.fromEncodedCertificate(issuerCertificate) - const issuerKey = getJwkFromKey(cert.publicKey) - + const issuerKey = issuerCertificate.publicJwk const alg = issuerKey.supportedSignatureAlgorithms.find(isMdocSupportedSignatureAlgorithm) if (!alg) { throw new MdocError( - `Unable to create sign mdoc. No supported signature algorithm found to sign mdoc for jwk with key type ${ - issuerKey.keyType + `Unable to create sign mdoc. No supported signature algorithm found to sign mdoc for jwk with key ${ + issuerKey.jwkTypehumanDescription }. Key supports algs ${issuerKey.supportedSignatureAlgorithms.join( ', ' )}. mdoc supports algs ${mdocSupporteSignatureAlgorithms.join(', ')}` @@ -167,8 +155,7 @@ export class Mdoc { { issuerPrivateKey: issuerKey.toJson(), alg, - issuerCertificate, - kid: cert.publicKey.fingerprint, + issuerCertificate: issuerCertificate.rawCertificate, }, mdocContext ) @@ -222,4 +209,12 @@ export class Mdoc { return { isValid: false, error: error.message } } } + + private toJSON() { + return this.base64Url + } + + private toString() { + return this.base64Url + } } diff --git a/packages/core/src/modules/mdoc/MdocContext.ts b/packages/core/src/modules/mdoc/MdocContext.ts index 07bc5530ba..81525182f4 100644 --- a/packages/core/src/modules/mdoc/MdocContext.ts +++ b/packages/core/src/modules/mdoc/MdocContext.ts @@ -1,17 +1,19 @@ import type { MdocContext, X509Context } from '@animo-id/mdoc' import type { AgentContext } from '../../agent' -import type { JwkJson } from '../../crypto' import { p256 } from '@noble/curves/p256' import { hkdf } from '@noble/hashes/hkdf' import { sha256 } from '@noble/hashes/sha2' -import { CredoWebCrypto, Hasher, getJwkFromJson, getJwkFromKey } from '../../crypto' +import { CredoWebCrypto, Hasher } from '../../crypto' import { Buffer, TypedArrayEncoder } from '../../utils' +import { KeyManagementApi, KmsJwkPublicAsymmetric, KnownJwaSignatureAlgorithm, PublicJwk } from '../kms' import { X509Certificate, X509Service } from '../x509' export const getMdocContext = (agentContext: AgentContext): MdocContext => { const crypto = new CredoWebCrypto(agentContext) + const kms = agentContext.resolve(KeyManagementApi) + return { crypto: { digest: async (input) => { @@ -45,38 +47,66 @@ export const getMdocContext = (agentContext: AgentContext): MdocContext => { sign: async (input) => { const { jwk, mac0 } = input const { data } = mac0.getRawSigningData() - return await agentContext.wallet.sign({ - data: Buffer.from(data), - key: getJwkFromJson(jwk as JwkJson).key, + + const publicJwk = PublicJwk.fromUnknown(jwk) + const algorithm = mac0.algName ?? publicJwk.signatureAlgorithm + + const { signature } = await kms.sign({ + data, + algorithm, + keyId: publicJwk.keyId, }) + + return signature }, verify: async (input) => { const { mac0, jwk, options } = input const { data, signature } = mac0.getRawVerificationData(options) - return await agentContext.wallet.verify({ - key: getJwkFromJson(jwk as JwkJson).key, - data: Buffer.from(data), - signature: new Buffer(signature), + + const publicJwk = PublicJwk.fromUnknown(jwk) + const algorithm = mac0.algName ?? publicJwk.signatureAlgorithm + + const { verified } = await kms.verify({ + key: jwk as KmsJwkPublicAsymmetric, + data, + algorithm, + signature, }) + + return verified }, }, sign1: { sign: async (input) => { const { jwk, sign1 } = input const { data } = sign1.getRawSigningData() - return await agentContext.wallet.sign({ - data: Buffer.from(data), - key: getJwkFromJson(jwk as JwkJson).key, + + const publicJwk = PublicJwk.fromUnknown(jwk) + const algorithm = sign1.algName ?? publicJwk.signatureAlgorithm + + const { signature } = await kms.sign({ + data, + algorithm: algorithm as KnownJwaSignatureAlgorithm, + keyId: publicJwk.keyId, }) + + return signature }, verify: async (input) => { const { sign1, jwk, options } = input const { data, signature } = sign1.getRawVerificationData(options) - return await agentContext.wallet.verify({ - key: getJwkFromJson(jwk as JwkJson).key, - data: Buffer.from(data), - signature: new Buffer(signature), + + const publicJwk = PublicJwk.fromUnknown(jwk) + const algorithm = sign1.algName ?? publicJwk.signatureAlgorithm + + const { verified } = await kms.verify({ + key: jwk as KmsJwkPublicAsymmetric, + data, + algorithm: algorithm as KnownJwaSignatureAlgorithm, + signature, }) + + return verified }, }, }, @@ -88,8 +118,8 @@ export const getMdocContext = (agentContext: AgentContext): MdocContext => { return x509Certificate.getIssuerNameField(field) }, getPublicKey: async (input) => { - const comp = X509Certificate.fromRawCertificate(input.certificate) - return getJwkFromKey(comp.publicKey).toJson() + const certificate = X509Certificate.fromRawCertificate(input.certificate) + return certificate.publicJwk.toJson() }, validateCertificateChain: async (input) => { const certificateChain = input.x5chain.map((cert) => X509Certificate.fromRawCertificate(cert).toString('pem')) diff --git a/packages/core/src/modules/mdoc/MdocDeviceResponse.ts b/packages/core/src/modules/mdoc/MdocDeviceResponse.ts index 51346100d8..112bb034e3 100644 --- a/packages/core/src/modules/mdoc/MdocDeviceResponse.ts +++ b/packages/core/src/modules/mdoc/MdocDeviceResponse.ts @@ -24,9 +24,8 @@ import { parseIssuerSigned, } from '@animo-id/mdoc' import { uuid } from '../../utils/uuid' +import { PublicJwk } from '../kms' import { ClaimFormat } from '../vc' - -import { Jwk } from '../../crypto' import { TypedArrayEncoder } from './../../utils' import { Mdoc } from './Mdoc' import { getMdocContext } from './MdocContext' @@ -216,9 +215,14 @@ export class MdocDeviceResponse { const combinedDeviceResponseMdoc = new MDoc() for (const document of options.mdocs) { - const deviceKeyJwk = document.deviceKeyJwk + const deviceKeyJwk = document.deviceKey if (!deviceKeyJwk) throw new MdocError(`Device key is missing in mdoc with doctype ${document.docType}`) + // Set keyId to legacy key id if it doesn't have a key id set + if (!deviceKeyJwk.hasKeyId) { + deviceKeyJwk.keyId = deviceKeyJwk.legacyKeyId + } + const alg = MdocDeviceResponse.getAlgForDeviceKeyJwk(deviceKeyJwk) // We do PEX filtering on a different layer, so we only include the needed input descriptor here @@ -261,10 +265,15 @@ export class MdocDeviceResponse { const combinedDeviceResponseMdoc = new MDoc() for (const document of options.mdocs) { - const deviceKeyJwk = document.deviceKeyJwk + const deviceKeyJwk = document.deviceKey if (!deviceKeyJwk) throw new MdocError(`Device key is missing in mdoc with doctype ${document.docType}`) const alg = MdocDeviceResponse.getAlgForDeviceKeyJwk(deviceKeyJwk) + // Set keyId to legacy key id if it doesn't have a key id set + if (!deviceKeyJwk.hasKeyId) { + deviceKeyJwk.keyId = deviceKeyJwk.legacyKeyId + } + const issuerSignedDocument = parseIssuerSigned(TypedArrayEncoder.fromBase64(document.base64Url), document.docType) const deviceRequestForDocument = DeviceRequest.from( @@ -396,12 +405,12 @@ export class MdocDeviceResponse { throw new MdocError('Unsupported session transcript option') } - private static getAlgForDeviceKeyJwk(jwk: Jwk) { + private static getAlgForDeviceKeyJwk(jwk: PublicJwk) { const signatureAlgorithm = jwk.supportedSignatureAlgorithms.find(isMdocSupportedSignatureAlgorithm) if (!signatureAlgorithm) { throw new MdocError( - `Unable to create mdoc device response. No supported signature algorithm found to sign device response for jwk with key type ${ - jwk.keyType + `Unable to create mdoc device response. No supported signature algorithm found to sign device response for jwk ${ + jwk.jwkTypehumanDescription }. Key supports algs ${jwk.supportedSignatureAlgorithms.join( ', ' )}. mdoc supports algs ${mdocSupporteSignatureAlgorithms.join(', ')}` diff --git a/packages/core/src/modules/mdoc/MdocOptions.ts b/packages/core/src/modules/mdoc/MdocOptions.ts index 11a9765a1c..e87c8b6d0f 100644 --- a/packages/core/src/modules/mdoc/MdocOptions.ts +++ b/packages/core/src/modules/mdoc/MdocOptions.ts @@ -1,7 +1,7 @@ import type { ValidityInfo } from '@animo-id/mdoc' -import type { Key } from '../../crypto/Key' import type { DifPresentationExchangeDefinition } from '../dif-presentation-exchange' -import type { EncodedX509Certificate } from '../x509' +import { PublicJwk } from '../kms' +import type { EncodedX509Certificate, X509Certificate } from '../x509' import { Mdoc } from './Mdoc' export { DateOnly } from '@animo-id/mdoc' @@ -74,8 +74,9 @@ export type MdocSignOptions = { /** * - * The trusted base64-encoded issuer certificate string in the DER-format. + * The X509 certificate to use for signing the mDOC. The certificate MUST have a + * publicJwk with key id configured, enabling signing with the KMS */ - issuerCertificate: string - holderKey: Key + issuerCertificate: X509Certificate + holderKey: PublicJwk } diff --git a/packages/core/src/modules/mdoc/__tests__/mdocDeviceResponse.test.ts b/packages/core/src/modules/mdoc/__tests__/mdocDeviceResponse.test.ts index bb67ab13dd..9deb582324 100644 --- a/packages/core/src/modules/mdoc/__tests__/mdocDeviceResponse.test.ts +++ b/packages/core/src/modules/mdoc/__tests__/mdocDeviceResponse.test.ts @@ -1,24 +1,30 @@ import { Optionality } from '@sphereon/pex-models' -import { getInMemoryAgentOptions } from '../../../../tests' +import { getAgentOptions } from '../../../../tests' import { Agent } from '../../../agent/Agent' -import { KeyType } from '../../../crypto' +import { PublicJwk } from '../../kms' import { X509Service } from '../../x509' import { Mdoc } from '../Mdoc' import { MdocDeviceResponse } from '../MdocDeviceResponse' describe('mdoc device-response test', () => { - const agent = new Agent(getInMemoryAgentOptions('mdoc-test-agent', {})) + const agent = new Agent(getAgentOptions('mdoc-test-agent', {})) beforeAll(async () => { await agent.initialize() }) test('can limit the disclosure', async () => { - const holderKey = await agent.context.wallet.createKey({ - keyType: KeyType.P256, + const holderKey = await agent.kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, }) - const issuerKey = await agent.context.wallet.createKey({ - keyType: KeyType.P256, + const issuerKey = await agent.kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, }) const currentDate = new Date() @@ -28,18 +34,16 @@ describe('mdoc device-response test', () => { const certificate = await X509Service.createCertificate(agent.context, { issuer: 'CN=credo', - authorityKey: issuerKey, + authorityKey: PublicJwk.fromPublicJwk(issuerKey.publicJwk), validity: { notBefore: currentDate, notAfter: nextDay, }, }) - const issuerCertificate = certificate.toString('pem') - const mdoc = await Mdoc.sign(agent.context, { docType: 'org.iso.18013.5.1.mDL', - holderKey: holderKey, + holderKey: PublicJwk.fromPublicJwk(holderKey.publicJwk), namespaces: { hello: { world: 'from-mdoc', @@ -47,7 +51,7 @@ describe('mdoc device-response test', () => { nicer: 'dicer', }, }, - issuerCertificate, + issuerCertificate: certificate, }) const limitedDisclosedPayload = MdocDeviceResponse.limitDisclosureToInputDescriptor({ diff --git a/packages/core/src/modules/mdoc/__tests__/mdocOpenId4VcDeviceResponse.test.ts b/packages/core/src/modules/mdoc/__tests__/mdocOpenId4VcDeviceResponse.test.ts index 08513ae71d..1baed06168 100644 --- a/packages/core/src/modules/mdoc/__tests__/mdocOpenId4VcDeviceResponse.test.ts +++ b/packages/core/src/modules/mdoc/__tests__/mdocOpenId4VcDeviceResponse.test.ts @@ -1,13 +1,12 @@ import type { DifPresentationExchangeDefinition } from '../../dif-presentation-exchange' import { cborEncode, parseDeviceResponse } from '@animo-id/mdoc' -import { Key as AskarKey, Jwk } from '@openwallet-foundation/askar-nodejs' -import { getInMemoryAgentOptions } from '../../../../tests' +import { getAgentOptions } from '../../../../tests' import { Agent } from '../../../agent/Agent' -import { KeyType } from '../../../crypto' -import { getJwkFromJson } from '../../../crypto/jose/jwk/transform' -import { Buffer, TypedArrayEncoder } from '../../../utils' +import { TypedArrayEncoder } from '../../../utils' +import { PublicJwk } from '../../kms' +import { X509Certificate } from '../../x509' import { Mdoc } from '../Mdoc' import { MdocDeviceResponse } from '../MdocDeviceResponse' @@ -17,22 +16,21 @@ const DEVICE_JWK_PUBLIC_P256 = { y: 'oxS1OAORJ7XNUHNfVFGeM8E0RQVFxWA62fJj-sxW03c', crv: 'P-256', use: undefined, -} +} as const const DEVICE_JWK_PRIVATE_P256 = { ...DEVICE_JWK_PUBLIC_P256, d: 'eRpAZr3eV5xMMnPG3kWjg90Y-bBff9LqmlQuk49HUtA', -} +} as const -// biome-ignore lint/suspicious/noExportsInTest: -export const ISSUER_PRIVATE_KEY_JWK_P256 = { +const ISSUER_PRIVATE_KEY_JWK_P256 = { kty: 'EC', kid: '1234', x: 'iTwtg0eQbcbNabf2Nq9L_VM_lhhPCq2s0Qgw2kRx29s', y: 'YKwXDRz8U0-uLZ3NSI93R_35eNkl6jHp6Qg8OCup7VM', crv: 'P-256', d: 'o6PrzBm1dCfSwqJHW6DVqmJOCQSIAosrCPfbFJDMNp4', -} +} as const const ISSUER_CERTIFICATE_P256 = `-----BEGIN CERTIFICATE----- MIICKjCCAdCgAwIBAgIUV8bM0wi95D7KN0TyqHE42ru4hOgwCgYIKoZIzj0EAwIw @@ -134,28 +132,28 @@ describe('mdoc device-response openid4vp test', () => { describe('P256', () => { beforeEach(async () => { - agent = new Agent(getInMemoryAgentOptions('mdoc-test-agent', {})) + agent = new Agent(getAgentOptions('mdoc-test-agent', {})) await agent.initialize() - const devicePrivateAskar = AskarKey.fromJwk({ jwk: Jwk.fromJson(DEVICE_JWK_PRIVATE_P256) }) - await agent.context.wallet.createKey({ - keyType: KeyType.P256, - privateKey: Buffer.from(devicePrivateAskar.secretBytes), + const importedDeviceKey = await agent.kms.importKey({ + privateJwk: DEVICE_JWK_PRIVATE_P256, }) + const deviceKeyPublicJwk = PublicJwk.fromPublicJwk(importedDeviceKey.publicJwk) - const issuerPrivateAskar = AskarKey.fromJwk({ jwk: Jwk.fromJson(ISSUER_PRIVATE_KEY_JWK_P256) }) - await agent.context.wallet.createKey({ - keyType: KeyType.P256, - privateKey: Buffer.from(issuerPrivateAskar.secretBytes), + const importedIssuerKey = await agent.kms.importKey({ + privateJwk: ISSUER_PRIVATE_KEY_JWK_P256, }) + const issuerCertificate = X509Certificate.fromEncodedCertificate(ISSUER_CERTIFICATE_P256) + issuerCertificate.keyId = importedIssuerKey.keyId + mdoc = await Mdoc.sign(agent.context, { docType: 'org.iso.18013.5.1.mDL', validityInfo: { signed: new Date('2023-10-24'), validUntil: new Date('2050-10-24'), }, - holderKey: getJwkFromJson(DEVICE_JWK_PUBLIC_P256).key, - issuerCertificate: ISSUER_CERTIFICATE_P256, + holderKey: deviceKeyPublicJwk, + issuerCertificate, namespaces: { 'org.iso.18013.5.1': { family_name: 'Jones', @@ -291,21 +289,27 @@ describe('mdoc device-response openid4vp test', () => { describe('EdDSA', () => { beforeEach(async () => { - agent = new Agent(getInMemoryAgentOptions('mdoc-test-agent-eddsa', {})) + agent = new Agent(getAgentOptions('mdoc-test-agent-eddsa', {})) await agent.initialize() }) test('should verify with EdDSA', async () => { - const issuerKey = await agent.context.wallet.createKey({ - keyType: KeyType.Ed25519, + const issuerKey = await agent.kms.createKey({ + type: { + kty: 'OKP', + crv: 'Ed25519', + }, }) - const holderKey = await agent.context.wallet.createKey({ - keyType: KeyType.Ed25519, + const holderKey = await agent.kms.createKey({ + type: { + kty: 'OKP', + crv: 'Ed25519', + }, }) const issuerCertificate = await agent.x509.createCertificate({ - authorityKey: issuerKey, + authorityKey: PublicJwk.fromPublicJwk(issuerKey.publicJwk), issuer: 'C=US,ST=New York', validity: { notBefore: new Date('2020-01-01'), @@ -319,8 +323,8 @@ describe('mdoc device-response openid4vp test', () => { signed: new Date('2023-10-24'), validUntil: new Date('2050-10-24'), }, - holderKey, - issuerCertificate: issuerCertificate.toString('pem'), + holderKey: PublicJwk.fromPublicJwk(holderKey.publicJwk), + issuerCertificate, namespaces: { 'org.iso.18013.5.1': { family_name: 'Jones', diff --git a/packages/core/src/modules/mdoc/__tests__/mdocProximityDeviceResponse.test.ts b/packages/core/src/modules/mdoc/__tests__/mdocProximityDeviceResponse.test.ts index 31ada3db0a..fa61e33f44 100644 --- a/packages/core/src/modules/mdoc/__tests__/mdocProximityDeviceResponse.test.ts +++ b/packages/core/src/modules/mdoc/__tests__/mdocProximityDeviceResponse.test.ts @@ -1,10 +1,9 @@ import { DeviceRequest, cborEncode, parseDeviceResponse } from '@animo-id/mdoc' -import { Key as AskarKey, Jwk } from '@openwallet-foundation/askar-nodejs' -import { Agent, KeyType } from '../../..' -import { getInMemoryAgentOptions } from '../../../../tests' -import { getJwkFromJson } from '../../../crypto/jose/jwk/transform' -import { Buffer, TypedArrayEncoder } from '../../../utils' +import { Agent, X509Certificate } from '../../..' +import { getAgentOptions } from '../../../../tests' +import { TypedArrayEncoder } from '../../../utils' +import { PublicJwk } from '../../kms' import { Mdoc } from '../Mdoc' import { MdocDeviceResponse } from '../MdocDeviceResponse' import { namespacesMapToRecord } from '../mdocUtil' @@ -15,22 +14,21 @@ const DEVICE_JWK_PUBLIC = { y: 'oxS1OAORJ7XNUHNfVFGeM8E0RQVFxWA62fJj-sxW03c', crv: 'P-256', use: undefined, -} +} as const const DEVICE_JWK_PRIVATE = { ...DEVICE_JWK_PUBLIC, d: 'eRpAZr3eV5xMMnPG3kWjg90Y-bBff9LqmlQuk49HUtA', -} +} as const -// biome-ignore lint/suspicious/noExportsInTest: -export const ISSUER_PRIVATE_KEY_JWK = { +const ISSUER_PRIVATE_KEY_JWK = { kty: 'EC', kid: '1234', x: 'iTwtg0eQbcbNabf2Nq9L_VM_lhhPCq2s0Qgw2kRx29s', y: 'YKwXDRz8U0-uLZ3NSI93R_35eNkl6jHp6Qg8OCup7VM', crv: 'P-256', d: 'o6PrzBm1dCfSwqJHW6DVqmJOCQSIAosrCPfbFJDMNp4', -} +} as const const ISSUER_CERTIFICATE = `-----BEGIN CERTIFICATE----- MIICKjCCAdCgAwIBAgIUV8bM0wi95D7KN0TyqHE42ru4hOgwCgYIKoZIzj0EAwIw @@ -80,28 +78,27 @@ describe('mdoc device-response proximity test', () => { let agent: Agent beforeEach(async () => { - agent = new Agent(getInMemoryAgentOptions('mdoc-test-agent', {})) + agent = new Agent(getAgentOptions('mdoc-test-agent', {})) await agent.initialize() - const devicePrivateAskar = AskarKey.fromJwk({ jwk: Jwk.fromJson(DEVICE_JWK_PRIVATE) }) - await agent.context.wallet.createKey({ - keyType: KeyType.P256, - privateKey: Buffer.from(devicePrivateAskar.secretBytes), + const importedDeviceKey = await agent.kms.importKey({ + privateJwk: DEVICE_JWK_PRIVATE, }) - const issuerPrivateAskar = AskarKey.fromJwk({ jwk: Jwk.fromJson(ISSUER_PRIVATE_KEY_JWK) }) - await agent.context.wallet.createKey({ - keyType: KeyType.P256, - privateKey: Buffer.from(issuerPrivateAskar.secretBytes), + const importedIssuerKey = await agent.kms.importKey({ + privateJwk: ISSUER_PRIVATE_KEY_JWK, }) + const issuerCertificate = X509Certificate.fromEncodedCertificate(ISSUER_CERTIFICATE) + issuerCertificate.publicJwk.keyId = importedIssuerKey.keyId + mdoc = await Mdoc.sign(agent.context, { docType: 'org.iso.18013.5.1.mDL', validityInfo: { signed: new Date('2023-10-24'), validUntil: new Date('2050-10-24'), }, - holderKey: getJwkFromJson(DEVICE_JWK_PUBLIC).key, - issuerCertificate: ISSUER_CERTIFICATE, + holderKey: PublicJwk.fromPublicJwk(importedDeviceKey.publicJwk), + issuerCertificate, namespaces: { 'org.iso.18013.5.1': { family_name: 'Jones', diff --git a/packages/core/src/modules/mdoc/__tests__/mdocServer.test.ts b/packages/core/src/modules/mdoc/__tests__/mdocServer.test.ts index 857bb17990..9843cdbff9 100644 --- a/packages/core/src/modules/mdoc/__tests__/mdocServer.test.ts +++ b/packages/core/src/modules/mdoc/__tests__/mdocServer.test.ts @@ -1,27 +1,15 @@ -import type { AgentContext } from '../../../agent' - -import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' import { getAgentConfig, getAgentContext } from '../../../../tests' -import { KeyType } from '../../../crypto' import { X509ModuleConfig, X509Service } from '../../x509' import { Mdoc } from '../Mdoc' +import { KeyManagementApi, P256PublicJwk, PublicJwk } from '../../kms' import { MdocDeviceResponse } from '../MdocDeviceResponse' import { sprindFunkeTestVectorBase64Url, sprindFunkeX509TrustedCertificate } from './mdoc.fixtures' +const agentConfig = getAgentConfig('mdoc') +const agentContext = getAgentContext({ registerInstances: [[X509ModuleConfig, new X509ModuleConfig()]], agentConfig }) +const kms = agentContext.resolve(KeyManagementApi) describe('mdoc service test', () => { - let wallet: InMemoryWallet - let agentContext: AgentContext - - beforeAll(async () => { - const agentConfig = getAgentConfig('mdoc') - wallet = new InMemoryWallet() - agentContext = getAgentContext({ wallet, registerInstances: [[X509ModuleConfig, new X509ModuleConfig()]] }) - - // biome-ignore lint/style/noNonNullAssertion: - await wallet.createAndOpen(agentConfig.walletConfig!) - }) - test('can get issuer-auth protected-header alg', async () => { const mdoc = Mdoc.fromBase64Url(sprindFunkeTestVectorBase64Url) expect(mdoc.alg).toBe('ES256') @@ -35,16 +23,22 @@ describe('mdoc service test', () => { test('can get device key', async () => { const mdoc = Mdoc.fromBase64Url(sprindFunkeTestVectorBase64Url) const deviceKey = mdoc.deviceKey - expect(deviceKey?.keyType).toBe(KeyType.P256) + expect(deviceKey?.is(P256PublicJwk)).toBe(true) expect(deviceKey?.fingerprint).toBe('zDnaeq8nbXthvXNTYAzxdyvdWXgm5ev5xLEUtjZpfj1YtQ5g2') }) test('can create and verify mdoc', async () => { - const holderKey = await agentContext.wallet.createKey({ - keyType: KeyType.P256, + const holderKey = await kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, }) - const issuerKey = await agentContext.wallet.createKey({ - keyType: KeyType.P256, + const issuerKey = await kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, }) const currentDate = new Date() @@ -53,7 +47,7 @@ describe('mdoc service test', () => { nextDay.setDate(currentDate.getDate() + 2) const certificate = await X509Service.createCertificate(agentContext, { - authorityKey: issuerKey, + authorityKey: PublicJwk.fromPublicJwk(issuerKey.publicJwk), validity: { notBefore: currentDate, notAfter: nextDay, @@ -61,18 +55,16 @@ describe('mdoc service test', () => { issuer: 'C=DE', }) - const issuerCertificate = certificate.toString('pem') - const mdoc = await Mdoc.sign(agentContext, { docType: 'org.iso.18013.5.1.mDL', - holderKey: holderKey, + holderKey: PublicJwk.fromPublicJwk(holderKey.publicJwk), namespaces: { hello: { world: 'world', nicer: 'dicer', }, }, - issuerCertificate, + issuerCertificate: certificate, }) expect(mdoc.alg).toBe('ES256') @@ -93,11 +85,17 @@ describe('mdoc service test', () => { }) test('throws error when mdoc is invalid (missing C= in cert)', async () => { - const holderKey = await agentContext.wallet.createKey({ - keyType: KeyType.P256, + const holderKey = await kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, }) - const issuerKey = await agentContext.wallet.createKey({ - keyType: KeyType.P256, + const issuerKey = await kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, }) const currentDate = new Date() @@ -106,7 +104,7 @@ describe('mdoc service test', () => { nextDay.setDate(currentDate.getDate() + 2) const certificate = await X509Service.createCertificate(agentContext, { - authorityKey: issuerKey, + authorityKey: PublicJwk.fromPublicJwk(issuerKey.publicJwk), validity: { notBefore: currentDate, notAfter: nextDay, @@ -114,18 +112,16 @@ describe('mdoc service test', () => { issuer: { commonName: 'hello' }, }) - const issuerCertificate = certificate.toString('pem') - const mdoc = await Mdoc.sign(agentContext, { docType: 'org.iso.18013.5.1.mDL', - holderKey: holderKey, + holderKey: PublicJwk.fromPublicJwk(holderKey.publicJwk), namespaces: { hello: { world: 'world', nicer: 'dicer', }, }, - issuerCertificate, + issuerCertificate: certificate, }) expect(mdoc.alg).toBe('ES256') diff --git a/packages/core/src/modules/mdoc/mdocSupportedAlgs.ts b/packages/core/src/modules/mdoc/mdocSupportedAlgs.ts index afceae62f1..a71b990ee6 100644 --- a/packages/core/src/modules/mdoc/mdocSupportedAlgs.ts +++ b/packages/core/src/modules/mdoc/mdocSupportedAlgs.ts @@ -1,13 +1,15 @@ -import { JwaSignatureAlgorithm } from '../../crypto' +import { KnownJwaSignatureAlgorithm, KnownJwaSignatureAlgorithms } from '../kms' export type MdocSupportedSignatureAlgorithm = (typeof mdocSupporteSignatureAlgorithms)[number] export const mdocSupporteSignatureAlgorithms = [ - JwaSignatureAlgorithm.ES256, - JwaSignatureAlgorithm.ES384, - JwaSignatureAlgorithm.ES512, - JwaSignatureAlgorithm.EdDSA, -] satisfies JwaSignatureAlgorithm[] + KnownJwaSignatureAlgorithms.ES256, + KnownJwaSignatureAlgorithms.ES384, + KnownJwaSignatureAlgorithms.ES512, + KnownJwaSignatureAlgorithms.EdDSA, +] satisfies KnownJwaSignatureAlgorithm[] -export function isMdocSupportedSignatureAlgorithm(alg: JwaSignatureAlgorithm): alg is MdocSupportedSignatureAlgorithm { +export function isMdocSupportedSignatureAlgorithm( + alg: KnownJwaSignatureAlgorithm +): alg is MdocSupportedSignatureAlgorithm { return mdocSupporteSignatureAlgorithms.includes(alg as MdocSupportedSignatureAlgorithm) } diff --git a/packages/core/src/modules/mdoc/repository/MdocRecord.ts b/packages/core/src/modules/mdoc/repository/MdocRecord.ts index c7823be214..17a32b2ce4 100644 --- a/packages/core/src/modules/mdoc/repository/MdocRecord.ts +++ b/packages/core/src/modules/mdoc/repository/MdocRecord.ts @@ -1,10 +1,10 @@ import type { TagsBase } from '../../../storage/BaseRecord' import type { Constructable } from '../../../utils/mixins' -import { type JwaSignatureAlgorithm } from '../../../crypto' import { BaseRecord } from '../../../storage/BaseRecord' import { JsonTransformer } from '../../../utils' import { uuid } from '../../../utils/uuid' +import { KnownJwaSignatureAlgorithm } from '../../kms' import { Mdoc } from '../Mdoc' export type DefaultMdocRecordTags = { @@ -14,7 +14,7 @@ export type DefaultMdocRecordTags = { * * The Jwa Signature Algorithm used to sign the Mdoc. */ - alg: JwaSignatureAlgorithm + alg: KnownJwaSignatureAlgorithm } export type MdocRecordStorageProps = { diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcOptions.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcOptions.ts index 3ecd613515..d65fd92bf1 100644 --- a/packages/core/src/modules/sd-jwt-vc/SdJwtVcOptions.ts +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcOptions.ts @@ -1,5 +1,6 @@ -import type { HashName, Jwk, JwkJson } from '../../crypto' -import type { EncodedX509Certificate } from '../x509' +import type { HashName } from '../../crypto' +import { PublicJwk } from '../kms' +import type { EncodedX509Certificate, X509Certificate } from '../x509' // TODO: extend with required claim names for input (e.g. vct) export type SdJwtVcPayload = Record @@ -22,24 +23,26 @@ export interface SdJwtVcHolderDidBinding { export interface SdJwtVcHolderJwkBinding { method: 'jwk' - jwk: JwkJson | Jwk + jwk: PublicJwk } export interface SdJwtVcIssuerDid { method: 'did' + // didUrl referencing a specific key in a did document. didUrl: string } export interface SdJwtVcIssuerX5c { method: 'x5c' + /** * * Array of base64-encoded certificate strings in the DER-format. * * The certificate containing the public key corresponding to the key used to digitally sign the JWS MUST be the first certificate. */ - x5c: string[] + x5c: X509Certificate[] /** * The issuer of the JWT. Should be a HTTPS URI. diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts index 2b57b95f98..e03b6dd35c 100644 --- a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts @@ -1,6 +1,5 @@ import type { SDJwt } from '@sd-jwt/core' import type { DisclosureFrame, PresentationFrame, Signer, Verifier } from '@sd-jwt/types' -import type { JwkJson, Key } from '../../crypto' import type { Query, QueryOptions } from '../../storage/StorageService' import type { SdJwtVcHeader, @@ -15,21 +14,21 @@ import type { import { decodeSdJwtSync } from '@sd-jwt/decode' import { selectDisclosures } from '@sd-jwt/present' import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc' -import { uint8ArrayToBase64Url } from '@sd-jwt/utils' import { injectable } from 'tsyringe' import { AgentContext } from '../../agent' -import { Hasher, Jwk, JwtPayload, getJwkFromJson, getJwkFromKey } from '../../crypto' +import { Hasher, JwtPayload } from '../../crypto' import { CredoError } from '../../error' import { X509Service } from '../../modules/x509/X509Service' import { JsonObject } from '../../types' import { TypedArrayEncoder, nowInSeconds } from '../../utils' import { getDomainFromUrl } from '../../utils/domain' import { fetchWithTimeout } from '../../utils/fetch' -import { DidResolverService, getKeyFromVerificationMethod, parseDid } from '../dids' +import { DidResolverService, DidsApi, getPublicJwkFromVerificationMethod, parseDid } from '../dids' import { ClaimFormat } from '../vc/index' import { EncodedX509Certificate, X509Certificate, X509ModuleConfig } from '../x509' +import { Jwk, KeyManagementApi, PublicJwk } from '../kms' import { SdJwtVcError } from './SdJwtVcError' import { decodeSdJwtVc, sdJwtVcHasher } from './decodeSdJwtVc' import { buildDisclosureFrameForPayload } from './disclosureFrame' @@ -66,7 +65,7 @@ export interface SdJwtVc< } export interface CnfPayload { - jwk?: JwkJson + jwk?: Jwk kid?: string } @@ -102,7 +101,7 @@ export class SdJwtVcService { throw new SdJwtVcError(`Unsupported hashing algorithm used: ${hashingAlgorithm}`) } - const issuer = await this.extractKeyFromIssuer(agentContext, options.issuer) + const issuer = await this.extractKeyFromIssuer(agentContext, options.issuer, true) // holer binding is optional const holderBinding = options.holder @@ -113,12 +112,12 @@ export class SdJwtVcService { alg: issuer.alg, typ: options.headerType ?? 'dc+sd-jwt', kid: issuer.kid, - x5c: issuer.x5c, + x5c: issuer.x5c?.map((cert) => cert.toString('base64')), } as const const sdjwt = new SDJwtVcInstance({ ...this.getBaseSdJwtConfig(agentContext), - signer: this.signer(agentContext, issuer.key), + signer: this.signer(agentContext, issuer.publicJwk), hashAlg: 'sha-256', signAlg: issuer.alg, }) @@ -202,9 +201,9 @@ export class SdJwtVcService { throw new SdJwtVcError("Verifier metadata provided, but credential has no 'cnf' claim to create a KB-JWT from") } - const holder = holderBinding ? await this.extractKeyFromHolderBinding(agentContext, holderBinding) : undefined + const holder = holderBinding ? await this.extractKeyFromHolderBinding(agentContext, holderBinding, true) : undefined sdjwt.config({ - kbSigner: holder ? this.signer(agentContext, holder.key) : undefined, + kbSigner: holder ? this.signer(agentContext, holder.publicJwk) : undefined, kbSignAlg: holder?.alg, }) @@ -296,8 +295,8 @@ export class SdJwtVcService { const holder = holderBinding ? await this.extractKeyFromHolderBinding(agentContext, holderBinding) : undefined sdjwt.config({ - verifier: this.verifier(agentContext, issuer.key), - kbVerifier: holder ? this.verifier(agentContext, holder.key) : undefined, + verifier: this.verifier(agentContext, issuer.publicJwk), + kbVerifier: holder ? this.verifier(agentContext, holder.publicJwk) : undefined, }) const requiredKeys = requiredClaimKeys ? [...requiredClaimKeys, 'vct'] : ['vct'] @@ -435,6 +434,13 @@ export class SdJwtVcService { await this.sdJwtVcRepository.update(agentContext, sdJwtVcRecord) } + private async resolveSigningPublicJwkFromDidUrl(agentContext: AgentContext, didUrl: string) { + const dids = agentContext.dependencyManager.resolve(DidsApi) + + const { publicJwk } = await dids.resolveVerificationMethodFromCreatedDidRecord(didUrl) + return publicJwk + } + private async resolveDidUrl(agentContext: AgentContext, didUrl: string) { const didResolver = agentContext.dependencyManager.resolve(DidResolverService) const didDocument = await didResolver.resolveDidDocument(agentContext, didUrl) @@ -448,31 +454,39 @@ export class SdJwtVcService { /** * @todo validate the JWT header (alg) */ - private signer(agentContext: AgentContext, key: Key): Signer { + private signer(agentContext: AgentContext, key: PublicJwk): Signer { + const kms = agentContext.resolve(KeyManagementApi) + return async (input: string) => { - const signedBuffer = await agentContext.wallet.sign({ key, data: TypedArrayEncoder.fromString(input) }) - return uint8ArrayToBase64Url(signedBuffer) + const result = await kms.sign({ + keyId: key.keyId, + data: TypedArrayEncoder.fromString(input), + algorithm: key.signatureAlgorithm, + }) + + return TypedArrayEncoder.toBase64URL(result.signature) } } /** * @todo validate the JWT header (alg) */ - private verifier(agentContext: AgentContext, key: Key): Verifier { - return async (message: string, signatureBase64Url: string) => { - if (!key) { - throw new SdJwtVcError('The public key used to verify the signature is missing') - } + private verifier(agentContext: AgentContext, key: PublicJwk): Verifier { + const kms = agentContext.resolve(KeyManagementApi) - return await agentContext.wallet.verify({ + return async (message: string, signatureBase64Url: string) => { + const result = await kms.verify({ signature: TypedArrayEncoder.fromBase64(signatureBase64Url), - key, + key: key.toJson(), data: TypedArrayEncoder.fromString(message), + algorithm: key.signatureAlgorithm, }) + + return result.verified } } - private async extractKeyFromIssuer(agentContext: AgentContext, issuer: SdJwtVcIssuer) { + private async extractKeyFromIssuer(agentContext: AgentContext, issuer: SdJwtVcIssuer, forSigning = false) { if (issuer.method === 'did') { const parsedDid = parseDid(issuer.didUrl) if (!parsedDid.fragment) { @@ -481,35 +495,55 @@ export class SdJwtVcService { ) } - const { verificationMethod } = await this.resolveDidUrl(agentContext, issuer.didUrl) - const key = getKeyFromVerificationMethod(verificationMethod) - const supportedSignatureAlgorithms = getJwkFromKey(key).supportedSignatureAlgorithms + let publicJwk: PublicJwk + if (forSigning) { + publicJwk = await this.resolveSigningPublicJwkFromDidUrl(agentContext, issuer.didUrl) + } else { + const { verificationMethod } = await this.resolveDidUrl(agentContext, issuer.didUrl) + publicJwk = getPublicJwkFromVerificationMethod(verificationMethod) + } + + const supportedSignatureAlgorithms = publicJwk.supportedSignatureAlgorithms if (supportedSignatureAlgorithms.length === 0) { - throw new SdJwtVcError(`No supported JWA signature algorithms found for key with keyType ${key.keyType}`) + throw new SdJwtVcError( + `No supported JWA signature algorithms found for key ${publicJwk.jwkTypehumanDescription}` + ) } const alg = supportedSignatureAlgorithms[0] return { alg, - key, + publicJwk, iss: parsedDid.did, kid: `#${parsedDid.fragment}`, } } + // FIXME: probably need to make the input an x509 certificate so we can attach a key id if (issuer.method === 'x5c') { - const leafCertificate = X509Service.getLeafCertificate(agentContext, { certificateChain: issuer.x5c }) - const key = leafCertificate.publicKey - const supportedSignatureAlgorithms = getJwkFromKey(key).supportedSignatureAlgorithms + const leafCertificate = issuer.x5c[0] + if (!leafCertificate) { + throw new SdJwtVcError("Empty 'x5c' array provided") + } + + // TODO: We don't have an x509 certificate record so we expect the key id to already be set + if (forSigning && !leafCertificate.publicJwk.hasKeyId) { + throw new SdJwtVcError("Expected leaf certificate in 'x5c' array to have a key id configured.") + } + + const publicJwk = leafCertificate.publicJwk + const supportedSignatureAlgorithms = publicJwk.supportedSignatureAlgorithms if (supportedSignatureAlgorithms.length === 0) { - throw new SdJwtVcError(`No supported JWA signature algorithms found for key with keyType ${key.keyType}`) + throw new SdJwtVcError( + `No supported JWA signature algorithms found for key ${publicJwk.jwkTypehumanDescription}` + ) } const alg = supportedSignatureAlgorithms[0] this.assertValidX5cJwtIssuer(agentContext, issuer.issuer, leafCertificate) return { - key, + publicJwk, iss: issuer.issuer, x5c: issuer.x5c, alg, @@ -574,7 +608,7 @@ export class SdJwtVcService { return { method: 'x5c', - x5c: sdJwtVc.jwt.header.x5c, + x5c: certificateChain, issuer: iss, } } @@ -623,7 +657,7 @@ export class SdJwtVcService { sdJwtVc: SDJwt ): SdJwtVcHolderBinding | null { if (!sdJwtVc.jwt?.payload) { - throw new SdJwtVcError('Credential not exist') + throw new SdJwtVcError('Unable to extract payload from SD-JWT VC') } if (!sdJwtVc.jwt?.payload.cnf) { @@ -634,7 +668,7 @@ export class SdJwtVcService { if (cnf.jwk) { return { method: 'jwk', - jwk: cnf.jwk, + jwk: PublicJwk.fromUnknown(cnf.jwk), } } if (cnf.kid) { @@ -650,7 +684,11 @@ export class SdJwtVcService { throw new SdJwtVcError("Unsupported credential holder binding. Only 'did' and 'jwk' are supported at the moment.") } - private async extractKeyFromHolderBinding(agentContext: AgentContext, holder: SdJwtVcHolderBinding) { + private async extractKeyFromHolderBinding( + agentContext: AgentContext, + holder: SdJwtVcHolderBinding, + forSigning = false + ) { if (holder.method === 'did') { const parsedDid = parseDid(holder.didUrl) if (!parsedDid.fragment) { @@ -659,17 +697,25 @@ export class SdJwtVcService { ) } - const { verificationMethod } = await this.resolveDidUrl(agentContext, holder.didUrl) - const key = getKeyFromVerificationMethod(verificationMethod) - const supportedSignatureAlgorithms = getJwkFromKey(key).supportedSignatureAlgorithms + let publicJwk: PublicJwk + if (forSigning) { + publicJwk = await this.resolveSigningPublicJwkFromDidUrl(agentContext, holder.didUrl) + } else { + const { verificationMethod } = await this.resolveDidUrl(agentContext, holder.didUrl) + publicJwk = getPublicJwkFromVerificationMethod(verificationMethod) + } + + const supportedSignatureAlgorithms = publicJwk.supportedSignatureAlgorithms if (supportedSignatureAlgorithms.length === 0) { - throw new SdJwtVcError(`No supported JWA signature algorithms found for key with keyType ${key.keyType}`) + throw new SdJwtVcError( + `No supported JWA signature algorithms found for key ${publicJwk.jwkTypehumanDescription}` + ) } const alg = supportedSignatureAlgorithms[0] return { alg, - key, + publicJwk, cnf: { // We need to include the whole didUrl here, otherwise the verifier // won't know which did it is associated with @@ -678,15 +724,20 @@ export class SdJwtVcService { } } if (holder.method === 'jwk') { - const jwk = holder.jwk instanceof Jwk ? holder.jwk : getJwkFromJson(holder.jwk) - const key = jwk.key - const alg = jwk.supportedSignatureAlgorithms[0] + const publicJwk = holder.jwk + const alg = publicJwk.supportedSignatureAlgorithms[0] + + // If there is no key id configured when signing, we assume this credential was issued before we included key ids + // and the we use the legacy key id. + if (forSigning && !publicJwk.hasKeyId) { + publicJwk.keyId = publicJwk.legacyKeyId + } return { alg, - key, + publicJwk, cnf: { - jwk: jwk.toJson(), + jwk: publicJwk.toJson(), }, } } @@ -695,10 +746,12 @@ export class SdJwtVcService { } private getBaseSdJwtConfig(agentContext: AgentContext): SdJwtVcConfig { + const kms = agentContext.resolve(KeyManagementApi) + return { hasher: sdJwtVcHasher, statusListFetcher: this.getStatusListFetcher(agentContext), - saltGenerator: agentContext.wallet.generateNonce, + saltGenerator: (length) => TypedArrayEncoder.toBase64URL(kms.randomBytes({ length }).bytes).slice(0, length), } } diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts index f1a82a12c1..1c2ac1b9d9 100644 --- a/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts @@ -1,11 +1,11 @@ -import type { AgentContext, Jwk, Key } from '@credo-ts/core' +import type { AgentContext } from '@credo-ts/core' import type { SdJwtVcHeader } from '../SdJwtVcOptions' import { randomUUID } from 'crypto' import { StatusList, createHeaderAndPayload } from '@sd-jwt/jwt-status-list' import { SDJWTException } from '@sd-jwt/utils' -import { agentDependencies, getInMemoryAgentOptions } from '../../../../tests' +import { agentDependencies, getAgentOptions } from '../../../../tests' import * as fetchUtils from '../../../utils/fetch' import { SdJwtVcService } from '../SdJwtVcService' import { SdJwtVcRepository } from '../repository' @@ -37,22 +37,17 @@ import { JwtPayload, KeyDidRegistrar, KeyDidResolver, - KeyType, TypedArrayEncoder, + X509Certificate, X509ModuleConfig, getDomainFromUrl, - getJwkFromKey, parseDid, } from '@credo-ts/core' - -const jwkJsonWithoutUse = (jwk: Jwk) => { - const jwkJson = jwk.toJson() - jwkJson.use = undefined - return jwkJson -} +import { transformSeedToPrivateJwk } from '../../../../../askar/src' +import { PublicJwk } from '../../kms' const agent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'sdjwtvcserviceagent', {}, {}, @@ -65,15 +60,17 @@ const agent = new Agent( ) ) -agent.context.wallet.generateNonce = jest.fn(() => Promise.resolve('salt')) +agent.kms.randomBytes = jest.fn(() => ({ bytes: TypedArrayEncoder.fromString('salt') })) Date.prototype.getTime = jest.fn(() => 1698151532000) jest.mock('../repository/SdJwtVcRepository') const SdJwtVcRepositoryMock = SdJwtVcRepository as jest.Mock +const simpleX509Certificate = X509Certificate.fromEncodedCertificate(simpleX509.trustedCertficate) + const generateStatusList = async ( agentContext: AgentContext, - key: Key, + key: PublicJwk, issuerDidUrl: string, length: number, revokedIndexes: number[] @@ -100,40 +97,88 @@ const generateStatusList = async ( const jwsService = agentContext.dependencyManager.resolve(JwsService) return jwsService.createJwsCompact(agentContext, { - key, + keyId: key.keyId, payload: JwtPayload.fromJson(payload), - protectedHeaderOptions: header, + protectedHeaderOptions: { + ...header, + alg: 'EdDSA', + }, }) } describe('SdJwtVcService', () => { const verifierDid = 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y' let issuerDidUrl: string - let issuerKey: Key - let holderKey: Key + let issuerKey: PublicJwk + let holderKey: PublicJwk let sdJwtVcService: SdJwtVcService beforeAll(async () => { await agent.initialize() - issuerKey = await agent.context.wallet.createKey({ - keyType: KeyType.Ed25519, + const issuerPrivateJwk = transformSeedToPrivateJwk({ seed: TypedArrayEncoder.fromString('00000000000000000000000000000000'), - }) + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }).privateJwk + issuerKey = PublicJwk.fromPublicJwk( + ( + await agent.kms.importKey({ + privateJwk: issuerPrivateJwk, + }) + ).publicJwk + ) const issuerDidKey = new DidKey(issuerKey) const issuerDidDocument = issuerDidKey.didDocument issuerDidUrl = (issuerDidDocument.verificationMethod ?? [])[0].id - await agent.dids.import({ didDocument: issuerDidDocument, did: issuerDidDocument.id }) - - holderKey = await agent.context.wallet.createKey({ - keyType: KeyType.Ed25519, - seed: TypedArrayEncoder.fromString('00000000000000000000000000000001'), + await agent.dids.import({ + didDocument: issuerDidDocument, + did: issuerDidDocument.id, + keys: [ + { + didDocumentRelativeKeyId: `#${issuerDidUrl.split('#')[1]}`, + kmsKeyId: issuerKey.keyId, + }, + ], }) + simpleX509Certificate.keyId = issuerKey.keyId + + const holderPrivateJwk = transformSeedToPrivateJwk({ + seed: TypedArrayEncoder.fromString('00000000000000000000000000000001'), + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }).privateJwk + + // We use hardcoded SD-JWT VCs which don't have a `kid` in the credential JWK + // So we set the kid to the legacy key id + holderPrivateJwk.kid = TypedArrayEncoder.toBase58(PublicJwk.fromPublicJwk(holderPrivateJwk).publicKey.publicKey) + + holderKey = PublicJwk.fromPublicJwk( + ( + await agent.kms.importKey({ + privateJwk: holderPrivateJwk, + }) + ).publicJwk + ) const holderDidKey = new DidKey(holderKey) const holderDidDocument = holderDidKey.didDocument - await agent.dids.import({ didDocument: holderDidDocument, did: holderDidDocument.id }) + const holderDidUrl = (holderDidDocument.verificationMethod ?? [])[0].id + await agent.dids.import({ + didDocument: holderDidDocument, + did: holderDidDocument.id, + keys: [ + { + kmsKeyId: holderKey.keyId, + didDocumentRelativeKeyId: `#${holderDidUrl.split('#')[1]}`, + }, + ], + }) const sdJwtVcRepositoryMock = new SdJwtVcRepositoryMock() sdJwtVcService = new SdJwtVcService(sdJwtVcRepositoryMock) @@ -149,11 +194,11 @@ describe('SdJwtVcService', () => { }, holder: { method: 'jwk', - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey, }, issuer: { method: 'x5c', - x5c: [simpleX509.trustedCertficate], + x5c: [simpleX509Certificate], issuer: 'some-issuer', }, }) @@ -168,11 +213,11 @@ describe('SdJwtVcService', () => { }, holder: { method: 'jwk', - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey, }, issuer: { method: 'x5c', - x5c: [simpleX509.trustedCertficate], + x5c: [simpleX509Certificate], issuer: simpleX509.certificateIssuer, }, headerType: 'vc+sd-jwt', @@ -192,7 +237,7 @@ describe('SdJwtVcService', () => { vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), iss: simpleX509.certificateIssuer, - cnf: { jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)) }, + cnf: { jwk: holderKey.toJson() }, }) }) @@ -206,7 +251,7 @@ describe('SdJwtVcService', () => { // FIXME: is it nicer API to just pass either didUrl or JWK? // Or none if you don't want to bind it? method: 'jwk', - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey, }, issuer: { method: 'did', @@ -231,7 +276,7 @@ describe('SdJwtVcService', () => { iat: Math.floor(new Date().getTime() / 1000), iss: parseDid(issuerDidUrl).did, cnf: { - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey.toJson(), }, }) }) @@ -276,10 +321,8 @@ describe('SdJwtVcService', () => { discloseableValue: false, }, holder: { - // FIXME: is it nicer API to just pass either didUrl or JWK? - // Or none if you don't want to bind it? method: 'jwk', - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey, }, issuer: { method: 'did', @@ -303,7 +346,7 @@ describe('SdJwtVcService', () => { value: false, discloseableValue: false, cnf: { - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey.toJson(), }, }) }) @@ -314,7 +357,7 @@ describe('SdJwtVcService', () => { disclosureFrame: { _sd: ['claim'] }, holder: { method: 'jwk', - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey, }, issuer: { method: 'did', @@ -335,10 +378,10 @@ describe('SdJwtVcService', () => { vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), iss: issuerDidUrl.split('#')[0], - _sd: ['vcvFU4DsFKTqQ1vl4nelJWXTb_-0dNoBks6iqNFptyg'], + _sd: ['LHLZVlumA3_k-zntrSL6ocULVh_uz0PQoupZS4hu15M'], _sd_alg: 'sha-256', cnf: { - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey.toJson(), }, }) @@ -348,7 +391,7 @@ describe('SdJwtVcService', () => { iss: issuerDidUrl.split('#')[0], claim: 'some-claim', cnf: { - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey.toJson(), }, }) }) @@ -380,7 +423,7 @@ describe('SdJwtVcService', () => { }, holder: { method: 'jwk', - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey, }, issuer: { method: 'did', @@ -401,7 +444,7 @@ describe('SdJwtVcService', () => { vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), address: { - _sd: ['NJnmct0BqBME1JfBlC6jRQVRuevpEONiYw7A7MHuJyQ', 'om5ZztZHB-Gd00LG21CV_xM4FaENSoiaOXnTAJNczB4'], + _sd: ['8Kl-6KGl7JjFrlN0ZKDPKzeRfo0oJ5Tv0F6cXgpmOCY', 'cxH6g51BOh8vDiQXW88Kq896DEVLZZ4mbuLO6z__5ds'], locality: 'Anytown', street_address: '123 Main St', }, @@ -409,16 +452,16 @@ describe('SdJwtVcService', () => { family_name: 'Doe', iss: issuerDidUrl.split('#')[0], _sd: [ - '1Cur2k2A2oIB5CshSIf_A_Kg-l26u_qKuWQ79P0Vdas', - 'R1zTUvOYHgcepj0jHypGHz9EHttVKft0yswbc9ETPbU', - 'eDqQpdTXJXbWhf-EsI7zw5X6OvYmFN-UZQQMesXwKPw', - 'pdDk2_XAKHo7gOAfwF1b7OdCUVTit2kJHaxSECQ9xfc', - 'psauKUNWEi09nu3Cl89xKXgmpWENZl5uy1N1nyn_jMk', - 'sN_ge0pHXF6qmsYnX1A9SdwJ8ch8aENkxbODsT74YwI', + '1oLbHVhfmVs2oA3vhFNTXhMw4lGu7ql9dZ0T7p-vWqE', + '2xuzS3kUrT6VPJD-MySIkQ47HIB-gcyzF5NDY19cPBw', + 'hn1gcrO_Q2HskW2Z_nzIrIl6KpgqldvScozutJdbhWM', + 'jc73t3yBoDs_pDYb03lEYKYvCbtCq9NhuJ6_5A7QNSs', + 'lKI_sY05pDIs9MDrjCO4v8XoDM963JXxrp9T2FNLyTY', + 'sl0hkY5LeVwy3rIjNaCl4P4CJ3C3v8Ip-GH2lB9Sd_A', ], _sd_alg: 'sha-256', cnf: { - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey.toJson(), }, }) @@ -441,7 +484,7 @@ describe('SdJwtVcService', () => { is_over_21: true, is_over_65: true, cnf: { - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey.toJson(), }, }) }) @@ -473,7 +516,7 @@ describe('SdJwtVcService', () => { }, holder: { method: 'jwk', - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey, }, issuer: { method: 'did', @@ -494,17 +537,17 @@ describe('SdJwtVcService', () => { family_name: 'Doe', iss: issuerDidUrl.split('#')[0], _sd: [ - '1Cur2k2A2oIB5CshSIf_A_Kg-l26u_qKuWQ79P0Vdas', - 'R1zTUvOYHgcepj0jHypGHz9EHttVKft0yswbc9ETPbU', - 'eDqQpdTXJXbWhf-EsI7zw5X6OvYmFN-UZQQMesXwKPw', - 'pdDk2_XAKHo7gOAfwF1b7OdCUVTit2kJHaxSECQ9xfc', - 'psauKUNWEi09nu3Cl89xKXgmpWENZl5uy1N1nyn_jMk', - 'sN_ge0pHXF6qmsYnX1A9SdwJ8ch8aENkxbODsT74YwI', - 'yPhxDEM7k7p7eQ9eHHC-Ca6VEA8bzebZpYu7vYmwG6c', + '1oLbHVhfmVs2oA3vhFNTXhMw4lGu7ql9dZ0T7p-vWqE', + '2xuzS3kUrT6VPJD-MySIkQ47HIB-gcyzF5NDY19cPBw', + 'RDQeb-TXvRaGsX5jV4W2-xAKutsaYZVm8qEvMtP71pc', + 'hn1gcrO_Q2HskW2Z_nzIrIl6KpgqldvScozutJdbhWM', + 'jc73t3yBoDs_pDYb03lEYKYvCbtCq9NhuJ6_5A7QNSs', + 'lKI_sY05pDIs9MDrjCO4v8XoDM963JXxrp9T2FNLyTY', + 'sl0hkY5LeVwy3rIjNaCl4P4CJ3C3v8Ip-GH2lB9Sd_A', ], _sd_alg: 'sha-256', cnf: { - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey.toJson(), }, }) @@ -527,7 +570,7 @@ describe('SdJwtVcService', () => { is_over_21: true, is_over_65: true, cnf: { - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey.toJson(), }, }) }) @@ -551,7 +594,7 @@ describe('SdJwtVcService', () => { iat: Math.floor(new Date().getTime() / 1000), iss: issuerDidUrl.split('#')[0], cnf: { - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey.toJson(), }, }) }) @@ -588,10 +631,10 @@ describe('SdJwtVcService', () => { vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), iss: issuerDidUrl.split('#')[0], - _sd: ['vcvFU4DsFKTqQ1vl4nelJWXTb_-0dNoBks6iqNFptyg'], + _sd: ['LHLZVlumA3_k-zntrSL6ocULVh_uz0PQoupZS4hu15M'], _sd_alg: 'sha-256', cnf: { - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey.toJson(), }, }) @@ -615,22 +658,22 @@ describe('SdJwtVcService', () => { family_name: 'Doe', iss: issuerDidUrl.split('#')[0], address: { - _sd: ['NJnmct0BqBME1JfBlC6jRQVRuevpEONiYw7A7MHuJyQ', 'om5ZztZHB-Gd00LG21CV_xM4FaENSoiaOXnTAJNczB4'], + _sd: ['8Kl-6KGl7JjFrlN0ZKDPKzeRfo0oJ5Tv0F6cXgpmOCY', 'cxH6g51BOh8vDiQXW88Kq896DEVLZZ4mbuLO6z__5ds'], locality: 'Anytown', street_address: '123 Main St', }, _sd_alg: 'sha-256', phone_number: '+1-202-555-0101', _sd: [ - '1Cur2k2A2oIB5CshSIf_A_Kg-l26u_qKuWQ79P0Vdas', - 'R1zTUvOYHgcepj0jHypGHz9EHttVKft0yswbc9ETPbU', - 'eDqQpdTXJXbWhf-EsI7zw5X6OvYmFN-UZQQMesXwKPw', - 'pdDk2_XAKHo7gOAfwF1b7OdCUVTit2kJHaxSECQ9xfc', - 'psauKUNWEi09nu3Cl89xKXgmpWENZl5uy1N1nyn_jMk', - 'sN_ge0pHXF6qmsYnX1A9SdwJ8ch8aENkxbODsT74YwI', + '1oLbHVhfmVs2oA3vhFNTXhMw4lGu7ql9dZ0T7p-vWqE', + '2xuzS3kUrT6VPJD-MySIkQ47HIB-gcyzF5NDY19cPBw', + 'hn1gcrO_Q2HskW2Z_nzIrIl6KpgqldvScozutJdbhWM', + 'jc73t3yBoDs_pDYb03lEYKYvCbtCq9NhuJ6_5A7QNSs', + 'lKI_sY05pDIs9MDrjCO4v8XoDM963JXxrp9T2FNLyTY', + 'sl0hkY5LeVwy3rIjNaCl4P4CJ3C3v8Ip-GH2lB9Sd_A', ], cnf: { - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey.toJson(), }, }) @@ -668,7 +711,7 @@ describe('SdJwtVcService', () => { street_address: '123 Main St', }, cnf: { - jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + jwk: holderKey.toJson(), }, }) }) @@ -715,7 +758,7 @@ describe('SdJwtVcService', () => { verifierMetadata: { issuedAt: new Date().getTime() / 1000, audience: verifierDid, - nonce: await agent.context.wallet.generateNonce(), + nonce: 'salt', }, }) @@ -753,7 +796,7 @@ describe('SdJwtVcService', () => { verifierMetadata: { issuedAt: new Date().getTime() / 1000, audience: verifierDid, - nonce: await agent.context.wallet.generateNonce(), + nonce: 'salt', }, }) @@ -772,7 +815,7 @@ describe('SdJwtVcService', () => { verifierMetadata: { issuedAt: new Date().getTime() / 1000, audience: verifierDid, - nonce: await agent.context.wallet.generateNonce(), + nonce: 'salt', }, presentationFrame: { is_over_65: true, @@ -791,7 +834,6 @@ describe('SdJwtVcService', () => { describe('SdJwtVcService.verify', () => { test('Verify sd-jwt-vc without disclosures', async () => { - const nonce = await agent.context.wallet.generateNonce() const presentation = await sdJwtVcService.present(agent.context, { compactSdJwtVc: simpleJwtVc, // no disclosures @@ -799,13 +841,13 @@ describe('SdJwtVcService', () => { verifierMetadata: { issuedAt: new Date().getTime() / 1000, audience: verifierDid, - nonce, + nonce: 'salt', }, }) const verificationResult = await sdJwtVcService.verify(agent.context, { compactSdJwtVc: presentation, - keyBinding: { audience: verifierDid, nonce }, + keyBinding: { audience: verifierDid, nonce: 'salt' }, requiredClaimKeys: ['claim'], }) @@ -826,7 +868,6 @@ describe('SdJwtVcService', () => { }) test('Verify x509 protected sd-jwt-vc without disclosures', async () => { - const nonce = await agent.context.wallet.generateNonce() const presentation = await sdJwtVcService.present(agent.context, { compactSdJwtVc: simpleX509.sdJwtVc, // no disclosures @@ -834,16 +875,16 @@ describe('SdJwtVcService', () => { verifierMetadata: { issuedAt: new Date().getTime() / 1000, audience: verifierDid, - nonce, + nonce: 'salt', }, }) const x509ModuleConfig = agent.context.dependencyManager.resolve(X509ModuleConfig) - await x509ModuleConfig.addTrustedCertificate(simpleX509.trustedCertficate) + x509ModuleConfig.addTrustedCertificate(simpleX509.trustedCertficate) const verificationResult = await sdJwtVcService.verify(agent.context, { compactSdJwtVc: presentation, - keyBinding: { audience: verifierDid, nonce }, + keyBinding: { audience: verifierDid, nonce: 'salt' }, requiredClaimKeys: ['claim'], }) @@ -1044,21 +1085,19 @@ describe('SdJwtVcService', () => { }) test('Verify sd-jwt-vc with a disclosure', async () => { - const nonce = await agent.context.wallet.generateNonce() - const presentation = await sdJwtVcService.present(agent.context, { compactSdJwtVc: sdJwtVcWithSingleDisclosure, verifierMetadata: { issuedAt: new Date().getTime() / 1000, audience: verifierDid, - nonce, + nonce: 'salt', }, presentationFrame: { claim: true }, }) const verificationResult = await sdJwtVcService.verify(agent.context, { compactSdJwtVc: presentation, - keyBinding: { audience: verifierDid, nonce }, + keyBinding: { audience: verifierDid, nonce: 'salt' }, requiredClaimKeys: ['vct', 'cnf', 'claim', 'iat'], }) @@ -1079,8 +1118,6 @@ describe('SdJwtVcService', () => { }) test('Verify sd-jwt-vc with multiple (nested) disclosure', async () => { - const nonce = await agent.context.wallet.generateNonce() - const presentation = await sdJwtVcService.present<{ is_over_65: boolean is_over_21: boolean @@ -1092,7 +1129,7 @@ describe('SdJwtVcService', () => { verifierMetadata: { issuedAt: new Date().getTime() / 1000, audience: verifierDid, - nonce, + nonce: 'salt', }, presentationFrame: { is_over_65: true, @@ -1107,7 +1144,7 @@ describe('SdJwtVcService', () => { const verificationResult = await sdJwtVcService.verify(agent.context, { compactSdJwtVc: presentation, - keyBinding: { audience: verifierDid, nonce }, + keyBinding: { audience: verifierDid, nonce: 'salt' }, // FIXME: this should be a requiredFrame to be consistent with the other methods // using frames requiredClaimKeys: [ diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.test.ts index c7cd77d7b7..632c6a3020 100644 --- a/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.test.ts +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.test.ts @@ -1,39 +1,70 @@ -import type { Key } from '@credo-ts/core' - import nock, { cleanAll } from 'nock' -import { getInMemoryAgentOptions } from '../../../../tests' +import { getAgentOptions } from '../../../../tests' + +import { Agent, DidKey, TypedArrayEncoder } from '@credo-ts/core' +import { transformSeedToPrivateJwk } from '../../../../../askar/src' +import { PublicJwk } from '../../kms' -import { Agent, DidKey, KeyType, TypedArrayEncoder, getJwkFromKey } from '@credo-ts/core' +const issuer = new Agent(getAgentOptions('sd-jwt-vc-issuer-agent')) +const holder = new Agent(getAgentOptions('sd-jwt-vc-holder-agent')) describe('sd-jwt-vc end to end test', () => { - const issuer = new Agent(getInMemoryAgentOptions('sd-jwt-vc-issuer-agent')) - let issuerKey: Key + let issuerKey: PublicJwk let issuerDidUrl: string - const holder = new Agent(getInMemoryAgentOptions('sd-jwt-vc-holder-agent')) - let holderKey: Key + let holderKey: PublicJwk - const verifier = new Agent(getInMemoryAgentOptions('sd-jwt-vc-verifier-agent')) + const verifier = new Agent(getAgentOptions('sd-jwt-vc-verifier-agent')) const verifierDid = 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y' beforeAll(async () => { await issuer.initialize() - issuerKey = await issuer.context.wallet.createKey({ - keyType: KeyType.Ed25519, + + const issuerPrivateJwk = transformSeedToPrivateJwk({ seed: TypedArrayEncoder.fromString('00000000000000000000000000000000'), - }) + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }).privateJwk + issuerKey = PublicJwk.fromPublicJwk( + ( + await issuer.kms.importKey({ + privateJwk: issuerPrivateJwk, + }) + ).publicJwk + ) const issuerDidKey = new DidKey(issuerKey) const issuerDidDocument = issuerDidKey.didDocument issuerDidUrl = (issuerDidDocument.verificationMethod ?? [])[0].id - await issuer.dids.import({ didDocument: issuerDidDocument, did: issuerDidDocument.id }) + await issuer.dids.import({ + didDocument: issuerDidDocument, + did: issuerDidDocument.id, + keys: [ + { + didDocumentRelativeKeyId: `#${issuerDidUrl.split('#')[1]}`, + kmsKeyId: issuerKey.keyId, + }, + ], + }) await holder.initialize() - holderKey = await holder.context.wallet.createKey({ - keyType: KeyType.Ed25519, + const holderPrivateJwk = transformSeedToPrivateJwk({ seed: TypedArrayEncoder.fromString('00000000000000000000000000000001'), - }) + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }).privateJwk + holderKey = PublicJwk.fromPublicJwk( + ( + await holder.kms.importKey({ + privateJwk: holderPrivateJwk, + }) + ).publicJwk + ) await verifier.initialize() }) @@ -63,7 +94,7 @@ describe('sd-jwt-vc end to end test', () => { payload: credential, holder: { method: 'jwk', - jwk: getJwkFromKey(holderKey), + jwk: holderKey, }, issuer: { didUrl: issuerDidUrl, @@ -97,6 +128,7 @@ describe('sd-jwt-vc end to end test', () => { claimFormat: 'vc+sd-jwt', compact: expect.any(String), encoded: expect.any(String), + kbJwt: undefined, header: { alg: 'EdDSA', kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', @@ -128,6 +160,7 @@ describe('sd-jwt-vc end to end test', () => { }, cnf: { jwk: { + kid: expect.any(String), crv: 'Ed25519', kty: 'OKP', x: 'oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo', @@ -147,6 +180,7 @@ describe('sd-jwt-vc end to end test', () => { birthdate: '1940-01-01', cnf: { jwk: { + kid: expect.any(String), crv: 'Ed25519', kty: 'OKP', x: 'oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo', @@ -183,7 +217,7 @@ describe('sd-jwt-vc end to end test', () => { const verifierMetadata = { audience: verifierDid, issuedAt: new Date().getTime() / 1000, - nonce: await verifier.wallet.generateNonce(), + nonce: TypedArrayEncoder.toBase64URL(verifier.kms.randomBytes({ length: 32 }).bytes), } const presentation = await holder.sdJwtVc.present({ diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts index d7ba91d69c..db1cb7df53 100644 --- a/packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts @@ -25,11 +25,11 @@ } */ export const simpleJwtVc = - 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.vLkigrBr1IIVRJeYE5DQx0rKUVzO3KT9T0XBATWJE89pWCyvB3Rzs8VD7qfi0vDk_QVCPIiHq1U1PsmSe4ZqCg~' + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImtpZCI6IkJuYm5RVzVWV295czZ4NnFZeEVVVnJFS0dZVzJHUzV2RzcxdkNNd3dmc1ltIiwiY3J2IjoiRWQyNTUxOSIsIngiOiJvRU5Wc3hPVWlINTRYOHdKTGFWa2ljQ1JrMDB3QklRNHNSZ2JrNTROOE1vIn19LCJpc3MiOiJkaWQ6a2V5Ono2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyIsImlhdCI6MTY5ODE1MTUzMn0.mVaO61d9YYRbzWATBztGw2axg-2zjYWtNp5BVxhoi4RW6VQGjlJtn8OY7j8RLnkcMYKVvakQO56_Rco-vy2kAA~' export const simpleX509 = { sdJwtVc: - 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsIng1YyI6WyJNSUhlTUlHUm9BTUNBUUlDRUIwYW80ZVVBZUhrQjg2dzhmSUVuR2N3QlFZREsyVndNQUF3SGhjTk1qUXdOekUyTVRNek5URTNXaGNOTWpReE1ESXpNVEkwTlRNeVdqQUFNQ293QlFZREsyVndBeUVBMWMra1AwdFlodlN2LzJCdzdvSlFiQ1dZT2JUY0IyS1VPVHB3K0x0TG85dWpJVEFmTUIwR0ExVWRFUVFXTUJTR0VtaDBkSEJ6T2k4dmFYTnpkV1Z5TG1OdmJUQUZCZ01yWlhBRFFRQkU0SmFrbTh2bjI1NUI4ZEFneWdiaFIwWlBTZkNFbmdGdWlXREJkeUFYalc2YWhpdDZtOGlsZW05MDhreGsyeUpOZ2hUSVNCbERod2tmcmx5UFJ4NE0iXX0.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuY29tIiwiaWF0IjoxNjk4MTUxNTMyfQ.d254hz7u-mziOtFA3yA9tVNNDP5_6eJL-owg9prcr1jzVnYoiRyjPvzY7NuKuDqN5PeOaZ2x5GFYp7VwYX5RBw~', + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsIng1YyI6WyJNSUhlTUlHUm9BTUNBUUlDRUIwYW80ZVVBZUhrQjg2dzhmSUVuR2N3QlFZREsyVndNQUF3SGhjTk1qUXdOekUyTVRNek5URTNXaGNOTWpReE1ESXpNVEkwTlRNeVdqQUFNQ293QlFZREsyVndBeUVBMWMra1AwdFlodlN2LzJCdzdvSlFiQ1dZT2JUY0IyS1VPVHB3K0x0TG85dWpJVEFmTUIwR0ExVWRFUVFXTUJTR0VtaDBkSEJ6T2k4dmFYTnpkV1Z5TG1OdmJUQUZCZ01yWlhBRFFRQkU0SmFrbTh2bjI1NUI4ZEFneWdiaFIwWlBTZkNFbmdGdWlXREJkeUFYalc2YWhpdDZtOGlsZW05MDhreGsyeUpOZ2hUSVNCbERod2tmcmx5UFJ4NE0iXX0.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImtpZCI6IkJuYm5RVzVWV295czZ4NnFZeEVVVnJFS0dZVzJHUzV2RzcxdkNNd3dmc1ltIiwiY3J2IjoiRWQyNTUxOSIsIngiOiJvRU5Wc3hPVWlINTRYOHdKTGFWa2ljQ1JrMDB3QklRNHNSZ2JrNTROOE1vIn19LCJpc3MiOiJodHRwczovL2lzc3Vlci5jb20iLCJpYXQiOjE2OTgxNTE1MzJ9.7k20YP-pSYEEb3HKTea6NUCa7dtEkL4x4DR1Ajnbr6kvVwRpLZCjFn-BgSGC5ElGgeF5qLpc-MEgcvV3Xo1lBQ~', trustedCertficate: 'MIHeMIGRoAMCAQICEB0ao4eUAeHkB86w8fIEnGcwBQYDK2VwMAAwHhcNMjQwNzE2MTMzNTE3WhcNMjQxMDIzMTI0NTMyWjAAMCowBQYDK2VwAyEA1c+kP0tYhvSv/2Bw7oJQbCWYObTcB2KUOTpw+LtLo9ujITAfMB0GA1UdEQQWMBSGEmh0dHBzOi8vaXNzdWVyLmNvbTAFBgMrZXADQQBE4Jakm8vn255B8dAgygbhR0ZPSfCEngFuiWDBdyAXjW6ahit6m8ilem908kxk2yJNghTISBlDhwkfrlyPRx4M', certificateIssuer: 'https://issuer.com', @@ -144,7 +144,7 @@ export const simpleJwtVcWithoutHolderBinding = } */ export const simpleJwtVcPresentation = - 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.vLkigrBr1IIVRJeYE5DQx0rKUVzO3KT9T0XBATWJE89pWCyvB3Rzs8VD7qfi0vDk_QVCPIiHq1U1PsmSe4ZqCg~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJzZF9oYXNoIjoiZjQ4WUJldlVHNUpWdUFITXJ5V1E0aTJPRjdYSm9JLWRMLWpqWXgtSHF4USJ9.skMqC7ej50kOeGEJZ_8J5eK1YqKN7vkqS_t8DQ4Y3i6DdN20eAXbaGMU4G4AOGk_hAYctTZwxaeQQEBX8pu5Cg' + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImtpZCI6IkJuYm5RVzVWV295czZ4NnFZeEVVVnJFS0dZVzJHUzV2RzcxdkNNd3dmc1ltIiwiY3J2IjoiRWQyNTUxOSIsIngiOiJvRU5Wc3hPVWlINTRYOHdKTGFWa2ljQ1JrMDB3QklRNHNSZ2JrNTROOE1vIn19LCJpc3MiOiJkaWQ6a2V5Ono2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyIsImlhdCI6MTY5ODE1MTUzMn0.mVaO61d9YYRbzWATBztGw2axg-2zjYWtNp5BVxhoi4RW6VQGjlJtn8OY7j8RLnkcMYKVvakQO56_Rco-vy2kAA~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJzZF9oYXNoIjoieF9UZmRzOWRIcHpuZmZjanpRcm93LWNLVDVybGxsbDd4YjJTTWhyMTkyUSJ9.OndMm3YfGko5Zzqdm6wM88mwjXVR8MXpvMmmE3lU9RoC719h4mWH6R0DC2qLC8wK1S9TvIF8ZDHKaAMlGvfYBw' /**sdJwtVcWithSingleDisclosure * { @@ -199,7 +199,7 @@ export const simpleJwtVcPresentation = } */ export const sdJwtVcWithSingleDisclosure = - 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZCI6WyJ2Y3ZGVTREc0ZLVHFRMXZsNG5lbEpXWFRiXy0wZE5vQmtzNmlxTkZwdHlnIl0sIl9zZF9hbGciOiJzaGEtMjU2In0.wX-7AyTsGMFDpgaw-TMjFK2zyywB94lKAwXlc4DtNoYjhnvKEe6eln1YhKTD_IIPNyTDOCT-TgtzA-8tCg9NCQ~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~' + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImtpZCI6IkJuYm5RVzVWV295czZ4NnFZeEVVVnJFS0dZVzJHUzV2RzcxdkNNd3dmc1ltIiwiY3J2IjoiRWQyNTUxOSIsIngiOiJvRU5Wc3hPVWlINTRYOHdKTGFWa2ljQ1JrMDB3QklRNHNSZ2JrNTROOE1vIn19LCJpc3MiOiJkaWQ6a2V5Ono2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyIsImlhdCI6MTY5ODE1MTUzMiwiX3NkIjpbIkxITFpWbHVtQTNfay16bnRyU0w2b2NVTFZoX3V6MFBRb3VwWlM0aHUxNU0iXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.u6NtGsC0QinrfCCRGcnTTcCqy4uyB-jywCKx3O00quMJW9KjspKGMjH-cp4p_-XLmtzLIqiurFvhR1Kbrvn6CQ~WyJjMkZzZEEiLCJjbGFpbSIsInNvbWUtY2xhaW0iXQ~' /**sdJwtVcWithSingleDisclosurePresentation * { @@ -267,7 +267,7 @@ export const sdJwtVcWithSingleDisclosure = } */ export const sdJwtVcWithSingleDisclosurePresentation = - 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZCI6WyJ2Y3ZGVTREc0ZLVHFRMXZsNG5lbEpXWFRiXy0wZE5vQmtzNmlxTkZwdHlnIl0sIl9zZF9hbGciOiJzaGEtMjU2In0.wX-7AyTsGMFDpgaw-TMjFK2zyywB94lKAwXlc4DtNoYjhnvKEe6eln1YhKTD_IIPNyTDOCT-TgtzA-8tCg9NCQ~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJzZF9oYXNoIjoiOUY1VlF3U1ZPN1pBd0lneWgxanJ3bkpXZ3k3ZlRJZDFtajFNUnA0MW5NOCJ9.9TcpFkSLYMbsQzkPMyqrT5kMk8sobEvTzfkwym5HvbTfEMa_J23LB-UFhY0FsBhe-1rYqnAykGuimQNaWIwODw' + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImtpZCI6IkJuYm5RVzVWV295czZ4NnFZeEVVVnJFS0dZVzJHUzV2RzcxdkNNd3dmc1ltIiwiY3J2IjoiRWQyNTUxOSIsIngiOiJvRU5Wc3hPVWlINTRYOHdKTGFWa2ljQ1JrMDB3QklRNHNSZ2JrNTROOE1vIn19LCJpc3MiOiJkaWQ6a2V5Ono2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyIsImlhdCI6MTY5ODE1MTUzMiwiX3NkIjpbIkxITFpWbHVtQTNfay16bnRyU0w2b2NVTFZoX3V6MFBRb3VwWlM0aHUxNU0iXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.u6NtGsC0QinrfCCRGcnTTcCqy4uyB-jywCKx3O00quMJW9KjspKGMjH-cp4p_-XLmtzLIqiurFvhR1Kbrvn6CQ~WyJjMkZzZEEiLCJjbGFpbSIsInNvbWUtY2xhaW0iXQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJzZF9oYXNoIjoiVXEtVjV1X3NfWmtscjJEX0NmZ2RlNzRFU1B0QmctRXloR2hmdHBXTWFkTSJ9.Fv2pj-s7tZj0mYb2Oh3d_qWhddnU5ZhywaLvLAe3-QJDggjExua0WXqYZhW6imGmLvUCikpWMP75pyNp9o_uBg' /**complexSdJwtVc * { @@ -399,7 +399,7 @@ export const sdJwtVcWithSingleDisclosurePresentation = } */ export const complexSdJwtVc = - 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2QiOlsiMUN1cjJrMkEyb0lCNUNzaFNJZl9BX0tnLWwyNnVfcUt1V1E3OVAwVmRhcyIsIlIxelRVdk9ZSGdjZXBqMGpIeXBHSHo5RUh0dFZLZnQweXN3YmM5RVRQYlUiLCJlRHFRcGRUWEpYYldoZi1Fc0k3enc1WDZPdlltRk4tVVpRUU1lc1h3S1B3IiwicGREazJfWEFLSG83Z09BZndGMWI3T2RDVVZUaXQya0pIYXhTRUNROXhmYyIsInBzYXVLVU5XRWkwOW51M0NsODl4S1hnbXBXRU5abDV1eTFOMW55bl9qTWsiLCJzTl9nZTBwSFhGNnFtc1luWDFBOVNkd0o4Y2g4YUVOa3hiT0RzVDc0WXdJIl0sIl9zZF9hbGciOiJzaGEtMjU2In0.Kkhrxy2acd52JTl4g_0x25D5d1QNCTbqHrD9Qu9HzXMxPMu_5T4z-cSiutDYb5cIdi9NzMXPe4MXax-fUymEDg~WyJzYWx0IiwicmVnaW9uIiwiQW55c3RhdGUiXQ~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwiYmlydGhkYXRlIiwiMTk0MC0wMS0wMSJd~WyJzYWx0IiwiaXNfb3Zlcl8xOCIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~' + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyI4S2wtNktHbDdKakZybE4wWktEUEt6ZVJmbzBvSjVUdjBGNmNYZ3BtT0NZIiwiY3hINmc1MUJPaDh2RGlRWFc4OEtxODk2REVWTFpaNG1idUxPNnpfXzVkcyJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJraWQiOiJCbmJuUVc1VldveXM2eDZxWXhFVVZyRUtHWVcyR1M1dkc3MXZDTXd3ZnNZbSIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZCI6WyIxb0xiSFZoZm1WczJvQTN2aEZOVFhoTXc0bEd1N3FsOWRaMFQ3cC12V3FFIiwiMnh1elMza1VyVDZWUEpELU15U0lrUTQ3SElCLWdjeXpGNU5EWTE5Y1BCdyIsImhuMWdjck9fUTJIc2tXMlpfbnpJcklsNktwZ3FsZHZTY296dXRKZGJoV00iLCJqYzczdDN5Qm9Ec19wRFliMDNsRVlLWXZDYnRDcTlOaHVKNl81QTdRTlNzIiwibEtJX3NZMDVwRElzOU1EcmpDTzR2OFhvRE05NjNKWHhycDlUMkZOTHlUWSIsInNsMGhrWTVMZVZ3eTNySWpOYUNsNFA0Q0ozQzN2OElwLUdIMmxCOVNkX0EiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.osvw_Favqx8KtupHNr_Rk-zR8iOIav0fQ5Lf_F1v1n1LjVtx-_CkNFSujUnkNfo5HPiQRsvyu5ab0UX6Z7vpCw~WyJjMkZzZEEiLCJyZWdpb24iLCJBbnlzdGF0ZSJd~WyJjMkZzZEEiLCJjb3VudHJ5IiwiVVMiXQ~WyJjMkZzZEEiLCJnaXZlbl9uYW1lIiwiSm9obiJd~WyJjMkZzZEEiLCJlbWFpbCIsImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJjMkZzZEEiLCJiaXJ0aGRhdGUiLCIxOTQwLTAxLTAxIl0~WyJjMkZzZEEiLCJpc19vdmVyXzE4Iix0cnVlXQ~WyJjMkZzZEEiLCJpc19vdmVyXzIxIix0cnVlXQ~WyJjMkZzZEEiLCJpc19vdmVyXzY1Iix0cnVlXQ~' /**complexSdJwtVcPresentation * { @@ -515,7 +515,7 @@ export const complexSdJwtVc = } */ export const complexSdJwtVcPresentation = - 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2QiOlsiMUN1cjJrMkEyb0lCNUNzaFNJZl9BX0tnLWwyNnVfcUt1V1E3OVAwVmRhcyIsIlIxelRVdk9ZSGdjZXBqMGpIeXBHSHo5RUh0dFZLZnQweXN3YmM5RVRQYlUiLCJlRHFRcGRUWEpYYldoZi1Fc0k3enc1WDZPdlltRk4tVVpRUU1lc1h3S1B3IiwicGREazJfWEFLSG83Z09BZndGMWI3T2RDVVZUaXQya0pIYXhTRUNROXhmYyIsInBzYXVLVU5XRWkwOW51M0NsODl4S1hnbXBXRU5abDV1eTFOMW55bl9qTWsiLCJzTl9nZTBwSFhGNnFtc1luWDFBOVNkd0o4Y2g4YUVOa3hiT0RzVDc0WXdJIl0sIl9zZF9hbGciOiJzaGEtMjU2In0.Kkhrxy2acd52JTl4g_0x25D5d1QNCTbqHrD9Qu9HzXMxPMu_5T4z-cSiutDYb5cIdi9NzMXPe4MXax-fUymEDg~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJzZF9oYXNoIjoiaFRtUklwNFQ1Y2ZqQlUxbTVvcXNNWDZuUlFObGpEdXZSSThTWnlTeWhsZyJ9.D0G1__PslfgjkwTC1082x3r8Wp5mf13977y7Ef2xhvDrOO7V3zio5BZzqrDwzXIi3Y5GA1Vv3ptqpUKMn14EBA' + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyI4S2wtNktHbDdKakZybE4wWktEUEt6ZVJmbzBvSjVUdjBGNmNYZ3BtT0NZIiwiY3hINmc1MUJPaDh2RGlRWFc4OEtxODk2REVWTFpaNG1idUxPNnpfXzVkcyJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJraWQiOiJCbmJuUVc1VldveXM2eDZxWXhFVVZyRUtHWVcyR1M1dkc3MXZDTXd3ZnNZbSIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZCI6WyIxb0xiSFZoZm1WczJvQTN2aEZOVFhoTXc0bEd1N3FsOWRaMFQ3cC12V3FFIiwiMnh1elMza1VyVDZWUEpELU15U0lrUTQ3SElCLWdjeXpGNU5EWTE5Y1BCdyIsImhuMWdjck9fUTJIc2tXMlpfbnpJcklsNktwZ3FsZHZTY296dXRKZGJoV00iLCJqYzczdDN5Qm9Ec19wRFliMDNsRVlLWXZDYnRDcTlOaHVKNl81QTdRTlNzIiwibEtJX3NZMDVwRElzOU1EcmpDTzR2OFhvRE05NjNKWHhycDlUMkZOTHlUWSIsInNsMGhrWTVMZVZ3eTNySWpOYUNsNFA0Q0ozQzN2OElwLUdIMmxCOVNkX0EiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.osvw_Favqx8KtupHNr_Rk-zR8iOIav0fQ5Lf_F1v1n1LjVtx-_CkNFSujUnkNfo5HPiQRsvyu5ab0UX6Z7vpCw~WyJjMkZzZEEiLCJpc19vdmVyXzY1Iix0cnVlXQ~WyJjMkZzZEEiLCJpc19vdmVyXzIxIix0cnVlXQ~WyJjMkZzZEEiLCJlbWFpbCIsImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJjMkZzZEEiLCJjb3VudHJ5IiwiVVMiXQ~WyJjMkZzZEEiLCJnaXZlbl9uYW1lIiwiSm9obiJd~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFZERTQSJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJzZF9oYXNoIjoiaXNhVjl6YmV6bmNNY3N3dUp4T05IRVpiYjN3c0lWRDk3UXVDTl9BcWVaNCJ9.eI-fj0vqX6WPGic-q5XYm4m6CyNIOCJ4301IislyFgXuh899HekOKfc0osTVoBe-TstbaI_NCu2rRix4NF8IBw' export const sdJwtVcPid = 'eyJ4NWMiOlsiTUlJQ2REQ0NBaHVnQXdJQkFnSUJBakFLQmdncWhrak9QUVFEQWpDQmlERUxNQWtHQTFVRUJoTUNSRVV4RHpBTkJnTlZCQWNNQmtKbGNteHBiakVkTUJzR0ExVUVDZ3dVUW5WdVpHVnpaSEoxWTJ0bGNtVnBJRWR0WWtneEVUQVBCZ05WQkFzTUNGUWdRMU1nU1VSRk1UWXdOQVlEVlFRRERDMVRVRkpKVGtRZ1JuVnVhMlVnUlZWRVNTQlhZV3hzWlhRZ1VISnZkRzkwZVhCbElFbHpjM1ZwYm1jZ1EwRXdIaGNOTWpRd05UTXhNRGd4TXpFM1doY05NalV3TnpBMU1EZ3hNekUzV2pCc01Rc3dDUVlEVlFRR0V3SkVSVEVkTUJzR0ExVUVDZ3dVUW5WdVpHVnpaSEoxWTJ0bGNtVnBJRWR0WWtneENqQUlCZ05WQkFzTUFVa3hNakF3QmdOVkJBTU1LVk5RVWtsT1JDQkdkVzVyWlNCRlZVUkpJRmRoYkd4bGRDQlFjbTkwYjNSNWNHVWdTWE56ZFdWeU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRU9GQnE0WU1LZzR3NWZUaWZzeXR3QnVKZi83RTdWaFJQWGlObTUyUzNxMUVUSWdCZFh5REsza1Z4R3hnZUhQaXZMUDN1dU12UzZpREVjN3FNeG12ZHVLT0JrRENCalRBZEJnTlZIUTRFRmdRVWlQaENrTEVyRFhQTFcyL0owV1ZlZ2h5dyttSXdEQVlEVlIwVEFRSC9CQUl3QURBT0JnTlZIUThCQWY4RUJBTUNCNEF3TFFZRFZSMFJCQ1l3SklJaVpHVnRieTV3YVdRdGFYTnpkV1Z5TG1KMWJtUmxjMlJ5ZFdOclpYSmxhUzVrWlRBZkJnTlZIU01FR0RBV2dCVFVWaGpBaVRqb0RsaUVHTWwyWXIrcnU4V1F2akFLQmdncWhrak9QUVFEQWdOSEFEQkVBaUFiZjVUemtjUXpoZldvSW95aTFWTjdkOEk5QnNGS20xTVdsdVJwaDJieUdRSWdLWWtkck5mMnhYUGpWU2JqVy9VLzVTNXZBRUM1WHhjT2FudXNPQnJvQmJVPSIsIk1JSUNlVENDQWlDZ0F3SUJBZ0lVQjVFOVFWWnRtVVljRHRDaktCL0gzVlF2NzJnd0NnWUlLb1pJemowRUF3SXdnWWd4Q3pBSkJnTlZCQVlUQWtSRk1ROHdEUVlEVlFRSERBWkNaWEpzYVc0eEhUQWJCZ05WQkFvTUZFSjFibVJsYzJSeWRXTnJaWEpsYVNCSGJXSklNUkV3RHdZRFZRUUxEQWhVSUVOVElFbEVSVEUyTURRR0ExVUVBd3d0VTFCU1NVNUVJRVoxYm10bElFVlZSRWtnVjJGc2JHVjBJRkJ5YjNSdmRIbHdaU0JKYzNOMWFXNW5JRU5CTUI0WERUSTBNRFV6TVRBMk5EZ3dPVm9YRFRNME1EVXlPVEEyTkRnd09Wb3dnWWd4Q3pBSkJnTlZCQVlUQWtSRk1ROHdEUVlEVlFRSERBWkNaWEpzYVc0eEhUQWJCZ05WQkFvTUZFSjFibVJsYzJSeWRXTnJaWEpsYVNCSGJXSklNUkV3RHdZRFZRUUxEQWhVSUVOVElFbEVSVEUyTURRR0ExVUVBd3d0VTFCU1NVNUVJRVoxYm10bElFVlZSRWtnVjJGc2JHVjBJRkJ5YjNSdmRIbHdaU0JKYzNOMWFXNW5JRU5CTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFWUd6ZHdGRG5jNytLbjVpYkF2Q09NOGtlNzdWUXhxZk1jd1pMOElhSUErV0NST2NDZm1ZL2dpSDkycU1ydTVwL2t5T2l2RTBSQy9JYmRNT052RG9VeWFObU1HUXdIUVlEVlIwT0JCWUVGTlJXR01DSk9PZ09XSVFZeVhaaXY2dTd4WkMrTUI4R0ExVWRJd1FZTUJhQUZOUldHTUNKT09nT1dJUVl5WFppdjZ1N3haQytNQklHQTFVZEV3RUIvd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnR0dNQW9HQ0NxR1NNNDlCQU1DQTBjQU1FUUNJR0VtN3drWktIdC9hdGI0TWRGblhXNnlybndNVVQydTEzNmdkdGwxMFk2aEFpQnVURnF2Vll0aDFyYnh6Q1AweFdaSG1RSzlrVnl4bjhHUGZYMjdFSXp6c3c9PSJdLCJraWQiOiJNSUdVTUlHT3BJR0xNSUdJTVFzd0NRWURWUVFHRXdKRVJURVBNQTBHQTFVRUJ3d0dRbVZ5YkdsdU1SMHdHd1lEVlFRS0RCUkNkVzVrWlhOa2NuVmphMlZ5WldrZ1IyMWlTREVSTUE4R0ExVUVDd3dJVkNCRFV5QkpSRVV4TmpBMEJnTlZCQU1NTFZOUVVrbE9SQ0JHZFc1clpTQkZWVVJKSUZkaGJHeGxkQ0JRY205MGIzUjVjR1VnU1hOemRXbHVaeUJEUVFJQkFnPT0iLCJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJwbGFjZV9vZl9iaXJ0aCI6eyJfc2QiOlsiaG9zSHlUSkpYRXRtbHdvcC13TU9QR1NWd1J3MTdzOW83M0kyc3plQlZEbyJdfSwiX3NkIjpbIjRDbDVaZ0trN256V09FZmFXa2xhZ0FqQU53QmRMeDJST25UVUhrS1JZY1UiLCI1amQtYXFEZFRfQnUtV2tGOXNvbW5NeEVISnRyVGxRYlRfRU9Eay1yUlNzIiwiNmZ6NEdoRVp5ZnpaRndNY1dOTF9CVHRwdHNEd3RrV1habjdqTl9rU1FnWSIsIkM3VG1jS1lsVHA0Ty05VnI0SEZ0cUdBQWUwNERHX3Fmcy04MzRpSVJMVG8iLCJQVEprTlByTEw3a3pvcFpPTnZWc2pmaWppSi1rdXc2MW5BTGJUWFpsOFhvIiwiUmd0eHNOSHBVNUZJN0xNa1ktb0Q3ZFZ4eUtNMkc4RUc5aHR2TVJpZFBQRSIsImJ4YUwyQ2tpX3B2VGxhYWRRU0MwMVlhakVQcXlYMzlORFdEU2dCU2RuZlEiXSwiYWRkcmVzcyI6eyJfc2QiOlsiOTZqVjZZXy04d2VtcVgxallMdkN1N3R4dVRRT01LMTczeUdZZ1FtOUNQQSIsIkdZdWk3cHZ4bW8yQXFlblY1WWdCQmtFdGs4WGdzNktiTUZHS0o3T1c3YmciLCJmZHQwWG14OEkyOGRmcm1iQTQtbDl5ZDI5anBWSzhkUFd5clIzQ1ZMWm9JIiwicXJnQzVxM25xZWxlOGpZbVkzQmt6QjBQeEFsdHkxNl9GaHJPa2FGaGtkWSJdfSwiaXNzdWluZ19jb3VudHJ5IjoiREUiLCJ2Y3QiOiJodHRwczovL2V4YW1wbGUuYm1pLmJ1bmQuZGUvY3JlZGVudGlhbC9waWQvMS4wIiwiaXNzdWluZ19hdXRob3JpdHkiOiJERSIsIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiaHR0cHM6Ly9kZW1vLnBpZC1pc3N1ZXIuYnVuZGVzZHJ1Y2tlcmVpLmRlL2MxIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Ik5lWF9abml3eERPSkRfS3lxZjY3OFYtWXgzZjMtRFoweUQ5WGVycEZtY2MiLCJ5IjoiZ3BvNUgweldhUE05eWM3TTJyZXg0SVo2R2ViOUoyODQyVDN0Nlg4ZnJBTSJ9fSwiZXhwIjoxNzMzNzA5NTE0LCJpYXQiOjE3MzI0OTk5MTQsImFnZV9lcXVhbF9vcl9vdmVyIjp7Il9zZCI6WyJIbUQtNnlpYVZPWVB6ZG1XTFBteU94enI4cTJnb3VPOUo5VjZVMFBPT1Y0IiwiSW5zX2JNOVZOaUJGV0ZNNXR1NXZ0M3RrWmhaUUJtY25LTUl5QU16dXYxcyIsImE5ZjNPaWp0NHByZGYtV2lvclkxZ09jZEliX1dDc1JwVzlCS2JPb0ZxUW8iLCJlQ19QbG9nMEMtZTJuN0s0WVNuU3NBQjRLS1dUdmxnMlpkOXhEQm1pRDVnIiwib2E2UVV5SGxQbmU5QW1tT29KTjdFQ1AwRHpEZy1TSVlKUnQtcVNheUxWSSIsInZUZlcwY2VQd0VDQXNUbTVWMFNaZlp6T0lLQUdmRHIyMTlqOWpmaFRIQUUiXX19.m5m6mQQu6O-4Y18VoIiv4mg4Jp5QPd0RStE8hPM_caqWo5prs8spXBB_0NBANGpqnNqEdK2yLRzGidZY8GRNHA~WyI5NmhWUFZxWDJYOU5rUU9IME5WLW5nIiwiZmFtaWx5X25hbWUiLCJNVVNURVJNQU5OIl0~WyJuME1ZOXVodmFCMEZ5YnZkQ2VDNHBnIiwiZ2l2ZW5fbmFtZSIsIkVSSUtBIl0~WyJIYmwzTGZHeXFOaE5rZ05GajZyV2RBIiwiYmlydGhkYXRlIiwiMTk2NC0wOC0xMiJd~WyJPcU9TTEdXNF9JTUdRLUdjRkpSS2pBIiwiYWdlX2JpcnRoX3llYXIiLDE5NjRd~WyJsdGpKV1k2cVM0V1p1bXlTNF9xcW5RIiwiYWdlX2luX3llYXJzIiw2MF0~WyJvVnFxdEY3LXdHSE5xNXJnSURlQTdnIiwiYmlydGhfZmFtaWx5X25hbWUiLCJHQUJMRVIiXQ~WyI2TDZaSjRyN2lCTmdRVjY3SXR1c1JRIiwibmF0aW9uYWxpdGllcyIsWyJERSJdXQ~WyJQLV9LSjhieTQ1NkJpenNWZ2N4UER3IiwiMTIiLHRydWVd~WyIwSmxOY0ZYd1FUYmMzMUF0SGlhY1lBIiwiMTQiLHRydWVd~WyJldjR6UE85ckZZc3QtRE55V2hzMVZRIiwiMTYiLHRydWVd~WyJ1OWxaSzljaE4wWVJWV082ZG9ySThRIiwiMTgiLHRydWVd~WyJnWkZqMUhmRS12azJFNmEzTzRESl93IiwiMjEiLHRydWVd~WyJweGNQWWIzZEFkYnhLMTdTUXR4SWRRIiwiNjUiLGZhbHNlXQ~WyJHU25uUnF0T2p0dl9FMk1Qd3l1bFZRIiwibG9jYWxpdHkiLCJCRVJMSU4iXQ~WyJoRmJNbnZGSWhtUXNpanpUT1Q1VFVnIiwibG9jYWxpdHkiLCJLw5ZMTiJd~WyJUc3lxN3RSSm9LMmEzeF9PWFlmMWp3IiwiY291bnRyeSIsIkRFIl0~WyJLQUwyVHFfSlZRLS1PMXBzYUJtSzhBIiwicG9zdGFsX2NvZGUiLCI1MTE0NyJd~WyJ4SFVxVkNwaGNSdVJYSVRZbDZsSndnIiwic3RyZWV0X2FkZHJlc3MiLCJIRUlERVNUUkHhup5FIDE3Il0~' diff --git a/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRecord.ts b/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRecord.ts index 63599c11b8..b247180873 100644 --- a/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRecord.ts +++ b/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRecord.ts @@ -5,10 +5,11 @@ import type { SdJwtVcTypeMetadata } from '../typeMetadata' import { decodeSdJwtSync } from '@sd-jwt/decode' -import { Hasher, type JwaSignatureAlgorithm } from '../../../crypto' +import { Hasher } from '../../../crypto' import { BaseRecord } from '../../../storage/BaseRecord' import { JsonTransformer } from '../../../utils' import { uuid } from '../../../utils/uuid' +import { KnownJwaSignatureAlgorithm } from '../../kms' import { decodeSdJwtVc } from '../decodeSdJwtVc' export type DefaultSdJwtVcRecordTags = { @@ -22,7 +23,7 @@ export type DefaultSdJwtVcRecordTags = { /** * The alg is the alg used to sign the SD-JWT */ - alg: JwaSignatureAlgorithm + alg: KnownJwaSignatureAlgorithm } export type SdJwtVcRecordStorageProps = { @@ -63,7 +64,7 @@ export class SdJwtVcRecord extends BaseRecord { const sdjwt = decodeSdJwtSync(this.compactSdJwtVc, Hasher.hash) const vct = sdjwt.jwt.payload.vct as string const sdAlg = sdjwt.jwt.payload._sd_alg as string | undefined - const alg = sdjwt.jwt.header.alg as JwaSignatureAlgorithm + const alg = sdjwt.jwt.header.alg as KnownJwaSignatureAlgorithm return { ...this._tags, diff --git a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts index 806a9f9d11..558840c62c 100644 --- a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts +++ b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts @@ -1,5 +1,5 @@ -import type { JwaSignatureAlgorithm } from '../../crypto/jose/jwa' import type { SingleOrArray } from '../../utils/type' +import { KnownJwaSignatureAlgorithm } from '../kms' import type { ProofPurpose, W3cJsonLdVerifiablePresentation } from './data-integrity' import type { W3cJsonLdVerifiableCredential } from './data-integrity/models/W3cJsonLdVerifiableCredential' import type { W3cJwtVerifiableCredential } from './jwt-vc/W3cJwtVerifiableCredential' @@ -57,7 +57,7 @@ export interface W3cJwtSignCredentialOptions extends W3cSignCredentialOptionsBas * * Must be a valid JWA signature algorithm. */ - alg: JwaSignatureAlgorithm + alg: KnownJwaSignatureAlgorithm } export interface W3cJsonLdSignCredentialOptions extends W3cSignCredentialOptionsBase { @@ -154,7 +154,7 @@ export interface W3cJwtSignPresentationOptions extends W3cSignPresentationOption * * Must be a valid JWA signature algorithm. */ - alg: JwaSignatureAlgorithm + alg: KnownJwaSignatureAlgorithm } interface W3cVerifyPresentationOptionsBase { diff --git a/packages/core/src/modules/vc/W3cCredentialsModule.ts b/packages/core/src/modules/vc/W3cCredentialsModule.ts index f8cce5a364..2dfc066c60 100644 --- a/packages/core/src/modules/vc/W3cCredentialsModule.ts +++ b/packages/core/src/modules/vc/W3cCredentialsModule.ts @@ -1,16 +1,15 @@ import type { DependencyManager, Module } from '../../plugins' -import type { W3cCredentialsModuleConfigOptions } from './W3cCredentialsModuleConfig' - -import { KeyType } from '../../crypto' import { VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, } from '../dids' +import type { W3cCredentialsModuleConfigOptions } from './W3cCredentialsModuleConfig' +import { Ed25519PublicJwk } from '../kms' import { W3cCredentialService } from './W3cCredentialService' import { W3cCredentialsApi } from './W3cCredentialsApi' import { W3cCredentialsModuleConfig } from './W3cCredentialsModuleConfig' -import { SignatureSuiteRegistry, SignatureSuiteToken } from './data-integrity/SignatureSuiteRegistry' +import { SignatureSuiteRegistry, SignatureSuiteToken, SuiteInfo } from './data-integrity/SignatureSuiteRegistry' import { W3cJsonLdCredentialService } from './data-integrity/W3cJsonLdCredentialService' import { Ed25519Signature2018, Ed25519Signature2020 } from './data-integrity/signature-suites' import { W3cJwtCredentialService } from './jwt-vc' @@ -46,13 +45,13 @@ export class W3cCredentialsModule implements Module { VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, ], - keyTypes: [KeyType.Ed25519], - }) + supportedPublicJwkType: [Ed25519PublicJwk], + } satisfies SuiteInfo) dependencyManager.registerInstance(SignatureSuiteToken, { suiteClass: Ed25519Signature2020, proofType: 'Ed25519Signature2020', verificationMethodTypes: [VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020], - keyTypes: [KeyType.Ed25519], - }) + supportedPublicJwkType: [Ed25519PublicJwk], + } satisfies SuiteInfo) } } diff --git a/packages/core/src/modules/vc/__tests__/W3CredentialsModule.test.ts b/packages/core/src/modules/vc/__tests__/W3CredentialsModule.test.ts index 73e0181b30..8b0c094d94 100644 --- a/packages/core/src/modules/vc/__tests__/W3CredentialsModule.test.ts +++ b/packages/core/src/modules/vc/__tests__/W3CredentialsModule.test.ts @@ -1,5 +1,5 @@ -import { KeyType } from '../../../crypto' import { DependencyManager } from '../../../plugins/DependencyManager' +import { Ed25519PublicJwk } from '../../kms' import { W3cCredentialService } from '../W3cCredentialService' import { W3cCredentialsModule } from '../W3cCredentialsModule' import { W3cCredentialsModuleConfig } from '../W3cCredentialsModuleConfig' @@ -34,13 +34,13 @@ describe('W3cCredentialsModule', () => { suiteClass: Ed25519Signature2018, verificationMethodTypes: ['Ed25519VerificationKey2018', 'Ed25519VerificationKey2020'], proofType: 'Ed25519Signature2018', - keyTypes: [KeyType.Ed25519], + supportedPublicJwkType: [Ed25519PublicJwk], }) expect(dependencyManager.registerInstance).toHaveBeenCalledWith(SignatureSuiteToken, { suiteClass: Ed25519Signature2020, verificationMethodTypes: ['Ed25519VerificationKey2020'], proofType: 'Ed25519Signature2020', - keyTypes: [KeyType.Ed25519], + supportedPublicJwkType: [Ed25519PublicJwk], }) }) }) diff --git a/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts index b3e001ab1c..3f77136352 100644 --- a/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts +++ b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts @@ -1,7 +1,5 @@ import type { AgentContext } from '../../../agent' -import type { Wallet } from '../../../wallet' -import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' import { getAgentConfig, getAgentContext, mockFunction } from '../../../../tests' import { JwsService } from '../../../crypto' import { JsonTransformer, asArray } from '../../../utils' @@ -40,17 +38,13 @@ const credentialsModuleConfig = new W3cCredentialsModuleConfig({ }) describe('W3cCredentialsService', () => { - let wallet: Wallet let agentContext: AgentContext let w3cCredentialService: W3cCredentialService let w3cCredentialsRepository: W3cCredentialRepository beforeAll(async () => { - wallet = new InMemoryWallet() - await wallet.createAndOpen(agentConfig.walletConfig) agentContext = getAgentContext({ agentConfig, - wallet, }) w3cCredentialsRepository = new W3cCredentialsRepositoryMock() w3cCredentialService = new W3cCredentialService( @@ -60,10 +54,6 @@ describe('W3cCredentialsService', () => { ) }) - afterAll(async () => { - await wallet.delete() - }) - describe('createPresentation', () => { it('should successfully create a presentation from single verifiable credential', async () => { const vc = JsonTransformer.fromJSON( diff --git a/packages/core/src/modules/vc/__tests__/W3cCredentialsApi.test.ts b/packages/core/src/modules/vc/__tests__/W3cCredentialsApi.test.ts index bd21fe7b43..787dbf398e 100644 --- a/packages/core/src/modules/vc/__tests__/W3cCredentialsApi.test.ts +++ b/packages/core/src/modules/vc/__tests__/W3cCredentialsApi.test.ts @@ -1,4 +1,4 @@ -import { getInMemoryAgentOptions } from '../../../../tests' +import { getAgentOptions } from '../../../../tests' import { Agent } from '../../../agent/Agent' import { JsonTransformer } from '../../../utils' import { W3cCredentialService } from '../W3cCredentialService' @@ -8,7 +8,7 @@ import { Ed25519Signature2018Fixtures } from '../data-integrity/__tests__/fixtur import { W3cJsonLdVerifiableCredential } from '../data-integrity/models' import { W3cCredentialRepository } from '../repository' -const agentOptions = getInMemoryAgentOptions( +const agentOptions = getAgentOptions( 'W3cCredentialsApi', {}, {}, @@ -40,8 +40,11 @@ describe('W3cCredentialsApi', () => { }) afterEach(async () => { - await agent.shutdown() - await agent.wallet.delete() + // TOOD: we probably need a way to delete a context on the agent, + // for tenants we do it on the tenants api, for the main context + // we can do it on the agent instance? So `agent.delete()` maybe? + await agent.dependencyManager.registeredModules.inMemory.onDeleteContext?.(agent.context) + agent.shutdown() }) it('Should successfully store a credential', async () => { diff --git a/packages/core/src/modules/vc/__tests__/dids/did_example_489398593.ts b/packages/core/src/modules/vc/__tests__/dids/did_example_489398593.ts deleted file mode 100644 index 9cee4d0e2c..0000000000 --- a/packages/core/src/modules/vc/__tests__/dids/did_example_489398593.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const DID_EXAMPLE_48939859 = { - '@context': 'https://www.w3.org/ns/did/v1', - id: 'did:example:489398593', - assertionMethod: [ - { - id: 'did:example:489398593#test', - type: 'Bls12381G2Key2020', - controller: 'did:example:489398593', - publicKeyBase58: - 'oqpWYKaZD9M1Kbe94BVXpr8WTdFBNZyKv48cziTiQUeuhm7sBhCABMyYG4kcMrseC68YTFFgyhiNeBKjzdKk9MiRWuLv5H4FFujQsQK2KTAtzU8qTBiZqBHMmnLF4PL7Ytu', - }, - ], -} diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC729nNiUKQ4pHHNYovae25gkkuvtsZmtpjnLYUj1r8Yd4ZRn3FaswicUWs2NYNuWXxQ7MgzAX7dqXxAFZXFvn2jhqGKpjm5xLwESYfhcDGdSrc9mgfu51w939BjmKmng5HvYK.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC729nNiUKQ4pHHNYovae25gkkuvtsZmtpjnLYUj1r8Yd4ZRn3FaswicUWs2NYNuWXxQ7MgzAX7dqXxAFZXFvn2jhqGKpjm5xLwESYfhcDGdSrc9mgfu51w939BjmKmng5HvYK.ts deleted file mode 100644 index 46ae84b94e..0000000000 --- a/packages/core/src/modules/vc/__tests__/dids/did_zUC729nNiUKQ4pHHNYovae25gkkuvtsZmtpjnLYUj1r8Yd4ZRn3FaswicUWs2NYNuWXxQ7MgzAX7dqXxAFZXFvn2jhqGKpjm5xLwESYfhcDGdSrc9mgfu51w939BjmKmng5HvYK.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4 = - { - '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], - id: 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - verificationMethod: [ - { - id: 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - type: 'JsonWebKey2020', - controller: - 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - publicKeyJwk: { - kty: 'EC', - crv: 'BLS12381_G2', - x: 'rvmIn58iMglCOixwxv7snWjuu8ooQteghivgqrchuIDH8DbG7pzF5io_k2t5HOW1DjcsVioEXLnIdSdUz8jJQq2r-B8zyw4CEiWAM9LUPnmmRDeVFVtA0YVaLo7DdkOn', - }, - }, - ], - assertionMethod: [ - 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - ], - authentication: [ - 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - ], - capabilityInvocation: [ - 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - ], - capabilityDelegation: [ - 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - ], - } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa.ts deleted file mode 100644 index 472bc1e84c..0000000000 --- a/packages/core/src/modules/vc/__tests__/dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa.ts +++ /dev/null @@ -1,54 +0,0 @@ -export const DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa = - { - '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/bbs/v1'], - alsoKnownAs: [], - controller: [], - verificationMethod: [ - { - id: 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', - type: 'Bls12381G2Key2020', - controller: - 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', - publicKeyBase58: - 'nZZe9Nizhaz9JGpgjysaNkWGg5TNEhpib5j6WjTUHJ5K46dedUrZ57PUFZBq9Xckv8mFJjx6G6Vvj2rPspq22BagdADEEEy2F8AVLE1DhuwWC5vHFa4fUhUwxMkH7B6joqG', - publicKeyBase64: undefined, - publicKeyJwk: undefined, - publicKeyHex: undefined, - publicKeyMultibase: undefined, - publicKeyPem: undefined, - blockchainAccountId: undefined, - ethereumAddress: undefined, - }, - ], - service: [], - authentication: [ - 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', - ], - assertionMethod: [ - 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', - ], - keyAgreement: [ - { - id: 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', - type: 'Bls12381G2Key2020', - controller: - 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', - publicKeyBase58: - 'nZZe9Nizhaz9JGpgjysaNkWGg5TNEhpib5j6WjTUHJ5K46dedUrZ57PUFZBq9Xckv8mFJjx6G6Vvj2rPspq22BagdADEEEy2F8AVLE1DhuwWC5vHFa4fUhUwxMkH7B6joqG', - publicKeyBase64: undefined, - publicKeyJwk: undefined, - publicKeyHex: undefined, - publicKeyMultibase: undefined, - publicKeyPem: undefined, - blockchainAccountId: undefined, - ethereumAddress: undefined, - }, - ], - capabilityInvocation: [ - 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', - ], - capabilityDelegation: [ - 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', - ], - id: 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', - } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD.ts deleted file mode 100644 index 968aec92bc..0000000000 --- a/packages/core/src/modules/vc/__tests__/dids/did_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const DID_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD = - { - '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], - id: 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', - verificationMethod: [ - { - id: 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', - type: 'JsonWebKey2020', - controller: - 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', - publicKeyJwk: { - kty: 'EC', - crv: 'BLS12381_G2', - x: 'hbLuuV4otX1HEALBmUGy_ryyTIcY4TsoZYm_UZPCPgITbXvn8YlvlVM_T6_D0ZrUByvZELEX6wXzKhSkCwEqawZOEhUk4iWFID4MR6nRD4icGm97LC4d58WHTfCZ5bXw', - }, - }, - ], - assertionMethod: [ - 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', - ], - authentication: [ - 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', - ], - capabilityInvocation: [ - 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', - ], - capabilityDelegation: [ - 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', - ], - } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh.ts deleted file mode 100644 index b3072fa575..0000000000 --- a/packages/core/src/modules/vc/__tests__/dids/did_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const DID_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh = - { - '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], - id: 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', - verificationMethod: [ - { - id: 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', - type: 'JsonWebKey2020', - controller: - 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', - publicKeyJwk: { - kty: 'EC', - crv: 'BLS12381_G2', - x: 'huBQv7qpuF5FI5bvaku1B8JSPHeHKPI-hhvcJ97I5vNdGtafbPfrPncV4NNXidkzDDASYgt22eMSVKX9Kc9iWFnPmprzDNUt1HhvtBrldXLlRegT93LOogEh7BwoKVGW', - }, - }, - ], - assertionMethod: [ - 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', - ], - authentication: [ - 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', - ], - capabilityInvocation: [ - 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', - ], - capabilityDelegation: [ - 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', - ], - } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn.ts deleted file mode 100644 index c2861e2a1a..0000000000 --- a/packages/core/src/modules/vc/__tests__/dids/did_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const DID_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn = - { - '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], - id: 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', - verificationMethod: [ - { - id: 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', - type: 'JsonWebKey2020', - controller: - 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', - publicKeyJwk: { - kty: 'EC', - crv: 'BLS12381_G2', - x: 'h5pno-Wq71ExNSbjZ91OJavpe0tA871-20TigCvQAs9jHtIV6KjXtX17Cmoz01dQBlPUFPOB5ILw2JeZ2MYtMOzCCYtnuour5XDuyYs6KTAXgYQ2nAlIFfmXXr9Jc48z', - }, - }, - ], - assertionMethod: [ - 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', - ], - authentication: [ - 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', - ], - capabilityInvocation: [ - 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', - ], - capabilityDelegation: [ - 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', - ], - } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN.ts deleted file mode 100644 index 3991dcd28b..0000000000 --- a/packages/core/src/modules/vc/__tests__/dids/did_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN = - { - '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/bls12381-2020/v1'], - id: 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - verificationMethod: [ - { - id: 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - type: 'Bls12381G2Key2020', - controller: - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - publicKeyBase58: - 'pegxn1a43zphf3uqGT4cx1bz8Ebb9QmoSWhQyP1qYTSeRuvWLGKJ5KcqaymnSj53YhCFbjr3tJAhqcaxxZ4Lry7KxkpLeA6GVf3Zb1x999dYp3k4jQzYa1PQXC6x1uCd9s4', - }, - ], - assertionMethod: [ - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - ], - authentication: [ - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - ], - capabilityInvocation: [ - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - ], - capabilityDelegation: [ - 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', - ], - } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ.ts deleted file mode 100644 index d369808fc9..0000000000 --- a/packages/core/src/modules/vc/__tests__/dids/did_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const DID_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ = - { - '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], - id: 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', - verificationMethod: [ - { - id: 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', - type: 'JsonWebKey2020', - controller: - 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', - publicKeyJwk: { - kty: 'EC', - crv: 'BLS12381_G2', - x: 'kSN7z0XGmPGn81aqNhL4zE-jF799YUzc7nl730o0nBsMZiZzwlqyNvemMYrWAGq5FCoaN0jpCkefgdRrMRPPD_6IK3w0g3ieFxNxdwX7NcGR8aihA9stCdTe0kx-ePJr', - }, - }, - ], - assertionMethod: [ - 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', - ], - authentication: [ - 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', - ], - capabilityInvocation: [ - 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', - ], - capabilityDelegation: [ - 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', - ], - } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox.ts deleted file mode 100644 index 5288ec249c..0000000000 --- a/packages/core/src/modules/vc/__tests__/dids/did_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const DID_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox = - { - '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], - id: 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', - verificationMethod: [ - { - id: 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', - type: 'JsonWebKey2020', - controller: - 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', - publicKeyJwk: { - kty: 'EC', - crv: 'BLS12381_G2', - x: 'pA1LXe8EGRU8PTpXfnG3fpJoIW394wpGpx8Q3V5Keh3PUM7j_PRLbk6XN3KJTv7cFesQeo_Q-knymniIm0Ugk9-RGKn65pRIy65aMa1ACfKfGTnnnTuJP4tWRHW2BaHb', - }, - }, - ], - assertionMethod: [ - 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', - ], - authentication: [ - 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', - ], - capabilityInvocation: [ - 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', - ], - capabilityDelegation: [ - 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', - ], - } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F.ts deleted file mode 100644 index 3e4bac3b13..0000000000 --- a/packages/core/src/modules/vc/__tests__/dids/did_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const DID_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F = - { - '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], - id: 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', - verificationMethod: [ - { - id: 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', - type: 'JsonWebKey2020', - controller: - 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', - publicKeyJwk: { - kty: 'EC', - crv: 'BLS12381_G2', - x: 'qULVOptm5i4PfW7r6Hu6wzw6BZRywAQcCi3V0q1VDidrf0bZ-rFUaP72vXRa1WkPAoWpjMjM-uYbDQJBQbgVXoFm4L5Qz3YG5ziHRGdVWChY_5TX8yV3fQOsLJDSnfZy', - }, - }, - ], - assertionMethod: [ - 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', - ], - authentication: [ - 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', - ], - capabilityInvocation: [ - 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', - ], - capabilityDelegation: [ - 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', - ], - } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4.ts deleted file mode 100644 index 46ae84b94e..0000000000 --- a/packages/core/src/modules/vc/__tests__/dids/did_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4 = - { - '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], - id: 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - verificationMethod: [ - { - id: 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - type: 'JsonWebKey2020', - controller: - 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - publicKeyJwk: { - kty: 'EC', - crv: 'BLS12381_G2', - x: 'rvmIn58iMglCOixwxv7snWjuu8ooQteghivgqrchuIDH8DbG7pzF5io_k2t5HOW1DjcsVioEXLnIdSdUz8jJQq2r-B8zyw4CEiWAM9LUPnmmRDeVFVtA0YVaLo7DdkOn', - }, - }, - ], - assertionMethod: [ - 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - ], - authentication: [ - 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - ], - capabilityInvocation: [ - 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - ], - capabilityDelegation: [ - 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', - ], - } diff --git a/packages/core/src/modules/vc/constants.ts b/packages/core/src/modules/vc/constants.ts index 3dacc0548a..990ad9cd49 100644 --- a/packages/core/src/modules/vc/constants.ts +++ b/packages/core/src/modules/vc/constants.ts @@ -5,7 +5,6 @@ export const SECURITY_CONTEXT_URL = SECURITY_CONTEXT_V2_URL export const SECURITY_X25519_CONTEXT_URL = 'https://w3id.org/security/suites/x25519-2019/v1' export const DID_V1_CONTEXT_URL = 'https://www.w3.org/ns/did/v1' export const CREDENTIALS_CONTEXT_V1_URL = 'https://www.w3.org/2018/credentials/v1' -export const SECURITY_CONTEXT_BBS_URL = 'https://w3id.org/security/bbs/v1' export const CREDENTIALS_ISSUER_URL = 'https://www.w3.org/2018/credentials#issuer' export const SECURITY_PROOF_URL = 'https://w3id.org/security#proof' export const SECURITY_SIGNATURE_URL = 'https://w3id.org/security#signature' diff --git a/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts b/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts index c5cc96816a..5c250ee88a 100644 --- a/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts +++ b/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts @@ -1,7 +1,6 @@ -import type { KeyType } from '../../../crypto' - import { CredoError } from '../../../error' import { injectAll, injectable } from '../../../plugins' +import { SupportedPublicJwk, SupportedPublicJwkClass } from '../../kms/jwk/PublicJwk' import { suites } from './libraries/jsonld-signatures' @@ -12,13 +11,15 @@ export interface SuiteInfo { suiteClass: typeof LinkedDataSignature proofType: string verificationMethodTypes: string[] - keyTypes: KeyType[] + supportedPublicJwkType: SupportedPublicJwkClass[] } @injectable() export class SignatureSuiteRegistry { private suiteMapping: SuiteInfo[] + // TODO: replace this signature suite token with just injecting and registering the suites + // on the registry. It's a bit ugly/awkward approach. public constructor(@injectAll(SignatureSuiteToken) suites: Array) { this.suiteMapping = suites.filter((suite): suite is SuiteInfo => suite !== 'default') } @@ -34,8 +35,10 @@ export class SignatureSuiteRegistry { return this.suiteMapping.find((x) => x.verificationMethodTypes.includes(verificationMethodType)) } - public getAllByKeyType(keyType: KeyType) { - return this.suiteMapping.filter((x) => x.keyTypes.includes(keyType)) + public getAllByPublicJwkType(publicJwkType: SupportedPublicJwkClass | SupportedPublicJwk) { + const publicJwkClass = + 'publicKey' in publicJwkType ? (publicJwkType.constructor as SupportedPublicJwkClass) : publicJwkType + return this.suiteMapping.filter((x) => x.supportedPublicJwkType.includes(publicJwkClass)) } public getByProofType(proofType: string) { diff --git a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts index 6824ecf8de..1459751469 100644 --- a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts +++ b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts @@ -1,5 +1,4 @@ import type { AgentContext } from '../../../agent/context' -import type { Key } from '../../../crypto/Key' import type { SingleOrArray } from '../../../utils' import type { W3cJsonLdSignCredentialOptions, @@ -9,19 +8,18 @@ import type { } from '../W3cCredentialServiceOptions' import type { W3cVerifyCredentialResult, W3cVerifyPresentationResult } from '../models' import type { W3cJsonCredential } from '../models/credential/W3cJsonCredential' -import type { W3cJsonLdDeriveProofOptions } from './deriveProof' -import { createWalletKeyPairClass } from '../../../crypto/WalletKeyPair' +import { createKmsKeyPairClass } from '../../../crypto/KmsKeyPair' import { CredoError } from '../../../error' import { injectable } from '../../../plugins' import { JsonTransformer, asArray } from '../../../utils' -import { VerificationMethod } from '../../dids' -import { getKeyFromVerificationMethod } from '../../dids/domain/key-type' +import { DidsApi, VerificationMethod, parseDid } from '../../dids' +import { getPublicJwkFromVerificationMethod } from '../../dids/domain/key-type' import { W3cCredentialsModuleConfig } from '../W3cCredentialsModuleConfig' import { w3cDate } from '../util' +import { PublicJwk } from '../../kms' import { SignatureSuiteRegistry } from './SignatureSuiteRegistry' -import { deriveProof } from './deriveProof' import { assertOnlyW3cJsonLdVerifiableCredentials } from './jsonldUtil' import jsonld from './libraries/jsonld' import vc from './libraries/vc' @@ -52,20 +50,21 @@ export class W3cJsonLdCredentialService { agentContext: AgentContext, options: W3cJsonLdSignCredentialOptions ): Promise { - const WalletKeyPair = createWalletKeyPairClass(agentContext.wallet) + const WalletKeyPair = createKmsKeyPairClass(agentContext) - const signingKey = await this.getPublicKeyFromVerificationMethod(agentContext, options.verificationMethod) + const signingKey = await this.getPublicJwkFromVerificationMethod(agentContext, options.verificationMethod) const suiteInfo = this.signatureSuiteRegistry.getByProofType(options.proofType) - if (!suiteInfo.keyTypes.includes(signingKey.keyType)) { + const suitesForKey = this.signatureSuiteRegistry.getAllByPublicJwkType(signingKey.jwk) + + if (!suitesForKey.some(({ suiteClass }) => suiteClass === suiteInfo.suiteClass)) { throw new CredoError('The key type of the verification method does not match the suite') } const keyPair = new WalletKeyPair({ controller: options.credential.issuerId, // should we check this against the verificationMethod.controller? id: options.verificationMethod, - key: signingKey, - wallet: agentContext.wallet, + publicJwk: signingKey, }) const SuiteClass = suiteInfo.suiteClass @@ -80,14 +79,20 @@ export class W3cJsonLdCredentialService { date: options.created ?? w3cDate(), }) - const result = await vc.issue({ - credential: JsonTransformer.toJSON(options.credential), - suite: suite, - purpose: options.proofPurpose, - documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), - }) + try { + const result = await vc.issue({ + credential: JsonTransformer.toJSON(options.credential), + suite: suite, + purpose: options.proofPurpose, + documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), + }) - return JsonTransformer.fromJSON(result, W3cJsonLdVerifiableCredential) + return JsonTransformer.fromJSON(result, W3cJsonLdVerifiableCredential) + } catch (error) { + throw new CredoError(`Error issuing W3C JSON-LD VC. ${error.message}`, { + cause: error, + }) + } } /** @@ -168,7 +173,7 @@ export class W3cJsonLdCredentialService { options: W3cJsonLdSignPresentationOptions ): Promise { // create keyPair - const WalletKeyPair = createWalletKeyPairClass(agentContext.wallet) + const WalletKeyPair = createKmsKeyPairClass(agentContext) const suiteInfo = this.signatureSuiteRegistry.getByProofType(options.proofType) @@ -176,9 +181,10 @@ export class W3cJsonLdCredentialService { throw new CredoError(`The requested proofType ${options.proofType} is not supported`) } - const signingKey = await this.getPublicKeyFromVerificationMethod(agentContext, options.verificationMethod) + const signingKey = await this.getPublicJwkFromVerificationMethod(agentContext, options.verificationMethod) + const suitesForKey = this.signatureSuiteRegistry.getAllByPublicJwkType(signingKey.jwk) - if (!suiteInfo.keyTypes.includes(signingKey.keyType)) { + if (!suitesForKey.some(({ suiteClass }) => suiteClass === suiteInfo.suiteClass)) { throw new CredoError('The key type of the verification method does not match the suite') } @@ -191,8 +197,7 @@ export class W3cJsonLdCredentialService { const keyPair = new WalletKeyPair({ controller: verificationMethodObject.controller as string, id: options.verificationMethod, - key: signingKey, - wallet: agentContext.wallet, + publicJwk: signingKey, }) const suite = new suiteInfo.suiteClass({ @@ -228,7 +233,7 @@ export class W3cJsonLdCredentialService { ): Promise { try { // create keyPair - const WalletKeyPair = createWalletKeyPairClass(agentContext.wallet) + const WalletKeyPair = createKmsKeyPairClass(agentContext) let proofs = options.presentation.proof @@ -298,32 +303,10 @@ export class W3cJsonLdCredentialService { } } - public async deriveProof( - agentContext: AgentContext, - options: W3cJsonLdDeriveProofOptions - ): Promise { - // TODO: make suite dynamic - const suiteInfo = this.signatureSuiteRegistry.getByProofType('BbsBlsSignatureProof2020') - const SuiteClass = suiteInfo.suiteClass - - const suite = new SuiteClass() - - const proof = await deriveProof(JsonTransformer.toJSON(options.credential), options.revealDocument, { - suite: suite, - documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), - }) - - return proof - } - public getVerificationMethodTypesByProofType(proofType: string): string[] { return this.signatureSuiteRegistry.getByProofType(proofType).verificationMethodTypes } - public getKeyTypesByProofType(proofType: string): string[] { - return this.signatureSuiteRegistry.getByProofType(proofType).keyTypes - } - public async getExpandedTypesForCredential(agentContext: AgentContext, credential: W3cJsonLdVerifiableCredential) { // Get the expanded types const expandedTypes: SingleOrArray = ( @@ -335,20 +318,35 @@ export class W3cJsonLdCredentialService { return asArray(expandedTypes) } - private async getPublicKeyFromVerificationMethod( + private async getPublicJwkFromVerificationMethod( agentContext: AgentContext, verificationMethod: string - ): Promise { + ): Promise { + const dids = agentContext.resolve(DidsApi) + const documentLoader = this.w3cCredentialsModuleConfig.documentLoader(agentContext) const verificationMethodObject = await documentLoader(verificationMethod) - const verificationMethodClass = JsonTransformer.fromJSON(verificationMethodObject.document, VerificationMethod) + const verificationMethodInstance = JsonTransformer.fromJSON(verificationMethodObject.document, VerificationMethod) + const did = parseDid(verificationMethod) + const publicJwk = getPublicJwkFromVerificationMethod(verificationMethodInstance) + + const [didRecord] = await dids.getCreatedDids({ did: did.did }) + + // For all modern uses of did bound credentials there MUST be a did record + if (didRecord) { + publicJwk.keyId = + didRecord.keys?.find(({ didDocumentRelativeKeyId }) => didDocumentRelativeKeyId === `#${did.fragment}`) + ?.kmsKeyId ?? publicJwk.legacyKeyId + } else { + // If we don't have a did record we assume legacy key id should be used. + publicJwk.keyId = publicJwk.legacyKeyId + } - const key = getKeyFromVerificationMethod(verificationMethodClass) - return key + return publicJwk } private getSignatureSuitesForCredential(agentContext: AgentContext, credential: W3cJsonLdVerifiableCredential) { - const WalletKeyPair = createWalletKeyPairClass(agentContext.wallet) + const WalletKeyPair = createKmsKeyPairClass(agentContext) let proofs = credential.proof diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts b/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts index 4c0e57d6a2..eafc08d995 100644 --- a/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts +++ b/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts @@ -1,14 +1,12 @@ -import type { AgentContext } from '../../../../agent' -import type { Wallet } from '../../../../wallet' - -import { InMemoryWallet } from '../../../../../../../tests/InMemoryWallet' -import { getAgentConfig, getAgentContext } from '../../../../../tests/helpers' -import { KeyType } from '../../../../crypto' +import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../../tests/helpers' import { TypedArrayEncoder, asArray } from '../../../../utils' import { JsonTransformer } from '../../../../utils/JsonTransformer' -import { WalletError } from '../../../../wallet/error' import { DidKey, + DidRepository, + DidsApi, + DidsModuleConfig, + KeyDidCreateOptions, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, } from '../../../dids' @@ -23,6 +21,13 @@ import { W3cJsonLdVerifiablePresentation } from '../models/W3cJsonLdVerifiablePr import { CredentialIssuancePurpose } from '../proof-purposes/CredentialIssuancePurpose' import { Ed25519Signature2018 } from '../signature-suites' +import { Subject } from 'rxjs' +import { InMemoryStorageService } from '../../../../../../../tests/InMemoryStorageService' +import { transformPrivateKeyToPrivateJwk } from '../../../../../../askar/src' +import { EventEmitter } from '../../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../../constants' +import { ConsoleLogger, LogLevel } from '../../../../logger' +import { Ed25519PublicJwk, KeyManagementApi, PublicJwk } from '../../../kms' import { customDocumentLoader } from './documentLoader' import { Ed25519Signature2018Fixtures } from './fixtures' @@ -35,45 +40,33 @@ const signatureSuiteRegistry = new SignatureSuiteRegistry([ VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, ], - keyTypes: [KeyType.Ed25519], + supportedPublicJwkType: [Ed25519PublicJwk], }, ]) +// biome-ignore lint/suspicious/noExplicitAny: +const inMemoryStorage = new InMemoryStorageService() const agentConfig = getAgentConfig('W3cJsonLdCredentialServiceTest') +const agentContext = getAgentContext({ + agentConfig, + registerInstances: [ + [InjectionSymbols.Logger, new ConsoleLogger(LogLevel.off)], + [DidsModuleConfig, new DidsModuleConfig({})], + [DidRepository, new DidRepository(inMemoryStorage, new EventEmitter(agentDependencies, new Subject()))], + ], +}) -describe('W3cJsonLdCredentialsService', () => { - let wallet: Wallet - let agentContext: AgentContext - let w3cJsonLdCredentialService: W3cJsonLdCredentialService - const privateKey = TypedArrayEncoder.fromString('testseed000000000000000000000001') - - beforeAll(async () => { - wallet = new InMemoryWallet() - await wallet.createAndOpen(agentConfig.walletConfig) - agentContext = getAgentContext({ - agentConfig, - wallet, - }) - w3cJsonLdCredentialService = new W3cJsonLdCredentialService( - signatureSuiteRegistry, - new W3cCredentialsModuleConfig({ - documentLoader: customDocumentLoader, - }) - ) +const w3cJsonLdCredentialService = new W3cJsonLdCredentialService( + signatureSuiteRegistry, + new W3cCredentialsModuleConfig({ + documentLoader: customDocumentLoader, }) +) - afterAll(async () => { - await wallet.delete() - }) +describe('W3cJsonLdCredentialsService', () => { + const privateKey = TypedArrayEncoder.fromString('testseed000000000000000000000001') describe('Utility methods', () => { - describe('getKeyTypesByProofType', () => { - it('should return the correct key types for Ed25519Signature2018 proof type', async () => { - const keyTypes = w3cJsonLdCredentialService.getKeyTypesByProofType('Ed25519Signature2018') - expect(keyTypes).toEqual([KeyType.Ed25519]) - }) - }) - describe('getVerificationMethodTypesByProofType', () => { it('should return the correct key types for Ed25519Signature2018 proof type', async () => { const verificationMethodTypes = @@ -89,14 +82,31 @@ describe('W3cJsonLdCredentialsService', () => { describe('Ed25519Signature2018', () => { let issuerDidKey: DidKey let verificationMethod: string + beforeAll(async () => { - // TODO: update to use did registrar - const issuerKey = await wallet.createKey({ - keyType: KeyType.Ed25519, - privateKey, + const kms = agentContext.resolve(KeyManagementApi) + const dids = agentContext.resolve(DidsApi) + + const importedKey = await kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey, + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }).privateJwk, + }) + const issuerKey = PublicJwk.fromPublicJwk(importedKey.publicJwk) + + await dids.create({ + method: 'key', + options: { + keyId: importedKey.keyId, + }, }) + issuerDidKey = new DidKey(issuerKey) - verificationMethod = `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}` + verificationMethod = `${issuerDidKey.did}#${issuerDidKey.publicJwk.fingerprint}` }) describe('signCredential', () => { @@ -134,7 +144,9 @@ describe('W3cJsonLdCredentialsService', () => { verificationMethod: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', }) - }).rejects.toThrowError(WalletError) + }).rejects.toThrow( + `No key management service supports 'sign' operation with algorithm 'EdDSA' that has a key with keyId 'HC8vuuvP8x9kVJizh2eujQjo2JwFQJz6w63szzdbu1Q7` + ) }) }) diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/documentLoader.ts b/packages/core/src/modules/vc/data-integrity/__tests__/documentLoader.ts index 9741520a09..9526b94ca5 100644 --- a/packages/core/src/modules/vc/data-integrity/__tests__/documentLoader.ts +++ b/packages/core/src/modules/vc/data-integrity/__tests__/documentLoader.ts @@ -3,20 +3,11 @@ import type { JsonObject } from '../../../../types' import type { DocumentLoaderResult } from '../libraries/jsonld' import { isDid } from '../../../../utils' -import { DID_EXAMPLE_48939859 } from '../../__tests__/dids/did_example_489398593' import { DID_SOV_QqEfJxe752NCmWqR5TssZ5 } from '../../__tests__/dids/did_sov_QqEfJxe752NCmWqR5TssZ5' import { DID_WEB_LAUNCHPAD } from '../../__tests__/dids/did_web_launchpad' import { DID_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL } from '../../__tests__/dids/did_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL' import { DID_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV } from '../../__tests__/dids/did_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV' -import { DID_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox } from '../../__tests__/dids/did_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox' -import { DID_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F } from '../../__tests__/dids/did_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F' -import { DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4 } from '../../__tests__/dids/did_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4' -import { DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa } from '../../__tests__/dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa' -import { DID_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD } from '../../__tests__/dids/did_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD' -import { DID_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh } from '../../__tests__/dids/did_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh' -import { DID_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn } from '../../__tests__/dids/did_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn' -import { DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN } from '../../__tests__/dids/did_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN' -import { DID_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ } from '../../__tests__/dids/did_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ' + import { SECURITY_CONTEXT_V3_URL } from '../../constants' import { DEFAULT_CONTEXTS } from '../libraries/contexts' import jsonld from '../libraries/jsonld' @@ -31,27 +22,6 @@ export const DOCUMENTS = { ...DEFAULT_CONTEXTS, [DID_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL.id]: DID_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL, [DID_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV.id]: DID_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV, - [DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa.id]: - DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa, - [DID_EXAMPLE_48939859.id]: DID_EXAMPLE_48939859, - [DID_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh.id]: - DID_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh, - [DID_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn.id]: - DID_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn, - [DID_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ.id]: - DID_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ, - [DID_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox.id]: - DID_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox, - [DID_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F.id]: - DID_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F, - [DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4.id]: - DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4, - [DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4.id]: - DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4, - [DID_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD.id]: - DID_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD, - [DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN.id]: - DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN, [DID_SOV_QqEfJxe752NCmWqR5TssZ5.id]: DID_SOV_QqEfJxe752NCmWqR5TssZ5, [DID_WEB_LAUNCHPAD.id]: DID_WEB_LAUNCHPAD, [SECURITY_CONTEXT_V3_URL]: SECURITY_V3_UNSTABLE, diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/bbs_v1.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/bbs_v1.ts deleted file mode 100644 index 897de0a4eb..0000000000 --- a/packages/core/src/modules/vc/data-integrity/libraries/contexts/bbs_v1.ts +++ /dev/null @@ -1,129 +0,0 @@ -export const BBS_V1 = { - '@context': { - '@version': 1.1, - id: '@id', - type: '@type', - BbsBlsSignature2020: { - '@id': 'https://w3id.org/security#BbsBlsSignature2020', - '@context': { - '@version': 1.1, - '@protected': true, - id: '@id', - type: '@type', - challenge: 'https://w3id.org/security#challenge', - created: { - '@id': 'http://purl.org/dc/terms/created', - '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', - }, - domain: 'https://w3id.org/security#domain', - proofValue: 'https://w3id.org/security#proofValue', - nonce: 'https://w3id.org/security#nonce', - proofPurpose: { - '@id': 'https://w3id.org/security#proofPurpose', - '@type': '@vocab', - '@context': { - '@version': 1.1, - '@protected': true, - id: '@id', - type: '@type', - assertionMethod: { - '@id': 'https://w3id.org/security#assertionMethod', - '@type': '@id', - '@container': '@set', - }, - authentication: { - '@id': 'https://w3id.org/security#authenticationMethod', - '@type': '@id', - '@container': '@set', - }, - }, - }, - verificationMethod: { - '@id': 'https://w3id.org/security#verificationMethod', - '@type': '@id', - }, - }, - }, - BbsBlsSignatureProof2020: { - '@id': 'https://w3id.org/security#BbsBlsSignatureProof2020', - '@context': { - '@version': 1.1, - '@protected': true, - id: '@id', - type: '@type', - - challenge: 'https://w3id.org/security#challenge', - created: { - '@id': 'http://purl.org/dc/terms/created', - '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', - }, - domain: 'https://w3id.org/security#domain', - nonce: 'https://w3id.org/security#nonce', - proofPurpose: { - '@id': 'https://w3id.org/security#proofPurpose', - '@type': '@vocab', - '@context': { - '@version': 1.1, - '@protected': true, - id: '@id', - type: '@type', - sec: 'https://w3id.org/security#', - assertionMethod: { - '@id': 'https://w3id.org/security#assertionMethod', - '@type': '@id', - '@container': '@set', - }, - authentication: { - '@id': 'https://w3id.org/security#authenticationMethod', - '@type': '@id', - '@container': '@set', - }, - }, - }, - proofValue: 'https://w3id.org/security#proofValue', - verificationMethod: { - '@id': 'https://w3id.org/security#verificationMethod', - '@type': '@id', - }, - }, - }, - Bls12381G1Key2020: { - '@id': 'https://w3id.org/security#Bls12381G1Key2020', - '@context': { - '@protected': true, - id: '@id', - type: '@type', - controller: { - '@id': 'https://w3id.org/security#controller', - '@type': '@id', - }, - revoked: { - '@id': 'https://w3id.org/security#revoked', - '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', - }, - publicKeyBase58: { - '@id': 'https://w3id.org/security#publicKeyBase58', - }, - }, - }, - Bls12381G2Key2020: { - '@id': 'https://w3id.org/security#Bls12381G2Key2020', - '@context': { - '@protected': true, - id: '@id', - type: '@type', - controller: { - '@id': 'https://w3id.org/security#controller', - '@type': '@id', - }, - revoked: { - '@id': 'https://w3id.org/security#revoked', - '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', - }, - publicKeyBase58: { - '@id': 'https://w3id.org/security#publicKeyBase58', - }, - }, - }, - }, -} diff --git a/packages/core/src/modules/vc/data-integrity/libraries/contexts/defaultContexts.ts b/packages/core/src/modules/vc/data-integrity/libraries/contexts/defaultContexts.ts index 210a67a04e..16ca458cb2 100644 --- a/packages/core/src/modules/vc/data-integrity/libraries/contexts/defaultContexts.ts +++ b/packages/core/src/modules/vc/data-integrity/libraries/contexts/defaultContexts.ts @@ -1,5 +1,4 @@ import { X25519_V1 } from './X25519_v1' -import { BBS_V1 } from './bbs_v1' import { CREDENTIALS_V1 } from './credentials_v1' import { DATA_INTEGRITY_V2 } from './dataIntegrity_v2' import { DID_V1 } from './did_v1' @@ -14,8 +13,6 @@ import { PRESENTATION_SUBMISSION } from './submission' import { VC_REVOCATION_LIST_2020 } from './vc_revocation_list_2020' export const DEFAULT_CONTEXTS = { - 'https://w3id.org/security/suites/bls12381-2020/v1': BBS_V1, - 'https://w3id.org/security/bbs/v1': BBS_V1, 'https://w3id.org/security/v1': SECURITY_V1, 'https://w3id.org/security/v2': SECURITY_V2, 'https://w3id.org/security/suites/x25519-2019/v1': X25519_V1, diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2020.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2020.ts index 3319e206c0..5c12a49880 100644 --- a/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2020.ts +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2020.ts @@ -1,13 +1,12 @@ -import type { DocumentLoader, JsonLdDoc, Proof, VerificationMethod } from '../../jsonldUtil' -import type { JwsLinkedDataSignatureOptions } from '../JwsLinkedDataSignature' - -import { Key } from '../../../../../crypto' -import { MultiBaseEncoder } from '../../../../../utils' +import { MultiBaseEncoder, TypedArrayEncoder } from '../../../../../utils' import { CREDENTIALS_CONTEXT_V1_URL, SECURITY_CONTEXT_URL } from '../../../constants' +import type { DocumentLoader, JsonLdDoc, Proof, VerificationMethod } from '../../jsonldUtil' import { _includesContext } from '../../jsonldUtil' import jsonld from '../../libraries/jsonld' +import type { JwsLinkedDataSignatureOptions } from '../JwsLinkedDataSignature' import { JwsLinkedDataSignature } from '../JwsLinkedDataSignature' +import { Ed25519PublicJwk, PublicJwk } from '../../../../kms' import { ED25519_SUITE_CONTEXT_URL_2020 } from './constants' import { ed25519Signature2020Context } from './context2020' @@ -97,7 +96,10 @@ export class Ed25519Signature2020 extends JwsLinkedDataSignature { // convert Ed25519VerificationKey2020 to Ed25519VerificationKey2018 if (_isEd2020Key(verificationMethod) && _includesEd2020Context(verificationMethod)) { // -- convert multibase to base58 -- - const publicKeyBase58 = Key.fromFingerprint(verificationMethod.publicKeyMultibase).publicKeyBase58 + const publicJwk = PublicJwk.fromFingerprint(verificationMethod.publicKeyMultibase) + if (!publicJwk.is(Ed25519PublicJwk)) { + throw new Error('Expected multibase key to be of type Ed25519.') + } // -- update type verificationMethod.type = 'Ed25519VerificationKey2018' @@ -105,7 +107,7 @@ export class Ed25519Signature2020 extends JwsLinkedDataSignature { verificationMethod = { ...verificationMethod, publicKeyMultibase: undefined, - publicKeyBase58, + publicKeyBase58: TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey), } } diff --git a/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts b/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts index eef0ee143f..d3a85bcae9 100644 --- a/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts +++ b/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts @@ -10,13 +10,17 @@ import type { import type { SingleValidationResult, W3cVerifyCredentialResult, W3cVerifyPresentationResult } from '../models' import { JwsService } from '../../../crypto' -import { getJwkClassFromJwaSignatureAlgorithm, getJwkFromKey } from '../../../crypto/jose/jwk' import { CredoError } from '../../../error' import { injectable } from '../../../plugins' import { MessageValidator, asArray, isDid } from '../../../utils' -import { DidResolverService, getKeyDidMappingByKeyType, getKeyFromVerificationMethod } from '../../dids' +import { DidResolverService, DidsApi, parseDid } from '../../dids' import { W3cJsonLdVerifiableCredential } from '../data-integrity' +import { + getPublicJwkFromVerificationMethod, + getSupportedVerificationMethodTypesForPublicJwk, +} from '../../dids/domain/key-type/keyDidMapping' +import { KnownJwaSignatureAlgorithm, PublicJwk } from '../../kms' import { W3cJwtVerifiableCredential } from './W3cJwtVerifiableCredential' import { W3cJwtVerifiablePresentation } from './W3cJwtVerifiablePresentation' import { getJwtPayloadFromCredential } from './credentialTransformer' @@ -52,14 +56,13 @@ export class W3cJwtCredentialService { throw new CredoError('Only did identifiers are supported as verification method') } - const verificationMethod = await this.resolveVerificationMethod(agentContext, options.verificationMethod, [ + const publicJwk = await this.resolveVerificationMethod(agentContext, options.verificationMethod, [ 'assertionMethod', ]) - const key = getKeyFromVerificationMethod(verificationMethod) const jwt = await this.jwsService.createJwsCompact(agentContext, { payload: jwtPayload, - key, + keyId: publicJwk.keyId, protectedHeaderOptions: { typ: 'JWT', alg: options.alg, @@ -126,8 +129,7 @@ export class W3cJwtCredentialService { credential, purpose: ['assertionMethod'], }) - const issuerPublicKey = getKeyFromVerificationMethod(issuerVerificationMethod) - const issuerPublicJwk = getJwkFromKey(issuerPublicKey) + const issuerPublicKey = getPublicJwkFromVerificationMethod(issuerVerificationMethod) let signatureResult: VerifyJwsResult | undefined = undefined try { @@ -137,7 +139,7 @@ export class W3cJwtCredentialService { // We have pre-fetched the key based on the issuer/signer of the credential jwsSigner: { method: 'did', - jwk: issuerPublicJwk, + jwk: issuerPublicKey, didUrl: issuerVerificationMethod.id, }, }) @@ -176,7 +178,7 @@ export class W3cJwtCredentialService { // Validate whether the `issuer` of the credential is also the signer const issuerIsSigner = signatureResult?.jwsSigners.some( - (jwsSigner) => jwsSigner.jwk.key.fingerprint === issuerPublicKey.fingerprint + (jwsSigner) => jwsSigner.jwk.fingerprint === issuerPublicKey.fingerprint ) if (!issuerIsSigner) { validationResults.validations.issuerIsSigner = { @@ -231,13 +233,11 @@ export class W3cJwtCredentialService { jwtPayload.additionalClaims.nonce = options.challenge jwtPayload.aud = options.domain - const verificationMethod = await this.resolveVerificationMethod(agentContext, options.verificationMethod, [ - 'authentication', - ]) + const publicJwk = await this.resolveVerificationMethod(agentContext, options.verificationMethod, ['authentication']) const jwt = await this.jwsService.createJwsCompact(agentContext, { payload: jwtPayload, - key: getKeyFromVerificationMethod(verificationMethod), + keyId: publicJwk.keyId, protectedHeaderOptions: { typ: 'JWT', alg: options.alg, @@ -309,8 +309,7 @@ export class W3cJwtCredentialService { credential: presentation, purpose: ['authentication'], }) - const proverPublicKey = getKeyFromVerificationMethod(proverVerificationMethod) - const proverPublicJwk = getJwkFromKey(proverPublicKey) + const proverPublicKey = getPublicJwkFromVerificationMethod(proverVerificationMethod) let signatureResult: VerifyJwsResult | undefined = undefined try { @@ -321,7 +320,7 @@ export class W3cJwtCredentialService { jwsSigner: { method: 'did', didUrl: proverVerificationMethod.id, - jwk: proverPublicJwk, + jwk: proverPublicKey, }, trustedCertificates: [], }) @@ -435,11 +434,20 @@ export class W3cJwtCredentialService { agentContext: AgentContext, verificationMethod: string, allowsPurposes?: DidPurpose[] - ): Promise { - const didResolver = agentContext.dependencyManager.resolve(DidResolverService) - const didDocument = await didResolver.resolveDidDocument(agentContext, verificationMethod) + ): Promise { + const dids = agentContext.resolve(DidsApi) + + const parsedDid = parseDid(verificationMethod) + const { didDocument, didRecord } = await dids.resolveCreatedDidRecordWithDocument(parsedDid.did) + const verificationMethodObject = didDocument.dereferenceKey(verificationMethod, allowsPurposes) + const publicJwk = getPublicJwkFromVerificationMethod(verificationMethodObject) - return didDocument.dereferenceKey(verificationMethod, allowsPurposes) + publicJwk.keyId = + didRecord.keys?.find(({ didDocumentRelativeKeyId }) => + verificationMethodObject.id.endsWith(didDocumentRelativeKeyId) + )?.kmsKeyId ?? publicJwk.legacyKeyId + + return publicJwk } /** @@ -506,10 +514,10 @@ export class W3cJwtCredentialService { } // Find the verificationMethod in the did document based on the alg and proofPurpose - const jwkClass = getJwkClassFromJwaSignatureAlgorithm(credential.jwt.header.alg) - if (!jwkClass) throw new CredoError(`Unsupported JWT alg '${credential.jwt.header.alg}'`) - - const { supportedVerificationMethodTypes } = getKeyDidMappingByKeyType(jwkClass.keyType) + const jwkClass = PublicJwk.supportedPublicJwkClassForSignatureAlgorithm( + credential.jwt.header.alg as KnownJwaSignatureAlgorithm + ) + const supportedVerificationMethodTypes = getSupportedVerificationMethodTypesForPublicJwk(jwkClass) const didDocument = await didResolver.resolveDidDocument(agentContext, signerId) const verificationMethods = @@ -519,12 +527,12 @@ export class W3cJwtCredentialService { if (verificationMethods.length === 0) { throw new CredoError( - `No verification methods found for signer '${signerId}' and key type '${jwkClass.keyType}' for alg '${credential.jwt.header.alg}'. Unable to determine which public key is associated with the credential.` + `No verification methods found for signer '${signerId}' and key type '${jwkClass.name}' for alg '${credential.jwt.header.alg}'. Unable to determine which public key is associated with the credential.` ) } if (verificationMethods.length > 1) { throw new CredoError( - `Multiple verification methods found for signer '${signerId}' and key type '${jwkClass.keyType}' for alg '${credential.jwt.header.alg}'. Unable to determine which public key is associated with the credential.` + `Multiple verification methods found for signer '${signerId}' and key type '${jwkClass.name}' for alg '${credential.jwt.header.alg}'. Unable to determine which public key is associated with the credential.` ) } diff --git a/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts b/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts index 61820fd68d..5402b96f22 100644 --- a/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts +++ b/packages/core/src/modules/vc/jwt-vc/__tests__/W3cJwtCredentialService.test.ts @@ -1,18 +1,29 @@ -import { InMemoryWallet } from '../../../../../../../tests/InMemoryWallet' -import { getAgentConfig, getAgentContext, testLogger } from '../../../../../tests' +import { Subject } from 'rxjs' +import { InMemoryStorageService } from '../../../../../../../tests/InMemoryStorageService' +import { AksarKeyManagementService, AskarModuleConfig, transformSeedToPrivateJwk } from '../../../../../../askar/src' +import { + agentDependencies, + getAgentConfig, + getAgentContext, + getAskarStoreConfig, + testLogger, +} from '../../../../../tests' +import { EventEmitter } from '../../../../agent/EventEmitter' import { InjectionSymbols } from '../../../../constants' -import { JwsService, KeyType } from '../../../../crypto' -import { JwaSignatureAlgorithm } from '../../../../crypto/jose/jwa' -import { getJwkFromKey } from '../../../../crypto/jose/jwk' +import { JwsService } from '../../../../crypto' import { ClassValidationError, CredoError } from '../../../../error' import { JsonTransformer } from '../../../../utils' -import { DidJwk, DidKey, DidRepository, DidsModuleConfig } from '../../../dids' +import { DidJwk, DidKey, DidRepository, DidsApi, DidsModuleConfig } from '../../../dids' +import { KeyManagementApi, KnownJwaSignatureAlgorithms, PublicJwk } from '../../../kms' import { X509ModuleConfig } from '../../../x509' import { CREDENTIALS_CONTEXT_V1_URL } from '../../constants' import { ClaimFormat, W3cCredential, W3cPresentation } from '../../models' import { W3cJwtCredentialService } from '../W3cJwtCredentialService' import { W3cJwtVerifiableCredential } from '../W3cJwtVerifiableCredential' +import { askar } from '@openwallet-foundation/askar-nodejs' +import { AskarStoreManager } from '../../../../../../askar/src/AskarStoreManager' +import { NodeFileSystem } from '../../../../../../node/src/NodeFileSystem' import { CredoEs256DidJwkJwtVc, CredoEs256DidJwkJwtVcIssuerSeed, @@ -23,41 +34,85 @@ import { import { didIonJwtVcPresentationProfileJwtVc } from './fixtures/jwt-vc-presentation-profile' import { didKeyTransmuteJwtVc, didKeyTransmuteJwtVp } from './fixtures/transmute-verifiable-data' +// biome-ignore lint/suspicious/noExplicitAny: +const storageSerivice = new InMemoryStorageService() const config = getAgentConfig('W3cJwtCredentialService') -const wallet = new InMemoryWallet() const agentContext = getAgentContext({ - wallet, registerInstances: [ [InjectionSymbols.Logger, testLogger], [DidsModuleConfig, new DidsModuleConfig()], - [DidRepository, {} as unknown as DidRepository], + [DidRepository, new DidRepository(storageSerivice, new EventEmitter(agentDependencies, new Subject()))], + [InjectionSymbols.StorageService, storageSerivice], [X509ModuleConfig, new X509ModuleConfig()], + [ + AskarStoreManager, + new AskarStoreManager( + new NodeFileSystem(), + new AskarModuleConfig({ + askar, + store: getAskarStoreConfig('W3cJwtCredentialService'), + }) + ), + ], ], + kmsBackends: [new AksarKeyManagementService()], agentConfig: config, }) - +const kms = agentContext.dependencyManager.resolve(KeyManagementApi) +const dids = agentContext.dependencyManager.resolve(DidsApi) const jwsService = new JwsService() const w3cJwtCredentialService = new W3cJwtCredentialService(jwsService) -// Runs in Node 18 because of usage of Askar describe('W3cJwtCredentialService', () => { let issuerDidJwk: DidJwk let holderDidKey: DidKey beforeAll(async () => { - await wallet.createAndOpen(config.walletConfig) - - const issuerKey = await agentContext.wallet.createKey({ - keyType: KeyType.P256, + const issuerPrivateJwk = transformSeedToPrivateJwk({ + type: { + kty: 'EC', + crv: 'P-256', + }, seed: CredoEs256DidJwkJwtVcIssuerSeed, + }).privateJwk + + const importedIssuerKey = await kms.importKey({ + privateJwk: issuerPrivateJwk, + }) + + issuerDidJwk = DidJwk.fromPublicJwk(PublicJwk.fromPublicJwk(importedIssuerKey.publicJwk)) + await dids.import({ + did: issuerDidJwk.did, + keys: [ + { + didDocumentRelativeKeyId: '#0', + kmsKeyId: importedIssuerKey.keyId, + }, + ], }) - issuerDidJwk = DidJwk.fromJwk(getJwkFromKey(issuerKey)) - const holderKey = await agentContext.wallet.createKey({ - keyType: KeyType.Ed25519, + const holderPrivateJwk = transformSeedToPrivateJwk({ + type: { + kty: 'OKP', + crv: 'Ed25519', + }, seed: CredoEs256DidJwkJwtVcSubjectSeed, + }).privateJwk + + const importedHolderKey = await kms.importKey({ + privateJwk: holderPrivateJwk, + }) + + holderDidKey = new DidKey(PublicJwk.fromPublicJwk(importedHolderKey.publicJwk)) + await dids.import({ + did: holderDidKey.did, + keys: [ + { + didDocumentRelativeKeyId: `#${holderDidKey.publicJwk.fingerprint}`, + kmsKeyId: importedHolderKey.keyId, + }, + ], }) - holderDidKey = new DidKey(holderKey) }) describe('signCredential', () => { @@ -65,7 +120,7 @@ describe('W3cJwtCredentialService', () => { const credential = JsonTransformer.fromJSON(Ed256DidJwkJwtVcUnsigned, W3cCredential) const vcJwt = await w3cJwtCredentialService.signCredential(agentContext, { - alg: JwaSignatureAlgorithm.ES256, + alg: KnownJwaSignatureAlgorithms.ES256, format: ClaimFormat.JwtVc, verificationMethod: issuerDidJwk.verificationMethodId, credential, @@ -90,23 +145,23 @@ describe('W3cJwtCredentialService', () => { await expect( w3cJwtCredentialService.signCredential(agentContext, { verificationMethod: 'hello', - alg: JwaSignatureAlgorithm.ES256, + alg: KnownJwaSignatureAlgorithms.ES256, credential: JsonTransformer.fromJSON(credentialJson, W3cCredential), format: ClaimFormat.JwtVc, }) - ).rejects.toThrowError('Only did identifiers are supported as verification method') + ).rejects.toThrow('Only did identifiers are supported as verification method') // Throw when not according to data model await expect( w3cJwtCredentialService.signCredential(agentContext, { verificationMethod: issuerDidJwk.verificationMethodId, - alg: JwaSignatureAlgorithm.ES256, + alg: KnownJwaSignatureAlgorithms.ES256, credential: JsonTransformer.fromJSON({ ...credentialJson, issuanceDate: undefined }, W3cCredential, { validate: false, }), format: ClaimFormat.JwtVc, }) - ).rejects.toThrowError( + ).rejects.toThrow( 'property issuanceDate has failed the following constraints: issuanceDate must be RFC 3339 date' ) @@ -114,11 +169,11 @@ describe('W3cJwtCredentialService', () => { await expect( w3cJwtCredentialService.signCredential(agentContext, { verificationMethod: `${issuerDidJwk.verificationMethodId}extra`, - alg: JwaSignatureAlgorithm.ES256, + alg: KnownJwaSignatureAlgorithms.ES256, credential: JsonTransformer.fromJSON(credentialJson, W3cCredential), format: ClaimFormat.JwtVc, }) - ).rejects.toThrowError( + ).rejects.toThrow( `Unable to locate verification method with id 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InpRT293SUMxZ1dKdGRkZEI1R0F0NGxhdTZMdDhJaHk3NzFpQWZhbS0xcGMiLCJ5IjoiY2pEXzdvM2dkUTF2Z2lReTNfc01HczdXcndDTVU5RlFZaW1BM0h4bk1sdyJ9#0extra' in purposes assertionMethod` ) }) @@ -288,11 +343,11 @@ describe('W3cJwtCredentialService', () => { const signedJwtVp = await w3cJwtCredentialService.signPresentation(agentContext, { presentation, - alg: JwaSignatureAlgorithm.EdDSA, + alg: KnownJwaSignatureAlgorithms.EdDSA, challenge: 'daf942ad-816f-45ee-a9fc-facd08e5abca', domain: 'example.com', format: ClaimFormat.JwtVp, - verificationMethod: `${holderDidKey.did}#${holderDidKey.key.fingerprint}`, + verificationMethod: `${holderDidKey.did}#${holderDidKey.publicJwk.fingerprint}`, }) expect(signedJwtVp.serializedJwt).toEqual(CredoEs256DidKeyJwtVp) diff --git a/packages/core/src/modules/x509/X509Api.ts b/packages/core/src/modules/x509/X509Api.ts index aaa0ebd20b..42bb9c49fa 100644 --- a/packages/core/src/modules/x509/X509Api.ts +++ b/packages/core/src/modules/x509/X509Api.ts @@ -15,24 +15,6 @@ export class X509Api { public config: X509ModuleConfig ) {} - /** - * Adds a trusted certificate to the X509 Module Config. - * - * @param certificate - */ - public addTrustedCertificate(certificate: string) { - this.config.addTrustedCertificate(certificate) - } - - /** - * Overwrites the trusted certificates in the X509 Module Config. - * - * @param certificate - */ - public async setTrustedCertificates(certificates?: [string, ...string[]]) { - this.config.setTrustedCertificates(certificates) - } - /** * Creates a X.509 certificate. * diff --git a/packages/core/src/modules/x509/X509Certificate.ts b/packages/core/src/modules/x509/X509Certificate.ts index 64ad65cd56..f4e5ecd3e4 100644 --- a/packages/core/src/modules/x509/X509Certificate.ts +++ b/packages/core/src/modules/x509/X509Certificate.ts @@ -13,12 +13,11 @@ import { id_ce_subjectKeyIdentifier, } from '@peculiar/asn1-x509' import * as x509 from '@peculiar/x509' - -import { Key } from '../../crypto/Key' import { CredoWebCrypto, CredoWebCryptoKey } from '../../crypto/webcrypto' -import { credoKeyTypeIntoCryptoKeyAlgorithm, spkiAlgorithmIntoCredoKeyType } from '../../crypto/webcrypto/utils' +import { publicJwkToCryptoKeyAlgorithm, spkiToPublicJwk } from '../../crypto/webcrypto/utils' import { TypedArrayEncoder } from '../../utils' +import { PublicJwk, assymetricPublicJwkMatches } from '../kms' import { X509Error } from './X509Error' import { convertName, @@ -55,22 +54,34 @@ export enum X509ExtendedKeyUsage { } export type X509CertificateOptions = { - publicKey: Key + publicJwk: PublicJwk privateKey?: Uint8Array x509Certificate: x509.X509Certificate } export class X509Certificate { - public publicKey: Key + public publicJwk: PublicJwk public privateKey?: Uint8Array private x509Certificate: x509.X509Certificate private constructor(options: X509CertificateOptions) { - this.publicKey = options.publicKey + this.publicJwk = options.publicJwk this.privateKey = options.privateKey this.x509Certificate = options.x509Certificate } + public set keyId(keyId: string) { + this.publicJwk.keyId = keyId + } + + public get keyId(): string { + return this.publicJwk.keyId + } + + public get hasKeyId(): boolean { + return this.publicJwk.hasKeyId + } + public static fromRawCertificate(rawCertificate: Uint8Array): X509Certificate { const certificate = new x509.X509Certificate(rawCertificate) return X509Certificate.parseCertificate(certificate) @@ -82,16 +93,13 @@ export class X509Certificate { } private static parseCertificate(certificate: x509.X509Certificate): X509Certificate { - const publicKey = AsnParser.parse(certificate.publicKey.rawData, SubjectPublicKeyInfo) + const spki = AsnParser.parse(certificate.publicKey.rawData, SubjectPublicKeyInfo) const privateKey = certificate.privateKey ? new Uint8Array(certificate.privateKey.rawData) : undefined - const keyType = spkiAlgorithmIntoCredoKeyType(publicKey.algorithm) - const publicKeyBytes = new Uint8Array(publicKey.subjectPublicKey) - - const key = new Key(publicKeyBytes, keyType) + const publicJwk = spkiToPublicJwk(spki) return new X509Certificate({ - publicKey: key, + publicJwk, privateKey, x509Certificate: certificate, }) @@ -194,18 +202,18 @@ export class X509Certificate { public static async create(options: X509CreateCertificateOptions, webCrypto: CredoWebCrypto) { const subjectPublicKey = options.subjectPublicKey ?? options.authorityKey - const isSelfSignedCertificate = options.authorityKey.publicKeyBase58 === subjectPublicKey.publicKeyBase58 + const isSelfSignedCertificate = assymetricPublicJwkMatches(options.authorityKey.toJson(), subjectPublicKey.toJson()) const signingKey = new CredoWebCryptoKey( options.authorityKey, - credoKeyTypeIntoCryptoKeyAlgorithm(options.authorityKey.keyType), + publicJwkToCryptoKeyAlgorithm(options.authorityKey), false, 'private', ['sign'] ) const publicKey = new CredoWebCryptoKey( subjectPublicKey, - credoKeyTypeIntoCryptoKeyAlgorithm(options.authorityKey.keyType), + publicJwkToCryptoKeyAlgorithm(options.authorityKey), true, 'public', ['verify'] @@ -215,12 +223,14 @@ export class X509Certificate { const extensions: Array = [] extensions.push( - createSubjectKeyIdentifierExtension(options.extensions?.subjectKeyIdentifier, { key: subjectPublicKey }) + createSubjectKeyIdentifierExtension(options.extensions?.subjectKeyIdentifier, { publicJwk: subjectPublicKey }) ) extensions.push(createKeyUsagesExtension(options.extensions?.keyUsage)) extensions.push(createExtendedKeyUsagesExtension(options.extensions?.extendedKeyUsage)) extensions.push( - createAuthorityKeyIdentifierExtension(options.extensions?.authorityKeyIdentifier, { key: options.authorityKey }) + createAuthorityKeyIdentifierExtension(options.extensions?.authorityKeyIdentifier, { + publicJwk: options.authorityKey, + }) ) extensions.push(createIssuerAlternativeNameExtension(options.extensions?.issuerAlternativeName)) extensions.push(createSubjectAlternativeNameExtension(options.extensions?.subjectAlternativeName)) @@ -244,7 +254,9 @@ export class X509Certificate { webCrypto ) - return X509Certificate.parseCertificate(certificate) + const certificateInstance = X509Certificate.parseCertificate(certificate) + if (subjectPublicKey.hasKeyId) certificateInstance.publicJwk.keyId = subjectPublicKey.keyId + return certificateInstance } if (!options.subject) { @@ -266,7 +278,9 @@ export class X509Certificate { webCrypto ) - return X509Certificate.parseCertificate(certificate) + const certificateInstance = X509Certificate.parseCertificate(certificate) + if (subjectPublicKey.hasKeyId) certificateInstance.publicJwk.keyId = subjectPublicKey.keyId + return certificateInstance } public get subject() { @@ -280,11 +294,11 @@ export class X509Certificate { public async verify( { verificationDate = new Date(), - publicKey, + publicJwk, skipSignatureVerification = false, }: { verificationDate: Date - publicKey?: Key + publicJwk?: PublicJwk /** * Whether to skip the verification of the signature and only perform other checks (such @@ -301,9 +315,9 @@ export class X509Certificate { webCrypto: CredoWebCrypto ) { let publicCryptoKey: CredoWebCryptoKey | undefined - if (publicKey) { - const cryptoKeyAlgorithm = credoKeyTypeIntoCryptoKeyAlgorithm(publicKey.keyType) - publicCryptoKey = new CredoWebCryptoKey(publicKey, cryptoKeyAlgorithm, true, 'public', ['verify']) + if (publicJwk) { + const cryptoKeyAlgorithm = publicJwkToCryptoKeyAlgorithm(publicJwk) + publicCryptoKey = new CredoWebCryptoKey(publicJwk, cryptoKeyAlgorithm, true, 'public', ['verify']) } // We use the library to validate the signature, but the date is manually verified @@ -358,8 +372,15 @@ export class X509Certificate { return this.x509Certificate.issuerName.getField(field) } - public toString(format: 'asn' | 'pem' | 'hex' | 'base64' | 'text' | 'base64url') { - return this.x509Certificate.toString(format) + /** + * @param format the format to export to, defaults to `pem` + */ + public toString(format?: 'asn' | 'pem' | 'hex' | 'base64' | 'text' | 'base64url') { + return this.x509Certificate.toString(format ?? 'pem') + } + + private toJSON() { + return this.toString() } public equal(certificate: X509Certificate) { diff --git a/packages/core/src/modules/x509/X509ModuleConfig.ts b/packages/core/src/modules/x509/X509ModuleConfig.ts index 747cba9c09..f1822eb670 100644 --- a/packages/core/src/modules/x509/X509ModuleConfig.ts +++ b/packages/core/src/modules/x509/X509ModuleConfig.ts @@ -88,7 +88,7 @@ export interface X509ModuleConfigOptions { * * Array of trusted base64-encoded certificate strings in the DER-format. */ - trustedCertificates?: [string, ...string[]] + trustedCertificates?: Array /** * Optional callback method that will be called to dynamically get trusted certificates for a verification. @@ -131,17 +131,25 @@ export class X509ModuleConfig { this.#getTrustedCertificatesForVerification = fn } - public setTrustedCertificates(trustedCertificates?: [string, ...string[]]) { - this.#trustedCertificates = trustedCertificates - ? trustedCertificates.map((certificate) => X509Certificate.fromEncodedCertificate(certificate)) - : undefined + public setTrustedCertificates(trustedCertificates?: Array) { + const certificateInstances = trustedCertificates?.map((trustedCertificate) => + typeof trustedCertificate === 'string' + ? X509Certificate.fromEncodedCertificate(trustedCertificate) + : trustedCertificate + ) + this.#trustedCertificates = trustedCertificates?.length ? certificateInstances : undefined } - public addTrustedCertificate(trustedCertificate: string) { + public addTrustedCertificate(trustedCertificate: string | X509Certificate) { + const certificateInstance = + typeof trustedCertificate === 'string' + ? X509Certificate.fromEncodedCertificate(trustedCertificate) + : trustedCertificate + if (!this.#trustedCertificates) { - this.#trustedCertificates = [X509Certificate.fromEncodedCertificate(trustedCertificate)] - return + this.#trustedCertificates = [] } - this.#trustedCertificates.push(X509Certificate.fromEncodedCertificate(trustedCertificate)) + + this.#trustedCertificates.push(certificateInstance) } } diff --git a/packages/core/src/modules/x509/X509Service.ts b/packages/core/src/modules/x509/X509Service.ts index b71d5344d3..31ab8b1ae3 100644 --- a/packages/core/src/modules/x509/X509Service.ts +++ b/packages/core/src/modules/x509/X509Service.ts @@ -103,7 +103,7 @@ export class X509Service { // Verify the certificate with the publicKey of the certificate above for (let i = 0; i < parsedChain.length; i++) { const cert = parsedChain[i] - const publicKey = previousCertificate ? previousCertificate.publicKey : undefined + const publicJwk = previousCertificate ? previousCertificate.publicJwk : undefined // The only scenario where this will trigger is if the trusted certificates and the x509 chain both do not contain the // intermediate/root certificate needed. E.g. for ISO 18013-5 mDL the root cert MUST NOT be in the chain. If the signer @@ -114,7 +114,7 @@ export class X509Service { // In this case we could skip the signature verification (not other verifications), as we already trust the signer certificate, // but i think the purpose of ISO 18013-5 mDL is that you trust the root certificate. If we can't verify the whole chain e.g. // when we receive a credential we have the chance it will fail later on. - const skipSignatureVerification = i === 0 && trustedCertificates && !publicKey + const skipSignatureVerification = i === 0 && trustedCertificates && !publicJwk // NOTE: at some point we might want to change this to throw an error instead of skipping the signature verification of the trusted // but it would basically prevent mDOCs from unknown issuers to be verified in the wallet. Verifiers should only trust the root certificate // anyway. @@ -126,7 +126,7 @@ export class X509Service { await cert.verify( { - publicKey, + publicJwk, verificationDate, skipSignatureVerification, }, diff --git a/packages/core/src/modules/x509/X509ServiceOptions.ts b/packages/core/src/modules/x509/X509ServiceOptions.ts index cb64b7ed70..b8ee7aa5f6 100644 --- a/packages/core/src/modules/x509/X509ServiceOptions.ts +++ b/packages/core/src/modules/x509/X509ServiceOptions.ts @@ -1,5 +1,5 @@ import type { GeneralNameType } from '@peculiar/x509' -import type { Key } from '../../crypto/Key' +import { PublicJwk } from '../kms' import type { X509Certificate, X509ExtendedKeyUsage, X509KeyUsage } from './X509Certificate' type AddMarkAsCritical>> = T & { @@ -92,7 +92,7 @@ export interface X509CreateCertificateOptions { * The Key that will be used to sign the X.509 Certificate * */ - authorityKey: Key + authorityKey: PublicJwk /** * @@ -102,7 +102,7 @@ export interface X509CreateCertificateOptions { * This means that the certificate is self-signed * */ - subjectPublicKey?: Key + subjectPublicKey?: PublicJwk /** * diff --git a/packages/core/src/modules/x509/__tests__/X509Service.test.ts b/packages/core/src/modules/x509/__tests__/X509Service.test.ts index 7e7a0efa74..44d47fd54c 100644 --- a/packages/core/src/modules/x509/__tests__/X509Service.test.ts +++ b/packages/core/src/modules/x509/__tests__/X509Service.test.ts @@ -1,16 +1,13 @@ -import type { AgentContext } from '../../../agent' - import { id_ce_basicConstraints, id_ce_extKeyUsage, id_ce_keyUsage } from '@peculiar/asn1-x509' import * as x509 from '@peculiar/x509' -import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' import { getAgentConfig, getAgentContext } from '../../../../tests' -import { KeyType } from '../../../crypto/KeyType' -import { P256Jwk, getJwkFromKey } from '../../../crypto/jose/jwk' import { X509Error } from '../X509Error' import { X509Service } from '../X509Service' -import { CredoWebCrypto, Hasher, Key, TypedArrayEncoder, X509ExtendedKeyUsage, X509KeyUsage } from '@credo-ts/core' +import { CredoWebCrypto, Hasher, TypedArrayEncoder, X509ExtendedKeyUsage, X509KeyUsage } from '@credo-ts/core' +import { NodeInMemoryKeyManagementStorage, NodeKeyManagementService } from '../../../../../node/src' +import { KeyManagementApi, KeyManagementModuleConfig, KmsJwkPublicEc, P256PublicJwk, PublicJwk } from '../../kms' /** * @@ -41,22 +38,28 @@ const getLastMonth = () => { return lastMonth } +const agentConfig = getAgentConfig('X509Service') +const agentContext = getAgentContext({ + agentConfig, +}) + +const kmsApi = new KeyManagementApi( + new KeyManagementModuleConfig({ + backends: [new NodeKeyManagementService(new NodeInMemoryKeyManagementStorage())], + }), + agentContext +) +agentContext.dependencyManager.registerInstance(KeyManagementApi, kmsApi) + describe('X509Service', () => { - let wallet: InMemoryWallet - let agentContext: AgentContext let certificateChain: Array beforeAll(async () => { - const agentConfig = getAgentConfig('X509Service') - wallet = new InMemoryWallet() - agentContext = getAgentContext({ wallet }) - - // biome-ignore lint/style/noNonNullAssertion: - await wallet.createAndOpen(agentConfig.walletConfig!) - - const rootKey = await wallet.createKey({ keyType: KeyType.P256 }) - const intermediateKey = await wallet.createKey({ keyType: KeyType.P256 }) - const leafKey = await wallet.createKey({ keyType: KeyType.P256 }) + const rootKey = PublicJwk.fromPublicJwk((await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } })).publicJwk) + const intermediateKey = PublicJwk.fromPublicJwk( + (await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } })).publicJwk + ) + const leafKey = PublicJwk.fromPublicJwk((await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } })).publicJwk) x509.cryptoProvider.set(new CredoWebCrypto(agentContext)) @@ -107,26 +110,22 @@ describe('X509Service', () => { x509.cryptoProvider.clear() }) - afterAll(async () => { - await wallet.close() - }) - it('should create a valid self-signed certificate', async () => { - const authorityKey = await wallet.createKey({ keyType: KeyType.P256 }) + const authorityKey = await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } }) const certificate = await X509Service.createCertificate(agentContext, { - authorityKey, + authorityKey: PublicJwk.fromPublicJwk(authorityKey.publicJwk), issuer: { commonName: 'credo' }, }) - expect(certificate.publicKey.keyType).toStrictEqual(KeyType.P256) - expect(certificate.publicKey.publicKey.length).toStrictEqual(65) + expect(certificate.publicJwk.toJson()).toMatchObject({ kty: 'EC', crv: 'P-256', kid: expect.any(String) }) + expect((certificate.publicJwk as PublicJwk).publicKey.publicKey.length).toStrictEqual(65) expect(certificate.subject).toStrictEqual('CN=credo') }) it('should create a valid self-signed certificate with a critical extension', async () => { - const authorityKey = await wallet.createKey({ keyType: KeyType.P256 }) + const authorityKey = await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } }) const certificate = await X509Service.createCertificate(agentContext, { - authorityKey, + authorityKey: PublicJwk.fromPublicJwk(authorityKey.publicJwk), issuer: { commonName: 'credo' }, extensions: { keyUsage: { @@ -145,9 +144,9 @@ describe('X509Service', () => { }) it('should create a valid self-signed certifcate with extensions', async () => { - const authorityKey = await wallet.createKey({ keyType: KeyType.P256 }) + const authorityKey = await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } }) const certificate = await X509Service.createCertificate(agentContext, { - authorityKey, + authorityKey: PublicJwk.fromPublicJwk(authorityKey.publicJwk), issuer: { commonName: 'credo' }, extensions: { subjectAlternativeName: { @@ -173,16 +172,18 @@ describe('X509Service', () => { expect(certificate.keyUsage).toStrictEqual(expect.arrayContaining([X509KeyUsage.DigitalSignature])) expect(certificate.extendedKeyUsage).toStrictEqual(expect.arrayContaining([X509ExtendedKeyUsage.MdlDs])) expect(certificate.subjectKeyIdentifier).toStrictEqual( - TypedArrayEncoder.toHex(Hasher.hash(authorityKey.publicKey, 'SHA-1')) + TypedArrayEncoder.toHex( + Hasher.hash((certificate.publicJwk as PublicJwk).publicKey.publicKey, 'SHA-1') + ) ) }) it('should create a valid self-signed certifcate as IACA Root + DCS for mDoc', async () => { - const authorityKey = await wallet.createKey({ keyType: KeyType.P256 }) - const documentSignerKey = await wallet.createKey({ keyType: KeyType.P256 }) + const authorityKey = await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } }) + const documentSignerKey = await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } }) const mdocRootCertificate = await X509Service.createCertificate(agentContext, { - authorityKey, + authorityKey: PublicJwk.fromPublicJwk(authorityKey.publicJwk), issuer: { commonName: 'credo', countryName: 'NL' }, validity: { notBefore: getLastMonth(), @@ -216,12 +217,23 @@ describe('X509Service', () => { expect(mdocRootCertificate).toMatchObject({ ianUriNames: expect.arrayContaining(['animo.id']), keyUsage: expect.arrayContaining([X509KeyUsage.KeyCertSign, X509KeyUsage.CrlSign]), - subjectKeyIdentifier: TypedArrayEncoder.toHex(Hasher.hash(authorityKey.publicKey, 'SHA-1')), + subjectKeyIdentifier: TypedArrayEncoder.toHex( + Hasher.hash((mdocRootCertificate.publicJwk as PublicJwk).publicKey.publicKey, 'SHA-1') + ), }) + const authorityJwk = PublicJwk.fromPublicJwk(authorityKey.publicJwk) + const authorityPublicKey = authorityJwk.publicKey + const documentSignerJwk = PublicJwk.fromPublicJwk(documentSignerKey.publicJwk) + const documentSignerPublicKey = documentSignerJwk.publicKey + + if (authorityPublicKey.kty !== 'EC' || documentSignerPublicKey.kty !== 'EC') { + throw new Error('invalid kty') + } + const mdocDocumentSignerCertificate = await X509Service.createCertificate(agentContext, { - authorityKey, - subjectPublicKey: new Key(documentSignerKey.publicKey, KeyType.P256), + authorityKey: PublicJwk.fromPublicJwk(authorityKey.publicJwk), + subjectPublicKey: PublicJwk.fromPublicJwk(documentSignerKey.publicJwk), issuer: mdocRootCertificate.issuer, subject: { commonName: 'credo dcs', countryName: 'NL' }, validity: { @@ -264,8 +276,8 @@ describe('X509Service', () => { sanUriNames: expect.arrayContaining(['paradym.id']), keyUsage: expect.arrayContaining([X509KeyUsage.DigitalSignature]), extendedKeyUsage: expect.arrayContaining([X509ExtendedKeyUsage.MdlDs]), - subjectKeyIdentifier: TypedArrayEncoder.toHex(Hasher.hash(documentSignerKey.publicKey, 'SHA-1')), - authorityKeyIdentifier: TypedArrayEncoder.toHex(Hasher.hash(authorityKey.publicKey, 'SHA-1')), + subjectKeyIdentifier: TypedArrayEncoder.toHex(Hasher.hash(documentSignerPublicKey.publicKey, 'SHA-1')), + authorityKeyIdentifier: TypedArrayEncoder.toHex(Hasher.hash(authorityPublicKey.publicKey, 'SHA-1')), }) // Verify chain where the root cert is trusted, but not in the chain @@ -287,12 +299,21 @@ describe('X509Service', () => { }) it('should create a valid leaf certificate', async () => { - const authorityKey = await wallet.createKey({ keyType: KeyType.P256 }) - const subjectKey = await wallet.createKey({ keyType: KeyType.P256 }) + const authorityKey = await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } }) + const subjectKey = await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } }) + + const authorityJwk = PublicJwk.fromPublicJwk(authorityKey.publicJwk) + const authorityPublicKey = authorityJwk.publicKey + const subjectJwk = PublicJwk.fromPublicJwk(subjectKey.publicJwk) + const subjectPublicKey = subjectJwk.publicKey + + if (authorityPublicKey.kty !== 'EC' || subjectPublicKey.kty !== 'EC') { + throw new Error('invalid kty') + } const certificate = await X509Service.createCertificate(agentContext, { - authorityKey, - subjectPublicKey: new Key(subjectKey.publicKey, KeyType.P256), + authorityKey: PublicJwk.fromPublicJwk(authorityKey.publicJwk), + subjectPublicKey: PublicJwk.fromPublicJwk(subjectKey.publicJwk), issuer: { commonName: 'credo' }, subject: { commonName: 'DCS credo' }, extensions: { @@ -302,13 +323,13 @@ describe('X509Service', () => { }) expect(certificate.subjectKeyIdentifier).toStrictEqual( - TypedArrayEncoder.toHex(Hasher.hash(subjectKey.publicKey, 'SHA-1')) + TypedArrayEncoder.toHex(Hasher.hash(subjectPublicKey.publicKey, 'SHA-1')) ) expect(certificate.authorityKeyIdentifier).toStrictEqual( - TypedArrayEncoder.toHex(Hasher.hash(authorityKey.publicKey, 'SHA-1')) + TypedArrayEncoder.toHex(Hasher.hash(authorityPublicKey.publicKey, 'SHA-1')) ) - expect(certificate.publicKey.keyType).toStrictEqual(KeyType.P256) - expect(certificate.publicKey.publicKey.length).toStrictEqual(65) + expect(authorityPublicKey.crv).toStrictEqual('P-256') + expect(authorityPublicKey.publicKey.length).toStrictEqual(65) expect(certificate.subject).toStrictEqual('CN=DCS credo') }) @@ -318,16 +339,18 @@ describe('X509Service', () => { const x509Certificate = X509Service.parseCertificate(agentContext, { encodedCertificate }) - expect(x509Certificate.publicKey.keyType).toStrictEqual(KeyType.P256) - expect(x509Certificate.publicKey.publicKey.length).toStrictEqual(65) - expect(x509Certificate.publicKey.publicKeyBase58).toStrictEqual( + const publicKey = x509Certificate.publicJwk.publicKey + if (publicKey.kty !== 'EC') { + throw new Error('uexpected kty value') + } + + expect(publicKey.crv).toStrictEqual('P-256') + expect(publicKey.publicKey.length).toStrictEqual(65) + expect(TypedArrayEncoder.toBase58(publicKey.publicKey)).toStrictEqual( 'QDaLvg9KroUnpuviZ9W7Q3DauqAuKiJN4sKC6cLo4HtxnpJCwwayNBLzRpsCHfHsLJsiKDeTCV8LqmCBSPkmiJNe' ) - const jwk = getJwkFromKey(x509Certificate.publicKey) - - expect(jwk).toBeInstanceOf(P256Jwk) - expect(jwk.toJson()).toMatchObject({ + expect(x509Certificate.publicJwk.toJson()).toMatchObject({ x: 'iTwtg0eQbcbNabf2Nq9L_VM_lhhPCq2s0Qgw2kRx29s', y: 'YKwXDRz8U0-uLZ3NSI93R_35eNkl6jHp6Qg8OCup7VM', }) @@ -351,21 +374,15 @@ describe('X509Service', () => { expect(validatedChain.length).toStrictEqual(3) const leafCertificate = validatedChain[validatedChain.length - 1] - - expect(leafCertificate).toMatchObject({ - publicKey: expect.objectContaining({ - keyType: KeyType.P256, - }), - privateKey: undefined, - }) + expect(leafCertificate.publicJwk.jwk).toBeInstanceOf(P256PublicJwk) }) it('should verify a certificate chain where the root certificate is not in the provided chain, but is in trusted certificates', async () => { - const authorityKey = await wallet.createKey({ keyType: KeyType.P256 }) - const documentSignerKey = await wallet.createKey({ keyType: KeyType.P256 }) + const authorityKey = await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } }) + const documentSignerKey = await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } }) const mdocRootCertificate = await X509Service.createCertificate(agentContext, { - authorityKey, + authorityKey: PublicJwk.fromPublicJwk(authorityKey.publicJwk), issuer: { commonName: 'credo', countryName: 'NL' }, validity: { notBefore: getLastMonth(), @@ -394,8 +411,8 @@ describe('X509Service', () => { }) const mdocDocumentSignerCertificate = await X509Service.createCertificate(agentContext, { - authorityKey, - subjectPublicKey: new Key(documentSignerKey.publicKey, KeyType.P256), + authorityKey: PublicJwk.fromPublicJwk(authorityKey.publicJwk), + subjectPublicKey: PublicJwk.fromPublicJwk(documentSignerKey.publicJwk), issuer: mdocRootCertificate.issuer, subject: { commonName: 'credo dcs', countryName: 'NL' }, validity: { @@ -437,11 +454,11 @@ describe('X509Service', () => { }) it('should not validate a certificate with a `notBefore` of > Date.now', async () => { - const authorityKey = await agentContext.wallet.createKey({ keyType: KeyType.P256 }) + const authorityKey = await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } }) const certificate = ( await X509Service.createCertificate(agentContext, { - authorityKey, + authorityKey: PublicJwk.fromPublicJwk(authorityKey.publicJwk), issuer: 'CN=credo', validity: { notBefore: getNextMonth(), @@ -458,11 +475,11 @@ describe('X509Service', () => { }) it('should not validate a certificate with a `notAfter` of < Date.now', async () => { - const authorityKey = await agentContext.wallet.createKey({ keyType: KeyType.P256 }) + const authorityKey = await kmsApi.createKey({ type: { kty: 'EC', crv: 'P-256' } }) const certificate = ( await X509Service.createCertificate(agentContext, { - authorityKey, + authorityKey: PublicJwk.fromPublicJwk(authorityKey.publicJwk), issuer: 'CN=credo', validity: { notAfter: getLastMonth(), @@ -498,8 +515,8 @@ describe('X509Service', () => { certificateChain: x5c, }) expect(chain.length).toStrictEqual(2) - expect(chain[0].publicKey.keyType).toStrictEqual(KeyType.P384) - expect(chain[1].publicKey.keyType).toStrictEqual(KeyType.P256) + expect((chain[0].publicJwk.toJson() as KmsJwkPublicEc).crv).toStrictEqual('P-384') + expect((chain[1].publicJwk.toJson() as KmsJwkPublicEc).crv).toStrictEqual('P-256') // Works with root certificate as trusted certificate await expect( @@ -538,7 +555,7 @@ describe('X509Service', () => { }) expect(chain.length).toStrictEqual(2) - expect(chain[0].publicKey.keyType).toStrictEqual(KeyType.P384) - expect(chain[1].publicKey.keyType).toStrictEqual(KeyType.P256) + expect((chain[0].publicJwk.toJson() as KmsJwkPublicEc).crv).toStrictEqual('P-384') + expect((chain[1].publicJwk.toJson() as KmsJwkPublicEc).crv).toStrictEqual('P-256') }) }) diff --git a/packages/core/src/modules/x509/utils/extensions.ts b/packages/core/src/modules/x509/utils/extensions.ts index 00a2134675..05fed44125 100644 --- a/packages/core/src/modules/x509/utils/extensions.ts +++ b/packages/core/src/modules/x509/utils/extensions.ts @@ -1,4 +1,4 @@ -import { Hasher, type Key } from '../../../crypto' +import { Hasher } from '../../../crypto/hashes/Hasher' import type { X509CertificateExtensionsOptions } from '../X509ServiceOptions' import { @@ -10,17 +10,19 @@ import { SubjectAlternativeNameExtension, SubjectKeyIdentifierExtension, } from '@peculiar/x509' - +import { publicJwkToSpki } from '../../../crypto/webcrypto/utils' import { TypedArrayEncoder } from '../../../utils' +import { PublicJwk } from '../../kms' import { IssuerAlternativeNameExtension } from '../extensions' export const createSubjectKeyIdentifierExtension = ( options: X509CertificateExtensionsOptions['subjectKeyIdentifier'], - additionalOptions: { key: Key } + additionalOptions: { publicJwk: PublicJwk } ) => { if (!options || !options.include) return - const hash = Hasher.hash(additionalOptions.key.publicKey, 'SHA-1') + const spki = publicJwkToSpki(additionalOptions.publicJwk) + const hash = Hasher.hash(new Uint8Array(spki.subjectPublicKey), 'SHA-1') return new SubjectKeyIdentifierExtension(TypedArrayEncoder.toHex(hash)) } @@ -41,11 +43,12 @@ export const createExtendedKeyUsagesExtension = (options: X509CertificateExtensi export const createAuthorityKeyIdentifierExtension = ( options: X509CertificateExtensionsOptions['authorityKeyIdentifier'], - additionalOptions: { key: Key } + additionalOptions: { publicJwk: PublicJwk } ) => { if (!options) return - const hash = Hasher.hash(additionalOptions.key.publicKey, 'SHA-1') + const spki = publicJwkToSpki(additionalOptions.publicJwk) + const hash = Hasher.hash(new Uint8Array(spki.subjectPublicKey), 'SHA-1') return new AuthorityKeyIdentifierExtension(TypedArrayEncoder.toHex(hash), options.markAsCritical) } diff --git a/packages/core/src/plugins/DependencyManager.ts b/packages/core/src/plugins/DependencyManager.ts index 365e894258..1bc8f57587 100644 --- a/packages/core/src/plugins/DependencyManager.ts +++ b/packages/core/src/plugins/DependencyManager.ts @@ -1,4 +1,5 @@ import type { DependencyContainer } from 'tsyringe' +import type { AgentContext } from '../agent' import type { ModulesMap } from '../agent/AgentModules' import type { Constructor } from '../utils/mixins' @@ -40,6 +41,133 @@ export class DependencyManager { } } + public async initializeModules(agentContext: AgentContext) { + if (agentContext.dependencyManager.container !== this.container) { + throw new CredoError( + `Method 'initializeModule' called on DependencyManager different from the agent context for which 'initializeModule' is called. Make sure to call 'initializeModule' on the DependencyManager associated with the agent context.` + ) + } + + for (const [moduleName, module] of Object.entries(this.registeredModules)) { + try { + await module.initialize?.(agentContext) + } catch (error) { + throw new CredoError( + `Error during call to 'initialize' method in module '${moduleName}' for agent context '${agentContext.contextCorrelationId}'.`, + { cause: error } + ) + } + } + } + + public async shutdownModules(agentContext: AgentContext) { + if (agentContext.dependencyManager.container !== this.container) { + throw new CredoError( + `Method 'shutdownModules' called on DependencyManager different from the agent context for which 'shutdownModules' is called. Make sure to call 'shutdownModules' on the DependencyManager associated with the agent context.` + ) + } + + for (const [moduleName, module] of Object.entries(this.registeredModules)) { + try { + await module.shutdown?.(agentContext) + } catch (error) { + throw new CredoError( + `Error during call to 'shutdown' method in module '${moduleName}' for agent context '${agentContext.contextCorrelationId}'.`, + { cause: error } + ) + } + } + } + + public async initializeAgentContext(agentContext: AgentContext) { + if (agentContext.dependencyManager.container !== this.container) { + throw new CredoError( + `Method 'initializeAgentContext' called on DependencyManager different from the agent context for which 'initializeAgentContext' is called. Make sure to call 'initializeAgentContext' on the DependencyManager associated with the agent context.` + ) + } + + for (const [moduleName, module] of Object.entries(this.registeredModules)) { + try { + await module.onInitializeContext?.(agentContext) + } catch (error) { + throw new CredoError( + `Error during call to 'onInitializeContext' method in module '${moduleName}' for agent context '${agentContext.contextCorrelationId}'.`, + { cause: error } + ) + } + } + } + + public async deleteAgentContext(agentContext: AgentContext) { + if (agentContext.dependencyManager.container !== this.container) { + throw new CredoError( + `Method 'deleteAgentContext' called on DependencyManager different from the agent context for which 'deleteAgentContext' is called. Make sure to call 'deleteAgentContext' on the DependencyManager associated with the agent context.` + ) + } + + try { + for (const [moduleName, module] of Object.entries(this.registeredModules)) { + try { + await module.onDeleteContext?.(agentContext) + } catch (error) { + throw new CredoError( + `Error during call to 'onDeleteContext' method in module '${moduleName}' for agent context '${agentContext.contextCorrelationId}'.`, + { cause: error } + ) + } + } + } finally { + await this.container.dispose() + } + } + + public async provisionAgentContext(agentContext: AgentContext) { + if (agentContext.dependencyManager.container !== this.container) { + throw new CredoError( + `Method 'provisionAgentContext' called on DependencyManager different from the agent context for which 'provisionAgentContext' is called. Make sure to call 'provisionAgentContext' on the DependencyManager associated with the agent context.` + ) + } + + for (const [moduleName, module] of Object.entries(this.registeredModules)) { + try { + await module.onProvisionContext?.(agentContext) + } catch (error) { + throw new CredoError( + `Error during call to 'onProvisionContext' method in module '${moduleName}' for agent context '${agentContext.contextCorrelationId}'.`, + { cause: error } + ) + } + } + + return agentContext + } + + public async closeAgentContext(agentContext: AgentContext) { + if (agentContext.dependencyManager.container !== this.container) { + throw new CredoError( + `Method 'closeAgentContext' called on DependencyManager different from the agent context for which 'closeAgentContext' is called. Make sure to call 'closeAgentContext' on the DependencyManager associated with the agent context.` + ) + } + + try { + for (const [moduleName, module] of Object.entries(this.registeredModules)) { + try { + await module.onCloseContext?.(agentContext) + } catch (error) { + throw new CredoError( + `Error during call to 'onCloseContext' method in module '${moduleName}' for agent context '${agentContext.contextCorrelationId}'.`, + { cause: error } + ) + } + } + } finally { + // NOTE: we support reinitialization of the root agent so we can't dispose of the agent context + if (!agentContext.isRootAgentContext) { + await this.container.dispose() + } + } + } + public registerSingleton(from: InjectionToken, to: InjectionToken): void public registerSingleton(token: Constructor): void // biome-ignore lint/suspicious/noExplicitAny: @@ -55,8 +183,8 @@ export class DependencyManager { this.container.registerInstance(token, instance) } - public isRegistered(token: InjectionToken): boolean { - return this.container.isRegistered(token) + public isRegistered(token: InjectionToken, recursive = false): boolean { + return this.container.isRegistered(token, recursive) } // biome-ignore lint/suspicious/noExplicitAny: @@ -70,15 +198,6 @@ export class DependencyManager { else this.container.register(token, token, { lifecycle: Lifecycle.ContainerScoped }) } - /** - * Dispose the dependency manager. Calls `.dispose()` on all instances that implement the `Disposable` interface and have - * been constructed by the `DependencyManager`. This means all instances registered using `registerInstance` won't have the - * dispose method called. - */ - public async dispose() { - await this.container.dispose() - } - public createChild() { return new DependencyManager(this.container.createChildContainer(), this.registeredModules) } diff --git a/packages/core/src/plugins/Module.ts b/packages/core/src/plugins/Module.ts index 65f92fbf9c..f14e4006a4 100644 --- a/packages/core/src/plugins/Module.ts +++ b/packages/core/src/plugins/Module.ts @@ -6,9 +6,54 @@ import type { DependencyManager } from './DependencyManager' export interface Module { api?: Constructor register(dependencyManager: DependencyManager): void + + /** + * Method that will be called to initialize a module. This method is only called once on startup + * with the root agent context. It is meant to initialize services and requirements that are used + * globally within the agent, such as a connection to an external ledger. + * + * For context specific dependencies it is recommened to use `onInitializeContext`, which will be + * called for every context that is initialized. + */ initialize?(agentContext: AgentContext): Promise + + /** + * Method that will be called to shutdown a module. This method is only called once on shutdown + * with the root agent context. It is meant to shutdown services and requirements that are used + * globally within the agent, such as a connection to an external ledger. + * + * For context specific dependencies it is recommened to use `onCloseContext`, which will be + * called for every context that is closed. + */ shutdown?(agentContext: AgentContext): Promise + /** + * Method that will be called when an agent context is deleted, and will allow for cleanup of + * data related to this agent context. + */ + onDeleteContext?(agentContext: AgentContext): Promise + + /** + * Method that will be called when an agent context is provisioned/created, and will allow for setting + * up of required services, data or other dependencies for an agent context. + * + * NOTE: this method will NOT be called for the root agent context as the framework + * does not know whether we are provisioning or initializing an existing agent. + */ + onProvisionContext?(agentContext: AgentContext): Promise + + /** + * Method that will be called when an agent context is initialized, and will allow for + * setting up of required services, data or other dependencies for an agent context. + */ + onInitializeContext?(agentContext: AgentContext): Promise + + /** + * Method that will be called when an agent context is closed, and will allow for + * closing of e.g. database sessions. + */ + onCloseContext?(agentContext: AgentContext): Promise + /** * List of updates that should be executed when the framework version is updated. */ diff --git a/packages/core/src/plugins/index.ts b/packages/core/src/plugins/index.ts index bf419032f6..711246f43a 100644 --- a/packages/core/src/plugins/index.ts +++ b/packages/core/src/plugins/index.ts @@ -1,4 +1,4 @@ export * from './DependencyManager' export * from './Module' export * from './utils' -export { inject, injectable, Disposable, injectAll } from 'tsyringe' +export { inject, injectable, injectAll } from 'tsyringe' diff --git a/packages/core/src/storage/BaseRecord.ts b/packages/core/src/storage/BaseRecord.ts index 7d047671ab..3f9a3a92d5 100644 --- a/packages/core/src/storage/BaseRecord.ts +++ b/packages/core/src/storage/BaseRecord.ts @@ -44,6 +44,10 @@ export abstract class BaseRecord< public readonly type = BaseRecord.type public static readonly type: string = 'BaseRecord' + @Exclude() + public readonly useCache = BaseRecord.useCache + public static readonly useCache: boolean = false + /** @inheritdoc {Metadata#Metadata} */ @MetadataTransformer() public metadata: Metadata = new Metadata({}) diff --git a/packages/core/src/storage/Repository.ts b/packages/core/src/storage/Repository.ts index 6db3895223..fe2a03750e 100644 --- a/packages/core/src/storage/Repository.ts +++ b/packages/core/src/storage/Repository.ts @@ -5,7 +5,7 @@ import type { RecordDeletedEvent, RecordSavedEvent, RecordUpdatedEvent } from '. import type { BaseRecordConstructor, Query, QueryOptions, StorageService } from './StorageService' import { RecordDuplicateError, RecordNotFoundError } from '../error' - +import { CachedStorageService } from '../modules/cache/CachedStorageService' import { RepositoryEventTypes } from './RepositoryEvents' // biome-ignore lint/suspicious/noExplicitAny: @@ -19,14 +19,22 @@ export class Repository> { storageService: StorageService, eventEmitter: EventEmitter ) { - this.storageService = storageService this.recordClass = recordClass + this.storageService = storageService this.eventEmitter = eventEmitter } + private getStorageService(agentContext: AgentContext): StorageService { + if (agentContext.dependencyManager.isRegistered(CachedStorageService)) { + return agentContext.resolve(CachedStorageService) + } + + return this.storageService + } + /** @inheritDoc {StorageService#save} */ public async save(agentContext: AgentContext, record: T): Promise { - await this.storageService.save(agentContext, record) + await this.getStorageService(agentContext).save(agentContext, record) this.eventEmitter.emit>(agentContext, { type: RepositoryEventTypes.RecordSaved, @@ -39,7 +47,7 @@ export class Repository> { /** @inheritDoc {StorageService#update} */ public async update(agentContext: AgentContext, record: T): Promise { - await this.storageService.update(agentContext, record) + await this.getStorageService(agentContext).update(agentContext, record) this.eventEmitter.emit>(agentContext, { type: RepositoryEventTypes.RecordUpdated, @@ -52,7 +60,7 @@ export class Repository> { /** @inheritDoc {StorageService#delete} */ public async delete(agentContext: AgentContext, record: T): Promise { - await this.storageService.delete(agentContext, record) + await this.getStorageService(agentContext).delete(agentContext, record) this.eventEmitter.emit>(agentContext, { type: RepositoryEventTypes.RecordDeleted, @@ -69,7 +77,7 @@ export class Repository> { * @returns */ public async deleteById(agentContext: AgentContext, id: string): Promise { - await this.storageService.deleteById(agentContext, this.recordClass, id) + await this.getStorageService(agentContext).deleteById(agentContext, this.recordClass, id) this.eventEmitter.emit>(agentContext, { type: RepositoryEventTypes.RecordDeleted, @@ -81,7 +89,7 @@ export class Repository> { /** @inheritDoc {StorageService#getById} */ public async getById(agentContext: AgentContext, id: string): Promise { - return this.storageService.getById(agentContext, this.recordClass, id) + return this.getStorageService(agentContext).getById(agentContext, this.recordClass, id) } /** @@ -91,7 +99,7 @@ export class Repository> { */ public async findById(agentContext: AgentContext, id: string): Promise { try { - return await this.storageService.getById(agentContext, this.recordClass, id) + return await this.getStorageService(agentContext).getById(agentContext, this.recordClass, id) } catch (error) { if (error instanceof RecordNotFoundError) return null @@ -101,12 +109,12 @@ export class Repository> { /** @inheritDoc {StorageService#getAll} */ public async getAll(agentContext: AgentContext): Promise { - return this.storageService.getAll(agentContext, this.recordClass) + return this.getStorageService(agentContext).getAll(agentContext, this.recordClass) } /** @inheritDoc {StorageService#findByQuery} */ public async findByQuery(agentContext: AgentContext, query: Query, queryOptions?: QueryOptions): Promise { - return this.storageService.findByQuery(agentContext, this.recordClass, query, queryOptions) + return this.getStorageService(agentContext).findByQuery(agentContext, this.recordClass, query, queryOptions) } /** diff --git a/packages/core/src/storage/StorageService.ts b/packages/core/src/storage/StorageService.ts index f19d760280..6837447884 100644 --- a/packages/core/src/storage/StorageService.ts +++ b/packages/core/src/storage/StorageService.ts @@ -29,6 +29,7 @@ export type Query> = AdvancedQuery | Simp export interface BaseRecordConstructor extends Constructor { type: string + useCache: boolean } // biome-ignore lint/suspicious/noExplicitAny: diff --git a/packages/core/src/storage/migration/StorageUpdateService.ts b/packages/core/src/storage/migration/StorageUpdateService.ts index 0125e1f00d..6f0c11526b 100644 --- a/packages/core/src/storage/migration/StorageUpdateService.ts +++ b/packages/core/src/storage/migration/StorageUpdateService.ts @@ -13,8 +13,6 @@ import { INITIAL_STORAGE_VERSION } from './updates' @injectable() export class StorageUpdateService { - private static STORAGE_VERSION_RECORD_ID = 'STORAGE_VERSION_RECORD_ID' - private logger: Logger private storageVersionRepository: StorageVersionRepository @@ -41,7 +39,7 @@ export class StorageUpdateService { this.logger.debug(`Setting current agent storage version to ${storageVersion}`) const storageVersionRecord = await this.storageVersionRepository.findById( agentContext, - StorageUpdateService.STORAGE_VERSION_RECORD_ID + StorageVersionRecord.storageVersionRecordId ) if (!storageVersionRecord) { @@ -49,7 +47,6 @@ export class StorageUpdateService { await this.storageVersionRepository.save( agentContext, new StorageVersionRecord({ - id: StorageUpdateService.STORAGE_VERSION_RECORD_ID, storageVersion, }) ) @@ -69,12 +66,11 @@ export class StorageUpdateService { public async getStorageVersionRecord(agentContext: AgentContext) { let storageVersionRecord = await this.storageVersionRepository.findById( agentContext, - StorageUpdateService.STORAGE_VERSION_RECORD_ID + StorageVersionRecord.storageVersionRecordId ) if (!storageVersionRecord) { storageVersionRecord = new StorageVersionRecord({ - id: StorageUpdateService.STORAGE_VERSION_RECORD_ID, storageVersion: INITIAL_STORAGE_VERSION, }) await this.storageVersionRepository.save(agentContext, storageVersionRecord) diff --git a/packages/core/src/storage/migration/UpdateAssistant.ts b/packages/core/src/storage/migration/UpdateAssistant.ts index 2ebb85532c..bd382d33f2 100644 --- a/packages/core/src/storage/migration/UpdateAssistant.ts +++ b/packages/core/src/storage/migration/UpdateAssistant.ts @@ -1,13 +1,9 @@ import type { BaseAgent } from '../../agent/BaseAgent' import type { Module } from '../../plugins' -import type { FileSystem } from '../FileSystem' import type { Update, UpdateConfig, UpdateToVersion } from './updates' -import { InjectionSymbols } from '../../constants' import { CredoError } from '../../error' import { isFirstVersionEqualToSecond, isFirstVersionHigherThanSecond, parseVersionString } from '../../utils/version' -import { WalletExportPathExistsError, WalletExportUnsupportedError } from '../../wallet/error' -import { WalletError } from '../../wallet/error/WalletError' import { StorageUpdateService } from './StorageUpdateService' import { StorageUpdateError } from './error/StorageUpdateError' @@ -15,7 +11,6 @@ import { CURRENT_FRAMEWORK_STORAGE_VERSION, DEFAULT_UPDATE_CONFIG, supportedUpda export interface UpdateAssistantUpdateOptions { updateToVersion?: UpdateToVersion - backupBeforeStorageUpdate?: boolean } // biome-ignore lint/suspicious/noExplicitAny: @@ -23,31 +18,18 @@ export class UpdateAssistant = BaseAgent> { private agent: Agent private storageUpdateService: StorageUpdateService private updateConfig: UpdateConfig - private fileSystem: FileSystem public constructor(agent: Agent, updateConfig: UpdateConfig = DEFAULT_UPDATE_CONFIG) { this.agent = agent this.updateConfig = updateConfig this.storageUpdateService = this.agent.dependencyManager.resolve(StorageUpdateService) - this.fileSystem = this.agent.dependencyManager.resolve(InjectionSymbols.FileSystem) } public async initialize() { if (this.agent.isInitialized) { throw new CredoError("Can't initialize UpdateAssistant after agent is initialized") } - - // Initialize the wallet if not already done - if (!this.agent.wallet.isInitialized && this.agent.config.walletConfig) { - await this.agent.wallet.initialize(this.agent.config.walletConfig) - } else if (!this.agent.wallet.isInitialized) { - throw new WalletError( - 'Wallet config has not been set on the agent config. ' + - 'Make sure to initialize the wallet yourself before initializing the update assistant, ' + - 'or provide the required wallet configuration in the agent constructor' - ) - } } public async isUpToDate(updateToVersion?: UpdateToVersion) { @@ -116,9 +98,6 @@ export class UpdateAssistant = BaseAgent> { const updateIdentifier = Date.now().toString() const updateToVersion = options?.updateToVersion - // By default do a backup first (should be explicitly disabled in case the wallet backend does not support export) - const createBackup = options?.backupBeforeStorageUpdate ?? true - try { this.agent.config.logger.info(`Starting update of agent storage with updateIdentifier ${updateIdentifier}`) const neededUpdates = await this.getNeededUpdates(updateToVersion) @@ -151,11 +130,6 @@ export class UpdateAssistant = BaseAgent> { `Starting update process. Total of ${neededUpdates.length} update(s) will be applied to update the agent storage from version ${fromVersion} to version ${toVersion}` ) - // Create backup in case migration goes wrong - if (createBackup) { - await this.createBackup(updateIdentifier) - } - try { for (const update of neededUpdates) { const registeredModules = Object.values(this.agent.dependencyManager.registeredModules) @@ -200,49 +174,14 @@ export class UpdateAssistant = BaseAgent> { `Successfully updated agent storage from version ${update.fromVersion} to version ${update.toVersion}` ) } - if (createBackup) { - // Delete backup file, as it is not needed anymore - await this.fileSystem.delete(this.getBackupPath(updateIdentifier)) - } } catch (error) { this.agent.config.logger.fatal('An error occurred while updating the wallet.', { error, }) - if (createBackup) { - this.agent.config.logger.debug('Restoring backup.') - // In the case of an error we want to restore the backup - await this.restoreBackup(updateIdentifier) - - // Delete backup file, as wallet was already restored (backup-error file will persist though) - await this.fileSystem.delete(this.getBackupPath(updateIdentifier)) - } - throw error } } catch (error) { - // Backup already exists at path - if (error instanceof WalletExportPathExistsError) { - const backupPath = this.getBackupPath(updateIdentifier) - const errorMessage = `Error updating storage with updateIdentifier ${updateIdentifier} because the backup at path ${backupPath} already exists` - this.agent.config.logger.fatal(errorMessage, { - error, - updateIdentifier, - backupPath, - }) - throw new StorageUpdateError(errorMessage, { cause: error }) - } - // Wallet backend does not support export - if (error instanceof WalletExportUnsupportedError) { - const errorMessage = `Error updating storage with updateIdentifier ${updateIdentifier} because the wallet backend does not support exporting. - Make sure to do a manual backup of your wallet and disable 'backupBeforeStorageUpdate' before proceeding.` - this.agent.config.logger.fatal(errorMessage, { - error, - updateIdentifier, - }) - throw new StorageUpdateError(errorMessage, { cause: error }) - } - this.agent.config.logger.error(`Error updating storage (updateIdentifier: ${updateIdentifier})`, { cause: error, }) @@ -254,43 +193,4 @@ export class UpdateAssistant = BaseAgent> { return updateIdentifier } - - private getBackupPath(backupIdentifier: string) { - return `${this.fileSystem.dataPath}/migration/backup/${backupIdentifier}` - } - - private async createBackup(backupIdentifier: string) { - const backupPath = this.getBackupPath(backupIdentifier) - - const walletKey = this.agent.wallet.walletConfig?.key - if (!walletKey) { - throw new CredoError("Could not extract wallet key from wallet module. Can't create backup") - } - - await this.agent.wallet.export({ key: walletKey, path: backupPath }) - this.agent.config.logger.info('Created backup of the wallet', { - backupPath, - }) - } - - private async restoreBackup(backupIdentifier: string) { - const backupPath = this.getBackupPath(backupIdentifier) - - const walletConfig = this.agent.wallet.walletConfig - if (!walletConfig) { - throw new CredoError('Could not extract wallet config from wallet module. Cannot restore backup') - } - - // Export and delete current wallet - await this.agent.wallet.export({ key: walletConfig.key, path: `${backupPath}-error` }) - await this.agent.wallet.delete() - - // Import backup - await this.agent.wallet.import(walletConfig, { key: walletConfig.key, path: backupPath }) - await this.agent.wallet.initialize(walletConfig) - - this.agent.config.logger.info(`Successfully restored wallet from backup ${backupIdentifier}`, { - backupPath, - }) - } } diff --git a/packages/core/src/storage/migration/__tests__/0.1.test.ts b/packages/core/src/storage/migration/__tests__/0.1.test.ts index f96fb54f72..12e8010119 100644 --- a/packages/core/src/storage/migration/__tests__/0.1.test.ts +++ b/packages/core/src/storage/migration/__tests__/0.1.test.ts @@ -4,22 +4,16 @@ import { readFileSync } from 'fs' import path from 'path' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' -import { RegisteredAskarTestWallet } from '../../../../../askar/tests/helpers' +import { InMemoryWalletModule } from '../../../../../../tests/InMemoryWalletModule' import { getDefaultDidcommModules } from '../../../../../didcomm/src/util/modules' import { Agent, utils } from '../../../../src' import { agentDependencies as dependencies } from '../../../../tests/helpers' import { InjectionSymbols } from '../../../constants' -import { DependencyManager } from '../../../plugins' import { UpdateAssistant } from '../UpdateAssistant' const backupDate = new Date('2022-01-21T22:50:20.522Z') jest.useFakeTimers().setSystemTime(backupDate) -const walletConfig = { - id: 'Wallet: 0.1 Update', - key: 'Key: 0.1 Update', -} - const mediationRoleUpdateStrategies: V0_1ToV0_2UpdateConfig['mediationRoleUpdateStrategy'][] = [ 'allMediator', 'allRecipient', @@ -35,21 +29,16 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { ) for (const mediationRoleUpdateStrategy of mediationRoleUpdateStrategies) { - const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) - - const agent = new Agent( - { - config: { label: 'Test Agent', walletConfig }, - dependencies, - modules: getDefaultDidcommModules(), + const agent = new Agent({ + config: { label: 'Test Agent' }, + dependencies, + modules: { + inMemory: new InMemoryWalletModule(), + ...getDefaultDidcommModules(), }, - dependencyManager - ) + }) + const storageService = agent.context.resolve(InjectionSymbols.StorageService) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { mediationRoleUpdateStrategy, @@ -85,7 +74,6 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { ) await agent.shutdown() - await agent.wallet.delete() } }) @@ -99,21 +87,16 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { 'utf8' ) - const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) - - const agent = new Agent( - { - config: { label: 'Test Agent', walletConfig }, - dependencies, - modules: getDefaultDidcommModules(), + const agent = new Agent({ + config: { label: 'Test Agent' }, + dependencies, + modules: { + inMemory: new InMemoryWalletModule(), + ...getDefaultDidcommModules(), }, - dependencyManager - ) + }) + const storageService = agent.context.resolve(InjectionSymbols.StorageService) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { mediationRoleUpdateStrategy: 'doNotChange', @@ -148,7 +131,6 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) @@ -163,21 +145,16 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { 'utf8' ) - const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) - - const agent = new Agent( - { - config: { label: 'Test Agent', walletConfig, autoUpdateStorageOnStartup: true }, - dependencies, - modules: getDefaultDidcommModules(), + const agent = new Agent({ + config: { label: 'Test Agent', autoUpdateStorageOnStartup: true }, + dependencies, + modules: { + inMemory: new InMemoryWalletModule(), + ...getDefaultDidcommModules(), }, - dependencyManager - ) + }) + const storageService = agent.context.resolve(InjectionSymbols.StorageService) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { mediationRoleUpdateStrategy: 'doNotChange', @@ -212,7 +189,6 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) @@ -227,25 +203,19 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { 'utf8' ) - const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) - - const agent = new Agent( - { - config: { - label: 'Test Agent', - walletConfig, - autoUpdateStorageOnStartup: true, - }, - modules: getDefaultDidcommModules(), - dependencies, + const agent = new Agent({ + config: { + label: 'Test Agent', + autoUpdateStorageOnStartup: true, }, - dependencyManager - ) + modules: { + inMemory: new InMemoryWalletModule(), + ...getDefaultDidcommModules(), + }, + dependencies, + }) + const storageService = agent.context.resolve(InjectionSymbols.StorageService) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { mediationRoleUpdateStrategy: 'doNotChange', @@ -280,7 +250,6 @@ describe('UpdateAssistant | v0.1 - v0.2', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) diff --git a/packages/core/src/storage/migration/__tests__/0.2.test.ts b/packages/core/src/storage/migration/__tests__/0.2.test.ts index efeec81273..46284ffff6 100644 --- a/packages/core/src/storage/migration/__tests__/0.2.test.ts +++ b/packages/core/src/storage/migration/__tests__/0.2.test.ts @@ -2,24 +2,18 @@ import { readFileSync } from 'fs' import path from 'path' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' -import { RegisteredAskarTestWallet } from '../../../../../askar/tests/helpers' +import { InMemoryWalletModule } from '../../../../../../tests/InMemoryWalletModule' import { MediatorRoutingRecord } from '../../../../../didcomm/src/modules' import { getDefaultDidcommModules } from '../../../../../didcomm/src/util/modules' import { Agent } from '../../../../src' import { agentDependencies } from '../../../../tests/helpers' import { InjectionSymbols } from '../../../constants' -import { DependencyManager } from '../../../plugins' import * as uuid from '../../../utils/uuid' import { UpdateAssistant } from '../UpdateAssistant' const backupDate = new Date('2023-01-21T22:50:20.522Z') jest.useFakeTimers().setSystemTime(backupDate) -const walletConfig = { - id: 'Wallet: 0.2 Update', - key: 'Key: 0.2 Update', -} - describe('UpdateAssistant | v0.2 - v0.3.1', () => { it('should correctly update proof records and create didcomm records', async () => { // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. @@ -31,24 +25,18 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { 'utf8' ) - const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) - - const agent = new Agent( - { - config: { - label: 'Test Agent', - walletConfig, - }, - dependencies: agentDependencies, - modules: getDefaultDidcommModules(), + const agent = new Agent({ + config: { + label: 'Test Agent', }, - dependencyManager - ) + dependencies: agentDependencies, + modules: { + inMemory: new InMemoryWalletModule(), + ...getDefaultDidcommModules(), + }, + }) + const storageService = agent.context.resolve(InjectionSymbols.StorageService) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { mediationRoleUpdateStrategy: 'doNotChange', @@ -87,7 +75,6 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) @@ -102,30 +89,18 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { 'utf8' ) - const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) - - const agent = new Agent( - { - config: { - label: 'Test Agent', - walletConfig, - autoUpdateStorageOnStartup: true, - }, - modules: getDefaultDidcommModules(), - dependencies: agentDependencies, + const agent = new Agent({ + config: { + label: 'Test Agent', + autoUpdateStorageOnStartup: true, }, - dependencyManager - ) - - // We need to manually initialize the wallet as we're using the in memory wallet service - // When we call agent.initialize() it will create the wallet and store the current framework - // version in the in memory storage service. We need to manually set the records between initializing - // the wallet and calling agent.initialize() - await agent.wallet.initialize(walletConfig) + modules: { + inMemory: new InMemoryWalletModule(), + ...getDefaultDidcommModules(), + }, + dependencies: agentDependencies, + }) + const storageService = agent.context.resolve(InjectionSymbols.StorageService) // Set storage after initialization. This mimics as if this wallet // is opened as an existing wallet instead of a new wallet @@ -141,7 +116,6 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) @@ -153,30 +127,18 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { const aliceDidRecordsString = readFileSync(path.join(__dirname, '__fixtures__/alice-8-dids-0.2.json'), 'utf8') - const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - - const agent = new Agent( - { - config: { - label: 'Test Agent', - walletConfig, - autoUpdateStorageOnStartup: true, - }, - dependencies: agentDependencies, - modules: getDefaultDidcommModules(), + const agent = new Agent({ + config: { + label: 'Test Agent', + autoUpdateStorageOnStartup: true, }, - dependencyManager - ) - - // We need to manually initialize the wallet as we're using the in memory wallet service - // When we call agent.initialize() it will create the wallet and store the current framework - // version in the in memory storage service. We need to manually set the records between initializing - // the wallet and calling agent.initialize() - await agent.wallet.initialize(walletConfig) + dependencies: agentDependencies, + modules: { + inMemory: new InMemoryWalletModule(), + ...getDefaultDidcommModules(), + }, + }) + const storageService = agent.context.resolve(InjectionSymbols.StorageService) // Set storage after initialization. This mimics as if this wallet // is opened as an existing wallet instead of a new wallet @@ -193,7 +155,6 @@ describe('UpdateAssistant | v0.2 - v0.3.1', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) diff --git a/packages/core/src/storage/migration/__tests__/0.3.test.ts b/packages/core/src/storage/migration/__tests__/0.3.test.ts index b76792018a..0d47478218 100644 --- a/packages/core/src/storage/migration/__tests__/0.3.test.ts +++ b/packages/core/src/storage/migration/__tests__/0.3.test.ts @@ -2,22 +2,16 @@ import { readFileSync } from 'fs' import path from 'path' import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' -import { RegisteredAskarTestWallet } from '../../../../../askar/tests/helpers' +import { InMemoryWalletModule } from '../../../../../../tests/InMemoryWalletModule' import { agentDependencies } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { InjectionSymbols } from '../../../constants' -import { DependencyManager } from '../../../plugins' import * as uuid from '../../../utils/uuid' import { UpdateAssistant } from '../UpdateAssistant' const backupDate = new Date('2023-03-18T22:50:20.522Z') jest.useFakeTimers().setSystemTime(backupDate) -const walletConfig = { - id: 'Wallet: 0.4 Update', - key: 'Key: 0.4 Update', -} - describe('UpdateAssistant | v0.3.1 - v0.4', () => { it('should correctly update the did records and remove cache records', async () => { // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. @@ -29,23 +23,17 @@ describe('UpdateAssistant | v0.3.1 - v0.4', () => { 'utf8' ) - const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) - - const agent = new Agent( - { - config: { - label: 'Test Agent', - walletConfig, - }, - dependencies: agentDependencies, + const agent = new Agent({ + config: { + label: 'Test Agent', }, - dependencyManager - ) + dependencies: agentDependencies, + modules: { + inMemory: new InMemoryWalletModule(), + }, + }) + const storageService = agent.context.resolve(InjectionSymbols.StorageService) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { mediationRoleUpdateStrategy: 'doNotChange', @@ -80,7 +68,6 @@ describe('UpdateAssistant | v0.3.1 - v0.4', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) @@ -95,22 +82,17 @@ describe('UpdateAssistant | v0.3.1 - v0.4', () => { 'utf8' ) - const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) - - const agent = new Agent( - { - config: { - label: 'Test Agent', - walletConfig, - }, - dependencies: agentDependencies, + const agent = new Agent({ + config: { + label: 'Test Agent', }, - dependencyManager - ) + dependencies: agentDependencies, + modules: { + inMemory: new InMemoryWalletModule(), + }, + }) + + const storageService = agent.context.resolve(InjectionSymbols.StorageService) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { @@ -146,7 +128,6 @@ describe('UpdateAssistant | v0.3.1 - v0.4', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) diff --git a/packages/core/src/storage/migration/__tests__/0.4.test.ts b/packages/core/src/storage/migration/__tests__/0.4.test.ts index 8f472826fd..e3d5c6f576 100644 --- a/packages/core/src/storage/migration/__tests__/0.4.test.ts +++ b/packages/core/src/storage/migration/__tests__/0.4.test.ts @@ -1,26 +1,19 @@ import { readFileSync } from 'fs' import path from 'path' - import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' -import { RegisteredAskarTestWallet } from '../../../../../askar/tests/helpers' +import { InMemoryWalletModule } from '../../../../../../tests/InMemoryWalletModule' import { getDefaultDidcommModules } from '../../../../../didcomm/src/util/modules' import { agentDependencies } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { InjectionSymbols } from '../../../constants' import { W3cCredentialsModule } from '../../../modules/vc' import { customDocumentLoader } from '../../../modules/vc/data-integrity/__tests__/documentLoader' -import { DependencyManager } from '../../../plugins' import * as uuid from '../../../utils/uuid' import { UpdateAssistant } from '../UpdateAssistant' const backupDate = new Date('2024-02-05T22:50:20.522Z') jest.useFakeTimers().setSystemTime(backupDate) -const walletConfig = { - id: 'Wallet: 0.5 Update', - key: 'Key: 0.5 Update', -} - describe('UpdateAssistant | v0.4 - v0.5', () => { it(`should correctly add 'type' tag to w3c records`, async () => { // We need to mock the uuid generation to make sure we generate consistent uuids for the new records created. @@ -32,28 +25,20 @@ describe('UpdateAssistant | v0.4 - v0.5', () => { 'utf8' ) - const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) - - const agent = new Agent( - { - config: { - label: 'Test Agent', - walletConfig, - }, - dependencies: agentDependencies, - modules: { - w3cCredentials: new W3cCredentialsModule({ - documentLoader: customDocumentLoader, - }), - }, + const agent = new Agent({ + config: { + label: 'Test Agent', }, - dependencyManager - ) + dependencies: agentDependencies, + modules: { + inMemory: new InMemoryWalletModule(), + w3cCredentials: new W3cCredentialsModule({ + documentLoader: customDocumentLoader, + }), + }, + }) + const storageService = agent.context.resolve(InjectionSymbols.StorageService) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { mediationRoleUpdateStrategy: 'doNotChange', @@ -88,7 +73,6 @@ describe('UpdateAssistant | v0.4 - v0.5', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) @@ -103,25 +87,16 @@ describe('UpdateAssistant | v0.4 - v0.5', () => { 'utf8' ) - const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) - // We need core DIDComm modules for this update to fully work - const agent = new Agent( - { - config: { - label: 'Test Agent', - walletConfig, - }, - modules: getDefaultDidcommModules(), - dependencies: agentDependencies, + const agent = new Agent({ + config: { + label: 'Test Agent', }, - dependencyManager - ) + modules: { ...getDefaultDidcommModules(), inMemory: new InMemoryWalletModule() }, + dependencies: agentDependencies, + }) + const storageService = agent.context.resolve(InjectionSymbols.StorageService) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { mediationRoleUpdateStrategy: 'doNotChange', @@ -156,7 +131,6 @@ describe('UpdateAssistant | v0.4 - v0.5', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) @@ -168,25 +142,16 @@ describe('UpdateAssistant | v0.4 - v0.5', () => { const aliceW3cCredentialRecordsString = readFileSync(path.join(__dirname, '__fixtures__/2-proofs-0.4.json'), 'utf8') - const dependencyManager = new DependencyManager() - const storageService = new InMemoryStorageService() - dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) - // We need core DIDComm modules for this update to fully work - const agent = new Agent( - { - config: { - label: 'Test Agent', - walletConfig, - }, - modules: getDefaultDidcommModules(), - dependencies: agentDependencies, + const agent = new Agent({ + config: { + label: 'Test Agent', }, - dependencyManager - ) + modules: { ...getDefaultDidcommModules(), inMemory: new InMemoryWalletModule() }, + dependencies: agentDependencies, + }) + const storageService = agent.context.resolve(InjectionSymbols.StorageService) const updateAssistant = new UpdateAssistant(agent, { v0_1ToV0_2: { mediationRoleUpdateStrategy: 'doNotChange', @@ -221,7 +186,6 @@ describe('UpdateAssistant | v0.4 - v0.5', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) diff --git a/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts b/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts index eac4d95755..3a5c20bc78 100644 --- a/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts +++ b/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts @@ -1,13 +1,13 @@ import type { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' import type { BaseRecord } from '../../BaseRecord' -import { getInMemoryAgentOptions } from '../../../../tests/helpers' +import { getAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' import { InjectionSymbols } from '../../../constants' import { UpdateAssistant } from '../UpdateAssistant' import { CURRENT_FRAMEWORK_STORAGE_VERSION } from '../updates' -const agentOptions = getInMemoryAgentOptions('UpdateAssistant', {}) +const agentOptions = getAgentOptions('UpdateAssistant', {}) describe('UpdateAssistant', () => { let updateAssistant: UpdateAssistant @@ -30,11 +30,13 @@ describe('UpdateAssistant', () => { afterEach(async () => { await agent.shutdown() - await agent.wallet.delete() }) describe('upgrade()', () => { it('should not upgrade records when upgrading after a new wallet is created', async () => { + // Make sure it's initialized + storageService.createRecordsForContext(agent.context) + const beforeStorage = JSON.stringify(storageService.contextCorrelationIdToRecords) await updateAssistant.update() diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap index d39faf48e5..18c988414c 100644 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap @@ -810,6 +810,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "autoAcceptConnection": undefined, "createdAt": "2022-04-30T13:02:21.577Z", "id": "1-4e4f-41d9-94c4-f49351b811f1", + "invitationInlineServiceKeys": undefined, "mediatorId": undefined, "metadata": {}, "outOfBandInvitation": { @@ -877,6 +878,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "autoAcceptConnection": undefined, "createdAt": "2022-04-30T13:02:21.608Z", "id": "2-4e4f-41d9-94c4-f49351b811f1", + "invitationInlineServiceKeys": undefined, "mediatorId": undefined, "metadata": {}, "outOfBandInvitation": { @@ -944,6 +946,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "autoAcceptConnection": false, "createdAt": "2022-04-30T13:02:21.628Z", "id": "3-4e4f-41d9-94c4-f49351b811f1", + "invitationInlineServiceKeys": undefined, "mediatorId": undefined, "metadata": {}, "outOfBandInvitation": { @@ -1011,6 +1014,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "autoAcceptConnection": undefined, "createdAt": "2022-04-30T13:02:21.635Z", "id": "4-4e4f-41d9-94c4-f49351b811f1", + "invitationInlineServiceKeys": undefined, "mediatorId": undefined, "metadata": {}, "outOfBandInvitation": { @@ -1078,6 +1082,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "autoAcceptConnection": false, "createdAt": "2022-04-30T13:02:21.641Z", "id": "5-4e4f-41d9-94c4-f49351b811f1", + "invitationInlineServiceKeys": undefined, "mediatorId": undefined, "metadata": {}, "outOfBandInvitation": { @@ -1140,6 +1145,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "autoAcceptConnection": true, "createdAt": "2022-04-30T13:02:21.646Z", "id": "6-4e4f-41d9-94c4-f49351b811f1", + "invitationInlineServiceKeys": undefined, "mediatorId": undefined, "metadata": {}, "outOfBandInvitation": { @@ -1207,6 +1213,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "autoAcceptConnection": true, "createdAt": "2022-04-30T13:02:21.653Z", "id": "7-4e4f-41d9-94c4-f49351b811f1", + "invitationInlineServiceKeys": undefined, "mediatorId": undefined, "metadata": {}, "outOfBandInvitation": { @@ -1508,6 +1515,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQmP96nW6vbNjzwPt19z1NYqhnAfgnAFqfLHcktkmdUFzhT", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"SDqTzbVuCowusqGBNbNDjH#1","controller":"SDqTzbVuCowusqGBNbNDjH","type":"Ed25519VerificationKey2018","publicKeyBase58":"EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq"}],"service":[{"id":"SDqTzbVuCowusqGBNbNDjH#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq"],"routingKeys":[]}],"authentication":[{"publicKey":"SDqTzbVuCowusqGBNbNDjH#1","type":"Ed25519SignatureAuthentication2018"}],"id":"SDqTzbVuCowusqGBNbNDjH"}", @@ -1578,6 +1586,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQmPbGa8KDwyjcw9UgwCCgJMV7jU5kKCyvBuwFVc88WxA56", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"GkEeb96MGT94K1HyQQzpj1#1","controller":"GkEeb96MGT94K1HyQQzpj1","type":"Ed25519VerificationKey2018","publicKeyBase58":"9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS"}],"service":[{"id":"GkEeb96MGT94K1HyQQzpj1#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS"],"routingKeys":[]}],"authentication":[{"publicKey":"GkEeb96MGT94K1HyQQzpj1#1","type":"Ed25519SignatureAuthentication2018"}],"id":"GkEeb96MGT94K1HyQQzpj1"}", @@ -1648,6 +1657,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQmRAfQ6J5qk4qcbHyoStFVkhusazLT9xQcFhdC9dhhQ1cJ", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"XajWZZmHGAWUvYCi7CApaG#1","controller":"XajWZZmHGAWUvYCi7CApaG","type":"Ed25519VerificationKey2018","publicKeyBase58":"HfkCHGAHTz3j33TRDkKMabYLdnr2FKuWcaXTLzZkZcCp"}],"service":[{"id":"XajWZZmHGAWUvYCi7CApaG#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["HfkCHGAHTz3j33TRDkKMabYLdnr2FKuWcaXTLzZkZcCp"],"routingKeys":[]}],"authentication":[{"publicKey":"XajWZZmHGAWUvYCi7CApaG#1","type":"Ed25519SignatureAuthentication2018"}],"id":"XajWZZmHGAWUvYCi7CApaG"}", @@ -1718,6 +1728,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQmSMBVNMDrh7fyE8bkAmk1ZatshjinpsEqPA3nx8JYjuKb", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"RtH4qxVPL1Dpmdv7GytjBv#1","controller":"RtH4qxVPL1Dpmdv7GytjBv","type":"Ed25519VerificationKey2018","publicKeyBase58":"EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF"}],"service":[{"id":"RtH4qxVPL1Dpmdv7GytjBv#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF"],"routingKeys":[]}],"authentication":[{"publicKey":"RtH4qxVPL1Dpmdv7GytjBv#1","type":"Ed25519SignatureAuthentication2018"}],"id":"RtH4qxVPL1Dpmdv7GytjBv"}", @@ -1788,6 +1799,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"YUH4t3KMkEJiXgmqsncrY9#1","controller":"YUH4t3KMkEJiXgmqsncrY9","type":"Ed25519VerificationKey2018","publicKeyBase58":"J9qc5VredX3YUBbFNoJz5oJpWPcUWURKVDiUv1DvYukX"}],"service":[{"id":"YUH4t3KMkEJiXgmqsncrY9#IndyAgentService","serviceEndpoint":"rxjs:faber","type":"IndyAgent","priority":0,"recipientKeys":["J9qc5VredX3YUBbFNoJz5oJpWPcUWURKVDiUv1DvYukX"],"routingKeys":[]}],"authentication":[{"publicKey":"YUH4t3KMkEJiXgmqsncrY9#1","type":"Ed25519SignatureAuthentication2018"}],"id":"YUH4t3KMkEJiXgmqsncrY9"}", @@ -1858,6 +1870,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQmZ2tdw35SaLncSHhf9zBv3e9QmJmLErZRSLsDdYowPHXy", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"WSwJQMBHGZbQsq9LDBTWjX#1","controller":"WSwJQMBHGZbQsq9LDBTWjX","type":"Ed25519VerificationKey2018","publicKeyBase58":"H3tENVV3HnfNi5tL9qYFh69CuzGG9skW4r8grYLZWXB3"}],"service":[{"id":"WSwJQMBHGZbQsq9LDBTWjX#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["H3tENVV3HnfNi5tL9qYFh69CuzGG9skW4r8grYLZWXB3"],"routingKeys":[]}],"authentication":[{"publicKey":"WSwJQMBHGZbQsq9LDBTWjX#1","type":"Ed25519SignatureAuthentication2018"}],"id":"WSwJQMBHGZbQsq9LDBTWjX"}", @@ -1928,6 +1941,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQma8LpnJ22GxQdyASV5jP6psacAGtJ6ytk4pVayYp4erRf", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"TMnQftvJJJwoYogYkQgVjg#1","controller":"TMnQftvJJJwoYogYkQgVjg","type":"Ed25519VerificationKey2018","publicKeyBase58":"FNEqnwqHoU6WVmYkQFeosoaESjx8wCAzFpFdMdEg3iH7"}],"service":[{"id":"TMnQftvJJJwoYogYkQgVjg#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["FNEqnwqHoU6WVmYkQFeosoaESjx8wCAzFpFdMdEg3iH7"],"routingKeys":[]}],"authentication":[{"publicKey":"TMnQftvJJJwoYogYkQgVjg#1","type":"Ed25519SignatureAuthentication2018"}],"id":"TMnQftvJJJwoYogYkQgVjg"}", @@ -1998,6 +2012,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"YKc7qhYN1TckZAMUf7jgwc#1","controller":"YKc7qhYN1TckZAMUf7jgwc","type":"Ed25519VerificationKey2018","publicKeyBase58":"J57UsQT3wa4FcivfKpvjgUtaPDScZhFJ8kd5Q2iR5sBT"}],"service":[{"id":"YKc7qhYN1TckZAMUf7jgwc#IndyAgentService","serviceEndpoint":"rxjs:faber","type":"IndyAgent","priority":0,"recipientKeys":["J57UsQT3wa4FcivfKpvjgUtaPDScZhFJ8kd5Q2iR5sBT"],"routingKeys":[]}],"authentication":[{"publicKey":"YKc7qhYN1TckZAMUf7jgwc#1","type":"Ed25519SignatureAuthentication2018"}],"id":"YKc7qhYN1TckZAMUf7jgwc"}", @@ -2068,6 +2083,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"Ak15GBhMYpdS8XX3QDMv31#1","controller":"Ak15GBhMYpdS8XX3QDMv31","type":"Ed25519VerificationKey2018","publicKeyBase58":"6JwodG44KanZhhSvDS3dNmWjmWyeVFYRPxVaBntqmSCi"}],"service":[{"id":"Ak15GBhMYpdS8XX3QDMv31#IndyAgentService","serviceEndpoint":"rxjs:faber","type":"IndyAgent","priority":0,"recipientKeys":["6JwodG44KanZhhSvDS3dNmWjmWyeVFYRPxVaBntqmSCi"],"routingKeys":[]}],"authentication":[{"publicKey":"Ak15GBhMYpdS8XX3QDMv31#1","type":"Ed25519SignatureAuthentication2018"}],"id":"Ak15GBhMYpdS8XX3QDMv31"}", @@ -2138,6 +2154,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"9jTqUnV4k5ucxbyxumAaV7#1","controller":"9jTqUnV4k5ucxbyxumAaV7","type":"Ed25519VerificationKey2018","publicKeyBase58":"5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU"}],"service":[{"id":"9jTqUnV4k5ucxbyxumAaV7#IndyAgentService","serviceEndpoint":"rxjs:faber","type":"IndyAgent","priority":0,"recipientKeys":["5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU"],"routingKeys":[]}],"authentication":[{"publicKey":"9jTqUnV4k5ucxbyxumAaV7#1","type":"Ed25519SignatureAuthentication2018"}],"id":"9jTqUnV4k5ucxbyxumAaV7"}", @@ -2208,6 +2225,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQmduuYkxRKJuVyvDqttdd9eDfBwDnF1DAU5FFQo4whx7Uw", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"WewvCdyBi4HL8ogyGviYVS#1","controller":"WewvCdyBi4HL8ogyGviYVS","type":"Ed25519VerificationKey2018","publicKeyBase58":"HARupCd5jxffp7M74mbDFuEnsquRgh4oaXsswxWeZZd7"}],"service":[{"id":"WewvCdyBi4HL8ogyGviYVS#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["HARupCd5jxffp7M74mbDFuEnsquRgh4oaXsswxWeZZd7"],"routingKeys":[]}],"authentication":[{"publicKey":"WewvCdyBi4HL8ogyGviYVS#1","type":"Ed25519SignatureAuthentication2018"}],"id":"WewvCdyBi4HL8ogyGviYVS"}", @@ -2278,6 +2296,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"3KAjJWF5NjiDTUm6JpPBQD#1","controller":"3KAjJWF5NjiDTUm6JpPBQD","type":"Ed25519VerificationKey2018","publicKeyBase58":"2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy"}],"service":[{"id":"3KAjJWF5NjiDTUm6JpPBQD#IndyAgentService","serviceEndpoint":"rxjs:faber","type":"IndyAgent","priority":0,"recipientKeys":["2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy"],"routingKeys":[]}],"authentication":[{"publicKey":"3KAjJWF5NjiDTUm6JpPBQD#1","type":"Ed25519SignatureAuthentication2018"}],"id":"3KAjJWF5NjiDTUm6JpPBQD"}", @@ -2348,6 +2367,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re ], }, "id": "did:peer:1zQmfDAtfDZcK4trJBsvVTXrBx9uaLCHSUZH9X2LFaAd3JKv", + "keys": undefined, "metadata": { "_internal/legacyDid": { "didDocumentString": "{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"Ud6AWCk6WrwfYKZUw5tJmt#1","controller":"Ud6AWCk6WrwfYKZUw5tJmt","type":"Ed25519VerificationKey2018","publicKeyBase58":"G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe"}],"service":[{"id":"Ud6AWCk6WrwfYKZUw5tJmt#IndyAgentService","serviceEndpoint":"rxjs:alice","type":"IndyAgent","priority":0,"recipientKeys":["G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe"],"routingKeys":[]}],"authentication":[{"publicKey":"Ud6AWCk6WrwfYKZUw5tJmt#1","type":"Ed25519SignatureAuthentication2018"}],"id":"Ud6AWCk6WrwfYKZUw5tJmt"}", diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap deleted file mode 100644 index 3b2cf87249..0000000000 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap +++ /dev/null @@ -1,204 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UpdateAssistant | Backup should create a backup 1`] = ` -[ - { - "_tags": { - "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", - "role": "issuer", - "state": "done", - "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", - }, - "autoAcceptCredential": "contentApproved", - "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", - "createdAt": "2022-03-21T22:50:20.522Z", - "credentialAttributes": [ - { - "mime-type": "text/plain", - "name": "name", - "value": "Alice", - }, - { - "mime-type": "text/plain", - "name": "age", - "value": "25", - }, - { - "mime-type": "text/plain", - "name": "dateOfBirth", - "value": "2020-01-01", - }, - ], - "credentials": [], - "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", - "metadata": { - "_internal/indyCredential": { - "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", - "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", - }, - }, - "protocolVersion": "v1", - "role": "issuer", - "state": "done", - "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", - "updatedAt": "2022-03-21T22:50:20.522Z", - }, - { - "_tags": { - "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", - "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", - "credentialIds": [ - "a77114e1-c812-4bff-a53c-3d5003fcc278", - ], - "role": "holder", - "state": "done", - "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", - }, - "autoAcceptCredential": "contentApproved", - "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", - "createdAt": "2022-03-21T22:50:20.535Z", - "credentialAttributes": [ - { - "mime-type": "text/plain", - "name": "name", - "value": "Alice", - }, - { - "mime-type": "text/plain", - "name": "age", - "value": "25", - }, - { - "mime-type": "text/plain", - "name": "dateOfBirth", - "value": "2020-01-01", - }, - ], - "credentials": [ - { - "credentialRecordId": "a77114e1-c812-4bff-a53c-3d5003fcc278", - "credentialRecordType": "indy", - }, - ], - "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", - "metadata": { - "_internal/indyCredential": { - "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", - "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", - }, - "_internal/indyRequest": { - "master_secret_blinding_data": { - "v_prime": "36456944381549782028917743247126995038265466209293312755125557271456380841610111892515020379470931691048072348420844231863825225515560265358581756565441268878364665494094789024845049226122885121039335781567964878826549149370097276812152226343824116049855825405977949749345353074025294938300401262824951638782220004732873597724698990420932910079362747837952520524827009393981876443737452031919055976088763615615890946142630576421462920865811255312740184209214306243871230276622595183415487741608569800898909023830922654063814555128779494528740438076748829436757078504882332589744263200806138145494157659396691564807976032319024007464003538934", - "vr_prime": null, - }, - "master_secret_name": "Wallet: PopulateWallet2", - "nonce": "373984270150786864433163", - }, - }, - "protocolVersion": "v1", - "role": "holder", - "state": "done", - "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", - "updatedAt": "2022-03-21T22:50:20.522Z", - }, - { - "_tags": { - "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", - "role": "issuer", - "state": "done", - "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", - }, - "autoAcceptCredential": "contentApproved", - "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", - "createdAt": "2022-03-21T22:50:20.740Z", - "credentialAttributes": [ - { - "mime-type": "text/plain", - "name": "name", - "value": "Alice", - }, - { - "mime-type": "text/plain", - "name": "age", - "value": "25", - }, - { - "mime-type": "text/plain", - "name": "dateOfBirth", - "value": "2020-01-01", - }, - ], - "credentials": [], - "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", - "metadata": { - "_internal/indyCredential": { - "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", - "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", - }, - }, - "protocolVersion": "v1", - "role": "issuer", - "state": "done", - "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", - "updatedAt": "2022-03-21T22:50:20.522Z", - }, - { - "_tags": { - "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", - "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", - "credentialIds": [ - "19c1f29f-d2df-486c-b8c6-950c403fa7d9", - ], - "role": "holder", - "state": "done", - "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", - }, - "autoAcceptCredential": "contentApproved", - "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", - "createdAt": "2022-03-21T22:50:20.746Z", - "credentialAttributes": [ - { - "mime-type": "text/plain", - "name": "name", - "value": "Alice", - }, - { - "mime-type": "text/plain", - "name": "age", - "value": "25", - }, - { - "mime-type": "text/plain", - "name": "dateOfBirth", - "value": "2020-01-01", - }, - ], - "credentials": [ - { - "credentialRecordId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", - "credentialRecordType": "indy", - }, - ], - "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", - "metadata": { - "_internal/indyCredential": { - "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", - "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", - }, - "_internal/indyRequest": { - "master_secret_blinding_data": { - "v_prime": "24405223168730122709164916892481085040205443709643249329100687534344659826655374235392514476392517756663433844139774514430993889493707631169979521764390851593418941181409704266182779162417466204970949168472702858363964258641437554267668466400711344128132909691514606077477555576087059339291048485225394874964325220472232903203038212033940680060605090839733163438385288769519855418153181511119637865605476043416048121313638627002888436809192752657860306784733123742838413845299796745569824223645588826964796075250758249133953560017373025169692866449286962430731916293683231375510684692358406054381559324718715654332979447698704161714028193478", - "vr_prime": null, - }, - "master_secret_name": "Wallet: PopulateWallet2", - "nonce": "698370616023883730498375", - }, - }, - "protocolVersion": "v1", - "role": "holder", - "state": "done", - "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", - "updatedAt": "2022-03-21T22:50:20.522Z", - }, -] -`; diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/backup-askar.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/migration-askar.test.ts.snap similarity index 98% rename from packages/core/src/storage/migration/__tests__/__snapshots__/backup-askar.test.ts.snap rename to packages/core/src/storage/migration/__tests__/__snapshots__/migration-askar.test.ts.snap index e0f7f189cc..f21c393aed 100644 --- a/packages/core/src/storage/migration/__tests__/__snapshots__/backup-askar.test.ts.snap +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/migration-askar.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UpdateAssistant | Backup | Aries Askar should create a backup 1`] = ` +exports[`UpdateAssistant | Aries Askar should create a backup 1`] = ` [ { "_tags": { diff --git a/packages/core/src/storage/migration/__tests__/backup-askar.test.ts b/packages/core/src/storage/migration/__tests__/backup-askar.test.ts deleted file mode 100644 index c730409b80..0000000000 --- a/packages/core/src/storage/migration/__tests__/backup-askar.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { FileSystem } from '../../FileSystem' -import type { StorageUpdateError } from '../error/StorageUpdateError' - -import { readFileSync, unlinkSync } from 'fs' -import path from 'path' - -import { askarModule } from '../../../../../askar/tests/helpers' -import { CredentialExchangeRecord, CredentialRepository } from '../../../../../didcomm/src/modules/credentials' -import { getAgentOptions, getAskarWalletConfig } from '../../../../tests/helpers' -import { Agent } from '../../../agent/Agent' -import { InjectionSymbols } from '../../../constants' -import { CredoError } from '../../../error' -import { JsonTransformer } from '../../../utils' -import { StorageUpdateService } from '../StorageUpdateService' -import { UpdateAssistant } from '../UpdateAssistant' - -const agentOptions = getAgentOptions( - 'UpdateAssistant | Backup | Aries Askar', - {}, - { - walletConfig: getAskarWalletConfig('UpdateAssistant | Backup | Aries Askar', { inMemory: false }), - }, - { - askar: askarModule, - } -) - -const aliceCredentialRecordsString = readFileSync( - path.join(__dirname, '__fixtures__/alice-4-credentials-0.1.json'), - 'utf8' -) - -const backupDate = new Date('2022-03-22T22:50:20.522Z') -jest.useFakeTimers().setSystemTime(backupDate) -const backupIdentifier = backupDate.getTime() - -describe('UpdateAssistant | Backup | Aries Askar', () => { - let updateAssistant: UpdateAssistant - let agent: Agent - let backupPath: string - - beforeEach(async () => { - agent = new Agent(agentOptions) - const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) - backupPath = `${fileSystem.dataPath}/migration/backup/${backupIdentifier}` - - // If tests fail it's possible the cleanup has been skipped. So remove before running tests - const doesFileSystemExist = await fileSystem.exists(backupPath) - if (doesFileSystemExist) { - unlinkSync(backupPath) - } - const doesbackupFileSystemExist = await fileSystem.exists(`${backupPath}-error`) - if (doesbackupFileSystemExist) { - unlinkSync(`${backupPath}-error`) - } - - updateAssistant = new UpdateAssistant(agent, { - v0_1ToV0_2: { - mediationRoleUpdateStrategy: 'allMediator', - }, - }) - - await updateAssistant.initialize() - }) - - afterEach(async () => { - await agent.shutdown() - await agent.wallet.delete() - }) - - it('should create a backup', async () => { - const aliceCredentialRecordsJson = JSON.parse(aliceCredentialRecordsString) - - // biome-ignore lint/suspicious/noExplicitAny: - const aliceCredentialRecords = Object.values(aliceCredentialRecordsJson).map((data: any) => { - const record = JsonTransformer.fromJSON(data.value, CredentialExchangeRecord) - - record.setTags(data.tags) - return record - }) - - const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) - const storageUpdateService = agent.dependencyManager.resolve(StorageUpdateService) - - // Add 0.1 data and set version to 0.1 - for (const credentialRecord of aliceCredentialRecords) { - await credentialRepository.save(agent.context, credentialRecord) - } - await storageUpdateService.setCurrentStorageVersion(agent.context, '0.1') - - // Expect an update is needed - expect(await updateAssistant.isUpToDate()).toBe(false) - - const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) - // Backup should not exist before update - expect(await fileSystem.exists(backupPath)).toBe(false) - - const walletSpy = jest.spyOn(agent.wallet, 'export') - - // Create update - await updateAssistant.update() - - // A wallet export should have been initiated - expect(walletSpy).toHaveBeenCalledWith({ key: agent.wallet.walletConfig?.key, path: backupPath }) - - // Backup should be cleaned after update - expect(await fileSystem.exists(backupPath)).toBe(false) - - expect( - (await credentialRepository.getAll(agent.context)).sort((a, b) => a.id.localeCompare(b.id)) - ).toMatchSnapshot() - }) - - it('should restore the backup if an error occurs during the update', async () => { - const aliceCredentialRecordsJson = JSON.parse(aliceCredentialRecordsString) - - // biome-ignore lint/suspicious/noExplicitAny: - const aliceCredentialRecords = Object.values(aliceCredentialRecordsJson).map((data: any) => { - const record = JsonTransformer.fromJSON(data.value, CredentialExchangeRecord) - - record.setTags(data.tags) - return record - }) - - const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) - const storageUpdateService = agent.dependencyManager.resolve(StorageUpdateService) - - // Add 0.1 data and set version to 0.1 - for (const credentialRecord of aliceCredentialRecords) { - await credentialRepository.save(agent.context, credentialRecord) - } - await storageUpdateService.setCurrentStorageVersion(agent.context, '0.1') - - // Expect an update is needed - expect(await updateAssistant.isUpToDate()).toBe(false) - jest.spyOn(updateAssistant, 'getNeededUpdates').mockResolvedValue([ - { - fromVersion: '0.1', - toVersion: '0.2', - doUpdate: async () => { - throw new CredoError("Uh oh I'm broken") - }, - }, - ]) - - const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) - // Backup should not exist before update - expect(await fileSystem.exists(backupPath)).toBe(false) - - let updateError: StorageUpdateError | undefined = undefined - - try { - await updateAssistant.update() - } catch (error) { - updateError = error - } - - expect(updateError?.cause?.message).toEqual("Uh oh I'm broken") - - // Only backup error should exist after update - expect(await fileSystem.exists(backupPath)).toBe(false) - expect(await fileSystem.exists(`${backupPath}-error`)).toBe(true) - - // Wallet should be same as when we started because of backup - expect((await credentialRepository.getAll(agent.context)).sort((a, b) => a.id.localeCompare(b.id))).toEqual( - aliceCredentialRecords.sort((a, b) => a.id.localeCompare(b.id)) - ) - }) -}) diff --git a/packages/core/src/storage/migration/__tests__/backup.test.ts b/packages/core/src/storage/migration/__tests__/backup.test.ts deleted file mode 100644 index 6431886996..0000000000 --- a/packages/core/src/storage/migration/__tests__/backup.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { FileSystem } from '../../FileSystem' -import type { StorageUpdateError } from '../error/StorageUpdateError' - -import { readFileSync, unlinkSync } from 'fs' -import path from 'path' - -import { askarModule } from '../../../../../askar/tests/helpers' -import { CredentialExchangeRecord, CredentialRepository } from '../../../../../didcomm/src/modules/credentials' -import { getAgentOptions, getAskarWalletConfig } from '../../../../tests/helpers' -import { Agent } from '../../../agent/Agent' -import { InjectionSymbols } from '../../../constants' -import { CredoError } from '../../../error' -import { JsonTransformer } from '../../../utils' -import { StorageUpdateService } from '../StorageUpdateService' -import { UpdateAssistant } from '../UpdateAssistant' - -const agentOptions = getAgentOptions( - 'UpdateAssistant | Backup', - {}, - { - walletConfig: getAskarWalletConfig('UpdateAssistant | Backup', { - inMemory: false, - }), - }, - { askar: askarModule } -) - -const aliceCredentialRecordsString = readFileSync( - path.join(__dirname, '__fixtures__/alice-4-credentials-0.1.json'), - 'utf8' -) - -const backupDate = new Date('2022-03-21T22:50:20.522Z') -jest.useFakeTimers().setSystemTime(backupDate) -const backupIdentifier = backupDate.getTime() - -describe('UpdateAssistant | Backup', () => { - let updateAssistant: UpdateAssistant - let agent: Agent - let backupPath: string - - beforeEach(async () => { - agent = new Agent(agentOptions) - const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) - backupPath = `${fileSystem.dataPath}/migration/backup/${backupIdentifier}` - - // If tests fail it's possible the cleanup has been skipped. So remove before running tests - const doesFileSystemExist = await fileSystem.exists(backupPath) - if (doesFileSystemExist) { - unlinkSync(backupPath) - } - const doesbackupFileSystemExist = await fileSystem.exists(`${backupPath}-error`) - if (doesbackupFileSystemExist) { - unlinkSync(`${backupPath}-error`) - } - - updateAssistant = new UpdateAssistant(agent, { - v0_1ToV0_2: { - mediationRoleUpdateStrategy: 'allMediator', - }, - }) - - await updateAssistant.initialize() - }) - - afterEach(async () => { - await agent.shutdown() - await agent.wallet.delete() - }) - - it('should create a backup', async () => { - const aliceCredentialRecordsJson = JSON.parse(aliceCredentialRecordsString) - - // biome-ignore lint/suspicious/noExplicitAny: - const aliceCredentialRecords = Object.values(aliceCredentialRecordsJson).map((data: any) => { - const record = JsonTransformer.fromJSON(data.value, CredentialExchangeRecord) - - record.setTags(data.tags) - return record - }) - - const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) - const storageUpdateService = agent.dependencyManager.resolve(StorageUpdateService) - - // Add 0.1 data and set version to 0.1 - for (const credentialRecord of aliceCredentialRecords) { - await credentialRepository.save(agent.context, credentialRecord) - } - await storageUpdateService.setCurrentStorageVersion(agent.context, '0.1') - - // Expect an update is needed - expect(await updateAssistant.isUpToDate()).toBe(false) - - const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) - // Backup should not exist before update - expect(await fileSystem.exists(backupPath)).toBe(false) - - const walletSpy = jest.spyOn(agent.wallet, 'export') - - // Create update - await updateAssistant.update() - - // A wallet export should have been initiated - expect(walletSpy).toHaveBeenCalledWith({ key: agent.wallet.walletConfig?.key, path: backupPath }) - - // Backup should be cleaned after update - expect(await fileSystem.exists(backupPath)).toBe(false) - - expect( - (await credentialRepository.getAll(agent.context)).sort((a, b) => a.id.localeCompare(b.id)) - ).toMatchSnapshot() - }) - - it('should restore the backup if an error occurs during the update', async () => { - const aliceCredentialRecordsJson = JSON.parse(aliceCredentialRecordsString) - - // biome-ignore lint/suspicious/noExplicitAny: - const aliceCredentialRecords = Object.values(aliceCredentialRecordsJson).map((data: any) => { - const record = JsonTransformer.fromJSON(data.value, CredentialExchangeRecord) - - record.setTags(data.tags) - return record - }) - - const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) - const storageUpdateService = agent.dependencyManager.resolve(StorageUpdateService) - - // Add 0.1 data and set version to 0.1 - for (const credentialRecord of aliceCredentialRecords) { - await credentialRepository.save(agent.context, credentialRecord) - } - await storageUpdateService.setCurrentStorageVersion(agent.context, '0.1') - - // Expect an update is needed - expect(await updateAssistant.isUpToDate()).toBe(false) - jest.spyOn(updateAssistant, 'getNeededUpdates').mockResolvedValue([ - { - fromVersion: '0.1', - toVersion: '0.2', - doUpdate: async () => { - throw new CredoError("Uh oh I'm broken") - }, - }, - ]) - - const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) - // Backup should not exist before update - expect(await fileSystem.exists(backupPath)).toBe(false) - - let updateError: StorageUpdateError | undefined = undefined - - try { - await updateAssistant.update() - } catch (error) { - updateError = error - } - - expect(updateError?.cause?.message).toEqual("Uh oh I'm broken") - - // Only backup error should exist after update - expect(await fileSystem.exists(backupPath)).toBe(false) - expect(await fileSystem.exists(`${backupPath}-error`)).toBe(true) - - // Wallet should be same as when we started because of backup - expect((await credentialRepository.getAll(agent.context)).sort((a, b) => a.id.localeCompare(b.id))).toEqual( - aliceCredentialRecords.sort((a, b) => a.id.localeCompare(b.id)) - ) - }) -}) diff --git a/packages/core/src/storage/migration/__tests__/migration-askar.test.ts b/packages/core/src/storage/migration/__tests__/migration-askar.test.ts new file mode 100644 index 0000000000..a5f407281e --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/migration-askar.test.ts @@ -0,0 +1,74 @@ +import { readFileSync } from 'fs' +import path from 'path' + +import { CredentialExchangeRecord, CredentialRepository } from '../../../../../didcomm/src/modules/credentials' +import { getAgentOptions } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { JsonTransformer } from '../../../utils' +import { StorageUpdateService } from '../StorageUpdateService' +import { UpdateAssistant } from '../UpdateAssistant' + +const agentOptions = getAgentOptions('UpdateAssistant | Aries Askar', undefined, undefined, undefined, { + requireDidcomm: true, +}) + +const aliceCredentialRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-4-credentials-0.1.json'), + 'utf8' +) + +const backupDate = new Date('2022-03-22T22:50:20.522Z') +jest.useFakeTimers().setSystemTime(backupDate) + +describe('UpdateAssistant | Aries Askar', () => { + let updateAssistant: UpdateAssistant + let agent: Agent + + beforeEach(async () => { + agent = new Agent(agentOptions) + updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'allMediator', + }, + }) + + await updateAssistant.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + }) + + it('should create a backup', async () => { + const aliceCredentialRecordsJson = JSON.parse(aliceCredentialRecordsString) + + // biome-ignore lint/suspicious/noExplicitAny: + const aliceCredentialRecords = Object.values(aliceCredentialRecordsJson).map((data: any) => { + const record = JsonTransformer.fromJSON(data.value, CredentialExchangeRecord) + + record.setTags(data.tags) + return record + }) + + const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) + const storageUpdateService = agent.dependencyManager.resolve(StorageUpdateService) + + // Add 0.1 data and set version to 0.1 + for (const credentialRecord of aliceCredentialRecords) { + await credentialRepository.save(agent.context, credentialRecord) + } + await storageUpdateService.setCurrentStorageVersion(agent.context, '0.1') + + // Expect an update is needed + expect(await updateAssistant.isUpToDate()).toBe(false) + + // Create update + await updateAssistant.update() + + expect(await updateAssistant.isUpToDate()).toBe(true) + + expect( + (await credentialRepository.getAll(agent.context)).sort((a, b) => a.id.localeCompare(b.id)) + ).toMatchSnapshot() + }) +}) diff --git a/packages/core/src/storage/migration/repository/StorageVersionRecord.ts b/packages/core/src/storage/migration/repository/StorageVersionRecord.ts index 3d39b652af..3065649983 100644 --- a/packages/core/src/storage/migration/repository/StorageVersionRecord.ts +++ b/packages/core/src/storage/migration/repository/StorageVersionRecord.ts @@ -1,10 +1,9 @@ import type { VersionString } from '../../../utils/version' -import { uuid } from '../../../utils/uuid' import { BaseRecord } from '../../BaseRecord' +import { CURRENT_FRAMEWORK_STORAGE_VERSION, STORAGE_VERSION_RECORD_ID } from '../updates' export interface StorageVersionRecordProps { - id?: string createdAt?: Date storageVersion: VersionString } @@ -19,7 +18,7 @@ export class StorageVersionRecord extends BaseRecord { super() if (props) { - this.id = props.id ?? uuid() + this.id = StorageVersionRecord.storageVersionRecordId this.createdAt = props.createdAt ?? new Date() this.storageVersion = props.storageVersion } @@ -28,4 +27,12 @@ export class StorageVersionRecord extends BaseRecord { public getTags() { return this._tags } + + public static get frameworkStorageVersion() { + return CURRENT_FRAMEWORK_STORAGE_VERSION + } + + public static get storageVersionRecordId() { + return STORAGE_VERSION_RECORD_ID + } } diff --git a/packages/core/src/storage/migration/updates.ts b/packages/core/src/storage/migration/updates.ts index 05d5e2b5bf..67aa87fbee 100644 --- a/packages/core/src/storage/migration/updates.ts +++ b/packages/core/src/storage/migration/updates.ts @@ -60,5 +60,7 @@ export const CURRENT_FRAMEWORK_STORAGE_VERSION = supportedUpdates[supportedUpdat typeof supportedUpdates >['toVersion'] +export const STORAGE_VERSION_RECORD_ID = 'STORAGE_VERSION_RECORD_ID' + type LastItem = T extends readonly [...infer _, infer U] ? U : T[0] | undefined export type UpdateToVersion = (typeof supportedUpdates)[number]['toVersion'] diff --git a/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts b/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts index 9012972157..a4a526da7e 100644 --- a/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts +++ b/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts @@ -1,23 +1,12 @@ import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' import { Agent } from '../../../../../agent/Agent' -import { AgentConfig } from '../../../../../agent/AgentConfig' import { W3cCredentialRecord, W3cCredentialRepository, W3cJsonLdVerifiableCredential } from '../../../../../modules/vc' import { W3cJsonLdCredentialService } from '../../../../../modules/vc/data-integrity/W3cJsonLdCredentialService' import { Ed25519Signature2018Fixtures } from '../../../../../modules/vc/data-integrity/__tests__/fixtures' import { JsonTransformer } from '../../../../../utils' import * as testModule from '../w3cCredentialRecord' -const dependencyManager = { - resolve: (_injectionToken: unknown) => { - // no-op - }, -} - const agentConfig = getAgentConfig('Migration W3cCredentialRecord 0.4-0.5') -const agentContext = getAgentContext({ - // biome-ignore lint/suspicious/noExplicitAny: - dependencyManager: dependencyManager as any, -}) const repository = { getAll: jest.fn(), @@ -28,26 +17,20 @@ const w3cJsonLdCredentialService = { getExpandedTypesForCredential: jest.fn().mockResolvedValue(['https://example.com#example']), } -dependencyManager.resolve = (injectionToken: unknown) => { - if (injectionToken === W3cJsonLdCredentialService) { - return w3cJsonLdCredentialService - } - if (injectionToken === W3cCredentialRepository) { - return repository - } - if (injectionToken === AgentConfig) { - return agentConfig - } - - throw new Error('unknown injection token') -} +const agentContext = getAgentContext({ + agentConfig, + registerInstances: [ + [W3cJsonLdCredentialService, w3cJsonLdCredentialService], + [W3cCredentialRepository, repository], + ], +}) jest.mock('../../../../../agent/Agent', () => { return { Agent: jest.fn(() => ({ config: agentConfig, context: agentContext, - dependencyManager, + dependencyManager: agentContext.dependencyManager, })), } }) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 9152f3f35e..fcd8e24363 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,49 +1,13 @@ -import type { Key } from './crypto' +import { Kms } from '.' import type { Logger } from './logger' -export enum KeyDerivationMethod { - /** default value in indy-sdk. Will be used when no value is provided */ - Argon2IMod = 'ARGON2I_MOD', - /** less secure, but faster */ - Argon2IInt = 'ARGON2I_INT', - /** raw wallet master key */ - Raw = 'RAW', -} - -export interface WalletStorageConfig { - type: string - [key: string]: unknown -} - -export interface WalletConfig { - id: string - key: string - keyDerivationMethod?: KeyDerivationMethod - storage?: WalletStorageConfig -} - -export interface WalletConfigRekey { - id: string - key: string - rekey: string - keyDerivationMethod?: KeyDerivationMethod - rekeyDerivationMethod?: KeyDerivationMethod -} - -export interface WalletExportImportConfig { - key: string - path: string -} - export interface InitConfig { /** * Agent public endpoints, sorted by priority (higher priority first) */ label: string - walletConfig?: WalletConfig logger?: Logger autoUpdateStorageOnStartup?: boolean - backupBeforeStorageUpdate?: boolean /** * Allow insecure http urls in places where this is usually required. @@ -77,6 +41,23 @@ export interface JsonObject { */ export type FlatArray = Arr extends ReadonlyArray ? FlatArray : Arr +/** + * Create an exclusive or, setting the other params to 'never' which helps with + * type narrowing + * + * @example + * ``` + * type Options = XOR<{ name: string }, { dateOfBirth: Date }> + * + * type Options = + * | { name: string; dateOfBirth?: never } + * | { name?: never; dateOfBirth: Date } + * ``` + */ +export type XOR = + | (T & { [P in keyof Omit]?: never }) + | (U & { [P in keyof Omit]?: never }) + /** * Get the awaited (resolved promise) type of Promise type. */ @@ -87,28 +68,9 @@ export type Awaited = T extends Promise ? U : never */ export type IsAny = unknown extends T ? ([keyof T] extends [never] ? false : true) : false -// FIXME: the following types are duplicated in DIDComm module. They were placed here to remove dependency -// to that module export interface ResolvedDidCommService { id: string serviceEndpoint: string - recipientKeys: Key[] - routingKeys: Key[] -} - -export interface PlaintextMessage { - '@type': string - '@id': string - '~thread'?: { - thid?: string - pthid?: string - } - [key: string]: unknown -} - -export type EncryptedMessage = { - protected: string - iv: string - ciphertext: string - tag: string + recipientKeys: Kms.PublicJwk[] + routingKeys: Kms.PublicJwk[] } diff --git a/packages/core/src/utils/object.ts b/packages/core/src/utils/object.ts new file mode 100644 index 0000000000..7f8614cd41 --- /dev/null +++ b/packages/core/src/utils/object.ts @@ -0,0 +1,26 @@ +export function isObject(item: unknown): item is Record { + return item != null && typeof item === 'object' && !Array.isArray(item) +} + +/** + * Deep merge two objects. + * @param target + * @param ...sources + */ +export function mergeDeep(target: unknown, ...sources: Array): unknown { + if (!sources.length) return target + const source = sources.shift() + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }) + mergeDeep(target[key], source[key]) + } else { + Object.assign(target, { [key]: source[key] }) + } + } + } + + return mergeDeep(target, ...sources) +} diff --git a/packages/core/src/utils/type.ts b/packages/core/src/utils/type.ts index 064ca0ce75..ee54c6ca3c 100644 --- a/packages/core/src/utils/type.ts +++ b/packages/core/src/utils/type.ts @@ -7,3 +7,10 @@ export type Optional = Pick, K> & Omit export const isJsonObject = (value: unknown): value is JsonObject => { return value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value) } + +// eslint-disable-next-line @typescript-eslint/ban-types +export type StringWithAutoComplete = AutoComplete | (string & {}) +// eslint-disable-next-line @typescript-eslint/ban-types +export type NumberWithAutoComplete = AutoComplete | (number & {}) + +export type CanBePromise = T | Promise diff --git a/packages/core/src/utils/zod-error.ts b/packages/core/src/utils/zod-error.ts new file mode 100644 index 0000000000..13d9268564 --- /dev/null +++ b/packages/core/src/utils/zod-error.ts @@ -0,0 +1,94 @@ +import type z from 'zod' +import { type ZodIssue, ZodIssueCode } from 'zod' + +/** + * Some code comes from `zod-validation-error` package (MIT License) and + * was slightly simplified to fit our needs. + */ +const constants = { + // biome-ignore lint/suspicious/noMisleadingCharacterClass: expected + identifierRegex: /[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*/u, + unionSeparator: ', or ', + issueSeparator: '\n\t- ', +} + +function escapeQuotes(str: string): string { + return str.replace(/"/g, '\\"') +} + +function joinPath(path: Array): string { + if (path.length === 1) { + return path[0].toString() + } + + return path.reduce((acc, item) => { + // handle numeric indices + if (typeof item === 'number') { + return `${acc}[${item.toString()}]` + } + + // handle quoted values + if (item.includes('"')) { + return `${acc}["${escapeQuotes(item)}"]` + } + + // handle special characters + if (!constants.identifierRegex.test(item)) { + return `${acc}["${item}"]` + } + + // handle normal values + const separator = acc.length === 0 ? '' : '.' + return acc + separator + item + }, '') +} +function getMessageFromZodIssue(issue: ZodIssue): string { + if (issue.code === ZodIssueCode.invalid_union) { + return getMessageFromUnionErrors(issue.unionErrors) + } + + if (issue.code === ZodIssueCode.invalid_arguments) { + return [issue.message, ...issue.argumentsError.issues.map((issue) => getMessageFromZodIssue(issue))].join( + constants.issueSeparator + ) + } + + if (issue.code === ZodIssueCode.invalid_return_type) { + return [issue.message, ...issue.returnTypeError.issues.map((issue) => getMessageFromZodIssue(issue))].join( + constants.issueSeparator + ) + } + + if (issue.path.length !== 0) { + // handle array indices + if (issue.path.length === 1) { + const identifier = issue.path[0] + + if (typeof identifier === 'number') { + return `${issue.message} at index ${identifier}` + } + } + + return `${issue.message} at "${joinPath(issue.path)}"` + } + + return issue.message +} + +function getMessageFromUnionErrors(unionErrors: z.ZodError[]): string { + return unionErrors + .reduce((acc, zodError) => { + const newIssues = zodError.issues.map((issue) => getMessageFromZodIssue(issue)).join(constants.issueSeparator) + + if (!acc.includes(newIssues)) acc.push(newIssues) + + return acc + }, []) + .join(constants.unionSeparator) +} + +export function formatZodError(error?: z.ZodError): string { + if (!error) return '' + + return `\t- ${error?.issues.map((issue) => getMessageFromZodIssue(issue)).join(constants.issueSeparator)}` +} diff --git a/packages/core/src/utils/zod.ts b/packages/core/src/utils/zod.ts new file mode 100644 index 0000000000..eec5bc68b3 --- /dev/null +++ b/packages/core/src/utils/zod.ts @@ -0,0 +1,34 @@ +import { z } from 'zod' + +import { ZodValidationError } from '../error' + +// biome-ignore lint/suspicious/noExplicitAny: +export type BaseSchema = z.Schema + +export function parseWithErrorHandling( + schema: Schema, + data: unknown, + customErrorMessage?: string +): z.output { + const parseResult = schema.safeParse(data) + + if (!parseResult.success) { + throw new ZodValidationError( + customErrorMessage ?? `Error validating schema with data ${JSON.stringify(data)}`, + parseResult.error + ) + } + + return parseResult.data +} + +const zUniqueArray = (item: TItem) => + z.array(item).refine((a) => new Set<(typeof a)[number]>(a).size === a.length, 'Array must have unique values') + +const zOptionalToUndefined = (item: TItem) => + z.optional(item.transform(() => undefined)) + +const zBase64Url = z.string().regex(/[a-zA-Z0-9_-]+/, 'Must be a base64url string') + +export * from 'zod' +export { zUniqueArray as uniqueArray, zOptionalToUndefined as optionalToUndefined, zBase64Url as base64Url } diff --git a/packages/core/src/wallet/Wallet.ts b/packages/core/src/wallet/Wallet.ts deleted file mode 100644 index 363e482dc5..0000000000 --- a/packages/core/src/wallet/Wallet.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { Key, KeyType } from '../crypto' -import type { KeyBackend } from '../crypto/KeyBackend' -import type { Disposable } from '../plugins' -import type { - EncryptedMessage, - PlaintextMessage, - WalletConfig, - WalletConfigRekey, - WalletExportImportConfig, -} from '../types' -import type { Buffer } from '../utils/buffer' - -// Split up into WalletManager and Wallet instance -// WalletManager is responsible for: -// - create, open, delete, close, export, import -// Wallet is responsible for: -// - createKey, sign, verify, pack, unpack, generateNonce, generateWalletKey - -// - Split storage initialization from wallet initialization, as storage and wallet are not required to be the same -// - wallet handles key management, signing, and encryption -// - storage handles record storage and retrieval - -export interface Wallet extends Disposable { - isInitialized: boolean - isProvisioned: boolean - - create(walletConfig: WalletConfig): Promise - createAndOpen(walletConfig: WalletConfig): Promise - open(walletConfig: WalletConfig): Promise - rotateKey(walletConfig: WalletConfigRekey): Promise - close(): Promise - delete(): Promise - - /** - * Export the wallet to a file at the given path and encrypt it with the given key. - * - * @throws {WalletExportPathExistsError} When the export path already exists - */ - export(exportConfig: WalletExportImportConfig): Promise - import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig): Promise - - /** - * Create a key with an optional private key and keyType. - * - * @param options.privateKey Buffer Private key (formerly called 'seed') - * @param options.keyType KeyType the type of key that should be created - * - * @returns a `Key` instance - * - * @throws {WalletError} When an unsupported keytype is requested - * @throws {WalletError} When the key could not be created - * @throws {WalletKeyExistsError} When the key already exists in the wallet - */ - createKey(options: WalletCreateKeyOptions): Promise - sign(options: WalletSignOptions): Promise - verify(options: WalletVerifyOptions): Promise - - pack(payload: Record, recipientKeys: string[], senderVerkey?: string): Promise - unpack(encryptedMessage: EncryptedMessage): Promise - generateNonce(): Promise - getRandomValues(length: number): Uint8Array - generateWalletKey(): Promise - - // Methods to faciliate OpenID4VP response encryption, should be unified/generalized at some - // point. Ideally all the didcomm/oid4vc/encryption/decryption is generalized, but it's a bit complex - // @note methods are optional to not introduce breaking changes - - /** - * Method that enables JWT encryption using ECDH-ES and AesA256Gcm and returns it as a compact JWE. - * This method is specifically added to support OpenID4VP response encryption using JARM and should later be - * refactored into a more generic method that supports encryption/decryption. - * - * @returns compact JWE - */ - directEncryptCompactJweEcdhEs?(options: WalletDirectEncryptCompactJwtEcdhEsOptions): Promise - - /** - * Method that enabled JWT encryption using ECDH-ES and AesA256Gcm and returns it as a compact JWE. - * This method is specifically added to support OpenID4VP response encryption using JARM and should later be - * refactored into a more generic method that supports encryption/decryption. - * - * @returns compact JWE - */ - directDecryptCompactJweEcdhEs?({ - compactJwe, - recipientKey, - }: { - compactJwe: string - recipientKey: Key - }): Promise - - /** - * Get the key types supported by the wallet implementation. - */ - supportedKeyTypes: KeyType[] -} - -export interface WalletCreateKeyOptions { - keyType: KeyType - seed?: Buffer - privateKey?: Buffer - keyBackend?: KeyBackend - keyId?: string -} - -export interface WalletSignOptions { - data: Buffer | Buffer[] - key: Key -} - -export interface WalletVerifyOptions { - data: Buffer | Buffer[] - key: Key - signature: Buffer -} - -export interface UnpackedMessageContext { - plaintextMessage: PlaintextMessage - senderKey?: string - recipientKey?: string -} - -export interface WalletDirectEncryptCompactJwtEcdhEsOptions { - recipientKey: Key - encryptionAlgorithm: 'A128GCM' | 'A256GCM' | 'A128CBC-HS256' - apu?: string - apv?: string - data: Buffer - header: Record -} - -export interface WalletDirectDecryptCompactJwtEcdhEsReturn { - data: Buffer - header: Record -} diff --git a/packages/core/src/wallet/WalletApi.ts b/packages/core/src/wallet/WalletApi.ts deleted file mode 100644 index 1f75dfde44..0000000000 --- a/packages/core/src/wallet/WalletApi.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { WalletConfig, WalletConfigRekey, WalletExportImportConfig } from '../types' -import type { Wallet, WalletCreateKeyOptions } from './Wallet' - -import { AgentContext } from '../agent' -import { InjectionSymbols } from '../constants' -import { Logger } from '../logger' -import { inject, injectable } from '../plugins' -import { StorageUpdateService } from '../storage' -import { CURRENT_FRAMEWORK_STORAGE_VERSION } from '../storage/migration/updates' - -import { WalletError } from './error/WalletError' -import { WalletNotFoundError } from './error/WalletNotFoundError' - -@injectable() -export class WalletApi { - private agentContext: AgentContext - private wallet: Wallet - private storageUpdateService: StorageUpdateService - private logger: Logger - private _walletConfig?: WalletConfig - - public constructor( - storageUpdateService: StorageUpdateService, - agentContext: AgentContext, - @inject(InjectionSymbols.Logger) logger: Logger - ) { - this.storageUpdateService = storageUpdateService - this.logger = logger - this.wallet = agentContext.wallet - this.agentContext = agentContext - } - - public get isInitialized() { - return this.wallet.isInitialized - } - - public get isProvisioned() { - return this.wallet.isProvisioned - } - - public get walletConfig() { - return this._walletConfig - } - - public async initialize(walletConfig: WalletConfig): Promise { - this.logger.info(`Initializing wallet '${walletConfig.id}'`, { - ...walletConfig, - key: walletConfig?.key ? '[*****]' : undefined, - storage: { - ...walletConfig?.storage, - credentials: walletConfig?.storage?.credentials ? '[*****]' : undefined, - }, - }) - - if (this.isInitialized) { - throw new WalletError( - 'Wallet instance already initialized. Close the currently opened wallet before re-initializing the wallet' - ) - } - - // Open wallet, creating if it doesn't exist yet - try { - await this.open(walletConfig) - } catch (error) { - // If the wallet does not exist yet, create it and try to open again - if (error instanceof WalletNotFoundError) { - // Keep the wallet open after creating it, this saves an extra round trip of closing/opening - // the wallet, which can save quite some time. - await this.createAndOpen(walletConfig) - } else { - throw error - } - } - } - - public async createAndOpen(walletConfig: WalletConfig): Promise { - // Always keep the wallet open, as we still need to store the storage version in the wallet. - await this.wallet.createAndOpen(walletConfig) - - this._walletConfig = walletConfig - - // Store the storage version in the wallet - await this.storageUpdateService.setCurrentStorageVersion(this.agentContext, CURRENT_FRAMEWORK_STORAGE_VERSION) - } - - public async create(walletConfig: WalletConfig): Promise { - await this.createAndOpen(walletConfig) - await this.close() - } - - public async open(walletConfig: WalletConfig): Promise { - await this.wallet.open(walletConfig) - this._walletConfig = walletConfig - } - - public async close(): Promise { - await this.wallet.close() - } - - public async rotateKey(walletConfig: WalletConfigRekey): Promise { - await this.wallet.rotateKey(walletConfig) - } - - public async generateNonce(): Promise { - return await this.wallet.generateNonce() - } - - public async delete(): Promise { - await this.wallet.delete() - } - - public async export(exportConfig: WalletExportImportConfig): Promise { - await this.wallet.export(exportConfig) - } - - public async import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig): Promise { - await this.wallet.import(walletConfig, importConfig) - } - - /** - * Create a key for and store it in the wallet. You can optionally provide a `privateKey` - * or `seed` for deterministic key generation. - * - * @param privateKey Buffer Private key (formerly called 'seed') - * @param seed Buffer (formerly called 'seed') - * @param keyType KeyType the type of key that should be created - * - * @returns a `Key` instance - * - * @throws {WalletError} When an unsupported `KeyType` is provided - * @throws {WalletError} When the key could not be created - */ - public async createKey(options: WalletCreateKeyOptions) { - return this.wallet.createKey(options) - } -} diff --git a/packages/core/src/wallet/WalletModule.ts b/packages/core/src/wallet/WalletModule.ts deleted file mode 100644 index 830838efb5..0000000000 --- a/packages/core/src/wallet/WalletModule.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { DependencyManager, Module } from '../plugins' - -import { WalletApi } from './WalletApi' - -// TODO: this should be moved into the modules directory -export class WalletModule implements Module { - public readonly api = WalletApi - - /** - * Registers the dependencies of the wallet module on the injection dependencyManager. - */ - public register(_dependencyManager: DependencyManager) { - // no-op, only API needs to be registered - } -} diff --git a/packages/core/src/wallet/__tests__/WalletModule.test.ts b/packages/core/src/wallet/__tests__/WalletModule.test.ts deleted file mode 100644 index a52a3a215f..0000000000 --- a/packages/core/src/wallet/__tests__/WalletModule.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { DependencyManager } from '../../plugins/DependencyManager' -import { WalletModule } from '../WalletModule' - -jest.mock('../../plugins/DependencyManager') -const DependencyManagerMock = DependencyManager as jest.Mock - -const dependencyManager = new DependencyManagerMock() - -describe('WalletModule', () => { - test('registers dependencies on the dependency manager', () => { - new WalletModule().register(dependencyManager) - }) -}) diff --git a/packages/core/src/wallet/error/WalletDuplicateError.ts b/packages/core/src/wallet/error/WalletDuplicateError.ts deleted file mode 100644 index 615b2563bb..0000000000 --- a/packages/core/src/wallet/error/WalletDuplicateError.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { WalletError } from './WalletError' - -export class WalletDuplicateError extends WalletError { - public constructor(message: string, { walletType, cause }: { walletType: string; cause?: Error }) { - super(`${walletType}: ${message}`, { cause }) - } -} diff --git a/packages/core/src/wallet/error/WalletExportPathExistsError.ts b/packages/core/src/wallet/error/WalletExportPathExistsError.ts deleted file mode 100644 index cf46e028e7..0000000000 --- a/packages/core/src/wallet/error/WalletExportPathExistsError.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { WalletError } from './WalletError' - -export class WalletExportPathExistsError extends WalletError { - public constructor(message: string, { cause }: { cause?: Error } = {}) { - super(message, { cause }) - } -} diff --git a/packages/core/src/wallet/error/WalletExportUnsupportedError.ts b/packages/core/src/wallet/error/WalletExportUnsupportedError.ts deleted file mode 100644 index db7a313e86..0000000000 --- a/packages/core/src/wallet/error/WalletExportUnsupportedError.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { WalletError } from './WalletError' - -export class WalletExportUnsupportedError extends WalletError { - public constructor(message: string, { cause }: { cause?: Error } = {}) { - super(message, { cause }) - } -} diff --git a/packages/core/src/wallet/error/WalletImportPathExistsError.ts b/packages/core/src/wallet/error/WalletImportPathExistsError.ts deleted file mode 100644 index 32d9b46d67..0000000000 --- a/packages/core/src/wallet/error/WalletImportPathExistsError.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { WalletError } from './WalletError' - -export class WalletImportPathExistsError extends WalletError { - public constructor(message: string, { cause }: { cause?: Error } = {}) { - super(message, { cause }) - } -} diff --git a/packages/core/src/wallet/error/WalletInvalidKeyError.ts b/packages/core/src/wallet/error/WalletInvalidKeyError.ts deleted file mode 100644 index b7a29de2d9..0000000000 --- a/packages/core/src/wallet/error/WalletInvalidKeyError.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { WalletError } from './WalletError' - -export class WalletInvalidKeyError extends WalletError { - public constructor(message: string, { walletType, cause }: { walletType: string; cause?: Error }) { - super(`${walletType}: ${message}`, { cause }) - } -} diff --git a/packages/core/src/wallet/error/WalletNotFoundError.ts b/packages/core/src/wallet/error/WalletNotFoundError.ts deleted file mode 100644 index a2e8d32d45..0000000000 --- a/packages/core/src/wallet/error/WalletNotFoundError.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { WalletError } from './WalletError' - -export class WalletNotFoundError extends WalletError { - public constructor(message: string, { walletType, cause }: { walletType: string; cause?: Error }) { - super(`${walletType}: ${message}`, { cause }) - } -} diff --git a/packages/core/src/wallet/error/index.ts b/packages/core/src/wallet/error/index.ts deleted file mode 100644 index 343fd83913..0000000000 --- a/packages/core/src/wallet/error/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { WalletDuplicateError } from './WalletDuplicateError' -export { WalletNotFoundError } from './WalletNotFoundError' -export { WalletInvalidKeyError } from './WalletInvalidKeyError' -export { WalletError } from './WalletError' -export { WalletKeyExistsError } from './WalletKeyExistsError' -export { WalletImportPathExistsError } from './WalletImportPathExistsError' -export { WalletExportPathExistsError } from './WalletExportPathExistsError' -export { WalletExportUnsupportedError } from './WalletExportUnsupportedError' diff --git a/packages/core/src/wallet/index.ts b/packages/core/src/wallet/index.ts deleted file mode 100644 index e60dcfdb68..0000000000 --- a/packages/core/src/wallet/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Wallet' -export * from './WalletApi' -export * from './WalletModule' diff --git a/packages/core/tests/agents.test.ts b/packages/core/tests/agents.test.ts index d96ca0e0ed..2fbd5d8842 100644 --- a/packages/core/tests/agents.test.ts +++ b/packages/core/tests/agents.test.ts @@ -3,15 +3,27 @@ import type { ConnectionRecord } from '../../didcomm/src' import { HandshakeProtocol } from '../../didcomm/src' import { Agent } from '../src/agent/Agent' -import { getInMemoryAgentOptions, waitForBasicMessage } from './helpers' +import { getAgentOptions, waitForBasicMessage } from './helpers' import { setupSubjectTransports } from './transport' -const aliceAgentOptions = getInMemoryAgentOptions('Agents Alice', { - endpoints: ['rxjs:alice'], -}) -const bobAgentOptions = getInMemoryAgentOptions('Agents Bob', { - endpoints: ['rxjs:bob'], -}) +const aliceAgentOptions = getAgentOptions( + 'Agents Alice', + { + endpoints: ['rxjs:alice'], + }, + undefined, + undefined, + { requireDidcomm: true } +) +const bobAgentOptions = getAgentOptions( + 'Agents Bob', + { + endpoints: ['rxjs:bob'], + }, + undefined, + undefined, + { requireDidcomm: true } +) describe('agents', () => { let aliceAgent: Agent @@ -21,9 +33,7 @@ describe('agents', () => { afterAll(async () => { await bobAgent.shutdown() - await bobAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('make a connection between agents', async () => { diff --git a/packages/core/tests/connections.test.ts b/packages/core/tests/connections.test.ts index de607bde04..f8ec016048 100644 --- a/packages/core/tests/connections.test.ts +++ b/packages/core/tests/connections.test.ts @@ -14,28 +14,47 @@ import { OutOfBandState } from '../../didcomm/src/modules/oob/domain/OutOfBandSt import { Agent } from '../src/agent/Agent' import { didKeyToVerkey } from '../src/modules/dids/helpers' -import { getInMemoryAgentOptions, waitForTrustPingResponseReceivedEvent } from './helpers' +import { getAgentOptions, waitForTrustPingResponseReceivedEvent } from './helpers' import { setupSubjectTransports } from './transport' -import { Key } from '@credo-ts/core' +import { TypedArrayEncoder } from '@credo-ts/core' +import { Ed25519PublicJwk, PublicJwk } from '../src/modules/kms' const faberAgent = new Agent( - getInMemoryAgentOptions('Faber Agent Connections', { - endpoints: ['rxjs:faber'], - }) + getAgentOptions( + 'Faber Agent Connections', + { + endpoints: ['rxjs:faber'], + }, + undefined, + undefined, + { requireDidcomm: true } + ) ) const aliceAgent = new Agent( - getInMemoryAgentOptions('Alice Agent Connections', { - endpoints: ['rxjs:alice'], - }) + getAgentOptions( + 'Alice Agent Connections', + { + endpoints: ['rxjs:alice'], + }, + undefined, + undefined, + { requireDidcomm: true } + ) ) const acmeAgent = new Agent( - getInMemoryAgentOptions('Acme Agent Connections', { - endpoints: ['rxjs:acme'], - }) + getAgentOptions( + 'Acme Agent Connections', + { + endpoints: ['rxjs:acme'], + }, + undefined, + undefined, + { requireDidcomm: true } + ) ) const mediatorAgent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'Mediator Agent Connections', { endpoints: ['rxjs:mediator'], @@ -45,7 +64,8 @@ const mediatorAgent = new Agent( mediator: new MediatorModule({ autoAcceptMediationRequests: true, }), - } + }, + { requireDidcomm: true } ) ) @@ -61,13 +81,9 @@ describe('connections', () => { afterEach(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() await acmeAgent.shutdown() - await acmeAgent.wallet.delete() await mediatorAgent.shutdown() - await mediatorAgent.wallet.delete() }) it('one agent should be able to send and receive a ping', async () => { @@ -235,9 +251,10 @@ describe('connections', () => { let { connectionRecord } = await faberAgent.modules.oob.receiveInvitation(mediatorOutOfBandInvitation) // biome-ignore lint/style/noNonNullAssertion: connectionRecord = await faberAgent.modules.connections.returnWhenIsConnected(connectionRecord?.id!) + // biome-ignore lint/style/noNonNullAssertion: - await faberAgent.modules.mediationRecipient.provision(connectionRecord!) - await faberAgent.modules.mediationRecipient.initialize() + const mediationRecord = await faberAgent.modules.mediationRecipient.provision(connectionRecord!) + faberAgent.modules.mediationRecipient.initiateMessagePickup(mediationRecord) // Create observable for event const keyAddMessageObservable = mediatorAgent.events @@ -304,19 +321,27 @@ describe('connections', () => { expect.arrayContaining([ { action: KeylistUpdateAction.add, - recipientKey: Key.fromFingerprint(faberOutOfBandRecord.getTags().recipientKeyFingerprints[0]).publicKeyBase58, + recipientKey: TypedArrayEncoder.toBase58( + ( + PublicJwk.fromFingerprint( + faberOutOfBandRecord.getTags().recipientKeyFingerprints[0] + ) as PublicJwk + ).publicKey.publicKey + ), }, { action: KeylistUpdateAction.add, - // biome-ignore lint/style/noNonNullAssertion: - recipientKey: (await faberAgent.dids.resolveDidDocument(faberAliceConnection.did!)).recipientKeys[0] - .publicKeyBase58, + recipientKey: TypedArrayEncoder.toBase58( + // biome-ignore lint/style/noNonNullAssertion: + (await faberAgent.dids.resolveDidDocument(faberAliceConnection.did!)).recipientKeys[0].publicKey.publicKey + ), }, { action: KeylistUpdateAction.add, - // biome-ignore lint/style/noNonNullAssertion: - recipientKey: (await faberAgent.dids.resolveDidDocument(faberAcmeConnection.did!)).recipientKeys[0] - .publicKeyBase58, + recipientKey: TypedArrayEncoder.toBase58( + // biome-ignore lint/style/noNonNullAssertion: + (await faberAgent.dids.resolveDidDocument(faberAcmeConnection.did!)).recipientKeys[0].publicKey.publicKey + ), }, ]) ) @@ -342,8 +367,10 @@ describe('connections', () => { }))[0] ).toEqual({ action: KeylistUpdateAction.remove, - // biome-ignore lint/style/noNonNullAssertion: - recipientKey: (await faberAgent.dids.resolveDidDocument(connection.did!)).recipientKeys[0].publicKeyBase58, + recipientKey: TypedArrayEncoder.toBase58( + // biome-ignore lint/style/noNonNullAssertion: + (await faberAgent.dids.resolveDidDocument(connection.did!)).recipientKeys[0].publicKey.publicKey + ), }) } }) diff --git a/packages/core/tests/generic-records.test.ts b/packages/core/tests/generic-records.test.ts index bdf605d517..9377b12ad2 100644 --- a/packages/core/tests/generic-records.test.ts +++ b/packages/core/tests/generic-records.test.ts @@ -3,9 +3,9 @@ import type { GenericRecord } from '../src/modules/generic-records/repository/Ge import { Agent } from '../src/agent/Agent' import { RecordNotFoundError } from '../src/error' -import { getInMemoryAgentOptions } from './helpers' +import { getAgentOptions } from './helpers' -const aliceAgentOptions = getInMemoryAgentOptions('Generic Records Alice', { +const aliceAgentOptions = getAgentOptions('Generic Records Alice', { endpoints: ['rxjs:alice'], }) @@ -20,7 +20,6 @@ describe('genericRecords', () => { afterAll(async () => { await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('store generic-record record', async () => { diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 68716f1e0e..d6e01cca49 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -1,5 +1,4 @@ import type { Observable } from 'rxjs' -import type { AskarWalletSqliteStorageConfig } from '../../askar/src/wallet' import type { AgentMessageProcessedEvent, BasicMessage, @@ -9,6 +8,7 @@ import type { ConnectionStateChangedEvent, CredentialState, CredentialStateChangedEvent, + OutOfBandInlineServiceKey, ProofStateChangedEvent, RevocationNotificationReceivedEvent, } from '../../didcomm/src' @@ -27,17 +27,13 @@ import type { InitConfig, InjectionToken, KeyDidCreateOptions, - Wallet, } from '../src' import type { AgentModulesInput, EmptyModuleMap } from '../src/agent/AgentModules' -import type { WalletConfig } from '../src/types' import { readFileSync } from 'fs' import path from 'path' import { ReplaySubject, firstValueFrom, lastValueFrom } from 'rxjs' import { catchError, filter, map, take, timeout } from 'rxjs/operators' - -import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' import { AgentEventTypes, BasicMessageEventTypes, @@ -57,22 +53,18 @@ import { OutOfBandState } from '../../didcomm/src/modules/oob/domain/OutOfBandSt import { OutOfBandInvitation } from '../../didcomm/src/modules/oob/messages' import { OutOfBandRecord } from '../../didcomm/src/modules/oob/repository' import { getDefaultDidcommModules } from '../../didcomm/src/util/modules' -import { agentDependencies } from '../../node/src' -import { - AgentConfig, - AgentContext, - DependencyManager, - DidsApi, - InjectionSymbols, - TypedArrayEncoder, - X509Api, -} from '../src' -import { Key, KeyType } from '../src/crypto' +import { NodeInMemoryKeyManagementStorage, NodeKeyManagementService, agentDependencies } from '../../node/src' +import { AgentConfig, AgentContext, DependencyManager, DidsApi, Kms, TypedArrayEncoder, X509Api } from '../src' import { DidKey } from '../src/modules/dids/methods/key' -import { KeyDerivationMethod } from '../src/types' import { sleep } from '../src/utils/sleep' import { uuid } from '../src/utils/uuid' +import { askar } from '@openwallet-foundation/askar-nodejs' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' +import { AskarModule } from '../../askar/src/AskarModule' +import { AskarModuleConfigStoreOptions } from '../../askar/src/AskarModuleConfig' +import { transformPrivateKeyToPrivateJwk } from '../../askar/src/utils' +import { KeyManagementApi, KeyManagementService, PublicJwk } from '../src/modules/kms' import testLogger, { TestLogger } from './logger' export const genesisPath = process.env.GENESIS_TXN_PATH @@ -86,7 +78,7 @@ export const taaVersion = (process.env.TEST_AGENT_TAA_VERSION ?? '1') as `${numb export const taaAcceptanceMechanism = process.env.TEST_AGENT_TAA_ACCEPTANCE_MECHANISM ?? 'accept' export { agentDependencies } -export function getAskarWalletConfig( +export function getAskarStoreConfig( name: string, { inMemory = true, @@ -97,16 +89,15 @@ export function getAskarWalletConfig( return { id: `Wallet: ${name} - ${random}`, key: 'DZ9hPqFWTPxemcGea72C1X1nusqk5wFNLq6QPjwXGqAa', // generated using indy.generateWalletKey - keyDerivationMethod: KeyDerivationMethod.Raw, - // Use in memory by default - storage: { + keyDerivationMethod: 'raw', + database: { type: 'sqlite', config: { inMemory, maxConnections, }, - } satisfies AskarWalletSqliteStorageConfig, - } satisfies WalletConfig + }, + } satisfies AskarModuleConfigStoreOptions } export function getAgentOptions( @@ -114,7 +105,7 @@ export function getAgentOptions = {}, extraConfig: Partial = {}, inputModules?: AgentModules, - inMemoryWallet = true + { requireDidcomm = false, inMemory = true }: { requireDidcomm?: boolean; inMemory?: boolean } = {} ): { config: InitConfig modules: AgentModules & DefaultAgentModulesInput @@ -124,7 +115,6 @@ export function getAgentOptions( - name: string, - didcommExtraConfig: Partial = {}, - extraConfig: Partial = {}, - inputModules?: AgentModules -): { - config: InitConfig - modules: AgentModules & DefaultAgentModulesInput - dependencies: AgentDependencies -} { - const random = uuid().slice(0, 4) - const config: InitConfig = { - label: `Agent: ${name} - ${random}`, - walletConfig: { - id: `Wallet: ${name} - ${random}`, - key: `Wallet: ${name}`, - }, - // TODO: determine the log level based on an environment variable. This will make it - // possible to run e.g. failed github actions in debug mode for extra logs - logger: TestLogger.fromLogger(testLogger, name), - ...extraConfig, - } - - const didcommConfig: DidCommModuleConfigOptions = { ...didcommExtraConfig } - - const m = (inputModules ?? {}) as AgentModulesInput const modules = { - ...getDefaultDidcommModules(didcommConfig), + ...(requireDidcomm + ? { + ...getDefaultDidcommModules(didcommConfig), + connections: + // Make sure connections module is always defined so we can set autoAcceptConnections + m.connections ?? + new ConnectionsModule({ + autoAcceptConnections: true, + }), + } + : {}), ...m, - inMemory: new InMemoryWalletModule(), - // Make sure connections module is always defined so we can set autoAcceptConnections - connections: - m.connections ?? - new ConnectionsModule({ - autoAcceptConnections: true, - }), + ..._kmsModules, } return { @@ -198,16 +159,33 @@ export function getInMemoryAgentOptions< } export async function importExistingIndyDidFromPrivateKey(agent: Agent, privateKey: Buffer) { - const key = await agent.wallet.createKey({ - keyType: KeyType.Ed25519, + const { privateJwk } = transformPrivateKeyToPrivateJwk({ privateKey, + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + }) + + const key = await agent.kms.importKey({ + privateJwk, }) + const publicJwk = Kms.PublicJwk.fromPublicJwk(key.publicJwk as Kms.KmsJwkPublicOkp & { crv: 'Ed25519' }) + // did is first 16 bytes of public key encoded as base58 - const unqualifiedIndyDid = TypedArrayEncoder.toBase58(key.publicKey.slice(0, 16)) + const unqualifiedIndyDid = TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey.slice(0, 16)) // import the did in the wallet so it can be used - await agent.dids.import({ did: `did:indy:pool:localtest:${unqualifiedIndyDid}` }) + await agent.dids.import({ + did: `did:indy:pool:localtest:${unqualifiedIndyDid}`, + keys: [ + { + didDocumentRelativeKeyId: '#verkey', + kmsKeyId: key.keyId, + }, + ], + }) return unqualifiedIndyDid } @@ -216,27 +194,28 @@ export function getAgentConfig( name: string, didcommConfig: Partial = {}, extraConfig: Partial = {} -): AgentConfig & { walletConfig: WalletConfig } { +): AgentConfig { const { config, dependencies } = getAgentOptions(name, didcommConfig, extraConfig) - return new AgentConfig(config, dependencies) as AgentConfig & { walletConfig: WalletConfig } + return new AgentConfig(config, dependencies) } export function getAgentContext({ dependencyManager = new DependencyManager(), - wallet, agentConfig, contextCorrelationId = 'mock', registerInstances = [], + kmsBackends = [new NodeKeyManagementService(new NodeInMemoryKeyManagementStorage())], + isRootAgentContext = true, }: { dependencyManager?: DependencyManager - wallet?: Wallet agentConfig?: AgentConfig contextCorrelationId?: string + kmsBackends?: KeyManagementService[] // Must be an array of arrays as objects can't have injection tokens // as keys (it must be number, string or symbol) registerInstances?: Array<[InjectionToken, unknown]> + isRootAgentContext?: boolean } = {}) { - if (wallet) dependencyManager.registerInstance(InjectionSymbols.Wallet, wallet) if (agentConfig) dependencyManager.registerInstance(AgentConfig, agentConfig) // Register custom instances on the dependency manager @@ -244,7 +223,17 @@ export function getAgentContext({ dependencyManager.registerInstance(token, instance) } - return new AgentContext({ dependencyManager, contextCorrelationId }) + const agentContext = new AgentContext({ dependencyManager, contextCorrelationId, isRootAgentContext }) + agentContext.dependencyManager.registerInstance( + Kms.KeyManagementModuleConfig, + new Kms.KeyManagementModuleConfig({ + backends: kmsBackends, + }) + ) + agentContext.dependencyManager.registerContextScoped(KeyManagementApi) + + agentContext.dependencyManager.registerInstance(AgentContext, agentContext) + return agentContext } export async function waitForProofExchangeRecord( @@ -674,8 +663,15 @@ export function getMockOutOfBand({ label, serviceEndpoint, recipientKeys = [ - new DidKey(Key.fromPublicKeyBase58('ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7', KeyType.Ed25519)).did, + new DidKey( + PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7'), + }) + ).did, ], + invitationInlineServiceKeys, mediatorId, role, state, @@ -691,6 +687,7 @@ export function getMockOutOfBand({ state?: OutOfBandState reusable?: boolean reuseConnectionId?: string + invitationInlineServiceKeys?: OutOfBandInlineServiceKey[] imageUrl?: string } = {}) { const options = { @@ -710,13 +707,14 @@ export function getMockOutOfBand({ const outOfBandInvitation = new OutOfBandInvitation(options) const outOfBandRecord = new OutOfBandRecord({ mediatorId, + invitationInlineServiceKeys, role: role || OutOfBandRole.Receiver, state: state || OutOfBandState.Initial, outOfBandInvitation: outOfBandInvitation, reusable, reuseConnectionId, tags: { - recipientKeyFingerprints: recipientKeys.map((didKey) => DidKey.fromDid(didKey).key.fingerprint), + recipientKeyFingerprints: recipientKeys.map((didKey) => DidKey.fromDid(didKey).publicJwk.fingerprint), }, }) return outOfBandRecord @@ -784,30 +782,57 @@ export async function retryUntilResult Promise>( export type CreateDidKidVerificationMethodReturn = Awaited> export async function createDidKidVerificationMethod(agentContext: AgentContext, secretKey?: string) { const dids = agentContext.dependencyManager.resolve(DidsApi) + const kms = agentContext.dependencyManager.resolve(KeyManagementApi) + + const { keyId, publicJwk } = secretKey + ? await kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + privateKey: TypedArrayEncoder.fromString(secretKey), + }).privateJwk, + }) + : await kms.createKey({ + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + }) + const didCreateResult = await dids.create({ method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: secretKey ? TypedArrayEncoder.fromString(secretKey) : undefined }, + options: { keyId }, }) const did = didCreateResult.didState.did as string const didKey = DidKey.fromDid(did) - const kid = `${did}#${didKey.key.fingerprint}` + const kid = `${did}#${didKey.publicJwk.fingerprint}` const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(kid, ['authentication']) if (!verificationMethod) throw new Error('No verification method found') - return { did, kid, verificationMethod, key: didKey.key } + return { did, kid, verificationMethod, publicJwk: PublicJwk.fromPublicJwk(publicJwk) } } -export async function createX509Certificate(agentContext: AgentContext, dns: string, key?: Key) { - const x509 = agentContext.dependencyManager.resolve(X509Api) +export async function createX509Certificate(agentContext: AgentContext, dns: string, key?: PublicJwk) { + const x509 = agentContext.resolve(X509Api) + const kms = agentContext.resolve(KeyManagementApi) + const certificate = await x509.createCertificate({ authorityKey: key ?? - (await agentContext.wallet.createKey({ - keyType: KeyType.Ed25519, - })), + Kms.PublicJwk.fromPublicJwk( + ( + await kms.createKey({ + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + }) + ).publicJwk + ), issuer: { countryName: 'DE', }, diff --git a/packages/core/tests/jsonld.ts b/packages/core/tests/jsonld.ts index 3e71cc934f..18ba635ce7 100644 --- a/packages/core/tests/jsonld.ts +++ b/packages/core/tests/jsonld.ts @@ -1,10 +1,4 @@ import type { AutoAcceptCredential, AutoAcceptProof, ConnectionRecord } from '../../didcomm/src' -import type { DefaultAgentModulesInput } from '../../didcomm/src/util/modules' -import type { EventReplaySubject } from './events' - -import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' -import { askarModule } from '../../askar/tests/helpers' -import { BbsModule } from '../../bbs-signatures/src/BbsModule' import { CredentialEventTypes, CredentialsModule, @@ -15,8 +9,10 @@ import { V2CredentialProtocol, V2ProofProtocol, } from '../../didcomm/src' +import type { DefaultAgentModulesInput } from '../../didcomm/src/util/modules' import { Agent, CacheModule, InMemoryLruCache, W3cCredentialsModule } from '../src' import { customDocumentLoader } from '../src/modules/vc/data-integrity/__tests__/documentLoader' +import type { EventReplaySubject } from './events' import { setupEventReplaySubjects } from './events' import { getAgentOptions, makeConnection } from './helpers' @@ -24,11 +20,13 @@ import { setupSubjectTransports } from './transport' export type JsonLdTestsAgent = Agent & DefaultAgentModulesInput> -export const getJsonLdModules = ({ - autoAcceptCredentials, - autoAcceptProofs, - useBbs = false, -}: { autoAcceptCredentials?: AutoAcceptCredential; autoAcceptProofs?: AutoAcceptProof; useBbs?: boolean } = {}) => +export const getJsonLdModules = ( + _name: string, + { + autoAcceptCredentials, + autoAcceptProofs, + }: { autoAcceptCredentials?: AutoAcceptCredential; autoAcceptProofs?: AutoAcceptProof } = {} +) => ({ credentials: new CredentialsModule({ credentialProtocols: [new V2CredentialProtocol({ credentialFormats: [new JsonLdCredentialFormatService()] })], @@ -44,15 +42,6 @@ export const getJsonLdModules = ({ cache: new CacheModule({ cache: new InMemoryLruCache({ limit: 100 }), }), - // We don't support signing provider in in memory wallet yet, so if BBS is used we need to use Askar - ...(useBbs - ? { - askar: askarModule, - bbs: new BbsModule(), - } - : { - inMemory: new InMemoryWalletModule(), - }), }) as const interface SetupJsonLdTestsReturn { @@ -92,7 +81,6 @@ export async function setupJsonLdTests< autoAcceptCredentials, autoAcceptProofs, createConnections, - useBbs = false, }: { issuerName: string holderName: string @@ -100,14 +88,7 @@ export async function setupJsonLdTests< autoAcceptCredentials?: AutoAcceptCredential autoAcceptProofs?: AutoAcceptProof createConnections?: CreateConnections - useBbs?: boolean }): Promise> { - const modules = getJsonLdModules({ - autoAcceptCredentials, - autoAcceptProofs, - useBbs, - }) - const issuerAgent = new Agent( getAgentOptions( issuerName, @@ -115,7 +96,11 @@ export async function setupJsonLdTests< endpoints: ['rxjs:issuer'], }, {}, - modules + getJsonLdModules(issuerName, { + autoAcceptCredentials, + autoAcceptProofs, + }), + { requireDidcomm: true } ) ) @@ -126,7 +111,11 @@ export async function setupJsonLdTests< endpoints: ['rxjs:holder'], }, {}, - modules + getJsonLdModules(holderName, { + autoAcceptCredentials, + autoAcceptProofs, + }), + { requireDidcomm: true } ) ) @@ -138,7 +127,11 @@ export async function setupJsonLdTests< endpoints: ['rxjs:verifier'], }, {}, - modules + getJsonLdModules(verifierName, { + autoAcceptCredentials, + autoAcceptProofs, + }), + { requireDidcomm: true } ) ) : undefined diff --git a/packages/core/tests/middleware.test.ts b/packages/core/tests/middleware.test.ts index 247702c0bd..b88db98701 100644 --- a/packages/core/tests/middleware.test.ts +++ b/packages/core/tests/middleware.test.ts @@ -14,23 +14,30 @@ import { } from '../../didcomm/src' import { Agent, JsonTransformer } from '../src' -import { - getInMemoryAgentOptions, - makeConnection, - waitForAgentMessageProcessedEvent, - waitForBasicMessage, -} from './helpers' +import { getAgentOptions, makeConnection, waitForAgentMessageProcessedEvent, waitForBasicMessage } from './helpers' const faberAgent = new Agent( - getInMemoryAgentOptions('Faber Message Handler Middleware', { - endpoints: ['rxjs:faber'], - }) + getAgentOptions( + 'Faber Message Handler Middleware', + { + endpoints: ['rxjs:faber'], + }, + undefined, + undefined, + { requireDidcomm: true } + ) ) const aliceAgent = new Agent( - getInMemoryAgentOptions('Alice Message Handler Middleware', { - endpoints: ['rxjs:alice'], - }) + getAgentOptions( + 'Alice Message Handler Middleware', + { + endpoints: ['rxjs:alice'], + }, + undefined, + undefined, + { requireDidcomm: true } + ) ) describe('Message Handler Middleware E2E', () => { @@ -57,9 +64,7 @@ describe('Message Handler Middleware E2E', () => { afterEach(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Correctly calls the fallback message handler if no message handler is defined', async () => { diff --git a/packages/core/tests/migration.test.ts b/packages/core/tests/migration.test.ts index 3b412cec47..ce8cfb4dad 100644 --- a/packages/core/tests/migration.test.ts +++ b/packages/core/tests/migration.test.ts @@ -1,12 +1,10 @@ -import type { VersionString } from '../src/utils/version' - -import { askarModule } from '../../askar/tests/helpers' import { Agent } from '../src/agent/Agent' import { UpdateAssistant } from '../src/storage/migration/UpdateAssistant' +import type { VersionString } from '../src/utils/version' import { getAgentOptions } from './helpers' -const agentOptions = getAgentOptions('Migration', {}, {}, { askar: askarModule }) +const agentOptions = getAgentOptions('Migration') describe('migration', () => { test('manually initiating the update assistant to perform an update', async () => { @@ -22,9 +20,7 @@ describe('migration', () => { } await agent.initialize() - await agent.shutdown() - await agent.wallet.delete() }) test('manually initiating the update, but storing the current framework version outside of the agent storage', async () => { @@ -48,7 +44,6 @@ describe('migration', () => { await agent.initialize() await agent.shutdown() - await agent.wallet.delete() }) test('Automatic update on agent startup', async () => { @@ -56,6 +51,5 @@ describe('migration', () => { await agent.initialize() await agent.shutdown() - await agent.wallet.delete() }) }) diff --git a/packages/core/tests/mocks/MockWallet.ts b/packages/core/tests/mocks/MockWallet.ts deleted file mode 100644 index aadd313b49..0000000000 --- a/packages/core/tests/mocks/MockWallet.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { Wallet } from '../../src' -import type { Key } from '../../src/crypto' -import type { EncryptedMessage, WalletConfig, WalletConfigRekey, WalletExportImportConfig } from '../../src/types' -import type { Buffer } from '../../src/utils/buffer' -import type { - UnpackedMessageContext, - WalletCreateKeyOptions, - WalletSignOptions, - WalletVerifyOptions, -} from '../../src/wallet' - -export class MockWallet implements Wallet { - public isInitialized = true - public isProvisioned = true - - public supportedKeyTypes = [] - - public create(_walletConfig: WalletConfig): Promise { - throw new Error('Method not implemented.') - } - public createAndOpen(_walletConfig: WalletConfig): Promise { - throw new Error('Method not implemented.') - } - public open(_walletConfig: WalletConfig): Promise { - throw new Error('Method not implemented.') - } - public rotateKey(_walletConfig: WalletConfigRekey): Promise { - throw new Error('Method not implemented.') - } - public close(): Promise { - throw new Error('Method not implemented.') - } - public delete(): Promise { - throw new Error('Method not implemented.') - } - public export(_exportConfig: WalletExportImportConfig): Promise { - throw new Error('Method not implemented.') - } - public import(_walletConfig: WalletConfig, _importConfig: WalletExportImportConfig): Promise { - throw new Error('Method not implemented.') - } - public pack( - _payload: Record, - _recipientKeys: string[], - _senderVerkey?: string - ): Promise { - throw new Error('Method not implemented.') - } - public unpack(_encryptedMessage: EncryptedMessage): Promise { - throw new Error('Method not implemented.') - } - public sign(_options: WalletSignOptions): Promise { - throw new Error('Method not implemented.') - } - public verify(_options: WalletVerifyOptions): Promise { - throw new Error('Method not implemented.') - } - - public createKey(_options: WalletCreateKeyOptions): Promise { - throw new Error('Method not implemented.') - } - - public generateNonce(): Promise { - throw new Error('Method not implemented.') - } - - public getRandomValues(_length: number): Uint8Array { - throw new Error('Method not implemented.') - } - - public generateWalletKey(): Promise { - throw new Error('Method not implemented.') - } - - public dispose() { - // Nothing to do here - } -} diff --git a/packages/core/tests/mocks/index.ts b/packages/core/tests/mocks/index.ts deleted file mode 100644 index 3dbf2226a2..0000000000 --- a/packages/core/tests/mocks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './MockWallet' diff --git a/packages/core/tests/multi-protocol-version.test.ts b/packages/core/tests/multi-protocol-version.test.ts index d9e5435232..981c294db7 100644 --- a/packages/core/tests/multi-protocol-version.test.ts +++ b/packages/core/tests/multi-protocol-version.test.ts @@ -12,15 +12,27 @@ import { } from '../../didcomm/src' import { Agent } from '../src/agent/Agent' -import { getInMemoryAgentOptions } from './helpers' +import { getAgentOptions } from './helpers' import { setupSubjectTransports } from './transport' -const aliceAgentOptions = getInMemoryAgentOptions('Multi Protocol Versions - Alice', { - endpoints: ['rxjs:alice'], -}) -const bobAgentOptions = getInMemoryAgentOptions('Multi Protocol Versions - Bob', { - endpoints: ['rxjs:bob'], -}) +const aliceAgentOptions = getAgentOptions( + 'Multi Protocol Versions - Alice', + { + endpoints: ['rxjs:alice'], + }, + undefined, + undefined, + { requireDidcomm: true } +) +const bobAgentOptions = getAgentOptions( + 'Multi Protocol Versions - Bob', + { + endpoints: ['rxjs:bob'], + }, + undefined, + undefined, + { requireDidcomm: true } +) describe('multi version protocols', () => { let aliceAgent: Agent @@ -28,9 +40,7 @@ describe('multi version protocols', () => { afterAll(async () => { await bobAgent.shutdown() - await bobAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('should successfully handle a message with a lower minor version than the currently supported version', async () => { diff --git a/packages/core/tests/oob-mediation-provision.test.ts b/packages/core/tests/oob-mediation-provision.test.ts index 7f5d0f3935..cad9574d84 100644 --- a/packages/core/tests/oob-mediation-provision.test.ts +++ b/packages/core/tests/oob-mediation-provision.test.ts @@ -9,13 +9,19 @@ import { } from '../../didcomm/src/modules/routing' import { Agent } from '../src/agent/Agent' -import { getInMemoryAgentOptions, waitForBasicMessage } from './helpers' +import { getAgentOptions, waitForBasicMessage } from './helpers' import { setupSubjectTransports } from './transport' -const faberAgentOptions = getInMemoryAgentOptions('OOB mediation provision - Faber Agent', { - endpoints: ['rxjs:faber'], -}) -const aliceAgentOptions = getInMemoryAgentOptions( +const faberAgentOptions = getAgentOptions( + 'OOB mediation provision - Faber Agent', + { + endpoints: ['rxjs:faber'], + }, + undefined, + undefined, + { requireDidcomm: true } +) +const aliceAgentOptions = getAgentOptions( 'OOB mediation provision - Alice Recipient Agent', { endpoints: ['rxjs:alice'], @@ -25,15 +31,17 @@ const aliceAgentOptions = getInMemoryAgentOptions( mediationRecipient: new MediationRecipientModule({ mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - } + }, + { requireDidcomm: true } ) -const mediatorAgentOptions = getInMemoryAgentOptions( +const mediatorAgentOptions = getAgentOptions( 'OOB mediation provision - Mediator Agent', { endpoints: ['rxjs:mediator'], }, {}, - { mediator: new MediatorModule({ autoAcceptMediationRequests: true }) } + { mediator: new MediatorModule({ autoAcceptMediationRequests: true }) }, + { requireDidcomm: true } ) describe('out of band with mediation set up with provision method', () => { @@ -45,9 +53,9 @@ describe('out of band with mediation set up with provision method', () => { multiUseInvitation: false, } - let faberAgent: Agent - let aliceAgent: Agent - let mediatorAgent: Agent + let faberAgent: Agent<(typeof faberAgentOptions)['modules']> + let aliceAgent: Agent<(typeof aliceAgentOptions)['modules']> + let mediatorAgent: Agent<(typeof mediatorAgentOptions)['modules']> let mediatorOutOfBandInvitation: OutOfBandInvitation @@ -66,19 +74,16 @@ describe('out of band with mediation set up with provision method', () => { mediatorOutOfBandInvitation = mediationOutOfBandRecord.outOfBandInvitation let { connectionRecord } = await aliceAgent.modules.oob.receiveInvitation(mediatorOutOfBandInvitation) - connectionRecord = await aliceAgent.modules.connections.returnWhenIsConnected(connectionRecord?.id) + // biome-ignore lint/style/noNonNullAssertion: + connectionRecord = await aliceAgent.modules.connections.returnWhenIsConnected(connectionRecord?.id!) // biome-ignore lint/style/noNonNullAssertion: await aliceAgent.modules.mediationRecipient.provision(connectionRecord!) - await aliceAgent.modules.mediationRecipient.initialize() }) afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() await mediatorAgent.shutdown() - await mediatorAgent.wallet.delete() }) test(`make a connection with ${HandshakeProtocol.DidExchange} on OOB invitation encoded in URL`, async () => { @@ -94,7 +99,8 @@ describe('out of band with mediation set up with provision method', () => { let { connectionRecord: aliceFaberConnection } = await aliceAgent.modules.oob.receiveInvitationFromUrl(urlMessage) - aliceFaberConnection = await aliceAgent.modules.connections.returnWhenIsConnected(aliceFaberConnection?.id) + // biome-ignore lint/style/noNonNullAssertion: + aliceFaberConnection = await aliceAgent.modules.connections.returnWhenIsConnected(aliceFaberConnection?.id!) expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) let [faberAliceConnection] = await faberAgent.modules.connections.findAllByOutOfBandId(outOfBandRecord.id) diff --git a/packages/core/tests/oob-mediation.test.ts b/packages/core/tests/oob-mediation.test.ts index 44d07c5fd1..58c7bf6976 100644 --- a/packages/core/tests/oob-mediation.test.ts +++ b/packages/core/tests/oob-mediation.test.ts @@ -19,12 +19,18 @@ import { import { Agent } from '../src/agent/Agent' import { didKeyToVerkey } from '../src/modules/dids/helpers' -import { getInMemoryAgentOptions, waitForBasicMessage } from './helpers' +import { getAgentOptions, waitForBasicMessage } from './helpers' -const faberAgentOptions = getInMemoryAgentOptions('OOB mediation - Faber Agent', { - endpoints: ['rxjs:faber'], -}) -const aliceAgentOptions = getInMemoryAgentOptions( +const faberAgentOptions = getAgentOptions( + 'OOB mediation - Faber Agent', + { + endpoints: ['rxjs:faber'], + }, + undefined, + undefined, + { requireDidcomm: true } +) +const aliceAgentOptions = getAgentOptions( 'OOB mediation - Alice Recipient Agent', { endpoints: ['rxjs:alice'], @@ -34,15 +40,17 @@ const aliceAgentOptions = getInMemoryAgentOptions( mediationRecipient: new MediationRecipientModule({ mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - } + }, + { requireDidcomm: true } ) -const mediatorAgentOptions = getInMemoryAgentOptions( +const mediatorAgentOptions = getAgentOptions( 'OOB mediation - Mediator Agent', { endpoints: ['rxjs:mediator'], }, {}, - { mediator: new MediatorModule({ autoAcceptMediationRequests: true }) } + { mediator: new MediatorModule({ autoAcceptMediationRequests: true }) }, + { requireDidcomm: true } ) describe('out of band with mediation', () => { @@ -126,11 +134,8 @@ describe('out of band with mediation', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() await mediatorAgent.shutdown() - await mediatorAgent.wallet.delete() }) test(`make a connection with ${HandshakeProtocol.DidExchange} on OOB invitation encoded in URL`, async () => { diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts index 928b0b0cce..3e02ff3018 100644 --- a/packages/core/tests/oob.test.ts +++ b/packages/core/tests/oob.test.ts @@ -26,17 +26,16 @@ import { OutOfBandRole } from '../../didcomm/src/modules/oob/domain/OutOfBandRol import { OutOfBandState } from '../../didcomm/src/modules/oob/domain/OutOfBandState' import { OutOfBandInvitation } from '../../didcomm/src/modules/oob/messages' import { Agent } from '../src/agent/Agent' -import { Key } from '../src/crypto' -import { JsonEncoder, JsonTransformer } from '../src/utils' +import { JsonEncoder, JsonTransformer, TypedArrayEncoder } from '../src/utils' import { TestMessage } from './TestMessage' -import { getInMemoryAgentOptions, waitForCredentialRecord } from './helpers' +import { getAgentOptions, waitForCredentialRecord } from './helpers' import testLogger from './logger' -import { CredoError } from '@credo-ts/core' +import { CredoError, Kms } from '@credo-ts/core' const faberAgent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'Faber Agent OOB', { endpoints: ['rxjs:faber'], @@ -44,11 +43,12 @@ const faberAgent = new Agent( {}, getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, - }) + }), + { requireDidcomm: true } ) ) const aliceAgent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'Alice Agent OOB', { endpoints: ['rxjs:alice'], @@ -58,7 +58,8 @@ const aliceAgent = new Agent( }, getAnonCredsIndyModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, - }) + }), + { requireDidcomm: true } ) ) @@ -137,9 +138,7 @@ describe('out of band', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) afterEach(async () => { @@ -375,12 +374,22 @@ describe('out of band', () => { }) test('make a connection based on old connection invitation with multiple endpoints uses first endpoint for invitation', async () => { + const routingKey = Kms.PublicJwk.fromFingerprint( + 'z6MkiP5ghmdLFh1GyGRQQQLVJhJtjQjTpxUY3AnY3h5gu3BE' + ) as Kms.PublicJwk + routingKey.keyId = routingKey.legacyKeyId + + const recipientKey = Kms.PublicJwk.fromFingerprint( + 'z6MkuXrzmDjBoy7r9LA1Czjv9eQXMGr9gt6JBH8zPUMKkCQH' + ) as Kms.PublicJwk + recipientKey.keyId = recipientKey.legacyKeyId + const { invitation } = await faberAgent.modules.oob.createLegacyInvitation({ ...makeConnectionConfig, routing: { endpoints: ['https://endpoint-1.com', 'https://endpoint-2.com'], - routingKeys: [Key.fromFingerprint('z6MkiP5ghmdLFh1GyGRQQQLVJhJtjQjTpxUY3AnY3h5gu3BE')], - recipientKey: Key.fromFingerprint('z6MkuXrzmDjBoy7r9LA1Czjv9eQXMGr9gt6JBH8zPUMKkCQH'), + routingKeys: [routingKey], + recipientKey, }, }) @@ -955,9 +964,9 @@ describe('out of band', () => { const faberCredentialRequest = await faberAgent.modules.credentials.findRequestMessage(faberCredentialRecord.id) expect(JsonTransformer.toJSON(faberCredentialRequest?.service)).toEqual({ - recipientKeys: [routing.recipientKey.publicKeyBase58], + recipientKeys: [TypedArrayEncoder.toBase58(routing.recipientKey.publicKey.publicKey)], serviceEndpoint: routing.endpoints[0], - routingKeys: routing.routingKeys.map((r) => r.publicKeyBase58), + routingKeys: routing.routingKeys.map((r) => TypedArrayEncoder.toBase58(r.publicKey.publicKey)), }) }) @@ -1029,9 +1038,9 @@ describe('out of band', () => { const faberCredentialRequest = await faberAgent.modules.credentials.findRequestMessage(faberCredentialRecord.id) expect(JsonTransformer.toJSON(faberCredentialRequest?.service)).toEqual({ - recipientKeys: [routing.recipientKey.publicKeyBase58], + recipientKeys: [TypedArrayEncoder.toBase58(routing.recipientKey.publicKey.publicKey)], serviceEndpoint: routing.endpoints[0], - routingKeys: routing.routingKeys.map((r) => r.publicKeyBase58), + routingKeys: routing.routingKeys.map((r) => TypedArrayEncoder.toBase58(r.publicKey.publicKey)), }) }) diff --git a/packages/core/tests/proofs-sub-protocol.e2e.test.ts b/packages/core/tests/proofs-sub-protocol.e2e.test.ts index 6b7bfa4e7d..30aabe665a 100644 --- a/packages/core/tests/proofs-sub-protocol.e2e.test.ts +++ b/packages/core/tests/proofs-sub-protocol.e2e.test.ts @@ -58,9 +58,7 @@ describe('Present Proof Subprotocol', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice starts with v1 proof proposal to Faber with parentThreadId', async () => { diff --git a/packages/didcomm/src/DidCommModule.ts b/packages/didcomm/src/DidCommModule.ts index 1ec65e4f5c..4b0eca2794 100644 --- a/packages/didcomm/src/DidCommModule.ts +++ b/packages/didcomm/src/DidCommModule.ts @@ -91,7 +91,6 @@ export class DidCommModule implements Module { } } - // TODO: Shall shutdown and initialize be part of API (so Agent can be stopped/restarted without creating a new instance)? public async shutdown(agentContext: AgentContext) { const messageReceiver = agentContext.dependencyManager.resolve(MessageReceiver) const messageSender = agentContext.dependencyManager.resolve(MessageSender) diff --git a/packages/didcomm/src/EnvelopeService.ts b/packages/didcomm/src/EnvelopeService.ts index 724f6e357e..1b89d98ebc 100644 --- a/packages/didcomm/src/EnvelopeService.ts +++ b/packages/didcomm/src/EnvelopeService.ts @@ -1,24 +1,237 @@ -import type { AgentContext } from '@credo-ts/core' +import { + AgentContext, + CredoError, + InjectionSymbols, + JsonEncoder, + Kms, + RecordNotFoundError, + TypedArrayEncoder, + inject, +} from '@credo-ts/core' import type { AgentMessage } from './AgentMessage' import type { EncryptedMessage, PlaintextMessage } from './types' -import { InjectionSymbols, Key, KeyType, Logger, inject, injectable } from '@credo-ts/core' +import { Logger, injectable } from '@credo-ts/core' import { DidCommModuleConfig } from './DidCommModuleConfig' -import { ForwardMessage } from './modules/routing/messages' +import { getResolvedDidcommServiceWithSigningKeyId } from './modules/connections/services/helpers' +import { OutOfBandRole } from './modules/oob/domain/OutOfBandRole' +import { OutOfBandRepository } from './modules/oob/repository/OutOfBandRepository' +import { OutOfBandRecordMetadataKeys } from './modules/oob/repository/outOfBandRecordMetadataTypes' +import { ForwardMessage } from './modules/routing/messages/ForwardMessage' +import { MediatorRoutingRepository } from './modules/routing/repository/MediatorRoutingRepository' +import { DidCommDocumentService } from './services/DidCommDocumentService' export interface EnvelopeKeys { - recipientKeys: Key[] - routingKeys: Key[] - senderKey: Key | null + recipientKeys: Kms.PublicJwk[] + routingKeys: Kms.PublicJwk[] + senderKey: Kms.PublicJwk | null } @injectable() export class EnvelopeService { private logger: Logger + private didcommDocumentService: DidCommDocumentService - public constructor(@inject(InjectionSymbols.Logger) logger: Logger) { + public constructor(@inject(InjectionSymbols.Logger) logger: Logger, didcommDocumentService: DidCommDocumentService) { this.logger = logger + this.didcommDocumentService = didcommDocumentService + } + + private async encryptDidcommV1Message( + agentContext: AgentContext, + message: PlaintextMessage, + recipientKeys: Kms.PublicJwk[], + senderKey?: Kms.PublicJwk | null + ): Promise { + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) + // Generally we would never generate the content encryption key outside of the KMS + // However how DIDcommV1 is specified to calcualte the aad we need the encrypted content + // encryption key, and thus we can't use the normal combined key agrement + encryption flow + const { bytes: contentEncryptionKey } = kms.randomBytes({ length: 32 }) + + const recipients: Array<{ + encrypted_key: string + header: { + kid: string + + // In case of Authcrypt + sender?: string + iv?: string + } + }> = [] + + for (const recipientKey of recipientKeys) { + let encryptedSender: string | undefined = undefined + + if (senderKey) { + // Encrypt the sender + const { encrypted } = await kms.encrypt({ + key: { + algorithm: 'ECDH-HSALSA20', + // DIDComm v1 uses Ed25519 keys but encryption happens with X25519 keys + externalPublicJwk: recipientKey.jwk.toX25519PublicJwk(), + }, + encryption: { + algorithm: 'XSALSA20-POLY1305', + }, + data: TypedArrayEncoder.fromString(TypedArrayEncoder.toBase58(senderKey.publicKey.publicKey)), + }) + + encryptedSender = TypedArrayEncoder.toBase64URL(encrypted) + } + + // Encrypt the key + const { encrypted, iv } = await kms.encrypt({ + key: { + algorithm: 'ECDH-HSALSA20', + externalPublicJwk: recipientKey.jwk.toX25519PublicJwk(), + + // Sender key only needed for Authcrypt + keyId: senderKey?.keyId, + }, + data: contentEncryptionKey, + encryption: { + algorithm: 'XSALSA20-POLY1305', + }, + }) + + recipients.push({ + encrypted_key: TypedArrayEncoder.toBase64URL(encrypted), + header: { + kid: TypedArrayEncoder.toBase58(recipientKey.publicKey.publicKey), + iv: iv ? TypedArrayEncoder.toBase64URL(iv) : undefined, + sender: encryptedSender, + }, + }) + } + + const protectedString = JsonEncoder.toBase64URL({ + enc: 'xchacha20poly1305_ietf', + typ: 'JWM/1.0', + alg: senderKey ? 'Authcrypt' : 'Anoncrypt', + recipients, + }) + + // Perofrm the actual encryption + const { encrypted, iv, tag } = await kms.encrypt({ + encryption: { + algorithm: 'XC20P', + aad: TypedArrayEncoder.fromString(protectedString), + }, + data: JsonEncoder.toBuffer(message), + key: { + kty: 'oct', + k: TypedArrayEncoder.toBase64URL(contentEncryptionKey), + }, + }) + + if (!iv || !tag) { + throw new CredoError("Expected 'iv' and 'tag' to be defined") + } + + return { + ciphertext: TypedArrayEncoder.toBase64URL(encrypted), + iv: TypedArrayEncoder.toBase64URL(iv), + tag: TypedArrayEncoder.toBase64URL(tag), + protected: protectedString, + } satisfies EncryptedMessage + } + + private async decryptDidcommV1Message(agentContext: AgentContext, encryptedMessage: EncryptedMessage) { + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) + const protectedJson = JsonEncoder.fromBase64(encryptedMessage.protected) + + const alg = protectedJson.alg as 'Anoncrypt' | 'Authcrypt' + if (alg !== 'Anoncrypt' && alg !== 'Authcrypt') { + throw new CredoError(`Unsupported pack algorithm: ${alg}`) + } + + if (protectedJson.enc !== 'xchacha20poly1305_ietf') { + throw new CredoError(`Unsupported enc algorithm: ${protectedJson.enc}`) + } + + let recipientKey: Kms.PublicJwk | null = null + let recipient: { + header: { + kid: string + iv?: string + sender?: string + } + encrypted_key: string + } | null = null + + for (const _recipient of protectedJson.recipients) { + recipientKey = await this.extractOurRecipientKeyWithKeyId(agentContext, _recipient) + + if (recipientKey) { + recipient = _recipient + } + } + + if (!recipientKey || !recipient) { + throw new CredoError('No corresponding recipient key found') + } + + if (alg === 'Authcrypt' && (!recipient.header.sender || !recipient.header.iv)) { + throw new CredoError('Sender and iv header values are required for Authcrypt') + } + + let senderPublicJwk: Kms.PublicJwk | undefined = undefined + if (recipient.header.sender) { + const { data } = await kms.decrypt({ + key: { + algorithm: 'ECDH-HSALSA20', + keyId: recipientKey.keyId, + }, + decryption: { + algorithm: 'XSALSA20-POLY1305', + }, + encrypted: TypedArrayEncoder.fromBase64(recipient.header.sender), + }) + + senderPublicJwk = Kms.PublicJwk.fromPublicKey({ + crv: 'Ed25519', + kty: 'OKP', + publicKey: TypedArrayEncoder.fromBase58(TypedArrayEncoder.toUtf8String(data)), + }) + } + + // Perofrm the actual decryption + const { data: contentEncryptionKey } = await kms.decrypt({ + decryption: { + algorithm: 'XSALSA20-POLY1305', + iv: recipient.header.iv ? TypedArrayEncoder.fromBase64(recipient.header.iv) : undefined, + }, + encrypted: TypedArrayEncoder.fromBase64(recipient.encrypted_key), + key: { + algorithm: 'ECDH-HSALSA20', + keyId: recipientKey.keyId, + + // Optionally we have a sender + externalPublicJwk: senderPublicJwk?.jwk.toX25519PublicJwk(), + }, + }) + + const { data: message } = await kms.decrypt({ + decryption: { + algorithm: 'XC20P', + iv: TypedArrayEncoder.fromBase64(encryptedMessage.iv), + tag: TypedArrayEncoder.fromBase64(encryptedMessage.tag), + aad: TypedArrayEncoder.fromString(encryptedMessage.protected), + }, + key: { + kty: 'oct', + k: TypedArrayEncoder.toBase64URL(contentEncryptionKey), + }, + encrypted: TypedArrayEncoder.fromBase64(encryptedMessage.ciphertext), + }) + + return { + plaintextMessage: JsonEncoder.fromBuffer(message), + senderKey: senderPublicJwk, + recipientKey, + } } public async packMessage( @@ -28,26 +241,24 @@ export class EnvelopeService { ): Promise { const didcommConfig = agentContext.dependencyManager.resolve(DidCommModuleConfig) - const { recipientKeys, routingKeys, senderKey } = keys - let recipientKeysBase58 = recipientKeys.map((key) => key.publicKeyBase58) - const routingKeysBase58 = routingKeys.map((key) => key.publicKeyBase58) - const senderKeyBase58 = senderKey?.publicKeyBase58 + const { routingKeys, senderKey } = keys + let recipientKeys = keys.recipientKeys // pass whether we want to use legacy did sov prefix const message = payload.toJSON({ useDidSovPrefixWhereAllowed: didcommConfig.useDidSovPrefixWhereAllowed }) this.logger.debug(`Pack outbound message ${message['@type']}`) - let encryptedMessage = await agentContext.wallet.pack(message, recipientKeysBase58, senderKeyBase58 ?? undefined) + let encryptedMessage = await this.encryptDidcommV1Message(agentContext, message, recipientKeys, senderKey) // If the message has routing keys (mediator) pack for each mediator - for (const routingKeyBase58 of routingKeysBase58) { + for (const routingKey of routingKeys) { const forwardMessage = new ForwardMessage({ // Forward to first recipient key - to: recipientKeysBase58[0], + to: TypedArrayEncoder.toBase58(recipientKeys[0].publicKey.publicKey), message: encryptedMessage, }) - recipientKeysBase58 = [routingKeyBase58] + recipientKeys = [routingKey] this.logger.debug('Forward message created', forwardMessage) const forwardJson = forwardMessage.toJSON({ @@ -55,7 +266,7 @@ export class EnvelopeService { }) // Forward messages are anon packed - encryptedMessage = await agentContext.wallet.pack(forwardJson, [routingKeyBase58], undefined) + encryptedMessage = await this.encryptDidcommV1Message(agentContext, forwardJson, [routingKey]) } return encryptedMessage @@ -65,18 +276,153 @@ export class EnvelopeService { agentContext: AgentContext, encryptedMessage: EncryptedMessage ): Promise { - const decryptedMessage = await agentContext.wallet.unpack(encryptedMessage) - const { recipientKey, senderKey, plaintextMessage } = decryptedMessage - return { - recipientKey: recipientKey ? Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519) : undefined, - senderKey: senderKey ? Key.fromPublicKeyBase58(senderKey, KeyType.Ed25519) : undefined, - plaintextMessage, + const decryptedMessage = await this.decryptDidcommV1Message(agentContext, encryptedMessage) + return decryptedMessage + } + + private async extractOurRecipientKeyWithKeyId( + agentContext: AgentContext, + recipient: { + header: { + kid: string + } + } + ): Promise | null> { + const kms = agentContext.resolve(Kms.KeyManagementApi) + + const publicKey = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58(recipient.header.kid), + }) + + // We need to find the associated did based on the recipient key + // so we can extract the kms key id from the did record. + try { + const { didDocument, didRecord } = + await this.didcommDocumentService.resolveCreatedDidRecordWithDocumentByRecipientKey(agentContext, publicKey) + + const verificationMethod = didDocument.findVerificationMethodByPublicKey(publicKey) + const kmsKeyId = didRecord.keys?.find(({ didDocumentRelativeKeyId }) => + verificationMethod.id.endsWith(didDocumentRelativeKeyId) + )?.kmsKeyId + + agentContext.config.logger.debug( + `Found did '${didRecord.did}' for recipient key '${publicKey.fingerprint}' for incoming didcomm message` + ) + + publicKey.keyId = kmsKeyId ?? publicKey.legacyKeyId + return publicKey + } catch (error) { + // If there is no did record yet, we first look at the mediator routing record + const mediatorRoutingRepository = agentContext.dependencyManager.resolve(MediatorRoutingRepository) + if (error instanceof RecordNotFoundError) { + const mediatorRoutingRecord = await mediatorRoutingRepository.findSingleByQuery(agentContext, { + routingKeyFingerprints: [publicKey.fingerprint], + }) + + if (mediatorRoutingRecord) { + agentContext.config.logger.debug( + `Found mediator routing record with id '${mediatorRoutingRecord.id}' for recipient key '${publicKey.fingerprint}' for incoming didcomm message` + ) + + const routingKey = mediatorRoutingRecord.routingKeysWithKeyId.find((routingKey) => + publicKey.equals(routingKey) + ) + + // This should not happen as we only get here if the tag matches + if (!routingKey) { + throw new CredoError( + `Expected to find key with fingerprint '${publicKey.fingerprint}' in routing keys of mediator routing record '${mediatorRoutingRecord.id}'` + ) + } + + if (routingKey) { + return routingKey + } + } + + // If there is no mediator routing record, we look at the out of band record + const outOfBandRepository = agentContext.dependencyManager.resolve(OutOfBandRepository) + const outOfBandRecord = await outOfBandRepository.findSingleByQuery(agentContext, { + $or: [ + // In case we are the creator of the out of band invitation we can query based on + // out of band invitation recipient key fingerprint + { + role: OutOfBandRole.Sender, + recipientKeyFingerprints: [publicKey.fingerprint], + }, + // In case we are the receiver of the out of band invitation we need to query + // for the recipient routing fingerprint + { + role: OutOfBandRole.Receiver, + recipientRoutingKeyFingerprint: publicKey.fingerprint, + }, + ], + }) + + if (outOfBandRecord?.role === OutOfBandRole.Sender) { + agentContext.config.logger.debug( + `Found out of band record with id '${outOfBandRecord.id}' and role '${outOfBandRecord.role}' for recipient key '${publicKey.fingerprint}' for incoming didcomm message` + ) + + for (const service of outOfBandRecord.outOfBandInvitation.getInlineServices()) { + const resolvedService = getResolvedDidcommServiceWithSigningKeyId( + service, + outOfBandRecord.invitationInlineServiceKeys + ) + const _recipientKey = resolvedService.recipientKeys.find((recipientKey) => recipientKey.equals(publicKey)) + + if (_recipientKey) { + return _recipientKey + } + } + } else if (outOfBandRecord?.role === OutOfBandRole.Receiver) { + agentContext.config.logger.debug( + `Found out of band record with id '${outOfBandRecord.id}' and role '${outOfBandRecord.role}' for recipient key '${publicKey.fingerprint}' for incoming didcomm message` + ) + + // If there is still no key we need to look at the metadata + const recipieintRouting = outOfBandRecord.metadata.get(OutOfBandRecordMetadataKeys.RecipientRouting) + if (recipieintRouting?.recipientKeyFingerprint === publicKey.fingerprint) { + publicKey.keyId = recipieintRouting.recipientKeyId ?? publicKey.legacyKeyId + return publicKey + } + } + + // If there is no did found, no out of band record found, and not mediator routing record + // this is either: + // - a connectionless oob exchange initiated before we added key ids. + // - a message for a mediator, where the mediator routing record is created before we added key ids + // + // We will check if the public key exists based on the base58 encoded public key. We can remove this flow once we create a migration + // that optimizes this flow. + const kmsJwkPublic = await kms + .getPublicKey({ + keyId: publicKey.legacyKeyId, + }) + .catch((error) => { + if (error instanceof Kms.KeyManagementKeyNotFoundError) return null + throw error + }) + if (kmsJwkPublic) { + agentContext.config.logger.debug( + `Found public key with legacy key id '${publicKey.legacyKeyId}' for recipient key '${publicKey.fingerprint}' for incoming didcomm message` + ) + + publicKey.keyId = publicKey.legacyKeyId + return publicKey + } + } } + + // no match found + return null } } export interface DecryptedMessageContext { plaintextMessage: PlaintextMessage - senderKey?: Key - recipientKey?: Key + senderKey?: Kms.PublicJwk + recipientKey: Kms.PublicJwk } diff --git a/packages/didcomm/src/MessageSender.ts b/packages/didcomm/src/MessageSender.ts index c694926d8a..3041490522 100644 --- a/packages/didcomm/src/MessageSender.ts +++ b/packages/didcomm/src/MessageSender.ts @@ -10,16 +10,16 @@ import type { EncryptedMessage, OutboundPackage } from './types' import { AgentContext, CredoError, - DidDocument, DidKey, - DidResolverService, + DidsApi, EventEmitter, InjectionSymbols, + Kms, Logger, MessageValidator, ResolvedDidCommService, - didKeyToInstanceOfKey, - getKeyFromVerificationMethod, + didKeyToEd25519PublicJwk, + getPublicJwkFromVerificationMethod, inject, injectable, utils, @@ -47,7 +47,6 @@ export class MessageSender { private transportService: TransportService private messagePickupRepository: MessagePickupRepository private logger: Logger - private didResolverService: DidResolverService private didCommDocumentService: DidCommDocumentService private eventEmitter: EventEmitter private _outboundTransports: OutboundTransport[] = [] @@ -57,7 +56,6 @@ export class MessageSender { transportService: TransportService, @inject(InjectionSymbols.MessagePickupRepository) messagePickupRepository: MessagePickupRepository, @inject(InjectionSymbols.Logger) logger: Logger, - didResolverService: DidResolverService, didCommDocumentService: DidCommDocumentService, eventEmitter: EventEmitter ) { @@ -65,7 +63,6 @@ export class MessageSender { this.transportService = transportService this.messagePickupRepository = messagePickupRepository this.logger = logger - this.didResolverService = didResolverService this.didCommDocumentService = didCommDocumentService this.eventEmitter = eventEmitter this._outboundTransports = [] @@ -272,33 +269,49 @@ export class MessageSender { ) } - let ourDidDocument: DidDocument - try { - ourDidDocument = await this.didResolverService.resolveDidDocument(agentContext, connection.did) - } catch (error) { - this.logger.error(`Unable to resolve DID Document for '${connection.did}`) + const dids = agentContext.resolve(DidsApi) + const { didDocument, didRecord } = await dids.resolveCreatedDidRecordWithDocument(connection.did).catch((error) => { + this.logger.error(`Unable to send message using connection '${connection.id}', unable to resolve did`, { + error, + }) this.emitMessageSentEvent(outboundMessageContext, OutboundMessageSendStatus.Undeliverable) - throw new MessageSendingError(`Unable to resolve DID Document for '${connection.did}`, { - outboundMessageContext, - cause: error, + throw new MessageSendingError( + `Unable to send message using connection '${connection.id}'. Unble to resolve did`, + { outboundMessageContext, cause: error } + ) + }) + + const authentication = didDocument.authentication + ?.map((a) => { + const verificationMethod = typeof a === 'string' ? didDocument.dereferenceVerificationMethod(a) : a + const publicJwk = getPublicJwkFromVerificationMethod(verificationMethod) + const kmsKeyId = didRecord.keys?.find((key) => + verificationMethod.id.endsWith(key.didDocumentRelativeKeyId) + )?.kmsKeyId + + // Set stored key id, or fallback to legacy key id + publicJwk.keyId = kmsKeyId ?? publicJwk.legacyKeyId + + return { verificationMethod, publicJwk, kmsKeyId } }) + .filter((v): v is typeof v & { publicJwk: Kms.PublicJwk } => + v.publicJwk.is(Kms.Ed25519PublicJwk) + ) + + // We take the first one with a kms key id. Otherwise we pick the first + const senderVerificationMethod = authentication?.find((a) => a.kmsKeyId !== undefined) ?? authentication?.[0] + if (!senderVerificationMethod) { + throw new MessageSendingError( + `Unable to determine sender key for did ${didRecord.did}, no available Ed25519 keys`, + { + outboundMessageContext, + } + ) } - const ourAuthenticationKeys = getAuthenticationKeys(ourDidDocument) - - // TODO We're selecting just the first authentication key. Is it ok? - // We can probably learn something from the didcomm-rust implementation, which looks at crypto compatibility to make sure the - // other party can decrypt the message. https://github.com/sicpa-dlab/didcomm-rust/blob/9a24b3b60f07a11822666dda46e5616a138af056/src/message/pack_encrypted/mod.rs#L33-L44 - // This will become more relevant when we support different encrypt envelopes. One thing to take into account though is that currently we only store the recipientKeys - // as defined in the didcomm services, while it could be for example that the first authentication key is not defined in the recipientKeys, in which case we wouldn't - // even be interoperable between two Credo agents. So we should either pick the first key that is defined in the recipientKeys, or we should make sure to store all - // keys defined in the did document as tags so we can retrieve it, even if it's not defined in the recipientKeys. This, again, will become simpler once we use didcomm v2 - // as the `from` field in a received message will identity the did used so we don't have to store all keys in tags to be able to find the connections associated with - // an incoming message. - const [firstOurAuthenticationKey] = ourAuthenticationKeys // If the returnRoute is already set we won't override it. This allows to set the returnRoute manually if this is desired. const shouldAddReturnRoute = - message.transport?.returnRoute === undefined && !this.transportService.hasInboundEndpoint(ourDidDocument) + message.transport?.returnRoute === undefined && !this.transportService.hasInboundEndpoint(didDocument) // Loop trough all available services and try to send the message for await (const service of services) { @@ -309,7 +322,7 @@ export class MessageSender { agentContext, serviceParams: { service, - senderKey: firstOurAuthenticationKey, + senderKey: senderVerificationMethod.publicJwk, returnRoute: shouldAddReturnRoute, }, connection, @@ -337,7 +350,7 @@ export class MessageSender { const keys = { recipientKeys: queueService.recipientKeys, routingKeys: queueService.routingKeys, - senderKey: firstOurAuthenticationKey, + senderKey: senderVerificationMethod.publicJwk, } const encryptedMessage = await this.envelopeService.packMessage(agentContext, message, keys) @@ -507,8 +520,8 @@ export class MessageSender { // Out of band inline service contains keys encoded as did:key references didCommServices.push({ id: service.id, - recipientKeys: service.recipientKeys.map(didKeyToInstanceOfKey), - routingKeys: service.routingKeys?.map(didKeyToInstanceOfKey) || [], + recipientKeys: service.recipientKeys.map(didKeyToEd25519PublicJwk), + routingKeys: service.routingKeys?.map(didKeyToEd25519PublicJwk) || [], serviceEndpoint: service.serviceEndpoint, }) } @@ -559,14 +572,3 @@ export class MessageSender { export function isDidCommTransportQueue(serviceEndpoint: string): serviceEndpoint is typeof DID_COMM_TRANSPORT_QUEUE { return serviceEndpoint === DID_COMM_TRANSPORT_QUEUE } - -function getAuthenticationKeys(didDocument: DidDocument) { - return ( - didDocument.authentication?.map((authentication) => { - const verificationMethod = - typeof authentication === 'string' ? didDocument.dereferenceVerificationMethod(authentication) : authentication - const key = getKeyFromVerificationMethod(verificationMethod) - return key - }) ?? [] - ) -} diff --git a/packages/didcomm/src/__tests__/MessageSender.test.ts b/packages/didcomm/src/__tests__/MessageSender.test.ts index e1e864bbce..bffb170239 100644 --- a/packages/didcomm/src/__tests__/MessageSender.test.ts +++ b/packages/didcomm/src/__tests__/MessageSender.test.ts @@ -9,11 +9,10 @@ import type { EncryptedMessage } from '../types' import { Subject } from 'rxjs' import { EventEmitter } from '../../../core/src/agent/EventEmitter' -import { Key, KeyType } from '../../../core/src/crypto' -import { DidDocument, VerificationMethod } from '../../../core/src/modules/dids' +import { DidDocument, DidDocumentRole, DidRecord, VerificationMethod } from '../../../core/src/modules/dids' +import { DidsApi } from '../../../core/src/modules/dids/DidsApi' import { DidCommV1Service } from '../../../core/src/modules/dids/domain/service/DidCommV1Service' -import { verkeyToInstanceOfKey } from '../../../core/src/modules/dids/helpers' -import { DidResolverService } from '../../../core/src/modules/dids/services/DidResolverService' +import { verkeyToPublicJwk } from '../../../core/src/modules/dids/helpers' import { TestMessage } from '../../../core/tests/TestMessage' import { agentDependencies, @@ -32,17 +31,18 @@ import { OutboundMessageContext, OutboundMessageSendStatus } from '../models' import { InMemoryMessagePickupRepository } from '../modules/message-pickup/storage' import { DidCommDocumentService } from '../services/DidCommDocumentService' +import { Kms, TypedArrayEncoder } from '@credo-ts/core' import { DummyTransportSession } from './stubs' jest.mock('../TransportService') jest.mock('../EnvelopeService') -jest.mock('../../../core/src/modules/dids/services/DidResolverService') +jest.mock('../../../core/src/modules/dids/DidsApi') jest.mock('../services/DidCommDocumentService') const logger = testLogger const TransportServiceMock = TransportService as jest.MockedClass -const DidResolverServiceMock = DidResolverService as jest.Mock +const DidsApiMock = DidsApi as jest.Mock const DidCommDocumentServiceMock = DidCommDocumentService as jest.Mock class DummyHttpOutboundTransport implements OutboundTransport { @@ -90,17 +90,26 @@ describe('MessageSender', () => { const enveloperService = new EnvelopeService() const envelopeServicePackMessageMock = mockFunction(enveloperService.packMessage) - const didResolverService = new DidResolverServiceMock() + const didsApi = new DidsApiMock() const didCommDocumentService = new DidCommDocumentServiceMock() const eventEmitter = new EventEmitter(agentDependencies, new Subject()) - const didResolverServiceResolveMock = mockFunction(didResolverService.resolveDidDocument) + const resolveCreatedDidRecordWithDocumentMock = mockFunction(didsApi.resolveCreatedDidRecordWithDocument) const didResolverServiceResolveDidServicesMock = mockFunction(didCommDocumentService.resolveServicesFromDid) const inboundMessage = new TestMessage() inboundMessage.setReturnRouting(ReturnRouteTypes.all) - const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) - const senderKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) + const recipientKey = Kms.PublicJwk.fromPublicKey({ + crv: 'Ed25519', + kty: 'OKP', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), + }) + const senderKey = Kms.PublicJwk.fromPublicKey({ + crv: 'Ed25519', + kty: 'OKP', + publicKey: TypedArrayEncoder.fromBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ'), + }) + const session = new DummyTransportSession('session-123') session.keys = { recipientKeys: [recipientKey], @@ -136,13 +145,15 @@ describe('MessageSender', () => { let connection: ConnectionRecord let outboundMessageContext: OutboundMessageContext const agentConfig = getAgentConfig('MessageSender') - const agentContext = getAgentContext() + const agentContext = getAgentContext({ + registerInstances: [[DidsApi, didsApi]], + }) const eventListenerMock = jest.fn() describe('sendMessage', () => { beforeEach(() => { TransportServiceMock.mockClear() - DidResolverServiceMock.mockClear() + DidsApiMock.mockClear() eventEmitter.on(AgentEventTypes.AgentMessageSent, eventListenerMock) @@ -153,7 +164,6 @@ describe('MessageSender', () => { transportService, messagePickupRepository, logger, - didResolverService, didCommDocumentService, eventEmitter ) @@ -171,7 +181,13 @@ describe('MessageSender', () => { const didDocumentInstance = getMockDidDocument({ service: [firstDidCommService, secondDidCommService], }) - didResolverServiceResolveMock.mockResolvedValue(didDocumentInstance) + resolveCreatedDidRecordWithDocumentMock.mockResolvedValue({ + didDocument: didDocumentInstance, + didRecord: new DidRecord({ + did: '', + role: DidDocumentRole.Created, + }), + }) didResolverServiceResolveDidServicesMock.mockResolvedValue([ getMockResolvedDidService(firstDidCommService), getMockResolvedDidService(secondDidCommService), @@ -203,7 +219,13 @@ describe('MessageSender', () => { test('throw error when there is no service or queue', async () => { messageSender.registerOutboundTransport(outboundTransport) - didResolverServiceResolveMock.mockResolvedValue(getMockDidDocument({ service: [] })) + resolveCreatedDidRecordWithDocumentMock.mockResolvedValue({ + didDocument: getMockDidDocument({ service: [] }), + didRecord: new DidRecord({ + did: '', + role: DidDocumentRole.Created, + }), + }) didResolverServiceResolveDidServicesMock.mockResolvedValue([]) await expect(messageSender.sendMessage(outboundMessageContext)).rejects.toThrow( @@ -282,12 +304,12 @@ describe('MessageSender', () => { test("throws an error if connection.theirDid starts with 'did:' but the resolver can't resolve the did document", async () => { messageSender.registerOutboundTransport(outboundTransport) - didResolverServiceResolveMock.mockRejectedValue( + resolveCreatedDidRecordWithDocumentMock.mockRejectedValue( new Error(`Unable to resolve did document for did '${connection.theirDid}': notFound`) ) await expect(messageSender.sendMessage(outboundMessageContext)).rejects.toThrowError( - `Unable to resolve DID Document for '${connection.did}` + `Unable to send message using connection 'test-123'. Unble to resolve did` ) expect(eventListenerMock).toHaveBeenCalledWith({ @@ -399,13 +421,13 @@ describe('MessageSender', () => { }) //@ts-ignore - expect(sendMessage.serviceParams.senderKey.publicKeyBase58).toEqual( - 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d' + expect(sendMessage.serviceParams.senderKey.fingerprint).toEqual( + 'z6MktFXxTu8tHkoE1Jtqj4ApYEg1c44qmU1p7kq7QZXBtJv1' ) //@ts-ignore - expect(sendMessage.serviceParams.service.recipientKeys.map((key) => key.publicKeyBase58)).toEqual([ - 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d', + expect(sendMessage.serviceParams.service.recipientKeys.map((key) => key.fingerprint)).toEqual([ + 'z6MktFXxTu8tHkoE1Jtqj4ApYEg1c44qmU1p7kq7QZXBtJv1', ]) expect(sendToServiceSpy).toHaveBeenCalledTimes(1) @@ -452,12 +474,12 @@ describe('MessageSender', () => { }) //@ts-ignore - expect(sendMessage.serviceParams.senderKey.publicKeyBase58).toEqual( - 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d' + expect(sendMessage.serviceParams.senderKey.fingerprint).toEqual( + 'z6MktFXxTu8tHkoE1Jtqj4ApYEg1c44qmU1p7kq7QZXBtJv1' ) //@ts-ignore - expect(sendMessage.serviceParams.service.recipientKeys.map((key) => key.publicKeyBase58)).toEqual([ - 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d', + expect(sendMessage.serviceParams.service.recipientKeys.map((key) => key.fingerprint)).toEqual([ + 'z6MktFXxTu8tHkoE1Jtqj4ApYEg1c44qmU1p7kq7QZXBtJv1', ]) expect(sendToServiceSpy).toHaveBeenCalledTimes(2) @@ -486,11 +508,21 @@ describe('MessageSender', () => { describe('sendMessageToService', () => { const service: ResolvedDidCommService = { id: 'out-of-band', - recipientKeys: [Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL')], + recipientKeys: [ + Kms.PublicJwk.fromPublicKey({ + crv: 'Ed25519', + kty: 'OKP', + publicKey: TypedArrayEncoder.fromBase58('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL'), + }), + ], routingKeys: [], serviceEndpoint: 'https://example.com', } - const senderKey = Key.fromFingerprint('z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th') + const senderKey = Kms.PublicJwk.fromPublicKey({ + crv: 'Ed25519', + kty: 'OKP', + publicKey: TypedArrayEncoder.fromBase58('z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'), + }) beforeEach(() => { outboundTransport = new DummyHttpOutboundTransport() @@ -499,7 +531,6 @@ describe('MessageSender', () => { transportService, new InMemoryMessagePickupRepository(agentConfig.logger), logger, - didResolverService, didCommDocumentService, eventEmitter ) @@ -642,7 +673,6 @@ describe('MessageSender', () => { transportService, messagePickupRepository, logger, - didResolverService, didCommDocumentService, eventEmitter ) @@ -697,7 +727,7 @@ function getMockResolvedDidService(service: DidCommV1Service | IndyAgentService) return { id: service.id, serviceEndpoint: service.serviceEndpoint, - recipientKeys: [verkeyToInstanceOfKey('EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d')], + recipientKeys: [verkeyToPublicJwk('EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d')], routingKeys: [], } } diff --git a/packages/didcomm/src/decorators/service/ServiceDecorator.ts b/packages/didcomm/src/decorators/service/ServiceDecorator.ts index 8d28990545..0e6869e9ed 100644 --- a/packages/didcomm/src/decorators/service/ServiceDecorator.ts +++ b/packages/didcomm/src/decorators/service/ServiceDecorator.ts @@ -1,6 +1,6 @@ import type { ResolvedDidCommService } from '@credo-ts/core' -import { utils, verkeyToInstanceOfKey } from '@credo-ts/core' +import { TypedArrayEncoder, utils, verkeyToPublicJwk } from '@credo-ts/core' import { IsArray, IsOptional, IsString } from 'class-validator' export interface ServiceDecoratorOptions { @@ -39,16 +39,16 @@ export class ServiceDecorator { public get resolvedDidCommService(): ResolvedDidCommService { return { id: utils.uuid(), - recipientKeys: this.recipientKeys.map(verkeyToInstanceOfKey), - routingKeys: this.routingKeys?.map(verkeyToInstanceOfKey) ?? [], + recipientKeys: this.recipientKeys.map(verkeyToPublicJwk), + routingKeys: this.routingKeys?.map(verkeyToPublicJwk) ?? [], serviceEndpoint: this.serviceEndpoint, } } public static fromResolvedDidCommService(service: ResolvedDidCommService): ServiceDecorator { return new ServiceDecorator({ - recipientKeys: service.recipientKeys.map((k) => k.publicKeyBase58), - routingKeys: service.routingKeys.map((k) => k.publicKeyBase58), + recipientKeys: service.recipientKeys.map((k) => TypedArrayEncoder.toBase58(k.publicKey.publicKey)), + routingKeys: service.routingKeys.map((k) => TypedArrayEncoder.toBase58(k.publicKey.publicKey)), serviceEndpoint: service.serviceEndpoint, }) } diff --git a/packages/didcomm/src/decorators/signature/SignatureDecoratorUtils.test.ts b/packages/didcomm/src/decorators/signature/SignatureDecoratorUtils.test.ts index 9d2060342f..14b86342fb 100644 --- a/packages/didcomm/src/decorators/signature/SignatureDecoratorUtils.test.ts +++ b/packages/didcomm/src/decorators/signature/SignatureDecoratorUtils.test.ts @@ -1,8 +1,6 @@ -import type { Wallet } from '../../../../core' - -import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' -import { KeyType, TypedArrayEncoder } from '../../../../core' -import { getAgentConfig } from '../../../../core/tests/helpers' +import { transformPrivateKeyToPrivateJwk } from '../../../../askar/src' +import { Kms, TypedArrayEncoder } from '../../../../core' +import { getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' import { SignatureDecorator } from './SignatureDecorator' import { signData, unpackAndVerifySignatureDecorator } from './SignatureDecoratorUtils' @@ -14,6 +12,11 @@ jest.mock('../../../../core/src/utils/timestamp', () => { } }) +const agentContext = getAgentContext({ + agentConfig: getAgentConfig('SignatureDecoratorUtilsTest'), +}) +const kms = agentContext.resolve(Kms.KeyManagementApi) + describe('Decorators | Signature | SignatureDecoratorUtils', () => { const data = { did: 'did', @@ -40,30 +43,24 @@ describe('Decorators | Signature | SignatureDecoratorUtils', () => { signer: 'GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa', }) - let wallet: Wallet - - beforeAll(async () => { - const config = getAgentConfig('SignatureDecoratorUtilsTest') - wallet = new InMemoryWallet() - // biome-ignore lint/style/noNonNullAssertion: - await wallet.createAndOpen(config.walletConfig!) - }) - - afterAll(async () => { - await wallet.delete() - }) - test('signData signs json object and returns SignatureDecorator', async () => { - const privateKey = TypedArrayEncoder.fromString('00000000000000000000000000000My1') - const key = await wallet.createKey({ privateKey, keyType: KeyType.Ed25519 }) + const privateJwk = transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('00000000000000000000000000000My1'), + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }).privateJwk + const createdKey = await kms.importKey({ privateJwk }) + const publicJwk = Kms.PublicJwk.fromPublicJwk(createdKey.publicJwk) - const result = await signData(data, wallet, key.publicKeyBase58) + const result = await signData(agentContext, data, publicJwk) expect(result).toEqual(signedData) }) test('unpackAndVerifySignatureDecorator unpacks signature decorator and verifies signature', async () => { - const result = await unpackAndVerifySignatureDecorator(signedData, wallet) + const result = await unpackAndVerifySignatureDecorator(agentContext, signedData) expect(result).toEqual(data) }) @@ -77,7 +74,7 @@ describe('Decorators | Signature | SignatureDecoratorUtils', () => { expect.assertions(1) try { - await unpackAndVerifySignatureDecorator(wronglySignedData, wallet) + await unpackAndVerifySignatureDecorator(agentContext, wronglySignedData) } catch (error) { expect(error.message).toEqual('Signature is not valid') } diff --git a/packages/didcomm/src/decorators/signature/SignatureDecoratorUtils.ts b/packages/didcomm/src/decorators/signature/SignatureDecoratorUtils.ts index 39e5f25072..906cf29ebf 100644 --- a/packages/didcomm/src/decorators/signature/SignatureDecoratorUtils.ts +++ b/packages/didcomm/src/decorators/signature/SignatureDecoratorUtils.ts @@ -1,6 +1,6 @@ -import type { Wallet } from '@credo-ts/core' +import type { AgentContext } from '@credo-ts/core' -import { Buffer, CredoError, JsonEncoder, Key, KeyType, TypedArrayEncoder, utils } from '@credo-ts/core' +import { Buffer, CredoError, JsonEncoder, Kms, TypedArrayEncoder, utils } from '@credo-ts/core' import { SignatureDecorator } from './SignatureDecorator' @@ -13,24 +13,28 @@ import { SignatureDecorator } from './SignatureDecorator' * @return Resulting data */ export async function unpackAndVerifySignatureDecorator( - decorator: SignatureDecorator, - wallet: Wallet + agentContext: AgentContext, + decorator: SignatureDecorator ): Promise> { const signerVerkey = decorator.signer - const key = Key.fromPublicKeyBase58(signerVerkey, KeyType.Ed25519) + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) + + const publicJwk = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58(signerVerkey), + }) // first 8 bytes are for 64 bit integer from unix epoch const signedData = TypedArrayEncoder.fromBase64(decorator.signatureData) const signature = TypedArrayEncoder.fromBase64(decorator.signature) - // const isValid = await wallet.verify(signerVerkey, signedData, signature) - const isValid = await wallet.verify({ signature, data: signedData, key }) + const result = await kms.verify({ algorithm: 'EdDSA', data: signedData, key: publicJwk.toJson(), signature }) - if (!isValid) { + if (!result.verified) { throw new CredoError('Signature is not valid') } - // TODO: return Connection instance instead of raw json return JsonEncoder.fromBuffer(signedData.slice(8)) } @@ -39,21 +43,25 @@ export async function unpackAndVerifySignatureDecorator( * * @param data the data to sign * @param wallet the wallet containing a key to use for signing - * @param signerKey signers verkey + * @param signerKey signer key * * @returns Resulting signature decorator. */ -export async function signData(data: unknown, wallet: Wallet, signerKey: string): Promise { +export async function signData( + agentContext: AgentContext, + data: unknown, + signerKey: Kms.PublicJwk +): Promise { + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) const dataBuffer = Buffer.concat([utils.timestamp(), JsonEncoder.toBuffer(data)]) - const key = Key.fromPublicKeyBase58(signerKey, KeyType.Ed25519) - const signatureBuffer = await wallet.sign({ key, data: dataBuffer }) + const result = await kms.sign({ data: dataBuffer, algorithm: 'EdDSA', keyId: signerKey.keyId }) const signatureDecorator = new SignatureDecorator({ signatureType: 'https://didcomm.org/signature/1.0/ed25519Sha512_single', - signature: TypedArrayEncoder.toBase64URL(signatureBuffer), + signature: TypedArrayEncoder.toBase64URL(result.signature), signatureData: TypedArrayEncoder.toBase64URL(dataBuffer), - signer: signerKey, + signer: TypedArrayEncoder.toBase58(signerKey.publicKey.publicKey), }) return signatureDecorator diff --git a/packages/didcomm/src/getOutboundMessageContext.ts b/packages/didcomm/src/getOutboundMessageContext.ts index 738baa5c4e..bce55ea4e7 100644 --- a/packages/didcomm/src/getOutboundMessageContext.ts +++ b/packages/didcomm/src/getOutboundMessageContext.ts @@ -4,7 +4,7 @@ import type { Routing } from './models' import type { ConnectionRecord } from './modules/connections/repository' import type { OutOfBandRecord } from './modules/oob' -import { CredoError, Key, utils } from '@credo-ts/core' +import { CredoError, Kms, utils } from '@credo-ts/core' import { ServiceDecorator } from './decorators/service/ServiceDecorator' import { OutboundMessageContext } from './models' @@ -91,6 +91,9 @@ export async function getConnectionlessOutboundMessageContext( `Creating outbound message context for message ${message.id} using connection-less exchange` ) + // FIXME: we should remove support for the flow where no out of band record is used. + // Users have had enough time to update to the OOB API which supports legacy connectionsless + // invitations as well const outOfBandRecord = await getOutOfBandRecordForMessage(agentContext, message) let { recipientService, ourService } = await getServicesForMessage(agentContext, { lastReceivedMessage, @@ -134,7 +137,7 @@ export async function getConnectionlessOutboundMessageContext( */ async function getOutOfBandRecordForMessage(agentContext: AgentContext, message: AgentMessage) { agentContext.config.logger.debug( - `Looking for out-of-band record for message ${message.id} with thread id ${message.threadId}` + `Looking for out-of-band record for message ${message.id} with thread id ${message.threadId} and type ${message.type}` ) const outOfBandRepository = agentContext.dependencyManager.resolve(OutOfBandRepository) @@ -177,7 +180,8 @@ async function getServicesForMessage( if (!ourService) { ourService = await outOfBandService.getResolvedServiceForOutOfBandServices( agentContext, - outOfBandRecord.outOfBandInvitation.getServices() + outOfBandRecord.outOfBandInvitation.getServices(), + outOfBandRecord.invitationInlineServiceKeys ) } @@ -205,6 +209,13 @@ async function getServicesForMessage( 'Could not find a service to send the message to. Please make sure the connection has a service or provide a service to send the message to.' ) } + + // We need to extract the kms key id for the connectinless exchange + const oobRecordRecipientRouting = outOfBandRecord?.metadata.get(OutOfBandRecordMetadataKeys.RecipientRouting) + if (oobRecordRecipientRouting && ourService) { + ourService.recipientKeys[0].keyId = + oobRecordRecipientRouting.recipientKeyId ?? ourService.recipientKeys[0].legacyKeyId + } } // we either miss ourService (even though a message was sent) or we miss recipientService // we check in separate if statements to provide a more specific error message @@ -242,7 +253,7 @@ async function createOurService( { outOfBandRecord, message }: { outOfBandRecord?: OutOfBandRecord; message: AgentMessage } ): Promise { agentContext.config.logger.debug( - `No previous sent message in thread for outbound message ${message.id}, setting up routing` + `No previous sent message in thread for outbound message ${message.id} with type ${message.type}, setting up routing` ) let routing: Routing | undefined = undefined @@ -250,10 +261,15 @@ async function createOurService( // Extract routing from out of band record if possible const oobRecordRecipientRouting = outOfBandRecord?.metadata.get(OutOfBandRecordMetadataKeys.RecipientRouting) if (oobRecordRecipientRouting) { + const recipientPublicJwk = Kms.PublicJwk.fromFingerprint( + oobRecordRecipientRouting.recipientKeyFingerprint + ) as Kms.PublicJwk + + recipientPublicJwk.keyId = oobRecordRecipientRouting.recipientKeyId ?? recipientPublicJwk.legacyKeyId routing = { - recipientKey: Key.fromFingerprint(oobRecordRecipientRouting.recipientKeyFingerprint), - routingKeys: oobRecordRecipientRouting.routingKeyFingerprints.map((fingerprint) => - Key.fromFingerprint(fingerprint) + recipientKey: recipientPublicJwk, + routingKeys: oobRecordRecipientRouting.routingKeyFingerprints.map( + (fingerprint) => Kms.PublicJwk.fromFingerprint(fingerprint) as Kms.PublicJwk ), endpoints: oobRecordRecipientRouting.endpoints, mediatorId: oobRecordRecipientRouting.mediatorId, @@ -265,6 +281,21 @@ async function createOurService( routing = await routingService.getRouting(agentContext, { mediatorId: outOfBandRecord?.mediatorId, }) + + // We need to store the routing so we can reference it in in the future. + if (outOfBandRecord) { + agentContext.config.logger.debug('Storing routing for out of band invitation.') + outOfBandRecord.metadata.set(OutOfBandRecordMetadataKeys.RecipientRouting, { + recipientKeyFingerprint: routing.recipientKey.fingerprint, + recipientKeyId: routing.recipientKey.keyId, + routingKeyFingerprints: routing.routingKeys.map((key) => key.fingerprint), + endpoints: routing.endpoints, + mediatorId: routing.mediatorId, + }) + outOfBandRecord.setTags({ recipientRoutingKeyFingerprint: routing.recipientKey.fingerprint }) + const outOfBandRepository = agentContext.resolve(OutOfBandRepository) + await outOfBandRepository.update(agentContext, outOfBandRecord) + } } return { diff --git a/packages/didcomm/src/models/InboundMessageContext.ts b/packages/didcomm/src/models/InboundMessageContext.ts index eb54c3c74a..c365b1a3fc 100644 --- a/packages/didcomm/src/models/InboundMessageContext.ts +++ b/packages/didcomm/src/models/InboundMessageContext.ts @@ -1,4 +1,4 @@ -import type { AgentContext, Key } from '@credo-ts/core' +import type { AgentContext, Kms } from '@credo-ts/core' import type { AgentMessage } from '../AgentMessage' import type { MessageHandler } from '../handlers' import type { ConnectionRecord } from '../modules/connections/repository' @@ -10,8 +10,8 @@ import { CredoError } from '@credo-ts/core' export interface MessageContextParams { connection?: ConnectionRecord sessionId?: string - senderKey?: Key - recipientKey?: Key + senderKey?: Kms.PublicJwk + recipientKey?: Kms.PublicJwk agentContext: AgentContext receivedAt?: Date encryptedMessage?: EncryptedMessage @@ -20,8 +20,8 @@ export interface MessageContextParams { export class InboundMessageContext { public connection?: ConnectionRecord public sessionId?: string - public senderKey?: Key - public recipientKey?: Key + public senderKey?: Kms.PublicJwk + public recipientKey?: Kms.PublicJwk public receivedAt: Date public readonly agentContext: AgentContext diff --git a/packages/didcomm/src/models/OutboundMessageContext.ts b/packages/didcomm/src/models/OutboundMessageContext.ts index 0cd52d309c..75b9d33dff 100644 --- a/packages/didcomm/src/models/OutboundMessageContext.ts +++ b/packages/didcomm/src/models/OutboundMessageContext.ts @@ -1,4 +1,4 @@ -import type { AgentContext, BaseRecord, Key, ResolvedDidCommService } from '@credo-ts/core' +import type { AgentContext, BaseRecord, Kms, ResolvedDidCommService } from '@credo-ts/core' import type { AgentMessage } from '../AgentMessage' import type { ConnectionRecord } from '../modules/connections/repository' import type { OutOfBandRecord } from '../modules/oob' @@ -7,7 +7,7 @@ import type { InboundMessageContext } from './InboundMessageContext' import { CredoError } from '@credo-ts/core' export interface ServiceMessageParams { - senderKey: Key + senderKey: Kms.PublicJwk service: ResolvedDidCommService returnRoute?: boolean } diff --git a/packages/didcomm/src/models/Routing.ts b/packages/didcomm/src/models/Routing.ts index c1b293f32d..3f4cddf4a0 100644 --- a/packages/didcomm/src/models/Routing.ts +++ b/packages/didcomm/src/models/Routing.ts @@ -1,8 +1,8 @@ -import type { Key } from '@credo-ts/core' +import type { Kms } from '@credo-ts/core' export interface Routing { endpoints: string[] - recipientKey: Key - routingKeys: Key[] + recipientKey: Kms.PublicJwk + routingKeys: Kms.PublicJwk[] mediatorId?: string } diff --git a/packages/didcomm/src/modules/basic-messages/__tests__/basic-messages.test.ts b/packages/didcomm/src/modules/basic-messages/__tests__/basic-messages.test.ts index f629f8c528..0a7a03955f 100644 --- a/packages/didcomm/src/modules/basic-messages/__tests__/basic-messages.test.ts +++ b/packages/didcomm/src/modules/basic-messages/__tests__/basic-messages.test.ts @@ -6,19 +6,31 @@ import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' -import { getInMemoryAgentOptions, makeConnection, waitForBasicMessage } from '../../../../../core/tests/helpers' +import { getAgentOptions, makeConnection, waitForBasicMessage } from '../../../../../core/tests/helpers' import testLogger from '../../../../../core/tests/logger' import { MessageSendingError } from '../../../errors' import { BasicMessage } from '../messages' import { BasicMessageRecord } from '../repository' -const faberConfig = getInMemoryAgentOptions('Faber Basic Messages', { - endpoints: ['rxjs:faber'], -}) - -const aliceConfig = getInMemoryAgentOptions('Alice Basic Messages', { - endpoints: ['rxjs:alice'], -}) +const faberConfig = getAgentOptions( + 'Faber Basic Messages', + { + endpoints: ['rxjs:faber'], + }, + undefined, + undefined, + { requireDidcomm: true } +) + +const aliceConfig = getAgentOptions( + 'Alice Basic Messages', + { + endpoints: ['rxjs:alice'], + }, + undefined, + undefined, + { requireDidcomm: true } +) describe('Basic Messages E2E', () => { let faberAgent: Agent @@ -48,9 +60,7 @@ describe('Basic Messages E2E', () => { afterEach(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice and Faber exchange messages', async () => { diff --git a/packages/didcomm/src/modules/connections/ConnectionsApi.ts b/packages/didcomm/src/modules/connections/ConnectionsApi.ts index b286c05d62..635e6bda8d 100644 --- a/packages/didcomm/src/modules/connections/ConnectionsApi.ts +++ b/packages/didcomm/src/modules/connections/ConnectionsApi.ts @@ -31,6 +31,7 @@ import { TrustPingMessageHandler, TrustPingResponseMessageHandler, } from './handlers' +import { ConnectionRequestMessage, DidExchangeRequestMessage } from './messages' import { HandshakeProtocol } from './models' import { ConnectionService, DidRotateService, TrustPingService } from './services' @@ -110,8 +111,10 @@ export class ConnectionsApi { routing = await this.routingService.getRouting(this.agentContext, { mediatorId: outOfBandRecord.mediatorId }) } - // biome-ignore lint/suspicious/noImplicitAnyLet: - let result + let result: { + message: DidExchangeRequestMessage | ConnectionRequestMessage + connectionRecord: ConnectionRecord + } if (protocol === HandshakeProtocol.DidExchange) { result = await this.didExchangeProtocol.createRequest(this.agentContext, outOfBandRecord, { label, @@ -180,8 +183,7 @@ export class ConnectionsApi { ? await this.routingService.getRouting(this.agentContext) : undefined - // biome-ignore lint/suspicious/noImplicitAnyLet: - let outboundMessageContext + let outboundMessageContext: OutboundMessageContext if (connectionRecord.protocol === HandshakeProtocol.DidExchange) { const message = await this.didExchangeProtocol.createResponse( this.agentContext, @@ -228,8 +230,7 @@ export class ConnectionsApi { public async acceptResponse(connectionId: string): Promise { const connectionRecord = await this.connectionService.getById(this.agentContext, connectionId) - // biome-ignore lint/suspicious/noImplicitAnyLet: - let outboundMessageContext + let outboundMessageContext: OutboundMessageContext if (connectionRecord.protocol === HandshakeProtocol.DidExchange) { if (!connectionRecord.outOfBandId) { throw new CredoError(`Connection ${connectionRecord.id} does not have outOfBandId!`) @@ -488,7 +489,9 @@ export class ConnectionsApi { if (didDocument) { await this.routingService.removeRouting(this.agentContext, { - recipientKeys: didDocument.recipientKeys, + recipientKeys: didDocument + .getRecipientKeysWithVerificationMethod({ mapX25519ToEd25519: true }) + .map(({ publicJwk }) => publicJwk), mediatorId: connection.mediatorId, }) } @@ -516,7 +519,9 @@ export class ConnectionsApi { if (mediatorRecord) { await this.routingService.removeRouting(this.agentContext, { - recipientKeys: did.didDocument.recipientKeys, + recipientKeys: did.didDocument + .getRecipientKeysWithVerificationMethod({ mapX25519ToEd25519: true }) + .map(({ publicJwk }) => publicJwk), mediatorId: mediatorRecord.id, }) } diff --git a/packages/didcomm/src/modules/connections/DidExchangeProtocol.ts b/packages/didcomm/src/modules/connections/DidExchangeProtocol.ts index b91c113891..c7273aff46 100644 --- a/packages/didcomm/src/modules/connections/DidExchangeProtocol.ts +++ b/packages/didcomm/src/modules/connections/DidExchangeProtocol.ts @@ -1,4 +1,4 @@ -import type { AgentContext, ResolvedDidCommService } from '@credo-ts/core' +import type { AgentContext, DidRecord, ResolvedDidCommService } from '@credo-ts/core' import type { Routing } from '../../models' import type { OutOfBandRecord } from '../oob/repository' import type { ConnectionRecord } from './repository' @@ -13,20 +13,15 @@ import { InjectionSymbols, JsonEncoder, JsonTransformer, - JwaSignatureAlgorithm, JwsService, - Key, - KeyType, + Kms, Logger, PeerDidNumAlgo, TypedArrayEncoder, base64ToBase64URL, - didKeyToInstanceOfKey, - didKeyToVerkey, getAlternativeDidsForPeerDid, - getJwkFromKey, - getKeyFromVerificationMethod, getNumAlgoFromPeerDid, + getPublicJwkFromVerificationMethod, inject, injectable, isDid, @@ -49,7 +44,11 @@ import { DidExchangeProblemReportError, DidExchangeProblemReportReason } from '. import { DidExchangeCompleteMessage, DidExchangeRequestMessage, DidExchangeResponseMessage } from './messages' import { DidExchangeRole, DidExchangeState, HandshakeProtocol } from './models' import { ConnectionService } from './services' -import { createPeerDidFromServices, getDidDocumentForCreatedDid, routingToServices } from './services/helpers' +import { + createPeerDidFromServices, + getResolvedDidcommServiceWithSigningKeyId, + routingToServices, +} from './services/helpers' interface DidExchangeRequestParams { label?: string @@ -64,6 +63,7 @@ interface DidExchangeRequestParams { @injectable() export class DidExchangeProtocol { private connectionService: ConnectionService + private didcommDocumentService: DidCommDocumentService private jwsService: JwsService private didRepository: DidRepository private logger: Logger @@ -72,11 +72,13 @@ export class DidExchangeProtocol { connectionService: ConnectionService, didRepository: DidRepository, jwsService: JwsService, + didcommDocumentService: DidCommDocumentService, @inject(InjectionSymbols.Logger) logger: Logger ) { this.connectionService = connectionService this.didRepository = didRepository this.jwsService = jwsService + this.didcommDocumentService = didcommDocumentService this.logger = logger } @@ -100,24 +102,29 @@ export class DidExchangeProtocol { // Create message const label = params.label ?? agentContext.config.label - // biome-ignore lint/suspicious/noImplicitAnyLet: - let didDocument - // biome-ignore lint/suspicious/noImplicitAnyLet: - let mediatorId + let didDocument: DidDocument + let didRecord: DidRecord + let mediatorId: string | undefined // If our did is specified, make sure we have all key material for it if (did) { - didDocument = await getDidDocumentForCreatedDid(agentContext, did) + const dids = agentContext.resolve(DidsApi) + const resolved = await dids.resolveCreatedDidRecordWithDocument(did) + didDocument = resolved.didDocument + didRecord = resolved.didRecord mediatorId = (await getMediationRecordForDidDocument(agentContext, didDocument))?.id - // Otherwise, create a did:peer based on the provided routing - } else { + } + // Otherwise, create a did:peer based on the provided routing + else { if (!routing) throw new CredoError(`'routing' must be defined if 'ourDid' is not specified`) - didDocument = await createPeerDidFromServices( + const resolved = await createPeerDidFromServices( agentContext, routingToServices(routing), config.peerNumAlgoForDidExchangeRequests ) + didDocument = resolved.didDocument + didRecord = resolved.didRecord mediatorId = routing.mediatorId } @@ -125,13 +132,22 @@ export class DidExchangeProtocol { const message = new DidExchangeRequestMessage({ label, parentThreadId, did: didDocument.id, goal, goalCode }) + const signingKeys = didDocument + .getRecipientKeysWithVerificationMethod({ mapX25519ToEd25519: true }) + .map(({ publicJwk, verificationMethod }) => { + // Bind the kmsKeyIds + const kmsKeyId = didRecord.keys?.find(({ didDocumentRelativeKeyId }) => + verificationMethod.id.endsWith(didDocumentRelativeKeyId) + )?.kmsKeyId + + publicJwk.keyId = kmsKeyId ?? publicJwk.legacyKeyId + + return publicJwk + }) + // Create sign attachment containing didDoc if (isValidPeerDid(didDocument.id) && getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { - const didDocAttach = await this.createSignedAttachment( - agentContext, - didDocument.toJSON(), - didDocument.recipientKeys.map((key) => key.publicKeyBase58) - ) + const didDocAttach = await this.createSignedAttachment(agentContext, didDocument.toJSON(), signingKeys) message.didDoc = didDocAttach } @@ -264,17 +280,17 @@ export class DidExchangeProtocol { throw new CredoError('Missing theirDid on connection record.') } + // Extract keys from the out of band record metadata + const inlineResolvedServices = outOfBandRecord.outOfBandInvitation + .getInlineServices() + .map((service) => getResolvedDidcommServiceWithSigningKeyId(service, outOfBandRecord.invitationInlineServiceKeys)) + let services: ResolvedDidCommService[] = [] + if (routing) { services = routingToServices(routing) - } else if (outOfBandRecord.outOfBandInvitation.getInlineServices().length > 0) { - const inlineServices = outOfBandRecord.outOfBandInvitation.getInlineServices() - services = inlineServices.map((service) => ({ - id: service.id, - serviceEndpoint: service.serviceEndpoint, - recipientKeys: service.recipientKeys.map(didKeyToInstanceOfKey), - routingKeys: service.routingKeys?.map(didKeyToInstanceOfKey) ?? [], - })) + } else if (inlineResolvedServices.length > 0) { + services = inlineResolvedServices } else { // We don't support using a did from the OOB invitation services currently, in this case we always pass routing to this method throw new CredoError( @@ -287,38 +303,35 @@ export class DidExchangeProtocol { ? getNumAlgoFromPeerDid(theirDid) : config.peerNumAlgoForDidExchangeRequests - const didcommDocumentService = agentContext.dependencyManager.resolve(DidCommDocumentService) - const didDocument = await createPeerDidFromServices(agentContext, services, numAlgo) + const { didDocument } = await createPeerDidFromServices(agentContext, services, numAlgo) const message = new DidExchangeResponseMessage({ did: didDocument.id, threadId }) // DID Rotate attachment should be signed with invitation keys - const invitationRecipientKeys = outOfBandRecord.outOfBandInvitation - .getInlineServices() - .map((s) => s.recipientKeys) - .reduce((acc, curr) => acc.concat(curr), []) + const invitationRecipientKeys = inlineResolvedServices.flatMap((s) => s.recipientKeys) // Consider also pure-DID services, used when DID Exchange is started with an implicit invitation or a public DID for (const did of outOfBandRecord.outOfBandInvitation.getDidServices()) { + const dids = agentContext.resolve(DidsApi) + const resolved = await dids.resolveCreatedDidRecordWithDocument(parseDid(did).did) invitationRecipientKeys.push( - ...(await didcommDocumentService.resolveServicesFromDid(agentContext, parseDid(did).did)).flatMap((service) => - service.recipientKeys.map((key) => key.publicKeyBase58) - ) + ...resolved.didDocument + .getRecipientKeysWithVerificationMethod({ mapX25519ToEd25519: true }) + .map(({ publicJwk, verificationMethod }) => { + const kmsKeyId = resolved.didRecord.keys?.find(({ didDocumentRelativeKeyId }) => + verificationMethod.id.endsWith(didDocumentRelativeKeyId) + )?.kmsKeyId + + publicJwk.keyId = kmsKeyId ?? publicJwk.legacyKeyId + return publicJwk + }) ) } if (numAlgo === PeerDidNumAlgo.GenesisDoc) { - message.didDoc = await this.createSignedAttachment( - agentContext, - didDocument.toJSON(), - Array.from(new Set(invitationRecipientKeys.map(didKeyToVerkey))) - ) + message.didDoc = await this.createSignedAttachment(agentContext, didDocument.toJSON(), invitationRecipientKeys) } else { // We assume any other case is a resolvable did (e.g. did:peer:2 or did:peer:4) - message.didRotate = await this.createSignedAttachment( - agentContext, - didDocument.id, - Array.from(new Set(invitationRecipientKeys.map(didKeyToVerkey))) - ) + message.didRotate = await this.createSignedAttachment(agentContext, didDocument.id, invitationRecipientKeys) } connectionRecord.did = didDocument.id @@ -357,9 +370,13 @@ export class DidExchangeProtocol { const didDocument = await this.resolveDidDocument( agentContext, message, - outOfBandRecord - .getTags() - .recipientKeyFingerprints.map((fingerprint) => Key.fromFingerprint(fingerprint).publicKeyBase58) + outOfBandRecord.getTags().recipientKeyFingerprints.map((fingerprint) => { + const publicJwk = Kms.PublicJwk.fromFingerprint(fingerprint) + if (!publicJwk.is(Kms.Ed25519PublicJwk)) { + throw new CredoError('Expected fingerprint to be of type Ed25519') + } + return publicJwk + }) ) if (isValidPeerDid(didDocument.id)) { @@ -468,9 +485,9 @@ export class DidExchangeProtocol { private async createSignedAttachment( agentContext: AgentContext, data: string | Record, - verkeys: string[] + signingKeys: Kms.PublicJwk[] ) { - this.logger.debug(`Creating signed attachment with keys ${JSON.stringify(verkeys)}`) + this.logger.debug('Creating signed attachment') const signedAttach = new Attachment({ mimeType: typeof data === 'string' ? undefined : 'application/json', data: new AttachmentData({ @@ -480,20 +497,19 @@ export class DidExchangeProtocol { }) await Promise.all( - verkeys.map(async (verkey) => { - const key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) - const kid = new DidKey(key).did + signingKeys.map(async (signingKey) => { + const kid = new DidKey(signingKey).did const payload = typeof data === 'string' ? TypedArrayEncoder.fromString(data) : JsonEncoder.toBuffer(data) const jws = await this.jwsService.createJws(agentContext, { payload, - key, + keyId: signingKey.keyId, header: { kid, }, protectedHeaderOptions: { - alg: JwaSignatureAlgorithm.EdDSA, - jwk: getJwkFromKey(key), + alg: Kms.KnownJwaSignatureAlgorithms.EdDSA, + jwk: signingKey, }, }) signedAttach.addJws(jws) @@ -515,12 +531,12 @@ export class DidExchangeProtocol { private async resolveDidDocument( agentContext: AgentContext, message: DidExchangeRequestMessage | DidExchangeResponseMessage, - invitationKeysBase58: string[] = [] + invitationKeys: Kms.PublicJwk[] = [] ) { // The only supported case where we expect to receive a did-document attachment is did:peer algo 1 return isDid(message.did, 'peer') && getNumAlgoFromPeerDid(message.did) === PeerDidNumAlgo.GenesisDoc - ? this.extractAttachedDidDocument(agentContext, message, invitationKeysBase58) - : this.extractResolvableDidDocument(agentContext, message, invitationKeysBase58) + ? this.extractAttachedDidDocument(agentContext, message, invitationKeys) + : this.extractResolvableDidDocument(agentContext, message, invitationKeys) } /** @@ -530,7 +546,7 @@ export class DidExchangeProtocol { private async extractResolvableDidDocument( agentContext: AgentContext, message: DidExchangeRequestMessage | DidExchangeResponseMessage, - invitationKeysBase58?: string[] + invitationKeys?: Kms.PublicJwk[] ) { // Validate did-rotate attachment in case of DID Exchange response if (message instanceof DidExchangeResponseMessage) { @@ -578,20 +594,27 @@ export class DidExchangeProtocol { const didKey = DidKey.fromDid(header.kid) return { method: 'did', - didUrl: `${didKey.did}#${didKey.key.fingerprint}`, - jwk: getJwkFromKey(didKey.key), + didUrl: `${didKey.did}#${didKey.publicJwk.fingerprint}`, + jwk: didKey.publicJwk, } }, }) + const jwsSignerKeys = jwsSigners.map((signer) => signer.jwk) + if (!jwsSignerKeys.every((key) => key.is(Kms.Ed25519PublicJwk))) { + throw new DidExchangeProblemReportError('Expected DID Rotate signature to be signed with Ed25519 key.', { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + }) + } + if ( !isValid || - !jwsSigners.every((jwsSigner) => invitationKeysBase58?.includes(jwsSigner.jwk.key.publicKeyBase58)) + !jwsSignerKeys.every((key) => invitationKeys?.some((invitationKey) => invitationKey.equals(key))) ) { throw new DidExchangeProblemReportError( `DID Rotate signature is invalid. isValid: ${isValid} signerKeys: ${JSON.stringify( - jwsSigners.map((jwsSigner) => jwsSigner.jwk.key.publicKeyBase58) - )} invitationKeys:${JSON.stringify(invitationKeysBase58)}`, + jwsSignerKeys.map((key) => key.fingerprint) + )} invitationKeys:${JSON.stringify(invitationKeys?.map((key) => key.fingerprint))}`, { problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, } @@ -624,7 +647,7 @@ export class DidExchangeProtocol { private async extractAttachedDidDocument( agentContext: AgentContext, message: DidExchangeRequestMessage | DidExchangeResponseMessage, - invitationKeysBase58: string[] = [] + invitationKeys: Kms.PublicJwk[] = [] ): Promise { if (!message.didDoc) { const problemCode = @@ -665,31 +688,29 @@ export class DidExchangeProtocol { const didKey = DidKey.fromDid(header.kid) return { method: 'did', - didUrl: `${didKey.did}#${didKey.key.fingerprint}`, - jwk: getJwkFromKey(didKey.key), + didUrl: `${didKey.did}#${didKey.publicJwk.fingerprint}`, + jwk: didKey.publicJwk, } }, }) const json = JsonEncoder.fromBase64(didDocumentAttachment.data.base64) const didDocument = JsonTransformer.fromJSON(json, DidDocument) - const didDocumentKeysBase58 = didDocument.authentication + const didDocumentKeys = didDocument.authentication ?.map((authentication) => { const verificationMethod = typeof authentication === 'string' ? didDocument.dereferenceVerificationMethod(authentication) : authentication - const key = getKeyFromVerificationMethod(verificationMethod) - return key.publicKeyBase58 + + const publicJwk = getPublicJwkFromVerificationMethod(verificationMethod) + return publicJwk }) - .concat(invitationKeysBase58) + .concat(invitationKeys) - this.logger.trace('JWS verification result', { isValid, jwsSigners, didDocumentKeysBase58 }) + this.logger.trace('JWS verification result', { isValid, jwsSigners }) - if ( - !isValid || - !jwsSigners.every((jwsSigner) => didDocumentKeysBase58?.includes(jwsSigner.jwk.key.publicKeyBase58)) - ) { + if (!isValid || !jwsSigners.every((jwsSigner) => didDocumentKeys?.some((key) => key.equals(jwsSigner.jwk)))) { const problemCode = message instanceof DidExchangeRequestMessage ? DidExchangeProblemReportReason.RequestNotAccepted diff --git a/packages/didcomm/src/modules/connections/__tests__/ConnectionService.test.ts b/packages/didcomm/src/modules/connections/__tests__/ConnectionService.test.ts index 74b2b8ed2b..4bf206a64c 100644 --- a/packages/didcomm/src/modules/connections/__tests__/ConnectionService.test.ts +++ b/packages/didcomm/src/modules/connections/__tests__/ConnectionService.test.ts @@ -1,12 +1,10 @@ import type { AgentContext } from '@credo-ts/core/src/agent' -import type { Wallet } from '@credo-ts/core/src/wallet/Wallet' import type { Routing } from '../../../models' import { Subject } from 'rxjs' -import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' +import { Kms, TypedArrayEncoder } from '@credo-ts/core' import { EventEmitter } from '../../../../../core/src/agent/EventEmitter' -import { Key, KeyType } from '../../../../../core/src/crypto' import { DidKey, IndyAgentService } from '../../../../../core/src/modules/dids' import { DidDocumentRole } from '../../../../../core/src/modules/dids/domain/DidDocumentRole' import { DidCommV1Service } from '../../../../../core/src/modules/dids/domain/service/DidCommV1Service' @@ -68,18 +66,16 @@ const outOfBandService = new OutOfBandServiceMock() const didRepository = new DidRepositoryMock() describe('ConnectionService', () => { - let wallet: Wallet let connectionRepository: ConnectionRepository let connectionService: ConnectionService let eventEmitter: EventEmitter let myRouting: Routing let agentContext: AgentContext + let kms: Kms.KeyManagementApi beforeAll(async () => { - wallet = new InMemoryWallet() agentContext = getAgentContext({ - wallet, agentConfig, registerInstances: [ [OutOfBandRepository, outOfBandRepository], @@ -88,19 +84,21 @@ describe('ConnectionService', () => { [DidCommModuleConfig, new DidCommModuleConfig({ endpoints: [endpoint], connectionImageUrl })], ], }) - await wallet.createAndOpen(agentConfig.walletConfig) - }) - - afterAll(async () => { - await wallet.delete() + kms = agentContext.resolve(Kms.KeyManagementApi) }) beforeEach(async () => { eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) connectionRepository = new ConnectionRepositoryMock() connectionService = new ConnectionService(agentConfig.logger, connectionRepository, didRepository, eventEmitter) + + const recipientKey = Kms.PublicJwk.fromFingerprint( + 'z6MkwFkSP4uv5PhhKJCGehtjuZedkotC7VF64xtMsxuM8R3W' + ) as Kms.PublicJwk + recipientKey.keyId = 'something-random' + myRouting = { - recipientKey: Key.fromFingerprint('z6MkwFkSP4uv5PhhKJCGehtjuZedkotC7VF64xtMsxuM8R3W'), + recipientKey, endpoints: [endpoint], routingKeys: [], mediatorId: 'fakeMediatorId', @@ -220,7 +218,11 @@ describe('ConnectionService', () => { expect.assertions(5) const theirDid = 'their-did' - const theirKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) + const theirKey = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ'), + }) const theirDidDoc = new DidDoc({ id: theirDid, publicKey: [], @@ -229,7 +231,7 @@ describe('ConnectionService', () => { new Ed25119Sig2018({ id: `${theirDid}#key-id`, controller: theirDid, - publicKeyBase58: theirKey.publicKeyBase58, + publicKeyBase58: TypedArrayEncoder.toBase58(theirKey.publicKey.publicKey), }) ), ], @@ -252,7 +254,11 @@ describe('ConnectionService', () => { const messageContext = new InboundMessageContext(connectionRequest, { agentContext, senderKey: theirKey, - recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), + recipientKey: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), + }), }) const outOfBand = getMockOutOfBand({ @@ -279,7 +285,12 @@ describe('ConnectionService', () => { }) const theirDid = 'their-did' - const theirKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) + const theirKey = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ'), + }) + const theirDidDoc = new DidDoc({ id: theirDid, publicKey: [], @@ -288,7 +299,7 @@ describe('ConnectionService', () => { new Ed25119Sig2018({ id: `${theirDid}#key-id`, controller: theirDid, - publicKeyBase58: theirKey.publicKeyBase58, + publicKeyBase58: TypedArrayEncoder.toBase58(theirKey.publicKey.publicKey), }) ), ], @@ -311,7 +322,11 @@ describe('ConnectionService', () => { agentContext, connection: connectionRecord, senderKey: theirKey, - recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), + recipientKey: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), + }), }) const outOfBand = getMockOutOfBand({ @@ -342,8 +357,16 @@ describe('ConnectionService', () => { const messageContext = new InboundMessageContext(connectionRequest, { agentContext, - recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), - senderKey: Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519), + recipientKey: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), + }), + senderKey: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ'), + }), }) const outOfBand = getMockOutOfBand({ role: OutOfBandRole.Sender, state: OutOfBandState.AwaitResponse }) @@ -358,8 +381,16 @@ describe('ConnectionService', () => { const inboundMessage = new InboundMessageContext(jest.fn()(), { agentContext, - recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), - senderKey: Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519), + recipientKey: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), + }), + senderKey: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ'), + }), }) const outOfBand = getMockOutOfBand({ role: OutOfBandRole.Receiver, state: OutOfBandState.AwaitResponse }) @@ -389,8 +420,9 @@ describe('ConnectionService', () => { it('returns a connection response message containing the information from the connection record', async () => { expect.assertions(2) - const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const key = await kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } }) + const publicJwk = Kms.PublicJwk.fromPublicJwk(key.publicJwk) + const did = indyDidFromPublicKeyBase58(TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey)) // Needed for signing connection~sig const mockConnection = getMockConnection({ @@ -401,13 +433,16 @@ describe('ConnectionService', () => { }, }) - const recipientKeys = [new DidKey(key)] - const outOfBand = getMockOutOfBand({ recipientKeys: recipientKeys.map((did) => did.did) }) + const recipientKeys = [new DidKey(publicJwk)] + const outOfBand = getMockOutOfBand({ + recipientKeys: recipientKeys.map((did) => did.did), + invitationInlineServiceKeys: [{ kmsKeyId: key.keyId, recipientKeyFingerprint: publicJwk.fingerprint }], + }) const publicKey = new Ed25119Sig2018({ id: `${did}#1`, controller: did, - publicKeyBase58: key.publicKeyBase58, + publicKeyBase58: TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey), }) const mockDidDoc = new DidDoc({ id: did, @@ -417,7 +452,11 @@ describe('ConnectionService', () => { new IndyAgentService({ id: `${did}#IndyAgentService-1`, serviceEndpoint: 'http://example.com', - recipientKeys: recipientKeys.map((did) => did.key.publicKeyBase58), + recipientKeys: recipientKeys.map((did) => { + const publicKey = did.publicJwk.publicKey + if (publicKey.kty !== 'OKP') throw new Error('expected okp') + return TypedArrayEncoder.toBase58(publicKey.publicKey) + }), routingKeys: [], }), ], @@ -436,7 +475,7 @@ describe('ConnectionService', () => { const plainConnection = JsonTransformer.toJSON(connection) expect(connectionRecord.state).toBe(DidExchangeState.ResponseSent) - expect(await unpackAndVerifySignatureDecorator(message.connectionSig, wallet)).toEqual(plainConnection) + expect(await unpackAndVerifySignatureDecorator(agentContext, message.connectionSig)).toEqual(plainConnection) }) it(`throws an error when connection role is ${DidExchangeRole.Requester} and not ${DidExchangeRole.Responder}`, async () => { @@ -480,11 +519,13 @@ describe('ConnectionService', () => { it('returns a connection record containing the information from the connection response', async () => { expect.assertions(2) - const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const key = await kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } }) + const publicJwk = Kms.PublicJwk.fromPublicJwk(key.publicJwk) + const did = indyDidFromPublicKeyBase58(TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey)) - const theirKey = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const theirDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const theirKey = await kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } }) + const theirPublicJwk = Kms.PublicJwk.fromPublicJwk(theirKey.publicJwk) + const theirDid = indyDidFromPublicKeyBase58(TypedArrayEncoder.toBase58(theirPublicJwk.publicKey.publicKey)) const connectionRecord = getMockConnection({ did, @@ -502,7 +543,7 @@ describe('ConnectionService', () => { new Ed25119Sig2018({ id: `${theirDid}#key-id`, controller: theirDid, - publicKeyBase58: theirKey.publicKeyBase58, + publicKeyBase58: TypedArrayEncoder.toBase58(theirPublicJwk.publicKey.publicKey), }) ), ], @@ -517,7 +558,7 @@ describe('ConnectionService', () => { }) const plainConnection = JsonTransformer.toJSON(otherPartyConnection) - const connectionSig = await signData(plainConnection, wallet, theirKey.publicKeyBase58) + const connectionSig = await signData(agentContext, plainConnection, theirPublicJwk) const connectionResponse = new ConnectionResponseMessage({ threadId: uuid(), @@ -525,19 +566,21 @@ describe('ConnectionService', () => { }) const outOfBandRecord = getMockOutOfBand({ - recipientKeys: [new DidKey(theirKey).did], + recipientKeys: [new DidKey(theirPublicJwk).did], }) const messageContext = new InboundMessageContext(connectionResponse, { agentContext, connection: connectionRecord, - senderKey: theirKey, - recipientKey: key, + senderKey: theirPublicJwk, + recipientKey: publicJwk, }) const processedConnection = await connectionService.processResponse(messageContext, outOfBandRecord) - // biome-ignore lint/style/noNonNullAssertion: - const peerDid = didDocumentJsonToNumAlgo1Did(convertToNewDidDocument(otherPartyConnection.didDoc!).toJSON()) + const peerDid = didDocumentJsonToNumAlgo1Did( + // biome-ignore lint/style/noNonNullAssertion: + convertToNewDidDocument(otherPartyConnection.didDoc!).didDocument.toJSON() + ) expect(processedConnection.state).toBe(DidExchangeState.ResponseReceived) expect(processedConnection.theirDid).toBe(peerDid) @@ -554,8 +597,16 @@ describe('ConnectionService', () => { const messageContext = new InboundMessageContext(jest.fn()(), { agentContext, connection: connectionRecord, - recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), - senderKey: Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519), + recipientKey: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), + }), + senderKey: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ'), + }), }) return expect(connectionService.processResponse(messageContext, outOfBandRecord)).rejects.toThrowError( @@ -566,11 +617,13 @@ describe('ConnectionService', () => { it('throws an error when the connection sig is not signed with the same key as the recipient key from the invitation', async () => { expect.assertions(1) - const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const key = await kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } }) + const publicJwk = Kms.PublicJwk.fromPublicJwk(key.publicJwk) + const did = indyDidFromPublicKeyBase58(TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey)) - const theirKey = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const theirDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const theirKey = await kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } }) + const theirPublicJwk = Kms.PublicJwk.fromPublicJwk(theirKey.publicJwk) + const theirDid = indyDidFromPublicKeyBase58(TypedArrayEncoder.toBase58(theirPublicJwk.publicKey.publicKey)) const connectionRecord = getMockConnection({ did, role: DidExchangeRole.Requester, @@ -587,7 +640,7 @@ describe('ConnectionService', () => { new Ed25119Sig2018({ id: `${theirDid}#key-id`, controller: theirDid, - publicKeyBase58: theirKey.publicKeyBase58, + publicKeyBase58: TypedArrayEncoder.toBase58(theirPublicJwk.publicKey.publicKey), }) ), ], @@ -601,7 +654,7 @@ describe('ConnectionService', () => { }), }) const plainConnection = JsonTransformer.toJSON(otherPartyConnection) - const connectionSig = await signData(plainConnection, wallet, theirKey.publicKeyBase58) + const connectionSig = await signData(agentContext, plainConnection, theirPublicJwk) const connectionResponse = new ConnectionResponseMessage({ threadId: uuid(), @@ -611,13 +664,13 @@ describe('ConnectionService', () => { // Recipient key `verkey` is not the same as theirVerkey which was used to sign message, // therefore it should cause a failure. const outOfBandRecord = getMockOutOfBand({ - recipientKeys: [new DidKey(key).did], + recipientKeys: [new DidKey(publicJwk).did], }) const messageContext = new InboundMessageContext(connectionResponse, { agentContext, connection: connectionRecord, - senderKey: theirKey, - recipientKey: key, + senderKey: theirPublicJwk, + recipientKey: publicJwk, }) return expect(connectionService.processResponse(messageContext, outOfBandRecord)).rejects.toThrowError( @@ -628,11 +681,13 @@ describe('ConnectionService', () => { it('throws an error when the message does not contain a DID Document', async () => { expect.assertions(1) - const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const key = await kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } }) + const publicJwk = Kms.PublicJwk.fromPublicJwk(key.publicJwk) + const did = indyDidFromPublicKeyBase58(TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey)) - const theirKey = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const theirDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const theirKey = await kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } }) + const theirPublicJwk = Kms.PublicJwk.fromPublicJwk(theirKey.publicJwk) + const theirDid = indyDidFromPublicKeyBase58(TypedArrayEncoder.toBase58(theirPublicJwk.publicKey.publicKey)) const connectionRecord = getMockConnection({ did, state: DidExchangeState.RequestSent, @@ -641,16 +696,24 @@ describe('ConnectionService', () => { const otherPartyConnection = new Connection({ did: theirDid }) const plainConnection = JsonTransformer.toJSON(otherPartyConnection) - const connectionSig = await signData(plainConnection, wallet, theirKey.publicKeyBase58) + const connectionSig = await signData(agentContext, plainConnection, theirPublicJwk) const connectionResponse = new ConnectionResponseMessage({ threadId: uuid(), connectionSig }) - const outOfBandRecord = getMockOutOfBand({ recipientKeys: [new DidKey(theirKey).did] }) + const outOfBandRecord = getMockOutOfBand({ recipientKeys: [new DidKey(theirPublicJwk).did] }) const messageContext = new InboundMessageContext(connectionResponse, { agentContext, connection: connectionRecord, - recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), - senderKey: Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519), + recipientKey: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), + }), + senderKey: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ'), + }), }) return expect(connectionService.processResponse(messageContext, outOfBandRecord)).rejects.toThrowError( @@ -823,26 +886,34 @@ describe('ConnectionService', () => { it('should not throw when a fully valid connection-less input is passed', async () => { expect.assertions(1) - const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) - const senderKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) + const recipientKey = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), + }) + const senderKey = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ'), + }) const lastSentMessage = new AgentMessage() lastSentMessage.setService({ - recipientKeys: [recipientKey.publicKeyBase58], + recipientKeys: [TypedArrayEncoder.toBase58(recipientKey.publicKey.publicKey)], serviceEndpoint: '', routingKeys: [], }) const lastReceivedMessage = new AgentMessage() lastReceivedMessage.setService({ - recipientKeys: [senderKey.publicKeyBase58], + recipientKeys: [TypedArrayEncoder.toBase58(senderKey.publicKey.publicKey)], serviceEndpoint: '', routingKeys: [], }) const message = new AgentMessage() message.setService({ - recipientKeys: [senderKey.publicKeyBase58], + recipientKeys: [TypedArrayEncoder.toBase58(senderKey.publicKey.publicKey)], serviceEndpoint: '', routingKeys: [], }) @@ -886,8 +957,16 @@ describe('ConnectionService', () => { it('should throw an error when lastSentMessage and recipientKey are present, but recipient key is not present in recipientKeys of previously sent message ~service decorator', async () => { expect.assertions(1) - const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) - const senderKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) + const recipientKey = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), + }) + const senderKey = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), + }) const lastSentMessage = new AgentMessage() lastSentMessage.setService({ @@ -908,7 +987,7 @@ describe('ConnectionService', () => { connectionService.assertConnectionOrOutOfBandExchange(messageContext, { lastSentMessage, }) - ).rejects.toThrowError('Recipient key 8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K not found in our service') + ).rejects.toThrowError('Recipient key z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th not found in our service') }) it('should throw an error when lastReceivedMessage is present, but senderVerkey is not ', async () => { @@ -955,8 +1034,16 @@ describe('ConnectionService', () => { const message = new AgentMessage() const messageContext = new InboundMessageContext(message, { agentContext, - senderKey: Key.fromPublicKeyBase58('randomKey', KeyType.Ed25519), - recipientKey: Key.fromPublicKeyBase58(senderKey, KeyType.Ed25519), + senderKey: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('randomKey'), + }), + recipientKey: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58(senderKey), + }), }) await expect( @@ -964,7 +1051,7 @@ describe('ConnectionService', () => { lastReceivedMessage, lastSentMessage, }) - ).rejects.toThrowError('Sender key randomKey not found in their service') + ).rejects.toThrow('Sender key z41yMxWDBqGD2Z not found in their service.') }) }) diff --git a/packages/didcomm/src/modules/connections/__tests__/InMemoryDidRegistry.ts b/packages/didcomm/src/modules/connections/__tests__/InMemoryDidRegistry.ts index f2f06abb06..5f0377736e 100644 --- a/packages/didcomm/src/modules/connections/__tests__/InMemoryDidRegistry.ts +++ b/packages/didcomm/src/modules/connections/__tests__/InMemoryDidRegistry.ts @@ -4,6 +4,7 @@ import type { DidCreateResult, DidDeactivateResult, DidDocument, + DidDocumentKey, DidRegistrar, DidResolutionResult, DidResolver, @@ -19,7 +20,14 @@ export class InMemoryDidRegistry implements DidRegistrar, DidResolver { private dids: Record = {} - public async create(agentContext: AgentContext, options: DidCreateOptions): Promise { + public async create( + agentContext: AgentContext, + options: DidCreateOptions & { + options: { + keys: DidDocumentKey[] + } + } + ): Promise { const { did, didDocument } = options if (!did || !didDocument) { @@ -40,6 +48,7 @@ export class InMemoryDidRegistry implements DidRegistrar, DidResolver { did: didDocument.id, role: DidDocumentRole.Created, didDocument, + keys: options.options.keys, }) const didRepository = agentContext.dependencyManager.resolve(DidRepository) await didRepository.save(agentContext, didRecord) diff --git a/packages/didcomm/src/modules/connections/__tests__/connection-manual.test.ts b/packages/didcomm/src/modules/connections/__tests__/connection-manual.test.ts index 088a98925f..ec00b1dee0 100644 --- a/packages/didcomm/src/modules/connections/__tests__/connection-manual.test.ts +++ b/packages/didcomm/src/modules/connections/__tests__/connection-manual.test.ts @@ -5,7 +5,7 @@ import { filter, first, map, timeout } from 'rxjs/operators' import { Agent } from '../../../../../core/src/agent/Agent' import { setupSubjectTransports } from '../../../../../core/tests' -import { getInMemoryAgentOptions } from '../../../../../core/tests/helpers' +import { getAgentOptions } from '../../../../../core/tests/helpers' import { ConnectionEventTypes } from '../ConnectionEvents' import { ConnectionsModule } from '../ConnectionsModule' import { DidExchangeState } from '../models' @@ -44,7 +44,7 @@ describe('Manual Connection Flow', () => { // This test was added to reproduce a bug where all connections based on a reusable invitation would use the same keys // This was only present in the manual flow, which is almost never used. it('can connect multiple times using the same reusable invitation without manually using the connections api', async () => { - const aliceAgentOptions = getInMemoryAgentOptions( + const aliceAgentOptions = getAgentOptions( 'Manual Connection Flow Alice', { endpoints: ['rxjs:alice'], @@ -56,9 +56,10 @@ describe('Manual Connection Flow', () => { connections: new ConnectionsModule({ autoAcceptConnections: false, }), - } + }, + { requireDidcomm: true } ) - const bobAgentOptions = getInMemoryAgentOptions( + const bobAgentOptions = getAgentOptions( 'Manual Connection Flow Bob', { endpoints: ['rxjs:bob'], @@ -70,9 +71,10 @@ describe('Manual Connection Flow', () => { connections: new ConnectionsModule({ autoAcceptConnections: false, }), - } + }, + { requireDidcomm: true } ) - const faberAgentOptions = getInMemoryAgentOptions( + const faberAgentOptions = getAgentOptions( 'Manual Connection Flow Faber', { endpoints: ['rxjs:faber'], @@ -82,7 +84,8 @@ describe('Manual Connection Flow', () => { connections: new ConnectionsModule({ autoAcceptConnections: false, }), - } + }, + { requireDidcomm: true } ) const aliceAgent = new Agent(aliceAgentOptions) @@ -145,11 +148,8 @@ describe('Manual Connection Flow', () => { expect(aliceConnectionRecord).toBeConnectedWith(faberAliceConnectionRecord) expect(bobConnectionRecord).toBeConnectedWith(faberBobConnectionRecord) - await aliceAgent.wallet.delete() await aliceAgent.shutdown() - await bobAgent.wallet.delete() await bobAgent.shutdown() - await faberAgent.wallet.delete() await faberAgent.shutdown() }) }) diff --git a/packages/didcomm/src/modules/connections/__tests__/did-rotate.test.ts b/packages/didcomm/src/modules/connections/__tests__/did-rotate.test.ts index 8b50c8fab5..36e1cf391a 100644 --- a/packages/didcomm/src/modules/connections/__tests__/did-rotate.test.ts +++ b/packages/didcomm/src/modules/connections/__tests__/did-rotate.test.ts @@ -8,7 +8,7 @@ import { createPeerDidDocumentFromServices } from '../../../../../core/src/modul import { uuid } from '../../../../../core/src/utils/uuid' import { setupSubjectTransports } from '../../../../../core/tests' import { - getInMemoryAgentOptions, + getAgentOptions, makeConnection, waitForAgentMessageProcessedEvent, waitForBasicMessage, @@ -30,12 +30,24 @@ describe('Rotation E2E tests', () => { let bobAliceConnection: ConnectionRecord | undefined beforeEach(async () => { - const aliceAgentOptions = getInMemoryAgentOptions('DID Rotate Alice', { - endpoints: ['rxjs:alice'], - }) - const bobAgentOptions = getInMemoryAgentOptions('DID Rotate Bob', { - endpoints: ['rxjs:bob'], - }) + const aliceAgentOptions = getAgentOptions( + 'DID Rotate Alice', + { + endpoints: ['rxjs:alice'], + }, + undefined, + undefined, + { requireDidcomm: true } + ) + const bobAgentOptions = getAgentOptions( + 'DID Rotate Bob', + { + endpoints: ['rxjs:bob'], + }, + undefined, + undefined, + { requireDidcomm: true } + ) aliceAgent = new Agent(aliceAgentOptions) bobAgent = new Agent(bobAgentOptions) @@ -48,9 +60,7 @@ describe('Rotation E2E tests', () => { afterEach(async () => { await aliceAgent.shutdown() - await aliceAgent.wallet.delete() await bobAgent.shutdown() - await bobAgent.wallet.delete() }) describe('Rotation from did:peer:1 to did:peer:4', () => { @@ -142,19 +152,25 @@ describe('Rotation E2E tests', () => { const didRouting = await aliceAgent.modules.mediationRecipient.getRouting({}) const did = `did:inmemory:${uuid()}` - const didDocument = createPeerDidDocumentFromServices([ - { - id: 'didcomm', - recipientKeys: [didRouting.recipientKey], - routingKeys: didRouting.routingKeys, - serviceEndpoint: didRouting.endpoints[0], - }, - ]) + const { didDocument, keys } = createPeerDidDocumentFromServices( + [ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ], + true + ) didDocument.id = did await aliceAgent.dids.create({ did, didDocument, + options: { + keys, + }, }) // Do did rotate @@ -210,19 +226,25 @@ describe('Rotation E2E tests', () => { const didRouting = await aliceAgent.modules.mediationRecipient.getRouting({}) const did = `did:inmemory:${uuid()}` - const didDocument = createPeerDidDocumentFromServices([ - { - id: 'didcomm', - recipientKeys: [didRouting.recipientKey], - routingKeys: didRouting.routingKeys, - serviceEndpoint: didRouting.endpoints[0], - }, - ]) + const { didDocument, keys } = createPeerDidDocumentFromServices( + [ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ], + true + ) didDocument.id = did await aliceAgent.dids.create({ did, didDocument, + options: { + keys, + }, }) const waitForAllDidRotate = Promise.all([waitForDidRotate(aliceAgent, {}), waitForDidRotate(bobAgent, {})]) @@ -285,19 +307,25 @@ describe('Rotation E2E tests', () => { const didRouting = await aliceAgent.modules.mediationRecipient.getRouting({}) const did = `did:inmemory:${uuid()}` - const didDocument = createPeerDidDocumentFromServices([ - { - id: 'didcomm', - recipientKeys: [didRouting.recipientKey], - routingKeys: didRouting.routingKeys, - serviceEndpoint: didRouting.endpoints[0], - }, - ]) + const { didDocument, keys } = createPeerDidDocumentFromServices( + [ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ], + true + ) didDocument.id = did await aliceAgent.dids.create({ did, didDocument, + options: { + keys, + }, }) // Do did rotate diff --git a/packages/didcomm/src/modules/connections/__tests__/didexchange-numalgo.test.ts b/packages/didcomm/src/modules/connections/__tests__/didexchange-numalgo.test.ts index b368c2eb33..783d93c2d9 100644 --- a/packages/didcomm/src/modules/connections/__tests__/didexchange-numalgo.test.ts +++ b/packages/didcomm/src/modules/connections/__tests__/didexchange-numalgo.test.ts @@ -7,7 +7,7 @@ import { Agent } from '../../../../../core/src/agent/Agent' import { DidsModule, PeerDidNumAlgo, createPeerDidDocumentFromServices } from '../../../../../core/src/modules/dids' import { uuid } from '../../../../../core/src/utils/uuid' import { setupSubjectTransports } from '../../../../../core/tests' -import { getInMemoryAgentOptions } from '../../../../../core/tests/helpers' +import { getAgentOptions } from '../../../../../core/tests/helpers' import { ConnectionEventTypes } from '../ConnectionEvents' import { ConnectionsModule } from '../ConnectionsModule' import { DidExchangeState } from '../models' @@ -94,7 +94,7 @@ async function didExchangeNumAlgoBaseTest(options: { // Make a common in-memory did registry for both agents const didRegistry = new InMemoryDidRegistry() - const aliceAgentOptions = getInMemoryAgentOptions( + const aliceAgentOptions = getAgentOptions( 'DID Exchange numalgo settings Alice', { endpoints: ['rxjs:alice'], @@ -108,9 +108,10 @@ async function didExchangeNumAlgoBaseTest(options: { peerNumAlgoForDidExchangeRequests: options.requesterNumAlgoSetting, }), dids: new DidsModule({ registrars: [didRegistry], resolvers: [didRegistry] }), - } + }, + { requireDidcomm: true } ) - const faberAgentOptions = getInMemoryAgentOptions( + const faberAgentOptions = getAgentOptions( 'DID Exchange numalgo settings Alice', { endpoints: ['rxjs:faber'], @@ -122,7 +123,8 @@ async function didExchangeNumAlgoBaseTest(options: { peerNumAlgoForDidExchangeRequests: options.responderNumAlgoSetting, }), dids: new DidsModule({ registrars: [didRegistry], resolvers: [didRegistry] }), - } + }, + { requireDidcomm: true } ) const aliceAgent = new Agent(aliceAgentOptions) @@ -139,27 +141,31 @@ async function didExchangeNumAlgoBaseTest(options: { const waitForAliceRequest = waitForRequest(faberAgent, 'alice') - // biome-ignore lint/suspicious/noImplicitAnyLet: - let ourDid - // biome-ignore lint/suspicious/noImplicitAnyLet: - let routing + let ourDid: string | undefined = undefined + if (options.createExternalDidForRequester) { // Create did externally const didRouting = await aliceAgent.modules.mediationRecipient.getRouting({}) ourDid = `did:inmemory:${uuid()}` - const didDocument = createPeerDidDocumentFromServices([ - { - id: 'didcomm', - recipientKeys: [didRouting.recipientKey], - routingKeys: didRouting.routingKeys, - serviceEndpoint: didRouting.endpoints[0], - }, - ]) + const { didDocument, keys } = createPeerDidDocumentFromServices( + [ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ], + true + ) didDocument.id = ourDid await aliceAgent.dids.create({ did: ourDid, didDocument, + options: { + keys, + }, }) } @@ -168,7 +174,6 @@ async function didExchangeNumAlgoBaseTest(options: { { autoAcceptInvitation: true, autoAcceptConnection: false, - routing, ourDid, } ) @@ -190,9 +195,7 @@ async function didExchangeNumAlgoBaseTest(options: { expect(aliceConnectionRecord).toBeConnectedWith(faberAliceConnectionRecord) - await aliceAgent.wallet.delete() await aliceAgent.shutdown() - await faberAgent.wallet.delete() await faberAgent.shutdown() } diff --git a/packages/didcomm/src/modules/connections/__tests__/helpers.test.ts b/packages/didcomm/src/modules/connections/__tests__/helpers.test.ts index af60cd9397..8c52106f0b 100644 --- a/packages/didcomm/src/modules/connections/__tests__/helpers.test.ts +++ b/packages/didcomm/src/modules/connections/__tests__/helpers.test.ts @@ -62,7 +62,7 @@ const didDoc = new DidDoc({ describe('convertToNewDidDocument', () => { test('create a new DidDocument and with authentication, publicKey and service from DidDoc', () => { const oldDocument = didDoc - const newDocument = convertToNewDidDocument(oldDocument) + const newDocument = convertToNewDidDocument(oldDocument).didDocument expect(newDocument.authentication).toEqual(['#EoGusetS', '#5UQ3drtE']) @@ -116,7 +116,7 @@ describe('convertToNewDidDocument', () => { }), ], }) - const newDocument = convertToNewDidDocument(oldDocument) + const newDocument = convertToNewDidDocument(oldDocument).didDocument expect(newDocument.service).toEqual([ new IndyAgentService({ @@ -151,7 +151,7 @@ describe('convertToNewDidDocument', () => { }), ], }) - const newDocument = convertToNewDidDocument(oldDocument) + const newDocument = convertToNewDidDocument(oldDocument).didDocument expect(newDocument.service).toEqual([ new IndyAgentService({ diff --git a/packages/didcomm/src/modules/connections/handlers/ConnectionRequestHandler.ts b/packages/didcomm/src/modules/connections/handlers/ConnectionRequestHandler.ts index 87b82eb375..08cec7eed9 100644 --- a/packages/didcomm/src/modules/connections/handlers/ConnectionRequestHandler.ts +++ b/packages/didcomm/src/modules/connections/handlers/ConnectionRequestHandler.ts @@ -9,6 +9,7 @@ import { CredoError, tryParseDid } from '@credo-ts/core' import { TransportService } from '../../../TransportService' import { OutboundMessageContext } from '../../../models' +import { OutOfBandState } from '../../oob/domain/OutOfBandState' import { ConnectionRequestMessage } from '../messages' import { HandshakeProtocol } from '../models' @@ -66,6 +67,10 @@ export class ConnectionRequestHandler implements MessageHandler { throw new CredoError(`A received did record for sender key ${senderKey.fingerprint} already exists.`) } + if (outOfBandRecord.state === OutOfBandState.Done) { + throw new CredoError('Out-of-band record has been already processed and it does not accept any new requests') + } + const connectionRecord = await this.connectionService.processRequest(messageContext, outOfBandRecord) // Associate the new connection with the session created for the inbound message @@ -74,6 +79,10 @@ export class ConnectionRequestHandler implements MessageHandler { transportService.setConnectionIdForSession(sessionId, connectionRecord.id) } + if (!outOfBandRecord.reusable) { + await this.outOfBandService.updateState(agentContext, outOfBandRecord, OutOfBandState.Done) + } + if (connectionRecord?.autoAcceptConnection ?? this.connectionsModuleConfig.autoAcceptConnections) { // TODO: Allow rotation of keys used in the invitation for new ones not only when out-of-band is reusable or // when there are no inline services in the invitation diff --git a/packages/didcomm/src/modules/connections/handlers/ConnectionResponseHandler.ts b/packages/didcomm/src/modules/connections/handlers/ConnectionResponseHandler.ts index e1aba6e0d8..cf0555a3d9 100644 --- a/packages/didcomm/src/modules/connections/handlers/ConnectionResponseHandler.ts +++ b/packages/didcomm/src/modules/connections/handlers/ConnectionResponseHandler.ts @@ -8,6 +8,7 @@ import { CredoError } from '@credo-ts/core' import { ReturnRouteTypes } from '../../../decorators/transport/TransportDecorator' import { OutboundMessageContext } from '../../../models' +import { OutOfBandState } from '../../oob/domain/OutOfBandState' import { ConnectionResponseMessage } from '../messages' import { DidExchangeRole } from '../models' @@ -77,6 +78,10 @@ export class ConnectionResponseHandler implements MessageHandler { messageContext.connection = connectionRecord const connection = await this.connectionService.processResponse(messageContext, outOfBandRecord) + if (!outOfBandRecord.reusable) { + await this.outOfBandService.updateState(messageContext.agentContext, outOfBandRecord, OutOfBandState.Done) + } + // TODO: should we only send ping message in case of autoAcceptConnection or always? // In AATH we have a separate step to send the ping. So for now we'll only do it // if auto accept is enable @@ -88,6 +93,7 @@ export class ConnectionResponseHandler implements MessageHandler { // Disable return routing as we don't want to receive a response for this message over the same channel // This has led to long timeouts as not all clients actually close an http socket if there is no response message message.setReturnRouting(ReturnRouteTypes.none) + return new OutboundMessageContext(message, { agentContext: messageContext.agentContext, connection }) } } diff --git a/packages/didcomm/src/modules/connections/handlers/DidExchangeCompleteHandler.ts b/packages/didcomm/src/modules/connections/handlers/DidExchangeCompleteHandler.ts index 6482efe351..6ed5b1bd28 100644 --- a/packages/didcomm/src/modules/connections/handlers/DidExchangeCompleteHandler.ts +++ b/packages/didcomm/src/modules/connections/handlers/DidExchangeCompleteHandler.ts @@ -3,8 +3,6 @@ import type { OutOfBandService } from '../../oob/OutOfBandService' import type { DidExchangeProtocol } from '../DidExchangeProtocol' import { CredoError, tryParseDid } from '@credo-ts/core' - -import { OutOfBandState } from '../../oob/domain/OutOfBandState' import { DidExchangeCompleteMessage } from '../messages' import { HandshakeProtocol } from '../models' @@ -47,9 +45,6 @@ export class DidExchangeCompleteHandler implements MessageHandler { throw new CredoError(`OutOfBand record for message ID ${message.thread?.parentThreadId} not found!`) } - if (!outOfBandRecord.reusable) { - await this.outOfBandService.updateState(messageContext.agentContext, outOfBandRecord, OutOfBandState.Done) - } await this.didExchangeProtocol.processComplete(messageContext, outOfBandRecord) return undefined diff --git a/packages/didcomm/src/modules/connections/handlers/DidExchangeRequestHandler.ts b/packages/didcomm/src/modules/connections/handlers/DidExchangeRequestHandler.ts index b2d8bead1f..2387dffd18 100644 --- a/packages/didcomm/src/modules/connections/handlers/DidExchangeRequestHandler.ts +++ b/packages/didcomm/src/modules/connections/handlers/DidExchangeRequestHandler.ts @@ -83,6 +83,10 @@ export class DidExchangeRequestHandler implements MessageHandler { transportService.setConnectionIdForSession(sessionId, connectionRecord.id) } + if (!outOfBandRecord.reusable) { + await this.outOfBandService.updateState(agentContext, outOfBandRecord, OutOfBandState.Done) + } + if (connectionRecord.autoAcceptConnection ?? this.connectionsModuleConfig.autoAcceptConnections) { // TODO We should add an option to not pass routing and therefore do not rotate keys and use the keys from the invitation // TODO: Allow rotation of keys used in the invitation for new ones not only when out-of-band is reusable diff --git a/packages/didcomm/src/modules/connections/handlers/DidExchangeResponseHandler.ts b/packages/didcomm/src/modules/connections/handlers/DidExchangeResponseHandler.ts index 2c5a256653..a4d0cf9271 100644 --- a/packages/didcomm/src/modules/connections/handlers/DidExchangeResponseHandler.ts +++ b/packages/didcomm/src/modules/connections/handlers/DidExchangeResponseHandler.ts @@ -95,6 +95,10 @@ export class DidExchangeResponseHandler implements MessageHandler { messageContext.connection = connectionRecord const connection = await this.didExchangeProtocol.processResponse(messageContext, outOfBandRecord) + if (!outOfBandRecord.reusable) { + await this.outOfBandService.updateState(agentContext, outOfBandRecord, OutOfBandState.Done) + } + // TODO: should we only send complete message in case of autoAcceptConnection or always? // In AATH we have a separate step to send the complete. So for now we'll only do it // if auto accept is enabled @@ -104,9 +108,6 @@ export class DidExchangeResponseHandler implements MessageHandler { // This has led to long timeouts as not all clients actually close an http socket if there is no response message message.setReturnRouting(ReturnRouteTypes.none) - if (!outOfBandRecord.reusable) { - await this.outOfBandService.updateState(agentContext, outOfBandRecord, OutOfBandState.Done) - } return new OutboundMessageContext(message, { agentContext, connection }) } } diff --git a/packages/didcomm/src/modules/connections/messages/ConnectionInvitationMessage.ts b/packages/didcomm/src/modules/connections/messages/ConnectionInvitationMessage.ts index 016274f8e3..7ebc5bde20 100644 --- a/packages/didcomm/src/modules/connections/messages/ConnectionInvitationMessage.ts +++ b/packages/didcomm/src/modules/connections/messages/ConnectionInvitationMessage.ts @@ -25,6 +25,9 @@ export interface DIDInvitationOptions { did: string } +export type ConnectionInvitationMessageOptions = BaseInvitationOptions & + (DIDInvitationOptions | InlineInvitationOptions) + /** * Message to invite another agent to create a connection * @@ -37,7 +40,7 @@ export class ConnectionInvitationMessage extends AgentMessage { * Create new ConnectionInvitationMessage instance. * @param options */ - public constructor(options: BaseInvitationOptions & (DIDInvitationOptions | InlineInvitationOptions)) { + public constructor(options: ConnectionInvitationMessageOptions) { super() if (options) { diff --git a/packages/didcomm/src/modules/connections/repository/ConnectionRecord.ts b/packages/didcomm/src/modules/connections/repository/ConnectionRecord.ts index 10dac64a7a..5fcb8e8b07 100644 --- a/packages/didcomm/src/modules/connections/repository/ConnectionRecord.ts +++ b/packages/didcomm/src/modules/connections/repository/ConnectionRecord.ts @@ -84,6 +84,9 @@ export class ConnectionRecord extends BaseRecord 0) { - didDoc = this.createDidDocFromOutOfBandDidCommServices(outOfBandRecord.outOfBandInvitation.getInlineServices()) + const result = this.createDidDocFromOutOfBandDidCommServices(outOfBandRecord) + didDoc = result.didDoc + keys = result.keys } else { // We don't support using a did from the OOB invitation services currently, in this case we always pass routing to this method throw new CredoError( @@ -231,6 +241,7 @@ export class ConnectionService { const { did: peerDid } = await this.createDid(agentContext, { role: DidDocumentRole.Created, didDoc, + keys, }) const connection = new Connection({ @@ -244,11 +255,34 @@ export class ConnectionService { throw new CredoError(`Connection record with id ${connectionRecord.id} does not have a thread id`) } - const signingKey = Key.fromFingerprint(outOfBandRecord.getTags().recipientKeyFingerprints[0]).publicKeyBase58 + let signingKey: Kms.PublicJwk + const firstService = outOfBandRecord.outOfBandInvitation.getServices()[0] + if (typeof firstService === 'string') { + const dids = agentContext.resolve(DidsApi) + const resolved = await dids.resolveCreatedDidRecordWithDocument(parseDid(firstService).did) + + const recipientKeys = resolved.didDocument.getRecipientKeysWithVerificationMethod({ mapX25519ToEd25519: true }) + if (recipientKeys.length === 0) { + throw new CredoError(`Unable to extract signing key for connection response from did '${firstService}'`) + } + + signingKey = recipientKeys[0].publicJwk + // TOOD: we probably need an util: addKeyIdToVerificationMethodKey + signingKey.keyId = + resolved.didRecord.keys?.find(({ didDocumentRelativeKeyId }) => + recipientKeys[0].verificationMethod.id.endsWith(didDocumentRelativeKeyId) + )?.kmsKeyId ?? signingKey.legacyKeyId + } else { + const service = getResolvedDidcommServiceWithSigningKeyId( + firstService, + outOfBandRecord.invitationInlineServiceKeys + ) + signingKey = service.recipientKeys[0] + } const connectionResponse = new ConnectionResponseMessage({ threadId: connectionRecord.threadId, - connectionSig: await signData(connectionJson, agentContext.wallet, signingKey), + connectionSig: await signData(agentContext, connectionJson, signingKey), }) connectionRecord.did = peerDid @@ -295,10 +329,7 @@ export class ConnectionService { let connectionJson = null try { - connectionJson = await unpackAndVerifySignatureDecorator( - message.connectionSig, - messageContext.agentContext.wallet - ) + connectionJson = await unpackAndVerifySignatureDecorator(messageContext.agentContext, message.connectionSig) } catch (error) { if (error instanceof CredoError) { throw new ConnectionProblemReportError(error.message, { @@ -314,9 +345,17 @@ export class ConnectionService { // as the recipient key(s) in the connection invitation message const signerVerkey = message.connectionSig.signer - const invitationKey = Key.fromFingerprint(outOfBandRecord.getTags().recipientKeyFingerprints[0]).publicKeyBase58 + const invitationKey = Kms.PublicJwk.fromFingerprint(outOfBandRecord.getTags().recipientKeyFingerprints[0]) + if (!invitationKey.is(Kms.Ed25519PublicJwk)) { + throw new ConnectionProblemReportError( + `Expected invitation key to be an Ed25519 key, found ${invitationKey.jwkTypehumanDescription}`, + { problemCode: ConnectionProblemReportReason.ResponseNotAccepted } + ) + } + + const invitationKeyBase58 = TypedArrayEncoder.toBase58(invitationKey.publicKey.publicKey) - if (signerVerkey !== invitationKey) { + if (signerVerkey !== invitationKeyBase58) { throw new ConnectionProblemReportError( `Connection object in connection response message is not signed with same key as recipient key in invitation expected='${invitationKey}' received='${signerVerkey}'`, { problemCode: ConnectionProblemReportReason.ResponseNotAccepted } @@ -497,14 +536,17 @@ export class ConnectionService { type: message.type, }) - const recipientKey = messageContext.recipientKey?.publicKeyBase58 - const senderKey = messageContext.senderKey?.publicKeyBase58 + const recipientKey = messageContext.recipientKey + const senderKey = messageContext.senderKey // set theirService to the value of lastReceivedMessage.service let theirService = messageContext.message?.service?.resolvedDidCommService ?? lastReceivedMessage?.service?.resolvedDidCommService let ourService = lastSentMessage?.service?.resolvedDidCommService + // FIXME: we should remove support for the flow where no out of band record is used. + // Users have had enough time to update to the OOB API which supports legacy connectionsless + // invitations as well // 1. check if there's an oob record associated. const outOfBandRepository = messageContext.agentContext.dependencyManager.resolve(OutOfBandRepository) const outOfBandService = messageContext.agentContext.dependencyManager.resolve(OutOfBandService) @@ -516,7 +558,8 @@ export class ConnectionService { if (outOfBandRecord?.role === OutOfBandRole.Sender) { ourService = await outOfBandService.getResolvedServiceForOutOfBandServices( messageContext.agentContext, - outOfBandRecord.outOfBandInvitation.getServices() + outOfBandRecord.outOfBandInvitation.getServices(), + outOfBandRecord.invitationInlineServiceKeys ) } else if (outOfBandRecord?.role === OutOfBandRole.Receiver) { theirService = await outOfBandService.getResolvedServiceForOutOfBandServices( @@ -552,17 +595,17 @@ export class ConnectionService { // Check if recipientKey is in ourService if (recipientKey && ourService) { - const recipientKeyFound = ourService.recipientKeys.some((key) => key.publicKeyBase58 === recipientKey) + const recipientKeyFound = ourService.recipientKeys.some((key) => recipientKey.equals(key)) if (!recipientKeyFound) { - throw new CredoError(`Recipient key ${recipientKey} not found in our service`) + throw new CredoError(`Recipient key ${recipientKey.fingerprint} not found in our service`) } } // Check if senderKey is in theirService if (senderKey && theirService) { - const senderKeyFound = theirService.recipientKeys.some((key) => key.publicKeyBase58 === senderKey) + const senderKeyFound = theirService.recipientKeys.some((key) => senderKey.equals(key)) if (!senderKeyFound) { - throw new CredoError(`Sender key ${senderKey} not found in their service.`) + throw new CredoError(`Sender key ${senderKey.fingerprint} not found in their service.`) } } } @@ -756,7 +799,10 @@ export class ConnectionService { public async findByKeys( agentContext: AgentContext, - { senderKey, recipientKey }: { senderKey: Key; recipientKey: Key } + { + senderKey, + recipientKey, + }: { senderKey: Kms.PublicJwk; recipientKey: Kms.PublicJwk } ) { const theirDidRecord = await this.didRepository.findReceivedDidByRecipientKey(agentContext, senderKey) if (theirDidRecord) { @@ -806,9 +852,16 @@ export class ConnectionService { return connectionRecord.connectionTypes || [] } - private async createDid(agentContext: AgentContext, { role, didDoc }: { role: DidDocumentRole; didDoc: DidDoc }) { + private async createDid( + agentContext: AgentContext, + { role, didDoc, keys }: { role: DidDocumentRole; didDoc: DidDoc; keys?: DidDocumentKey[] } + ) { + if (keys && role !== DidDocumentRole.Created) { + throw new CredoError(`keys can only be provided for did documents when the role is '${DidDocumentRole.Created}'`) + } + // Convert the legacy did doc to a new did document - const didDocument = convertToNewDidDocument(didDoc) + const { didDocument, keys: updatedKeys } = convertToNewDidDocument(didDoc, keys) // Assert that the keys we are going to use for creating a did document haven't already been used in another did document if (role === DidDocumentRole.Created) { @@ -821,6 +874,7 @@ export class ConnectionService { did: peerDid, role, didDocument, + keys: updatedKeys, }) // Store the unqualified did with the legacy did document in the metadata @@ -844,12 +898,20 @@ export class ConnectionService { } private createDidDoc(routing: Routing) { - const indyDid = utils.indyDidFromPublicKeyBase58(routing.recipientKey.publicKeyBase58) + const recipientKeyBase58 = TypedArrayEncoder.toBase58(routing.recipientKey.publicKey.publicKey) + const indyDid = utils.indyDidFromPublicKeyBase58(recipientKeyBase58) + + const keys: DidDocumentKey[] = [ + { + didDocumentRelativeKeyId: '#1', + kmsKeyId: routing.recipientKey.keyId, + }, + ] const publicKey = new Ed25119Sig2018({ id: `${indyDid}#1`, controller: indyDid, - publicKeyBase58: routing.recipientKey.publicKeyBase58, + publicKeyBase58: recipientKeyBase58, }) const auth = new ReferencedAuthentication(publicKey, authenticationTypes.Ed25519VerificationKey2018) @@ -860,31 +922,37 @@ export class ConnectionService { new IndyAgentService({ id: `${indyDid}#IndyAgentService-${index + 1}`, serviceEndpoint: endpoint, - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), + recipientKeys: [recipientKeyBase58], + routingKeys: routing.routingKeys.map((key) => TypedArrayEncoder.toBase58(key.publicKey.publicKey)), // Order of endpoint determines priority priority: index, }) ) - return new DidDoc({ - id: indyDid, - authentication: [auth], - service: services, - publicKey: [publicKey], - }) + return { + didDoc: new DidDoc({ + id: indyDid, + authentication: [auth], + service: services, + publicKey: [publicKey], + }), + keys, + } } - private createDidDocFromOutOfBandDidCommServices(services: OutOfBandDidCommService[]) { - const [recipientDidKey] = services[0].recipientKeys + private createDidDocFromOutOfBandDidCommServices(outOfBandRecord: OutOfBandRecord) { + const services = outOfBandRecord.outOfBandInvitation + .getInlineServices() + .map((service) => getResolvedDidcommServiceWithSigningKeyId(service, outOfBandRecord.invitationInlineServiceKeys)) - const recipientKey = DidKey.fromDid(recipientDidKey).key - const did = utils.indyDidFromPublicKeyBase58(recipientKey.publicKeyBase58) + const [recipientKey] = services[0].recipientKeys + const recipientKeyBase58 = TypedArrayEncoder.toBase58(recipientKey.publicKey.publicKey) + const did = utils.indyDidFromPublicKeyBase58(recipientKeyBase58) const publicKey = new Ed25119Sig2018({ id: `${did}#1`, controller: did, - publicKeyBase58: recipientKey.publicKeyBase58, + publicKeyBase58: recipientKeyBase58, }) const auth = new ReferencedAuthentication(publicKey, authenticationTypes.Ed25519VerificationKey2018) @@ -895,18 +963,23 @@ export class ConnectionService { new IndyAgentService({ id: `${did}#IndyAgentService-${index + 1}`, serviceEndpoint: service.serviceEndpoint, - recipientKeys: [recipientKey.publicKeyBase58], - routingKeys: service.routingKeys?.map(didKeyToVerkey), + recipientKeys: [recipientKeyBase58], + routingKeys: service.routingKeys?.map((publicJwk) => + TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey) + ), priority: index, }) ) - return new DidDoc({ - id: did, - authentication: [auth], - service, - publicKey: [publicKey], - }) + return { + didDoc: new DidDoc({ + id: did, + authentication: [auth], + service, + publicKey: [publicKey], + }), + keys: [{ didDocumentRelativeKeyId: '#1', kmsKeyId: recipientKey.keyId }] satisfies DidDocumentKey[], + } } public async returnWhenIsConnected( diff --git a/packages/didcomm/src/modules/connections/services/DidRotateService.ts b/packages/didcomm/src/modules/connections/services/DidRotateService.ts index ba87789faf..01e6907407 100644 --- a/packages/didcomm/src/modules/connections/services/DidRotateService.ts +++ b/packages/didcomm/src/modules/connections/services/DidRotateService.ts @@ -1,4 +1,4 @@ -import type { AgentContext } from '@credo-ts/core' +import type { AgentContext, DidDocument, DidRecord } from '@credo-ts/core' import type { InboundMessageContext, Routing } from '../../../models' import type { ConnectionDidRotatedEvent } from '../ConnectionEvents' import type { ConnectionRecord } from '../repository' @@ -7,6 +7,7 @@ import { CredoError, DidRepository, DidResolverService, + DidsApi, EventEmitter, InjectionSymbols, Logger, @@ -27,7 +28,7 @@ import { DidRotateAckMessage, DidRotateMessage, DidRotateProblemReportMessage, H import { ConnectionMetadataKeys } from '../repository/ConnectionMetadataTypes' import { ConnectionService } from './ConnectionService' -import { createPeerDidFromServices, getDidDocumentForCreatedDid, routingToServices } from './helpers' +import { createPeerDidFromServices, routingToServices } from './helpers' @injectable() export class DidRotateService { @@ -51,7 +52,8 @@ export class DidRotateService { ) { const { connection, toDid, routing } = options - const config = agentContext.dependencyManager.resolve(ConnectionsModuleConfig) + const config = agentContext.resolve(ConnectionsModuleConfig) + const dids = agentContext.resolve(DidsApi) // Do not allow to receive concurrent did rotation flows const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) @@ -60,14 +62,12 @@ export class DidRotateService { throw new CredoError(`There is already an existing opened did rotation flow for connection id ${connection.id}`) } - // biome-ignore lint/suspicious/noImplicitAnyLet: - let didDocument - // biome-ignore lint/suspicious/noImplicitAnyLet: - let mediatorId + let resolvedDid: { didRecord: DidRecord; didDocument: DidDocument } + let mediatorId: string | undefined // If did is specified, make sure we have all key material for it if (toDid) { - didDocument = await getDidDocumentForCreatedDid(agentContext, toDid) - mediatorId = (await getMediationRecordForDidDocument(agentContext, didDocument))?.id + resolvedDid = await dids.resolveCreatedDidRecordWithDocument(toDid) + mediatorId = (await getMediationRecordForDidDocument(agentContext, resolvedDid.didDocument))?.id // Otherwise, create a did:peer based on the provided routing } else { @@ -75,7 +75,7 @@ export class DidRotateService { throw new CredoError('Routing configuration must be defined when rotating to a new peer did') } - didDocument = await createPeerDidFromServices( + resolvedDid = await createPeerDidFromServices( agentContext, routingToServices(routing), config.peerNumAlgoForDidRotation @@ -83,17 +83,17 @@ export class DidRotateService { mediatorId = routing.mediatorId } - const message = new DidRotateMessage({ toDid: didDocument.id }) + const message = new DidRotateMessage({ toDid: resolvedDid.didDocument.id }) // We set new info into connection metadata for further 'sealing' it once we receive an acknowledge // All messages sent in-between will be using previous connection information connection.metadata.set(ConnectionMetadataKeys.DidRotate, { threadId: message.threadId, - did: didDocument.id, + did: resolvedDid.didDocument.id, mediatorId, }) - await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + await agentContext.resolve(ConnectionService).update(agentContext, connection) return message } diff --git a/packages/didcomm/src/modules/connections/services/helpers.ts b/packages/didcomm/src/modules/connections/services/helpers.ts index b50d1bfd26..001f039e92 100644 --- a/packages/didcomm/src/modules/connections/services/helpers.ts +++ b/packages/didcomm/src/modules/connections/services/helpers.ts @@ -1,4 +1,4 @@ -import type { AgentContext, DidDocument, PeerDidNumAlgo, ResolvedDidCommService } from '@credo-ts/core' +import { AgentContext, DidDocumentKey, Kms, PeerDidNumAlgo, ResolvedDidCommService } from '@credo-ts/core' import type { Routing } from '../../../models' import type { DidDoc, PublicKey } from '../models' @@ -10,16 +10,16 @@ import { DidRepository, DidsApi, IndyAgentService, - Key, - KeyType, + TypedArrayEncoder, createPeerDidDocumentFromServices, didDocumentJsonToNumAlgo1Did, getEd25519VerificationKey2018, } from '@credo-ts/core' - +import { OutOfBandDidCommService } from '../../oob/domain/OutOfBandDidCommService' +import { OutOfBandInlineServiceKey } from '../../oob/repository/OutOfBandRecord' import { EmbeddedAuthentication } from '../models' -export function convertToNewDidDocument(didDoc: DidDoc): DidDocument { +export function convertToNewDidDocument(didDoc: DidDoc, keys?: DidDocumentKey[]) { const didDocumentBuilder = new DidDocumentBuilder('') const oldIdNewIdMapping: { [key: string]: string } = {} @@ -94,7 +94,13 @@ export function convertToNewDidDocument(didDoc: DidDoc): DidDocument { const peerDid = didDocumentJsonToNumAlgo1Did(didDocument.toJSON()) didDocument.id = peerDid - return didDocument + return { + didDocument, + keys: keys?.map((key) => ({ + ...key, + didDocumentRelativeKeyId: oldIdNewIdMapping[key.didDocumentRelativeKeyId], + })), + } } function normalizeId(fullId: string): `#${string}` { @@ -115,10 +121,14 @@ function convertPublicKeyToVerificationMethod(publicKey: PublicKey) { throw new CredoError(`Public key ${publicKey.id} does not have value property`) } const publicKeyBase58 = publicKey.value - const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519) + const ed25519Key = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58(publicKeyBase58), + }) return getEd25519VerificationKey2018({ id: `#${publicKeyBase58.slice(0, 8)}`, - key: ed25519Key, + publicJwk: ed25519Key, controller: '#id', }) } @@ -132,23 +142,12 @@ export function routingToServices(routing: Routing): ResolvedDidCommService[] { })) } -export async function getDidDocumentForCreatedDid(agentContext: AgentContext, did: string) { - // Ensure that the DID has been created by us - const didRecord = await agentContext.dependencyManager.resolve(DidRepository).findCreatedDid(agentContext, did) - if (!didRecord) { - throw new CredoError(`Could not find created did ${did}`) - } - - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - return await didsApi.resolveDidDocument(did) -} - /** * Asserts that the keys we are going to use for creating a did document haven't already been used in another did document * Due to how DIDComm v1 works (only reference the key not the did in encrypted message) we can't have multiple dids containing * the same key as we won't know which did (and thus which connection) a message is intended for. */ -export async function assertNoCreatedDidExistsForKeys(agentContext: AgentContext, recipientKeys: Key[]) { +export async function assertNoCreatedDidExistsForKeys(agentContext: AgentContext, recipientKeys: Kms.PublicJwk[]) { const didRepository = agentContext.dependencyManager.resolve(DidRepository) const recipientKeyFingerprints = recipientKeys.map((key) => key.fingerprint) @@ -181,7 +180,7 @@ export async function createPeerDidFromServices( const didsApi = agentContext.dependencyManager.resolve(DidsApi) // Create did document without the id property - const didDocument = createPeerDidDocumentFromServices(services) + const { didDocument, keys } = createPeerDidDocumentFromServices(services, true) // Assert that the keys we are going to use for creating a did document haven't already been used in another did document await assertNoCreatedDidExistsForKeys(agentContext, didDocument.recipientKeys) @@ -192,6 +191,7 @@ export async function createPeerDidFromServices( didDocument, options: { numAlgo, + keys, }, }) @@ -199,5 +199,27 @@ export async function createPeerDidFromServices( throw new CredoError(`Did document creation failed: ${JSON.stringify(result.didState)}`) } - return result.didState.didDocument + // FIXME: didApi.create should return the did document + return didsApi.resolveCreatedDidRecordWithDocument(result.didState.did) +} + +export function getResolvedDidcommServiceWithSigningKeyId( + outOfBandDidcommService: OutOfBandDidCommService, + /** + * Optional keys for the inline services + */ + inlineServiceKeys?: OutOfBandInlineServiceKey[] +) { + const resolvedService = outOfBandDidcommService.resolvedDidCommService + + // Make sure the key id is set for service keys + for (const recipientKey of resolvedService.recipientKeys) { + const kmsKeyId = inlineServiceKeys?.find( + ({ recipientKeyFingerprint }) => recipientKeyFingerprint === recipientKey.fingerprint + )?.kmsKeyId + + recipientKey.keyId = kmsKeyId ?? recipientKey.legacyKeyId + } + + return resolvedService } diff --git a/packages/didcomm/src/modules/credentials/formats/jsonld/__tests__/JsonLdCredentialFormatService.test.ts b/packages/didcomm/src/modules/credentials/formats/jsonld/__tests__/JsonLdCredentialFormatService.test.ts index 6a9c635936..251f38b0b3 100644 --- a/packages/didcomm/src/modules/credentials/formats/jsonld/__tests__/JsonLdCredentialFormatService.test.ts +++ b/packages/didcomm/src/modules/credentials/formats/jsonld/__tests__/JsonLdCredentialFormatService.test.ts @@ -555,11 +555,7 @@ describe('JsonLd CredentialFormatService', () => { expect(areCredentialsEqual).toBe(true) const inputDoc2 = { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/citizenship/v1', - 'https://w3id.org/security/bbs/v1', - ], + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/citizenship/v1'], } message2.data = new AttachmentData({ base64: JsonEncoder.toBase64(inputDoc2), diff --git a/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.test.ts b/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.test.ts index 7d843dc4f3..51368e6231 100644 --- a/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.test.ts +++ b/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.test.ts @@ -13,31 +13,32 @@ import { storePreCreatedAnonCredsDefinition, } from '../../../../../../../anoncreds/tests/preCreatedAnonCredsDefinition' import { Agent } from '../../../../../../../core/src/agent/Agent' -import { getInMemoryAgentOptions, waitForCredentialRecordSubject } from '../../../../../../../core/tests/helpers' +import { getAgentOptions, waitForCredentialRecordSubject } from '../../../../../../../core/tests/helpers' import testLogger from '../../../../../../../core/tests/logger' -import { MessageReceiver } from '../../../../../MessageReceiver' import { CredentialEventTypes } from '../../../CredentialEvents' import { AutoAcceptCredential } from '../../../models/CredentialAutoAcceptType' import { CredentialState } from '../../../models/CredentialState' import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' import { V2CredentialPreview } from '../messages' -const faberAgentOptions = getInMemoryAgentOptions( +const faberAgentOptions = getAgentOptions( 'Faber connection-less Credentials V2', { endpoints: ['rxjs:faber'], }, {}, - getAnonCredsIndyModules() + getAnonCredsIndyModules(), + { requireDidcomm: true } ) -const aliceAgentOptions = getInMemoryAgentOptions( +const aliceAgentOptions = getAgentOptions( 'Alice connection-less Credentials V2', { endpoints: ['rxjs:alice'], }, {}, - getAnonCredsIndyModules() + getAnonCredsIndyModules(), + { requireDidcomm: true } ) const credentialPreview = V2CredentialPreview.fromRecord({ @@ -87,9 +88,7 @@ describe('V2 Connectionless Credentials', () => { afterEach(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Faber starts with connection-less credential offer to Alice', async () => { @@ -106,13 +105,13 @@ describe('V2 Connectionless Credentials', () => { protocolVersion: 'v2', }) - const { message: offerMessage } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ + const { invitationUrl } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ recordId: faberCredentialRecord.id, message, domain: 'https://a-domain.com', }) - await aliceAgent.dependencyManager.resolve(MessageReceiver).receiveMessage(offerMessage.toJSON()) + await aliceAgent.modules.oob.receiveInvitationFromUrl(invitationUrl) let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { threadId: faberCredentialRecord.threadId, @@ -205,14 +204,14 @@ describe('V2 Connectionless Credentials', () => { autoAcceptCredential: AutoAcceptCredential.ContentApproved, }) - const { message: offerMessage } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ + const { invitationUrl } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ recordId: faberCredentialRecord.id, message, domain: 'https://a-domain.com', }) // Receive Message - await aliceAgent.context.dependencyManager.resolve(MessageReceiver).receiveMessage(offerMessage.toJSON()) + await aliceAgent.modules.oob.receiveInvitationFromUrl(invitationUrl) // Wait for it to be processed let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { diff --git a/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.test.ts b/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.test.ts index c765cca09b..0c0288b4cc 100644 --- a/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.test.ts +++ b/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2-credentials-auto-accept.test.ts @@ -60,9 +60,7 @@ describe('V2 Credentials Auto Accept', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test("Alice starts with V2 credential proposal to Faber, both with autoAcceptCredential on 'always'", async () => { @@ -185,9 +183,7 @@ describe('V2 Credentials Auto Accept', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test("Alice starts with V2 credential proposal to Faber, both with autoAcceptCredential on 'contentApproved'", async () => { diff --git a/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2-credentials.e2e.test.ts b/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2-credentials.e2e.test.ts index fe5efe412a..7626e5c68c 100644 --- a/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2-credentials.e2e.test.ts +++ b/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2-credentials.e2e.test.ts @@ -76,9 +76,7 @@ describe('v2 credentials', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice starts with V2 credential proposal to Faber', async () => { diff --git a/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.connectionless-credentials.test.ts b/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.connectionless-credentials.test.ts index a3ead3d38e..66f71e939d 100644 --- a/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.connectionless-credentials.test.ts +++ b/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.connectionless-credentials.test.ts @@ -1,14 +1,12 @@ -import type { EventReplaySubject, JsonLdTestsAgent } from '../../../../../../../core/tests' -import type { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' - -import { KeyType } from '../../../../../../../core/src/crypto' +import { transformPrivateKeyToPrivateJwk } from '../../../../../../../askar/src' import { CREDENTIALS_CONTEXT_V1_URL } from '../../../../../../../core/src/modules/vc/constants' import { TypedArrayEncoder } from '../../../../../../../core/src/utils' +import type { EventReplaySubject, JsonLdTestsAgent } from '../../../../../../../core/tests' import { setupJsonLdTests, waitForCredentialRecordSubject } from '../../../../../../../core/tests' import testLogger from '../../../../../../../core/tests/logger' -import { MessageReceiver } from '../../../../../MessageReceiver' import { CredentialState } from '../../../models' import { CredentialExchangeRecord } from '../../../repository' +import type { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' const signCredentialOptions = { credential: { @@ -47,17 +45,30 @@ describe('credentials', () => { createConnections: false, })) - await faberAgent.context.wallet.createKey({ - privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), - keyType: KeyType.Ed25519, + const key = await faberAgent.kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }).privateJwk, + }) + + await faberAgent.dids.import({ + did: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + keys: [ + { + didDocumentRelativeKeyId: '#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + kmsKeyId: key.keyId, + }, + ], }) }) afterEach(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Faber starts with V2 W3C connection-less credential offer to Alice', async () => { @@ -93,14 +104,13 @@ describe('credentials', () => { }, }) - const { message: connectionlessOfferMessage } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ + const { invitationUrl } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ recordId: faberCredentialRecord.id, message, domain: 'https://a-domain.com', }) - await aliceAgent.context.dependencyManager - .resolve(MessageReceiver) - .receiveMessage(connectionlessOfferMessage.toJSON()) + + await aliceAgent.modules.oob.receiveInvitationFromUrl(invitationUrl) let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { threadId: faberCredentialRecord.threadId, diff --git a/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials-auto-accept.test.ts b/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials-auto-accept.test.ts index feedf5d1a1..311c46eaa8 100644 --- a/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials-auto-accept.test.ts +++ b/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials-auto-accept.test.ts @@ -1,9 +1,8 @@ -import type { JsonLdTestsAgent } from '../../../../../../../core/tests' - -import { KeyType } from '../../../../../../../core/src/crypto' +import { transformPrivateKeyToPrivateJwk } from '../../../../../../../askar/src' import { CredoError } from '../../../../../../../core/src/error/CredoError' import { CREDENTIALS_CONTEXT_V1_URL } from '../../../../../../../core/src/modules/vc/constants' import { TypedArrayEncoder } from '../../../../../../../core/src/utils' +import type { JsonLdTestsAgent } from '../../../../../../../core/tests' import { setupJsonLdTests } from '../../../../../../../core/tests' import { waitForCredentialRecord } from '../../../../../../../core/tests/helpers' import testLogger from '../../../../../../../core/tests/logger' @@ -48,17 +47,30 @@ describe('V2 Credentials - JSON-LD - Auto Accept Always', () => { autoAcceptCredentials: AutoAcceptCredential.Always, })) - await faberAgent.context.wallet.createKey({ - privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), - keyType: KeyType.Ed25519, + const key = await faberAgent.kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }).privateJwk, + }) + + await faberAgent.dids.import({ + did: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + keys: [ + { + didDocumentRelativeKeyId: '#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + kmsKeyId: key.keyId, + }, + ], }) }) afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test("Alice starts with V2 credential proposal to Faber, both with autoAcceptCredential on 'always'", async () => { @@ -150,17 +162,30 @@ describe('V2 Credentials - JSON-LD - Auto Accept Always', () => { autoAcceptCredentials: AutoAcceptCredential.ContentApproved, })) - await faberAgent.context.wallet.createKey({ - privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), - keyType: KeyType.Ed25519, + const key = await faberAgent.kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }).privateJwk, + }) + + await faberAgent.dids.import({ + did: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + keys: [ + { + didDocumentRelativeKeyId: '#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + kmsKeyId: key.keyId, + }, + ], }) }) afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test("Alice starts with V2 credential proposal to Faber, both with autoAcceptCredential on 'contentApproved'", async () => { diff --git a/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.e2e.test.ts b/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.e2e.test.ts index 3e6e617a20..7aad389bc9 100644 --- a/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.e2e.test.ts +++ b/packages/didcomm/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.e2e.test.ts @@ -11,15 +11,15 @@ import { getAnonCredsIndyModules, prepareForAnonCredsIssuance, } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' +import { transformPrivateKeyToPrivateJwk } from '../../../../../../../askar/src' import { Agent } from '../../../../../../../core/src/agent/Agent' -import { KeyType } from '../../../../../../../core/src/crypto' import { CacheModule, InMemoryLruCache } from '../../../../../../../core/src/modules/cache' import { W3cCredentialsModule } from '../../../../../../../core/src/modules/vc' import { customDocumentLoader } from '../../../../../../../core/src/modules/vc/data-integrity/__tests__/documentLoader' import { TypedArrayEncoder } from '../../../../../../../core/src/utils' import { JsonTransformer } from '../../../../../../../core/src/utils/JsonTransformer' import { - getInMemoryAgentOptions, + getAgentOptions, makeConnection, setupEventReplaySubjects, setupSubjectTransports, @@ -37,11 +37,7 @@ import { V2CredentialPreview } from '../messages' const signCredentialOptions = { credential: { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/citizenship/v1', - 'https://w3id.org/security/bbs/v1', - ], + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/citizenship/v1'], id: 'https://issuer.oidp.uscis.gov/credentials/83627465', type: ['VerifiableCredential', 'PermanentResidentCard'], issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', @@ -113,23 +109,25 @@ describe('V2 Credentials - JSON-LD - Ed25519', () => { beforeAll(async () => { faberAgent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'Faber Agent Indy/JsonLD', { endpoints: ['rxjs:faber'], }, {}, - getIndyJsonLdModules() + getIndyJsonLdModules(), + { requireDidcomm: true } ) ) aliceAgent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'Alice Agent Indy/JsonLD', { endpoints: ['rxjs:alice'], }, {}, - getIndyJsonLdModules() + getIndyJsonLdModules(), + { requireDidcomm: true } ) ) @@ -147,17 +145,30 @@ describe('V2 Credentials - JSON-LD - Ed25519', () => { }) credentialDefinitionId = credentialDefinition.credentialDefinitionId - await faberAgent.context.wallet.createKey({ - privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), - keyType: KeyType.Ed25519, + const key = await faberAgent.kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + type: { + crv: 'Ed25519', + kty: 'OKP', + }, + }).privateJwk, + }) + + await faberAgent.dids.import({ + did: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + keys: [ + { + didDocumentRelativeKeyId: '#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + kmsKeyId: key.keyId, + }, + ], }) }) afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice starts with V2 (ld format, Ed25519 signature) credential proposal to Faber', async () => { @@ -381,11 +392,7 @@ describe('V2 Credentials - JSON-LD - Ed25519', () => { const credentialOfferJson = offerMessage?.offerAttachments[1].getDataAsJson() expect(credentialOfferJson).toMatchObject({ credential: { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/citizenship/v1', - 'https://w3id.org/security/bbs/v1', - ], + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/citizenship/v1'], id: 'https://issuer.oidp.uscis.gov/credentials/83627465', type: ['VerifiableCredential', 'PermanentResidentCard'], issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', @@ -513,11 +520,7 @@ describe('V2 Credentials - JSON-LD - Ed25519', () => { const credentialMessage = await faberAgent.modules.credentials.findCredentialMessage(faberCredentialRecord.id) const w3cCredential = credentialMessage?.credentialAttachments[1].getDataAsJson() expect(w3cCredential).toMatchObject({ - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/citizenship/v1', - 'https://w3id.org/security/bbs/v1', - ], + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/citizenship/v1'], id: 'https://issuer.oidp.uscis.gov/credentials/83627465', type: ['VerifiableCredential', 'PermanentResidentCard'], issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', diff --git a/packages/didcomm/src/modules/discover-features/__tests__/v1-discover-features.test.ts b/packages/didcomm/src/modules/discover-features/__tests__/v1-discover-features.test.ts index d7fed05750..421cdba2a3 100644 --- a/packages/didcomm/src/modules/discover-features/__tests__/v1-discover-features.test.ts +++ b/packages/didcomm/src/modules/discover-features/__tests__/v1-discover-features.test.ts @@ -8,18 +8,30 @@ import { ReplaySubject } from 'rxjs' import { Agent } from '../../../../../core/src/agent/Agent' import { setupSubjectTransports } from '../../../../../core/tests' -import { getInMemoryAgentOptions, makeConnection } from '../../../../../core/tests/helpers' +import { getAgentOptions, makeConnection } from '../../../../../core/tests/helpers' import { DiscoverFeaturesEventTypes } from '../DiscoverFeaturesEvents' import { waitForDisclosureSubject, waitForQuerySubject } from './helpers' -const faberAgentOptions = getInMemoryAgentOptions('Faber Discover Features V1 E2E', { - endpoints: ['rxjs:faber'], -}) - -const aliceAgentOptions = getInMemoryAgentOptions('Alice Discover Features V1 E2E', { - endpoints: ['rxjs:alice'], -}) +const faberAgentOptions = getAgentOptions( + 'Faber Discover Features V1 E2E', + { + endpoints: ['rxjs:faber'], + }, + undefined, + undefined, + { requireDidcomm: true } +) + +const aliceAgentOptions = getAgentOptions( + 'Alice Discover Features V1 E2E', + { + endpoints: ['rxjs:alice'], + }, + undefined, + undefined, + { requireDidcomm: true } +) describe('v1 discover features', () => { let faberAgent: Agent @@ -39,9 +51,7 @@ describe('v1 discover features', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Faber asks Alice for revocation notification protocol support', async () => { diff --git a/packages/didcomm/src/modules/discover-features/__tests__/v2-discover-features.test.ts b/packages/didcomm/src/modules/discover-features/__tests__/v2-discover-features.test.ts index b8c33ddffd..a4783197e5 100644 --- a/packages/didcomm/src/modules/discover-features/__tests__/v2-discover-features.test.ts +++ b/packages/didcomm/src/modules/discover-features/__tests__/v2-discover-features.test.ts @@ -8,19 +8,31 @@ import { ReplaySubject } from 'rxjs' import { Agent } from '../../../../../core/src/agent/Agent' import { setupSubjectTransports } from '../../../../../core/tests' -import { getInMemoryAgentOptions, makeConnection } from '../../../../../core/tests/helpers' +import { getAgentOptions, makeConnection } from '../../../../../core/tests/helpers' import { Feature, GoalCode } from '../../../models' import { DiscoverFeaturesEventTypes } from '../DiscoverFeaturesEvents' import { waitForDisclosureSubject, waitForQuerySubject } from './helpers' -const faberAgentOptions = getInMemoryAgentOptions('Faber Discover Features V2 E2E', { - endpoints: ['rxjs:faber'], -}) - -const aliceAgentOptions = getInMemoryAgentOptions('Alice Discover Features V2 E2E', { - endpoints: ['rxjs:alice'], -}) +const faberAgentOptions = getAgentOptions( + 'Faber Discover Features V2 E2E', + { + endpoints: ['rxjs:faber'], + }, + undefined, + undefined, + { requireDidcomm: true } +) + +const aliceAgentOptions = getAgentOptions( + 'Alice Discover Features V2 E2E', + { + endpoints: ['rxjs:alice'], + }, + undefined, + undefined, + { requireDidcomm: true } +) describe('v2 discover features', () => { let faberAgent: Agent @@ -40,9 +52,7 @@ describe('v2 discover features', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Faber asks Alice for issue credential protocol support', async () => { diff --git a/packages/didcomm/src/modules/message-pickup/MessagePickupModule.ts b/packages/didcomm/src/modules/message-pickup/MessagePickupModule.ts index 31da8b6222..7f4da42617 100644 --- a/packages/didcomm/src/modules/message-pickup/MessagePickupModule.ts +++ b/packages/didcomm/src/modules/message-pickup/MessagePickupModule.ts @@ -67,9 +67,14 @@ export class MessagePickupModule { + // We only support initialization of message pickup for the root agent + if (!agentContext.isRootAgentContext) return + // FIXME: this does not take into account multi-tenant agents, need to think how to separate based on context + const messagePickupSessionService = agentContext.dependencyManager.resolve(MessagePickupSessionService) messagePickupSessionService.start(agentContext) } } diff --git a/packages/didcomm/src/modules/message-pickup/__tests__/MessagePickupModule.test.ts b/packages/didcomm/src/modules/message-pickup/__tests__/MessagePickupModule.test.ts index ca1dee64ff..5fc5f571a4 100644 --- a/packages/didcomm/src/modules/message-pickup/__tests__/MessagePickupModule.test.ts +++ b/packages/didcomm/src/modules/message-pickup/__tests__/MessagePickupModule.test.ts @@ -64,6 +64,9 @@ describe('MessagePickupModule', () => { expect(messagePickupProtocol.register).toHaveBeenCalledTimes(1) expect(messagePickupProtocol.register).toHaveBeenCalledWith(messageHandlerRegistry, featureRegistry) + expect(messagePickupSessionSessionService.start).not.toHaveBeenCalled() + + await messagePickupModule.onInitializeContext(agentContext) expect(messagePickupSessionSessionService.start).toHaveBeenCalledTimes(1) // TODO: add test in each protocol to verify that it is properly registered in the feature registry diff --git a/packages/didcomm/src/modules/message-pickup/__tests__/pickup.test.ts b/packages/didcomm/src/modules/message-pickup/__tests__/pickup.test.ts index 55c489d9b2..0911d73e9c 100644 --- a/packages/didcomm/src/modules/message-pickup/__tests__/pickup.test.ts +++ b/packages/didcomm/src/modules/message-pickup/__tests__/pickup.test.ts @@ -6,7 +6,7 @@ import { SubjectInboundTransport } from '../../../../../../tests/transport/Subje import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' import { Agent } from '../../../../../core/src/agent/Agent' import { - getInMemoryAgentOptions, + getAgentOptions, waitForAgentMessageProcessedEvent, waitForBasicMessage, } from '../../../../../core/tests/helpers' @@ -15,8 +15,11 @@ import { MediatorModule } from '../../routing' import { MessageForwardingStrategy } from '../../routing/MessageForwardingStrategy' import { V2MessagesReceivedMessage, V2StatusMessage } from '../protocol' -const recipientOptions = getInMemoryAgentOptions('Mediation Pickup Loop Recipient') -const mediatorOptions = getInMemoryAgentOptions( +const recipientOptions = getAgentOptions('Mediation Pickup Loop Recipient', undefined, undefined, undefined, { + requireDidcomm: true, + inMemory: false, +}) +const mediatorOptions = getAgentOptions( 'Mediation Pickup Loop Mediator', { endpoints: ['wss://mediator'], @@ -27,7 +30,8 @@ const mediatorOptions = getInMemoryAgentOptions( autoAcceptMediationRequests: true, messageForwardingStrategy: MessageForwardingStrategy.QueueAndLiveModeDelivery, }), - } + }, + { requireDidcomm: true, inMemory: false } ) describe('E2E Pick Up protocol', () => { @@ -38,9 +42,7 @@ describe('E2E Pick Up protocol', () => { await recipientAgent.modules.mediationRecipient.stopMessagePickup() await recipientAgent.shutdown() - await recipientAgent.wallet.delete() await mediatorAgent.shutdown() - await mediatorAgent.wallet.delete() }) test('E2E manual Pick Up V1 loop', async () => { @@ -76,7 +78,7 @@ describe('E2E Pick Up protocol', () => { ) recipientMediatorConnection = await recipientAgent.modules.connections.returnWhenIsConnected( - recipientMediatorConnection?.id + recipientMediatorConnection.id ) let [mediatorRecipientConnection] = await mediatorAgent.modules.connections.findAllByOutOfBandId( diff --git a/packages/didcomm/src/modules/oob/OutOfBandApi.ts b/packages/didcomm/src/modules/oob/OutOfBandApi.ts index c5943a9ac5..3916146475 100644 --- a/packages/didcomm/src/modules/oob/OutOfBandApi.ts +++ b/packages/didcomm/src/modules/oob/OutOfBandApi.ts @@ -13,7 +13,7 @@ import { InjectionSymbols, JsonEncoder, JsonTransformer, - Key, + Kms, Logger, filterContextCorrelationId, inject, @@ -50,7 +50,7 @@ import { HandshakeReuseAcceptedHandler } from './handlers/HandshakeReuseAccepted import { outOfBandServiceToInlineKeysNumAlgo2Did } from './helpers' import { InvitationType, OutOfBandInvitation } from './messages' import { OutOfBandRepository } from './repository' -import { OutOfBandRecord } from './repository/OutOfBandRecord' +import { OutOfBandInlineServiceKey, OutOfBandRecord } from './repository/OutOfBandRecord' import { OutOfBandRecordMetadataKeys } from './repository/outOfBandRecordMetadataTypes' const didCommProfiles = ['didcomm/aip1', 'didcomm/aip2;env=rfc19'] @@ -201,12 +201,19 @@ export class OutOfBandApi { throw new CredoError("Both 'routing' and 'invitationDid' cannot be provided at the same time.") } + const invitationInlineServiceKeys: OutOfBandInlineServiceKey[] = [] if (config.invitationDid) { services = [config.invitationDid] } else { const routing = config.routing ?? (await this.routingService.getRouting(this.agentContext, {})) mediatorId = routing?.mediatorId + services = routing.endpoints.map((endpoint, index) => { + // Store the key id for the recipient key + invitationInlineServiceKeys.push({ + kmsKeyId: routing.recipientKey.keyId, + recipientKeyFingerprint: routing.recipientKey.fingerprint, + }) return new OutOfBandDidCommService({ id: `#inline-${index}`, serviceEndpoint: endpoint, @@ -246,6 +253,7 @@ export class OutOfBandApi { outOfBandInvitation: outOfBandInvitation, reusable: multiUseInvitation, autoAcceptConnection, + invitationInlineServiceKeys, tags: { recipientKeyFingerprints, }, @@ -465,10 +473,12 @@ export class OutOfBandApi { this.logger.debug('Storing routing for out of band invitation.') outOfBandRecord.metadata.set(OutOfBandRecordMetadataKeys.RecipientRouting, { recipientKeyFingerprint: routing.recipientKey.fingerprint, + recipientKeyId: routing.recipientKey.keyId, routingKeyFingerprints: routing.routingKeys.map((key) => key.fingerprint), endpoints: routing.endpoints, mediatorId: routing.mediatorId, }) + outOfBandRecord.setTags({ recipientRoutingKeyFingerprint: routing.recipientKey.fingerprint }) } // If the invitation was converted from another legacy format, we store this, as its needed for some flows @@ -542,9 +552,16 @@ export class OutOfBandApi { // recipient routing from the receiveInvitation method. const recipientRouting = outOfBandRecord.metadata.get(OutOfBandRecordMetadataKeys.RecipientRouting) if (!routing && recipientRouting) { + const recipientPublicJwk = Kms.PublicJwk.fromFingerprint( + recipientRouting.recipientKeyFingerprint + ) as Kms.PublicJwk + recipientPublicJwk.keyId = recipientRouting.recipientKeyId ?? recipientPublicJwk.legacyKeyId + routing = { - recipientKey: Key.fromFingerprint(recipientRouting.recipientKeyFingerprint), - routingKeys: recipientRouting.routingKeyFingerprints.map((fingerprint) => Key.fromFingerprint(fingerprint)), + recipientKey: recipientPublicJwk, + routingKeys: recipientRouting.routingKeyFingerprints.map( + (fingerprint) => Kms.PublicJwk.fromFingerprint(fingerprint) as Kms.PublicJwk + ), endpoints: recipientRouting.endpoints, mediatorId: recipientRouting.mediatorId, } @@ -559,8 +576,7 @@ export class OutOfBandApi { if (handshakeProtocols && handshakeProtocols.length > 0) { this.logger.debug('Out of band message contains handshake protocols.') - // biome-ignore lint/suspicious/noImplicitAnyLet: - let connectionRecord + let connectionRecord: ConnectionRecord | undefined = undefined if (existingConnection && reuseConnection) { this.logger.debug( `Connection already exists and reuse is enabled. Reusing an existing connection with ID ${existingConnection.id}.` @@ -705,7 +721,11 @@ export class OutOfBandApi { outOfBandRecord.outOfBandInvitation.getDidServices().length === 0 && (relatedConnections.length === 0 || outOfBandRecord.reusable) ) { - const recipientKeys = outOfBandRecord.getTags().recipientKeyFingerprints.map((item) => Key.fromFingerprint(item)) + const recipientKeys = outOfBandRecord + .getTags() + .recipientKeyFingerprints.map( + (item) => Kms.PublicJwk.fromFingerprint(item) as Kms.PublicJwk + ) await this.routingService.removeRouting(this.agentContext, { recipientKeys, @@ -965,12 +985,17 @@ export class OutOfBandApi { ) recipientKeyFingerprints.push( ...resolvedDidCommServices - // biome-ignore lint/performance/noAccumulatingSpread: - .reduce((aggr, { recipientKeys }) => [...aggr, ...recipientKeys], []) + .reduce[]>( + // biome-ignore lint/performance/noAccumulatingSpread: + (aggr, { recipientKeys }) => [...aggr, ...recipientKeys], + [] + ) .map((key) => key.fingerprint) ) } else { - recipientKeyFingerprints.push(...service.recipientKeys.map((didKey) => DidKey.fromDid(didKey).key.fingerprint)) + recipientKeyFingerprints.push( + ...service.recipientKeys.map((didKey) => DidKey.fromDid(didKey).publicJwk.fingerprint) + ) } } diff --git a/packages/didcomm/src/modules/oob/OutOfBandService.ts b/packages/didcomm/src/modules/oob/OutOfBandService.ts index ea02ce0cb0..2f03f54f17 100644 --- a/packages/didcomm/src/modules/oob/OutOfBandService.ts +++ b/packages/didcomm/src/modules/oob/OutOfBandService.ts @@ -1,4 +1,4 @@ -import type { AgentContext, Key, Query, QueryOptions } from '@credo-ts/core' +import type { AgentContext, Kms, Query, QueryOptions } from '@credo-ts/core' import type { InboundMessageContext } from '../../models' import type { ConnectionRecord, HandshakeProtocol } from '../connections' import type { OutOfBandDidCommService } from './domain' @@ -8,19 +8,20 @@ import { CredoError, DidsApi, EventEmitter, injectable, parseDid } from '@credo- import { DidCommDocumentService } from '../../services' +import { getResolvedDidcommServiceWithSigningKeyId } from '../connections/services/helpers' import { OutOfBandEventTypes } from './domain/OutOfBandEvents' import { OutOfBandRole } from './domain/OutOfBandRole' import { OutOfBandState } from './domain/OutOfBandState' import { HandshakeReuseMessage, OutOfBandInvitation } from './messages' import { HandshakeReuseAcceptedMessage } from './messages/HandshakeReuseAcceptedMessage' -import { OutOfBandRecord, OutOfBandRepository } from './repository' +import { OutOfBandInlineServiceKey, OutOfBandRecord, OutOfBandRepository } from './repository' export interface CreateFromImplicitInvitationConfig { did: string threadId: string handshakeProtocols: HandshakeProtocol[] autoAcceptConnection?: boolean - recipientKey: Key + recipientKey: Kms.PublicJwk } @injectable() @@ -233,7 +234,10 @@ export class OutOfBandService { }) } - public async findCreatedByRecipientKey(agentContext: AgentContext, recipientKey: Key) { + public async findCreatedByRecipientKey( + agentContext: AgentContext, + recipientKey: Kms.PublicJwk + ) { return this.outOfBandRepository.findSingleByQuery(agentContext, { recipientKeyFingerprints: [recipientKey.fingerprint], role: OutOfBandRole.Sender, @@ -260,7 +264,11 @@ export class OutOfBandService { */ public async getResolvedServiceForOutOfBandServices( agentContext: AgentContext, - services: Array + services: Array, + /** + * Optional keys for the inline services + */ + inlineServiceKeys?: OutOfBandInlineServiceKey[] ) { for (const service of services) { if (typeof service === 'string') { @@ -268,7 +276,7 @@ export class OutOfBandService { if (didService) return didService } else { - return service.resolvedDidCommService + return getResolvedDidcommServiceWithSigningKeyId(service, inlineServiceKeys) } } diff --git a/packages/didcomm/src/modules/oob/__tests__/OutOfBandService.test.ts b/packages/didcomm/src/modules/oob/__tests__/OutOfBandService.test.ts index f8e61beb0c..8e39203403 100644 --- a/packages/didcomm/src/modules/oob/__tests__/OutOfBandService.test.ts +++ b/packages/didcomm/src/modules/oob/__tests__/OutOfBandService.test.ts @@ -2,8 +2,8 @@ import type { DidCommDocumentService } from '../../../services' import { Subject } from 'rxjs' +import { Kms, TypedArrayEncoder } from '@credo-ts/core' import { EventEmitter } from '../../../../../core/src/agent/EventEmitter' -import { Key, KeyType } from '../../../../../core/src/crypto' import { CredoError } from '../../../../../core/src/error' import { agentDependencies, @@ -25,7 +25,11 @@ import { OutOfBandRepository } from '../repository' jest.mock('../repository/OutOfBandRepository') const OutOfBandRepositoryMock = OutOfBandRepository as jest.Mock -const key = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) +const key = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), +}) const agentContext = getAgentContext() diff --git a/packages/didcomm/src/modules/oob/__tests__/connect-to-self.test.ts b/packages/didcomm/src/modules/oob/__tests__/connect-to-self.test.ts index fb8da7f6c6..435eaab8d6 100644 --- a/packages/didcomm/src/modules/oob/__tests__/connect-to-self.test.ts +++ b/packages/didcomm/src/modules/oob/__tests__/connect-to-self.test.ts @@ -5,13 +5,19 @@ import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' import { Agent } from '../../../../../core' -import { getInMemoryAgentOptions } from '../../../../../core/tests/helpers' +import { getAgentOptions } from '../../../../../core/tests/helpers' import { DidExchangeState, HandshakeProtocol } from '../../connections' import { OutOfBandState } from '../domain/OutOfBandState' -const faberAgentOptions = getInMemoryAgentOptions('Faber Agent OOB Connect to Self', { - endpoints: ['rxjs:faber'], -}) +const faberAgentOptions = getAgentOptions( + 'Faber Agent OOB Connect to Self', + { + endpoints: ['rxjs:faber'], + }, + undefined, + undefined, + { requireDidcomm: true } +) describe('out of band', () => { let faberAgent: Agent @@ -31,7 +37,6 @@ describe('out of band', () => { afterEach(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() }) describe('connect with self', () => { diff --git a/packages/didcomm/src/modules/oob/__tests__/implicit.test.ts b/packages/didcomm/src/modules/oob/__tests__/implicit.test.ts index 9ca334d59a..836fa32ff6 100644 --- a/packages/didcomm/src/modules/oob/__tests__/implicit.test.ts +++ b/packages/didcomm/src/modules/oob/__tests__/implicit.test.ts @@ -1,5 +1,5 @@ +import { type DidDocumentKey, Kms } from '@credo-ts/core' import { Agent } from '../../../../../core/src/agent/Agent' -import { KeyType } from '../../../../../core/src/crypto' import { DidCommV1Service, DidDocumentBuilder, @@ -10,13 +10,13 @@ import { getEd25519VerificationKey2018, } from '../../../../../core/src/modules/dids' import { setupSubjectTransports } from '../../../../../core/tests' -import { getInMemoryAgentOptions, waitForConnectionRecord } from '../../../../../core/tests/helpers' +import { getAgentOptions, waitForConnectionRecord } from '../../../../../core/tests/helpers' import { DidExchangeState, HandshakeProtocol } from '../../connections' import { InMemoryDidRegistry } from '../../connections/__tests__/InMemoryDidRegistry' const inMemoryDidsRegistry = new InMemoryDidRegistry() -const faberAgentOptions = getInMemoryAgentOptions( +const faberAgentOptions = getAgentOptions( 'Faber Agent OOB Implicit', { endpoints: ['rxjs:faber'], @@ -27,9 +27,10 @@ const faberAgentOptions = getInMemoryAgentOptions( resolvers: [inMemoryDidsRegistry], registrars: [inMemoryDidsRegistry], }), - } + }, + { requireDidcomm: true } ) -const aliceAgentOptions = getInMemoryAgentOptions( +const aliceAgentOptions = getAgentOptions( 'Alice Agent OOB Implicit', { endpoints: ['rxjs:alice'], @@ -40,7 +41,8 @@ const aliceAgentOptions = getInMemoryAgentOptions( resolvers: [inMemoryDidsRegistry], registrars: [inMemoryDidsRegistry], }), - } + }, + { requireDidcomm: true } ) describe('out of band implicit', () => { @@ -58,9 +60,7 @@ describe('out of band implicit', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) afterEach(async () => { @@ -238,15 +238,19 @@ describe('out of band implicit', () => { }) async function createInMemoryDid(agent: Agent, endpoint: string) { - const ed25519Key = await agent.wallet.createKey({ - keyType: KeyType.Ed25519, + const ed25519Key = await agent.kms.createKey({ + type: { + kty: 'OKP', + crv: 'Ed25519', + }, }) + const publicJwk = Kms.PublicJwk.fromPublicJwk(ed25519Key.publicJwk) - const did = `did:inmemory:${ed25519Key.fingerprint}` + const did = `did:inmemory:${publicJwk.fingerprint}` const builder = new DidDocumentBuilder(did) const ed25519VerificationMethod = getEd25519VerificationKey2018({ - key: ed25519Key, - id: `${did}#${ed25519Key.fingerprint}`, + publicJwk, + id: `${did}#${publicJwk.fingerprint}`, controller: did, }) @@ -286,7 +290,19 @@ async function createInMemoryDid(agent: Agent, endpoint: string) { // Create the did:inmemory did const { didState: { state }, - } = await agent.dids.create({ did, didDocument: builder.build() }) + } = await agent.dids.create({ + did, + didDocument: builder.build(), + options: { + keys: [ + { + didDocumentRelativeKeyId: `#${publicJwk.fingerprint}`, + kmsKeyId: ed25519Key.keyId, + } satisfies DidDocumentKey, + ], + }, + }) + if (state !== 'finished') { throw new Error('Error creating DID') } diff --git a/packages/didcomm/src/modules/oob/converters.ts b/packages/didcomm/src/modules/oob/converters.ts index 64a8d637d4..c187b586f8 100644 --- a/packages/didcomm/src/modules/oob/converters.ts +++ b/packages/didcomm/src/modules/oob/converters.ts @@ -9,14 +9,16 @@ import { verkeyToDidKey, } from '@credo-ts/core' -import { ConnectionInvitationMessage } from '../connections/messages/ConnectionInvitationMessage' +import { + ConnectionInvitationMessage, + ConnectionInvitationMessageOptions, +} from '../connections/messages/ConnectionInvitationMessage' import { OutOfBandDidCommService } from './domain/OutOfBandDidCommService' import { InvitationType, OutOfBandInvitation } from './messages/OutOfBandInvitation' export function convertToNewInvitation(oldInvitation: ConnectionInvitationMessage) { - // biome-ignore lint/suspicious/noImplicitAnyLet: - let service + let service: string | OutOfBandDidCommService if (oldInvitation.did) { service = oldInvitation.did @@ -52,8 +54,7 @@ export function convertToOldInvitation(newInvitation: OutOfBandInvitation) { // Taking first service, as we can only include one service in a legacy invitation. const [service] = newInvitation.getServices() - // biome-ignore lint/suspicious/noImplicitAnyLet: - let options + let options: ConnectionInvitationMessageOptions if (typeof service === 'string') { options = { id: newInvitation.id, @@ -91,12 +92,12 @@ export function outOfBandServiceToNumAlgo4Did(service: OutOfBandDidCommService) // FIXME: this should actually be local key references, not did:key:123#456 references recipientKeys: service.recipientKeys.map((recipientKey) => { const did = DidKey.fromDid(recipientKey) - return `${did.did}#${did.key.fingerprint}` + return `${did.did}#${did.publicJwk.fingerprint}` }), // Map did:key:xxx to actual did:key:xxx#123 routingKeys: service.routingKeys?.map((routingKey) => { const did = DidKey.fromDid(routingKey) - return `${did.did}#${did.key.fingerprint}` + return `${did.did}#${did.publicJwk.fingerprint}` }), }) ) diff --git a/packages/didcomm/src/modules/oob/domain/OutOfBandDidCommService.ts b/packages/didcomm/src/modules/oob/domain/OutOfBandDidCommService.ts index a4710c7d8d..83dc97c7b6 100644 --- a/packages/didcomm/src/modules/oob/domain/OutOfBandDidCommService.ts +++ b/packages/didcomm/src/modules/oob/domain/OutOfBandDidCommService.ts @@ -1,7 +1,7 @@ import type { ResolvedDidCommService } from '@credo-ts/core' import type { ValidationOptions } from 'class-validator' -import { DidDocumentService, DidKey, IsUri, isDid } from '@credo-ts/core' +import { CredoError, DidDocumentService, DidKey, IsUri, Kms, isDid } from '@credo-ts/core' import { ArrayNotEmpty, IsOptional, IsString, ValidateBy, buildMessage, isString } from 'class-validator' export class OutOfBandDidCommService extends DidDocumentService { @@ -42,8 +42,23 @@ export class OutOfBandDidCommService extends DidDocumentService { public get resolvedDidCommService(): ResolvedDidCommService { return { id: this.id, - recipientKeys: this.recipientKeys.map((didKey) => DidKey.fromDid(didKey).key), - routingKeys: this.routingKeys?.map((didKey) => DidKey.fromDid(didKey).key) ?? [], + recipientKeys: this.recipientKeys.map((didKey) => { + const publicJwk = DidKey.fromDid(didKey).publicJwk + if (!publicJwk.is(Kms.Ed25519PublicJwk)) { + throw new CredoError('Expected recipient key for didcomm service to be of type Ed25519') + } + + return publicJwk + }), + routingKeys: + this.routingKeys?.map((didKey) => { + const publicJwk = DidKey.fromDid(didKey).publicJwk + if (!publicJwk.is(Kms.Ed25519PublicJwk)) { + throw new CredoError('Expected recipient key for didcomm service to be of type Ed25519') + } + + return publicJwk + }) ?? [], serviceEndpoint: this.serviceEndpoint, } } diff --git a/packages/didcomm/src/modules/oob/helpers.ts b/packages/didcomm/src/modules/oob/helpers.ts index 569f0e55cb..b67b954367 100644 --- a/packages/didcomm/src/modules/oob/helpers.ts +++ b/packages/didcomm/src/modules/oob/helpers.ts @@ -6,7 +6,7 @@ import { DidKey, createPeerDidDocumentFromServices, didDocumentToNumAlgo2Did, - didKeyToInstanceOfKey, + didKeyToEd25519PublicJwk, } from '@credo-ts/core' // This method is kept to support searching for existing connections created by @@ -21,12 +21,12 @@ export function outOfBandServiceToInlineKeysNumAlgo2Did(service: OutOfBandDidCom accept: service.accept, recipientKeys: service.recipientKeys.map((recipientKey) => { const did = DidKey.fromDid(recipientKey) - return `${did.did}#${did.key.fingerprint}` + return `${did.did}#${did.publicJwk.fingerprint}` }), // Map did:key:xxx to actual did:key:xxx#123 routingKeys: service.routingKeys?.map((routingKey) => { const did = DidKey.fromDid(routingKey) - return `${did.did}#${did.key.fingerprint}` + return `${did.did}#${did.publicJwk.fingerprint}` }), }) ) @@ -38,14 +38,17 @@ export function outOfBandServiceToInlineKeysNumAlgo2Did(service: OutOfBandDidCom } export function outOfBandServiceToNumAlgo2Did(service: OutOfBandDidCommService) { - const didDocument = createPeerDidDocumentFromServices([ - { - id: service.id, - recipientKeys: service.recipientKeys.map(didKeyToInstanceOfKey), - serviceEndpoint: service.serviceEndpoint, - routingKeys: service.routingKeys?.map(didKeyToInstanceOfKey) ?? [], - }, - ]) + const { didDocument } = createPeerDidDocumentFromServices( + [ + { + id: service.id, + recipientKeys: service.recipientKeys.map(didKeyToEd25519PublicJwk), + serviceEndpoint: service.serviceEndpoint, + routingKeys: service.routingKeys?.map(didKeyToEd25519PublicJwk) ?? [], + }, + ], + false + ) const did = didDocumentToNumAlgo2Did(didDocument) diff --git a/packages/didcomm/src/modules/oob/repository/OutOfBandRecord.ts b/packages/didcomm/src/modules/oob/repository/OutOfBandRecord.ts index 686a72c7d5..3f16d33942 100644 --- a/packages/didcomm/src/modules/oob/repository/OutOfBandRecord.ts +++ b/packages/didcomm/src/modules/oob/repository/OutOfBandRecord.ts @@ -9,6 +9,11 @@ import { Type } from 'class-transformer' import { getThreadIdFromPlainTextMessage } from '../../../util/thread' import { OutOfBandInvitation } from '../messages' +export interface OutOfBandInlineServiceKey { + recipientKeyFingerprint: string + kmsKeyId: string +} + type DefaultOutOfBandRecordTags = { role: OutOfBandRole state: OutOfBandState @@ -22,7 +27,19 @@ type DefaultOutOfBandRecordTags = { } interface CustomOutOfBandRecordTags extends TagsBase { + /** + * The fingerprints of the recipient keys from the out of band invitation. + * When we created the invitation this will be our keys, when we received this + * invitation it will be the other parties' keys. + */ recipientKeyFingerprints: string[] + + /** + * The fingerprint from the {@link OutOfBandRecordMetadataKeys.RecipientRouting} recipient key. + * + * This will always be a key from our recipient + */ + recipientRoutingKeyFingerprint?: string } export interface OutOfBandRecordProps { @@ -39,6 +56,11 @@ export interface OutOfBandRecordProps { mediatorId?: string reuseConnectionId?: string threadId?: string + + /** + * The keys associated with the inline services of the out of band invitation + */ + invitationInlineServiceKeys?: OutOfBandInlineServiceKey[] } export class OutOfBandRecord extends BaseRecord< @@ -56,6 +78,11 @@ export class OutOfBandRecord extends BaseRecord< public mediatorId?: string public reuseConnectionId?: string + /** + * The keys associated with the inline services of the out of band invitation + */ + invitationInlineServiceKeys?: Array + public static readonly type = 'OutOfBandRecord' public readonly type = OutOfBandRecord.type @@ -73,6 +100,7 @@ export class OutOfBandRecord extends BaseRecord< this.reusable = props.reusable ?? false this.mediatorId = props.mediatorId this.reuseConnectionId = props.reuseConnectionId + this.invitationInlineServiceKeys = props.invitationInlineServiceKeys this._tags = props.tags ?? { recipientKeyFingerprints: [] } } } diff --git a/packages/didcomm/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts b/packages/didcomm/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts index 079339a9bf..4397035a2e 100644 --- a/packages/didcomm/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts +++ b/packages/didcomm/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts @@ -8,6 +8,10 @@ export enum OutOfBandRecordMetadataKeys { export type OutOfBandRecordMetadata = { [OutOfBandRecordMetadataKeys.RecipientRouting]: { recipientKeyFingerprint: string + /** + * Optional key id to use for the recipient key. If not configured the legacy base58 encoded public key will be used as the key id + */ + recipientKeyId?: string routingKeyFingerprints: string[] endpoints: string[] mediatorId?: string diff --git a/packages/didcomm/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/didcomm/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index ce7b3ed4df..5ded469417 100644 --- a/packages/didcomm/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/didcomm/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -38,7 +38,9 @@ import { DifPresentationExchangeService, DifPresentationExchangeSubmissionLocation, JsonTransformer, + Kms, MdocDeviceResponse, + TypedArrayEncoder, W3cCredentialService, W3cJsonLdVerifiablePresentation, W3cJwtVerifiablePresentation, @@ -119,12 +121,15 @@ export class DifPresentationExchangeProofFormatService const presentationDefinition = proposalAttachment.getDataAsJson() ps.validatePresentationDefinition(presentationDefinition) + const kms = agentContext.resolve(Kms.KeyManagementApi) const attachment = this.getFormatData( { presentation_definition: presentationDefinition, options: { // NOTE: we always want to include a challenge to prevent replay attacks - challenge: presentationExchangeFormat?.options?.challenge ?? (await agentContext.wallet.generateNonce()), + challenge: + presentationExchangeFormat?.options?.challenge ?? + TypedArrayEncoder.toBase64URL(kms.randomBytes({ length: 32 }).bytes), domain: presentationExchangeFormat?.options?.domain, }, } satisfies DifPresentationExchangeRequest, @@ -154,12 +159,13 @@ export class DifPresentationExchangeProofFormatService attachmentId, }) + const kms = agentContext.resolve(Kms.KeyManagementApi) const attachment = this.getFormatData( { presentation_definition: presentationDefinition, options: { // NOTE: we always want to include a challenge to prevent replay attacks - challenge: options?.challenge ?? (await agentContext.wallet.generateNonce()), + challenge: options?.challenge ?? TypedArrayEncoder.toBase64URL(kms.randomBytes({ length: 32 }).bytes), domain: options?.domain, }, } satisfies DifPresentationExchangeRequest, @@ -202,10 +208,11 @@ export class DifPresentationExchangeProofFormatService credentials = ps.selectCredentialsForRequest(credentialsForRequest) } + const kms = agentContext.resolve(Kms.KeyManagementApi) const presentation = await ps.createPresentation(agentContext, { presentationDefinition, credentialsForInputDescriptor: credentials, - challenge: options?.challenge ?? (await agentContext.wallet.generateNonce()), + challenge: options?.challenge ?? TypedArrayEncoder.toBase64URL(kms.randomBytes({ length: 32 }).bytes), domain: options?.domain, }) diff --git a/packages/didcomm/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/didcomm/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts index a0d2190fd9..e8f64eb84f 100644 --- a/packages/didcomm/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts +++ b/packages/didcomm/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -16,7 +16,7 @@ import { W3cJsonLdVerifiableCredential, W3cJsonLdVerifiablePresentation, } from '../../../../../../../core/src/modules/vc' -import { getInMemoryAgentOptions } from '../../../../../../../core/tests' +import { getAgentOptions } from '../../../../../../../core/tests' import { ProofsModule } from '../../../ProofsModule' import { ProofRole, ProofState } from '../../../models' import { V2ProofProtocol } from '../../../protocol' @@ -96,7 +96,7 @@ describe('Presentation Exchange ProofFormatService', () => { beforeAll(async () => { agent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'PresentationExchangeProofFormatService', {}, {}, @@ -105,7 +105,8 @@ describe('Presentation Exchange ProofFormatService', () => { proofs: new ProofsModule({ proofProtocols: [new V2ProofProtocol({ proofFormats: [new DifPresentationExchangeProofFormatService()] })], }), - } + }, + { requireDidcomm: true } ) ) diff --git a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-anoncreds-unqualified-proofs.e2e.test.ts b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-anoncreds-unqualified-proofs.e2e.test.ts index fd41494e43..1c4251db9c 100644 --- a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-anoncreds-unqualified-proofs.e2e.test.ts +++ b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-anoncreds-unqualified-proofs.e2e.test.ts @@ -82,9 +82,7 @@ describe('Present Proof', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice starts with proof proposal to Faber', async () => { diff --git a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts index 8f8f32c969..6c9c2536b3 100644 --- a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts +++ b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts @@ -15,7 +15,7 @@ import { import { Agent } from '../../../../../../../core' import { uuid } from '../../../../../../../core/src/utils/uuid' import { - getInMemoryAgentOptions, + getAgentOptions, makeConnection, setupEventReplaySubjects, testLogger, @@ -32,7 +32,6 @@ import { MediationRecipientModule, MediatorModule, MediatorPickupStrategy, - MessageReceiver, ProofEventTypes, ProofState, } from '../../../../../../src' @@ -43,7 +42,6 @@ describe('V2 Connectionless Proofs - Indy', () => { afterEach(async () => { for (const agent of agents) { await agent.shutdown() - await agent.wallet.delete() } }) @@ -118,13 +116,13 @@ describe('V2 Connectionless Proofs - Indy', () => { }, }) - const { message: requestMessage } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ + const { invitationUrl } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ recordId: faberProofExchangeRecord.id, message, domain: 'https://a-domain.com', }) - await aliceAgent.dependencyManager.resolve(MessageReceiver).receiveMessage(requestMessage.toJSON()) + await aliceAgent.modules.oob.receiveInvitationFromUrl(invitationUrl) testLogger.test('Alice waits for presentation request from Faber') let aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, { @@ -241,13 +239,14 @@ describe('V2 Connectionless Proofs - Indy', () => { autoAcceptProof: AutoAcceptProof.ContentApproved, }) - const { message: requestMessage } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ - recordId: faberProofExchangeRecord.id, - message, - domain: 'https://a-domain.com', - }) + const { invitationUrl, message: requestMessage } = + await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'https://a-domain.com', + }) - await aliceAgent.dependencyManager.resolve(MessageReceiver).receiveMessage(requestMessage.toJSON()) + await aliceAgent.modules.oob.receiveInvitationFromUrl(invitationUrl) await waitForProofExchangeRecordSubject(aliceReplay, { state: ProofState.Done, @@ -270,7 +269,7 @@ describe('V2 Connectionless Proofs - Indy', () => { const unique = uuid().substring(0, 4) - const mediatorOptions = getInMemoryAgentOptions( + const mediatorOptions = getAgentOptions( `Connectionless proofs with mediator Mediator-${unique}`, { endpoints: ['rxjs:mediator'], @@ -283,7 +282,8 @@ describe('V2 Connectionless Proofs - Indy', () => { mediator: new MediatorModule({ autoAcceptMediationRequests: true, }), - } + }, + { requireDidcomm: true } ) const mediatorMessages = new Subject() @@ -305,7 +305,7 @@ describe('V2 Connectionless Proofs - Indy', () => { handshakeProtocols: [HandshakeProtocol.Connections], }) - const faberOptions = getInMemoryAgentOptions( + const faberOptions = getAgentOptions( `Connectionless proofs with mediator Faber-${unique}`, {}, {}, @@ -319,10 +319,11 @@ describe('V2 Connectionless Proofs - Indy', () => { }), mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - } + }, + { requireDidcomm: true } ) - const aliceOptions = getInMemoryAgentOptions( + const aliceOptions = getAgentOptions( `Connectionless proofs with mediator Alice-${unique}`, {}, {}, @@ -336,18 +337,17 @@ describe('V2 Connectionless Proofs - Indy', () => { }), mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - } + }, + { requireDidcomm: true } ) const faberAgent = new Agent(faberOptions) faberAgent.modules.didcomm.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await faberAgent.initialize() - await faberAgent.modules.mediationRecipient.initialize() const aliceAgent = new Agent(aliceOptions) aliceAgent.modules.didcomm.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() - await aliceAgent.modules.mediationRecipient.initialize() const [faberReplay, aliceReplay] = setupEventReplaySubjects( [faberAgent, aliceAgent], @@ -423,11 +423,12 @@ describe('V2 Connectionless Proofs - Indy', () => { autoAcceptProof: AutoAcceptProof.ContentApproved, }) - const { message: requestMessage } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ - recordId: faberProofExchangeRecord.id, - message, - domain: 'https://a-domain.com', - }) + const { message: requestMessage, invitationUrl } = + await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'https://a-domain.com', + }) const mediationRecord = await faberAgent.modules.mediationRecipient.findDefaultMediator() if (!mediationRecord) throw new Error('Faber agent has no default mediator') @@ -440,7 +441,7 @@ describe('V2 Connectionless Proofs - Indy', () => { }, }) - await aliceAgent.dependencyManager.resolve(MessageReceiver).receiveMessage(requestMessage.toJSON()) + await aliceAgent.modules.oob.receiveInvitationFromUrl(invitationUrl) await waitForProofExchangeRecordSubject(aliceReplay, { state: ProofState.Done, @@ -526,17 +527,18 @@ describe('V2 Connectionless Proofs - Indy', () => { autoAcceptProof: AutoAcceptProof.ContentApproved, }) - const { message: requestMessage } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ - recordId: faberProofExchangeRecord.id, - message, - domain: 'rxjs:faber', - }) + const { message: requestMessage, invitationUrl } = + await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'rxjs:faber', + }) for (const transport of faberAgent.modules.didcomm.outboundTransports) { await faberAgent.modules.didcomm.unregisterOutboundTransport(transport) } - await aliceAgent.dependencyManager.resolve(MessageReceiver).receiveMessage(requestMessage.toJSON()) + await aliceAgent.modules.oob.receiveInvitationFromUrl(invitationUrl) await waitForProofExchangeRecordSubject(aliceReplay, { state: ProofState.Done, threadId: requestMessage.threadId, @@ -610,11 +612,12 @@ describe('V2 Connectionless Proofs - Indy', () => { autoAcceptProof: AutoAcceptProof.ContentApproved, }) - const { message: requestMessage } = await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ - recordId: faberProofExchangeRecord.id, - message, - domain: 'rxjs:faber', - }) + const { message: requestMessage, invitationUrl } = + await faberAgent.modules.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'rxjs:faber', + }) for (const transport of faberAgent.modules.didcomm.outboundTransports) { await faberAgent.modules.didcomm.unregisterOutboundTransport(transport) @@ -624,7 +627,7 @@ describe('V2 Connectionless Proofs - Indy', () => { state: ProofState.RequestReceived, }) - await aliceAgent.dependencyManager.resolve(MessageReceiver).receiveMessage(requestMessage.toJSON()) + await aliceAgent.modules.oob.receiveInvitationFromUrl(invitationUrl) const aliceProofExchangeRecord = await aliceProofExchangeRecordPromise await aliceAgent.modules.proofs.declineRequest({ diff --git a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-negotiation.e2e.test.ts b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-negotiation.e2e.test.ts index ef4947486d..b5b05c4ae7 100644 --- a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-negotiation.e2e.test.ts +++ b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-negotiation.e2e.test.ts @@ -63,9 +63,7 @@ describe('V2 Proofs Negotiation - Indy', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Proof negotiation between Alice and Faber', async () => { diff --git a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-presentation.e2e.test.ts b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-presentation.e2e.test.ts index b4e063b8a8..49cd3b1ea8 100644 --- a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-presentation.e2e.test.ts +++ b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-presentation.e2e.test.ts @@ -60,9 +60,7 @@ describe('V2 Proofs - Indy', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice Creates and sends Proof Proposal to Faber', async () => { diff --git a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-request.e2e.test.ts b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-request.e2e.test.ts index f350aa8d56..af722fa7e2 100644 --- a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-request.e2e.test.ts +++ b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proof-request.e2e.test.ts @@ -60,9 +60,7 @@ describe('V2 Proofs - Indy', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice Creates and sends Proof Proposal to Faber', async () => { diff --git a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs-auto-accept.e2e.test.ts b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs-auto-accept.e2e.test.ts index 0fced5b44c..92f1e5d725 100644 --- a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs-auto-accept.e2e.test.ts +++ b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs-auto-accept.e2e.test.ts @@ -58,9 +58,7 @@ describe('Auto accept present proof', () => { afterAll(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test("Alice starts with proof proposal to Faber, both with autoAcceptProof on 'always'", async () => { @@ -188,9 +186,7 @@ describe('Auto accept present proof', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test("Alice starts with proof proposal to Faber, both with autoAcceptProof on 'contentApproved'", async () => { diff --git a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts index ffb48de330..b73ab01256 100644 --- a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts +++ b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-indy-proofs.e2e.test.ts @@ -81,9 +81,7 @@ describe('Present Proof', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice starts with proof proposal to Faber', async () => { diff --git a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.test.ts b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.test.ts index 312e7c02e5..eb5f2a0b0c 100644 --- a/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.test.ts +++ b/packages/didcomm/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.test.ts @@ -1,7 +1,7 @@ import type { Agent } from '../../../../../../../core' import type { getJsonLdModules } from '../../../../../../../core/tests' -import { CREDENTIALS_CONTEXT_V1_URL, KeyType, TypedArrayEncoder } from '../../../../../../../core' +import { CREDENTIALS_CONTEXT_V1_URL, TypedArrayEncoder } from '../../../../../../../core' import { setupJsonLdTests, waitForCredentialRecord, waitForProofExchangeRecord } from '../../../../../../../core/tests' import testLogger from '../../../../../../../core/tests/logger' import { DidCommMessageRepository } from '../../../../../repository' @@ -10,6 +10,7 @@ import { ProofState } from '../../../models/ProofState' import { V2PresentationMessage, V2RequestPresentationMessage } from '../messages' import { V2ProposePresentationMessage } from '../messages/V2ProposePresentationMessage' +import { transformPrivateKeyToPrivateJwk } from '../../../../../../../askar/src' import { TEST_INPUT_DESCRIPTORS_CITIZENSHIP } from './fixtures' const jsonld = { @@ -56,14 +57,38 @@ describe('Present Proof', () => { autoAcceptCredentials: AutoAcceptCredential.Always, })) - await issuerAgent.wallet.createKey({ - privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), - keyType: KeyType.Ed25519, + const issuerKey = await issuerAgent.kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + type: { kty: 'OKP', crv: 'Ed25519' }, + }).privateJwk, }) - await proverAgent.wallet.createKey({ - privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), - keyType: KeyType.Ed25519, + await issuerAgent.dids.import({ + did: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + keys: [ + { + didDocumentRelativeKeyId: '#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + kmsKeyId: issuerKey.keyId, + }, + ], + }) + + const proverKey = await proverAgent.kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + type: { kty: 'OKP', crv: 'Ed25519' }, + }).privateJwk, + }) + + await proverAgent.dids.import({ + did: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + keys: [ + { + didDocumentRelativeKeyId: '#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + kmsKeyId: proverKey.keyId, + }, + ], }) await issuerAgent.modules.credentials.offerCredential({ @@ -78,9 +103,7 @@ describe('Present Proof', () => { afterAll(async () => { testLogger.test('Shutting down both agents') await proverAgent.shutdown() - await proverAgent.wallet.delete() await verifierAgent.shutdown() - await verifierAgent.wallet.delete() }) test('Prover Creates and sends Proof Proposal to a Verifier', async () => { diff --git a/packages/didcomm/src/modules/routing/MediationRecipientApi.ts b/packages/didcomm/src/modules/routing/MediationRecipientApi.ts index ab85c3da0d..b8937bda7a 100644 --- a/packages/didcomm/src/modules/routing/MediationRecipientApi.ts +++ b/packages/didcomm/src/modules/routing/MediationRecipientApi.ts @@ -25,14 +25,12 @@ import { MessageHandlerRegistry } from '../../MessageHandlerRegistry' import { MessageSender } from '../../MessageSender' import { OutboundMessageContext } from '../../models' import { TransportEventTypes } from '../../transport' -import { ConnectionsApi } from '../connections' import { ConnectionMetadataKeys } from '../connections/repository/ConnectionMetadataTypes' import { ConnectionService } from '../connections/services' import { DiscoverFeaturesApi } from '../discover-features' import { MessagePickupApi } from '../message-pickup/MessagePickupApi' import { V1BatchPickupMessage } from '../message-pickup/protocol/v1' import { V2StatusMessage } from '../message-pickup/protocol/v2' -import { OutOfBandApi } from '../oob' import { MediationRecipientModuleConfig } from './MediationRecipientModuleConfig' import { MediatorPickupStrategy } from './MediatorPickupStrategy' @@ -98,27 +96,6 @@ export class MediationRecipientApi { this.registerMessageHandlers(messageHandlerRegistry) } - public async initialize() { - // Connect to mediator through provided invitation if provided in config - // Also requests mediation ans sets as default mediator - // Because this requires the connections module, we do this in the agent constructor - if (this.config.mediatorInvitationUrl) { - this.agentContext.config.logger.debug('Provision mediation with invitation', { - mediatorInvitationUrl: this.config.mediatorInvitationUrl, - }) - const mediationConnection = await this.getMediationConnection(this.config.mediatorInvitationUrl) - await this.provision(mediationConnection) - } - - // Poll for messages from mediator - const defaultMediator = await this.findDefaultMediator() - if (defaultMediator) { - this.initiateMessagePickup(defaultMediator).catch((error) => { - this.logger.warn(`Error initiating message pickup with mediator ${defaultMediator.id}`, { error }) - }) - } - } - private async sendMessage(outboundMessageContext: OutboundMessageContext, pickupStrategy?: MediatorPickupStrategy) { const mediatorPickupStrategy = pickupStrategy ?? this.config.mediatorPickupStrategy const transportPriority = @@ -488,6 +465,7 @@ export class MediationRecipientApi { * @param connection connection record which will be used for mediation * @returns mediation record */ + // TODO: we should rename this method, to something that is more descriptive public async provision(connection: ConnectionRecord) { this.logger.debug('Connection completed, requesting mediation') @@ -516,37 +494,4 @@ export class MediationRecipientApi { messageHandlerRegistry.registerMessageHandler(new MediationDenyHandler(this.mediationRecipientService)) //messageHandlerRegistry.registerMessageHandler(new KeylistListHandler(this.mediationRecipientService)) // TODO: write this } - - protected async getMediationConnection(mediatorInvitationUrl: string) { - const connectionsApi = this.agentContext.dependencyManager.resolve(ConnectionsApi) - const oobApi = this.agentContext.dependencyManager.resolve(OutOfBandApi) - - const outOfBandInvitation = await oobApi.parseInvitation(mediatorInvitationUrl) - - const outOfBandRecord = await oobApi.findByReceivedInvitationId(outOfBandInvitation.id) - const [connection] = outOfBandRecord ? await connectionsApi.findAllByOutOfBandId(outOfBandRecord.id) : [] - - if (!connection) { - this.agentContext.config.logger.debug('Mediation connection does not exist, creating connection') - // We don't want to use the current default mediator when connecting to another mediator - const routing = await this.getRouting({ useDefaultMediator: false }) - - this.agentContext.config.logger.debug('Routing created', routing) - const { connectionRecord: newConnection } = await oobApi.receiveInvitation(outOfBandInvitation, { - routing, - }) - this.agentContext.config.logger.debug('Mediation invitation processed', { outOfBandInvitation }) - - if (!newConnection) { - throw new CredoError('No connection record to provision mediation.') - } - - return connectionsApi.returnWhenIsConnected(newConnection.id) - } - - if (!connection.isReady) { - return connectionsApi.returnWhenIsConnected(connection.id) - } - return connection - } } diff --git a/packages/didcomm/src/modules/routing/MediationRecipientModule.ts b/packages/didcomm/src/modules/routing/MediationRecipientModule.ts index 938c4eba56..10fd69b1dc 100644 --- a/packages/didcomm/src/modules/routing/MediationRecipientModule.ts +++ b/packages/didcomm/src/modules/routing/MediationRecipientModule.ts @@ -1,9 +1,12 @@ +import { CredoError } from '@credo-ts/core' import type { AgentContext, DependencyManager, Module } from '@credo-ts/core' import type { MediationRecipientModuleConfigOptions } from './MediationRecipientModuleConfig' import { FeatureRegistry } from '../../FeatureRegistry' import { Protocol } from '../../models' +import { ConnectionsApi } from '../connections' +import { OutOfBandApi } from '../oob' import { MediationRecipientApi } from './MediationRecipientApi' import { MediationRecipientModuleConfig } from './MediationRecipientModuleConfig' import { MediationRole } from './models' @@ -43,4 +46,73 @@ export class MediationRecipientModule implements Module { }) ) } + + public async onCloseContext(agentContext: AgentContext): Promise { + // Q: Can we also just call stop for non-defult context? + if (!agentContext.isRootAgentContext) return + + const mediationRecipientApi = agentContext.dependencyManager.resolve(MediationRecipientApi) + await mediationRecipientApi.stopMessagePickup() + } + + public async onInitializeContext(agentContext: AgentContext): Promise { + // We only support mediation config for the root agent context + if (!agentContext.isRootAgentContext) return + + const mediationRecipientApi = agentContext.dependencyManager.resolve(MediationRecipientApi) + + // Connect to mediator through provided invitation if provided in config + // Also requests mediation ans sets as default mediator + if (this.config.mediatorInvitationUrl) { + agentContext.config.logger.debug('Provision mediation with invitation', { + mediatorInvitationUrl: this.config.mediatorInvitationUrl, + }) + const mediationConnection = await this.getMediationConnection(agentContext, this.config.mediatorInvitationUrl) + await mediationRecipientApi.provision(mediationConnection) + } + + // Poll for messages from mediator + const defaultMediator = await mediationRecipientApi.findDefaultMediator() + if (defaultMediator) { + mediationRecipientApi.initiateMessagePickup(defaultMediator).catch((error) => { + agentContext.config.logger.warn(`Error initiating message pickup with mediator ${defaultMediator.id}`, { + error, + }) + }) + } + } + + protected async getMediationConnection(agentContext: AgentContext, mediatorInvitationUrl: string) { + const oobApi = agentContext.dependencyManager.resolve(OutOfBandApi) + const connectionsApi = agentContext.dependencyManager.resolve(ConnectionsApi) + const mediationRecipientApi = agentContext.dependencyManager.resolve(MediationRecipientApi) + + const outOfBandInvitation = await oobApi.parseInvitation(mediatorInvitationUrl) + const outOfBandRecord = await oobApi.findByReceivedInvitationId(outOfBandInvitation.id) + const [connection] = outOfBandRecord ? await connectionsApi.findAllByOutOfBandId(outOfBandRecord.id) : [] + + if (!connection) { + agentContext.config.logger.debug('Mediation connection does not exist, creating connection') + // We don't want to use the current default mediator when connecting to another mediator + const routing = await mediationRecipientApi.getRouting({ useDefaultMediator: false }) + + agentContext.config.logger.debug('Routing created', routing) + const { connectionRecord: newConnection } = await oobApi.receiveInvitation(outOfBandInvitation, { + routing, + }) + agentContext.config.logger.debug('Mediation invitation processed', { outOfBandInvitation }) + + if (!newConnection) { + throw new CredoError('No connection record to provision mediation.') + } + + return connectionsApi.returnWhenIsConnected(newConnection.id) + } + + if (!connection.isReady) { + return connectionsApi.returnWhenIsConnected(connection.id) + } + + return connection + } } diff --git a/packages/didcomm/src/modules/routing/MediatorModule.ts b/packages/didcomm/src/modules/routing/MediatorModule.ts index 40bdc14198..1e316bf6e5 100644 --- a/packages/didcomm/src/modules/routing/MediatorModule.ts +++ b/packages/didcomm/src/modules/routing/MediatorModule.ts @@ -42,6 +42,11 @@ export class MediatorModule implements Module { roles: [MediationRole.Mediator], }) ) + } + + public async onInitializeContext(agentContext: AgentContext): Promise { + // Mediator initialization only supported for root agent + if (!agentContext.isRootAgentContext) return const mediatorService = agentContext.dependencyManager.resolve(MediatorService) agentContext.config.logger.debug('Mediator routing record not loaded yet, retrieving from storage') diff --git a/packages/didcomm/src/modules/routing/__tests__/mediation.test.ts b/packages/didcomm/src/modules/routing/__tests__/mediation.test.ts index d3e544d31f..9addf99714 100644 --- a/packages/didcomm/src/modules/routing/__tests__/mediation.test.ts +++ b/packages/didcomm/src/modules/routing/__tests__/mediation.test.ts @@ -1,6 +1,5 @@ import type { SubjectMessage } from '../../../../../../tests/transport/SubjectInboundTransport' import type { AgentDependencies } from '../../../../../core/src/agent/AgentDependencies' -import type { AgentModulesInput } from '../../../../../core/src/agent/AgentModules' import type { InitConfig } from '../../../../../core/src/types' import { Subject } from 'rxjs' @@ -9,19 +8,25 @@ import { SubjectInboundTransport } from '../../../../../../tests/transport/Subje import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' import { Agent } from '../../../../../core/src/agent/Agent' import { sleep } from '../../../../../core/src/utils/sleep' -import { getInMemoryAgentOptions, waitForBasicMessage } from '../../../../../core/tests/helpers' +import { getAgentOptions, waitForBasicMessage } from '../../../../../core/tests/helpers' import { ConnectionRecord, HandshakeProtocol } from '../../connections' import { MediationRecipientModule } from '../MediationRecipientModule' import { MediatorModule } from '../MediatorModule' import { MediatorPickupStrategy } from '../MediatorPickupStrategy' import { MediationState } from '../models/MediationState' -const getRecipientAgentOptions = (useDidKeyInProtocols = true) => - getInMemoryAgentOptions('Mediation: Recipient', { - useDidKeyInProtocols, - }) +const getRecipientAgentOptions = (useDidKeyInProtocols = true, inMemory = true) => + getAgentOptions( + 'Mediation: Recipient', + { + useDidKeyInProtocols, + }, + undefined, + undefined, + { requireDidcomm: true, inMemory } + ) const getMediatorAgentOptions = (useDidKeyInProtocols = true) => - getInMemoryAgentOptions( + getAgentOptions( 'Mediation: Mediator', { endpoints: ['rxjs:mediator'], @@ -32,37 +37,41 @@ const getMediatorAgentOptions = (useDidKeyInProtocols = true) => mediator: new MediatorModule({ autoAcceptMediationRequests: true, }), - } + }, + { requireDidcomm: true } ) -const senderAgentOptions = getInMemoryAgentOptions('Mediation: Sender', { - endpoints: ['rxjs:sender'], -}) +const senderAgentOptions = getAgentOptions( + 'Mediation: Sender', + { + endpoints: ['rxjs:sender'], + }, + undefined, + undefined, + { requireDidcomm: true } +) describe('mediator establishment', () => { - let recipientAgent: Agent - let mediatorAgent: Agent - let senderAgent: Agent + let recipientAgent: Agent['modules']> + let mediatorAgent: Agent['modules']> + let senderAgent: Agent<(typeof senderAgentOptions)['modules']> afterEach(async () => { await recipientAgent?.shutdown() - await recipientAgent?.wallet.delete() await mediatorAgent?.shutdown() - await mediatorAgent?.wallet.delete() await senderAgent?.shutdown() - await senderAgent?.wallet.delete() }) const e2eMediationTest = async ( mediatorAgentOptions: { readonly config: InitConfig readonly dependencies: AgentDependencies - modules: AgentModulesInput + modules: ReturnType['modules'] }, recipientAgentOptions: { config: InitConfig dependencies: AgentDependencies - modules: AgentModulesInput + modules: ReturnType['modules'] } ) => { const mediatorMessages = new Subject() @@ -103,12 +112,12 @@ describe('mediator establishment', () => { recipientAgent.modules.didcomm.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) recipientAgent.modules.didcomm.registerInboundTransport(new SubjectInboundTransport(recipientMessages)) await recipientAgent.initialize() - await recipientAgent.modules.mediationRecipient.initialize() const recipientMediator = await recipientAgent.modules.mediationRecipient.findDefaultMediator() - const recipientMediatorConnection = await recipientAgent.modules.connections.getById( - recipientMediator?.connectionId - ) + if (!recipientMediator) { + throw new Error('expected recipientMediator') + } + const recipientMediatorConnection = await recipientAgent.modules.connections.getById(recipientMediator.connectionId) expect(recipientMediatorConnection).toBeInstanceOf(ConnectionRecord) expect(recipientMediatorConnection?.isReady).toBe(true) @@ -141,8 +150,11 @@ describe('mediator establishment', () => { recipientInvitation.toUrl({ domain: 'https://example.com/ssi' }) ) + if (!senderRecipientConnection) { + throw new Error('expected senderRecipientConnection') + } senderRecipientConnection = await senderAgent.modules.connections.returnWhenIsConnected( - senderRecipientConnection?.id + senderRecipientConnection.id ) let [recipientSenderConnection] = await recipientAgent.modules.connections.findAllByOutOfBandId( @@ -210,7 +222,7 @@ describe('mediator establishment', () => { handshakeProtocols: [HandshakeProtocol.Connections], }) - const recipientAgentOptions = getRecipientAgentOptions() + const recipientAgentOptions = getRecipientAgentOptions(undefined, false) // Initialize recipient with mediation connections invitation recipientAgent = new Agent({ ...recipientAgentOptions, @@ -227,25 +239,22 @@ describe('mediator establishment', () => { recipientAgent.modules.didcomm.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) recipientAgent.modules.didcomm.registerInboundTransport(new SubjectInboundTransport(recipientMessages)) await recipientAgent.initialize() - await recipientAgent.modules.mediationRecipient.initialize() const recipientMediator = await recipientAgent.modules.mediationRecipient.findDefaultMediator() - const recipientMediatorConnection = await recipientAgent.modules.connections.getById( - recipientMediator?.connectionId - ) - expect(recipientMediatorConnection?.isReady).toBe(true) + if (!recipientMediator) { + throw new Error('expected recipientMediator') + } + + const recipientMediatorConnection = await recipientAgent.modules.connections.getById(recipientMediator.connectionId) + expect(recipientMediatorConnection.isReady).toBe(true) const [mediatorRecipientConnection] = await mediatorAgent.modules.connections.findAllByOutOfBandId( mediatorOutOfBandRecord.id ) - expect(mediatorRecipientConnection?.isReady).toBe(true) - - // biome-ignore lint/style/noNonNullAssertion: - expect(mediatorRecipientConnection).toBeConnectedWith(recipientMediatorConnection!) - // biome-ignore lint/style/noNonNullAssertion: - expect(recipientMediatorConnection).toBeConnectedWith(mediatorRecipientConnection!) - - expect(recipientMediator?.state).toBe(MediationState.Granted) + expect(mediatorRecipientConnection.isReady).toBe(true) + expect(mediatorRecipientConnection).toBeConnectedWith(recipientMediatorConnection) + expect(recipientMediatorConnection).toBeConnectedWith(mediatorRecipientConnection) + expect(recipientMediator.state).toBe(MediationState.Granted) await recipientAgent.modules.mediationRecipient.stopMessagePickup() @@ -270,8 +279,11 @@ describe('mediator establishment', () => { recipientInvitation.toUrl({ domain: 'https://example.com/ssi' }) ) + if (!senderRecipientConnection) { + throw new Error('expected senderRecipientConnection') + } senderRecipientConnection = await senderAgent.modules.connections.returnWhenIsConnected( - senderRecipientConnection?.id + senderRecipientConnection.id ) const [recipientSenderConnection] = await recipientAgent.modules.connections.findAllByOutOfBandId( recipientOutOfBandRecord.id diff --git a/packages/didcomm/src/modules/routing/repository/MediationRecord.ts b/packages/didcomm/src/modules/routing/repository/MediationRecord.ts index 49a49525c4..1d3e9d02c5 100644 --- a/packages/didcomm/src/modules/routing/repository/MediationRecord.ts +++ b/packages/didcomm/src/modules/routing/repository/MediationRecord.ts @@ -40,7 +40,15 @@ export class MediationRecord public connectionId!: string public threadId!: string public endpoint?: string + + /** + * Base58 encoded recipient keys + */ public recipientKeys!: string[] + + /** + * Base58 encoded routing keys + */ public routingKeys!: string[] @Transform(({ value }) => { diff --git a/packages/didcomm/src/modules/routing/repository/MediatorRoutingRecord.ts b/packages/didcomm/src/modules/routing/repository/MediatorRoutingRecord.ts index 434a56d734..9a8a85428c 100644 --- a/packages/didcomm/src/modules/routing/repository/MediatorRoutingRecord.ts +++ b/packages/didcomm/src/modules/routing/repository/MediatorRoutingRecord.ts @@ -1,16 +1,37 @@ import type { TagsBase } from '@credo-ts/core' -import { BaseRecord, utils } from '@credo-ts/core' +import { BaseRecord, CredoError, Kms, TypedArrayEncoder, utils } from '@credo-ts/core' export interface MediatorRoutingRecordProps { id?: string createdAt?: Date - routingKeys?: string[] + routingKeys?: MediatorRoutingRecordRoutingKey[] tags?: TagsBase } -export class MediatorRoutingRecord extends BaseRecord implements MediatorRoutingRecordProps { - public routingKeys!: string[] +export interface MediatorRoutingRecordRoutingKey { + /** + * The routing key fingerprint + */ + routingKeyFingerprint: string + + /** + * The key id in the KMS + */ + kmsKeyId: string +} + +export type DefaultMediatorRoutingRecordTags = { + routingKeyFingerprints: string[] +} + +export class MediatorRoutingRecord extends BaseRecord { + // TODO: update routing keys here to a did, so we can just point to a did here + // and reuse all the key management logic we already have in place for dids + + // String values are base58 encoded keys, previously used + // The array of objects is the new format, including a key id + public routingKeys!: Array public static readonly type = 'MediatorRoutingRecord' public readonly type = MediatorRoutingRecord.type @@ -25,7 +46,43 @@ export class MediatorRoutingRecord extends BaseRecord implements MediatorRouting } } + public get routingKeysWithKeyId() { + return this.routingKeys.map((routingKey) => { + // routing keys in base58 format use the legacy key id + if (typeof routingKey === 'string') { + const publicJwk = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58(routingKey), + }) + publicJwk.keyId = publicJwk.legacyKeyId + + return publicJwk + } + + // routing keys using new structure, have a key id defined + const publicJwk = Kms.PublicJwk.fromFingerprint(routingKey.routingKeyFingerprint) + publicJwk.keyId = routingKey.kmsKeyId + + if (!publicJwk.is(Kms.Ed25519PublicJwk)) { + throw new CredoError('Expected mediator routing record key to be of type Ed25519.') + } + return publicJwk + }) + } + public getTags() { - return this._tags + return { + ...this._tags, + routingKeyFingerprints: this.routingKeys.map((routingKey) => + typeof routingKey === 'string' + ? Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58(routingKey), + }).fingerprint + : routingKey.routingKeyFingerprint + ), + } } } diff --git a/packages/didcomm/src/modules/routing/services/MediationRecipientService.ts b/packages/didcomm/src/modules/routing/services/MediationRecipientService.ts index a214269e14..0cf3bac362 100644 --- a/packages/didcomm/src/modules/routing/services/MediationRecipientService.ts +++ b/packages/didcomm/src/modules/routing/services/MediationRecipientService.ts @@ -1,4 +1,4 @@ -import type { AgentContext, Query, QueryOptions } from '@credo-ts/core' +import { AgentContext, Kms, Query, QueryOptions } from '@credo-ts/core' import type { AgentMessage } from '../../../AgentMessage' import type { InboundMessageContext, Routing } from '../../../models' import type { ConnectionRecord } from '../../connections/repository' @@ -10,8 +10,7 @@ import { CredoError, DidKey, EventEmitter, - Key, - KeyType, + TypedArrayEncoder, didKeyToVerkey, filterContextCorrelationId, injectable, @@ -150,7 +149,7 @@ export class MediationRecipientService { public async keylistUpdateAndAwait( agentContext: AgentContext, mediationRecord: MediationRecord, - updates: { recipientKey: Key; action: KeylistUpdateAction }[], + updates: { recipientKey: Kms.PublicJwk; action: KeylistUpdateAction }[], timeoutMs = 15000 // TODO: this should be a configurable value in agent config ): Promise { const connection = await this.connectionService.getById(agentContext, mediationRecord.connectionId) @@ -170,7 +169,9 @@ export class MediationRecipientService { (item) => new KeylistUpdate({ action: item.action, - recipientKey: useDidKey ? new DidKey(item.recipientKey).did : item.recipientKey.publicKeyBase58, + recipientKey: useDidKey + ? new DidKey(item.recipientKey).did + : TypedArrayEncoder.toBase58(item.recipientKey.publicKey.publicKey), }) ) ) @@ -242,7 +243,9 @@ export class MediationRecipientService { ...routing, mediatorId: mediationRecord.id, endpoints: mediationRecord.endpoint ? [mediationRecord.endpoint] : routing.endpoints, - routingKeys: mediationRecord.routingKeys.map((key) => Key.fromPublicKeyBase58(key, KeyType.Ed25519)), + routingKeys: mediationRecord.routingKeys.map((key) => + Kms.PublicJwk.fromPublicKey({ kty: 'OKP', crv: 'Ed25519', publicKey: TypedArrayEncoder.fromBase58(key) }) + ), } } diff --git a/packages/didcomm/src/modules/routing/services/MediatorService.ts b/packages/didcomm/src/modules/routing/services/MediatorService.ts index 2937094585..12f0581a5f 100644 --- a/packages/didcomm/src/modules/routing/services/MediatorService.ts +++ b/packages/didcomm/src/modules/routing/services/MediatorService.ts @@ -6,11 +6,13 @@ import type { ForwardMessage, MediationRequestMessage } from '../messages' import { CredoError, + DidKey, EventEmitter, InjectionSymbols, - KeyType, + Kms, Logger, RecordDuplicateError, + TypedArrayEncoder, didKeyToVerkey, inject, injectable, @@ -73,8 +75,9 @@ export class MediatorService { if (mediatorRoutingRecord) { // Return the routing keys this.logger.debug(`Returning mediator routing keys ${mediatorRoutingRecord.routingKeys}`) - return mediatorRoutingRecord.routingKeys + return mediatorRoutingRecord.routingKeysWithKeyId } + throw new CredoError('Mediator has not been initialized yet.') } @@ -196,11 +199,13 @@ export class MediatorService { const didcommConfig = agentContext.dependencyManager.resolve(DidCommModuleConfig) const useDidKey = didcommConfig.useDidKeyInProtocols + const routingKeys = (await this.getRoutingKeys(agentContext)).map((routingKey) => + useDidKey ? new DidKey(routingKey).did : TypedArrayEncoder.toBase58(routingKey.publicKey.publicKey) + ) + const message = new MediationGrantMessage({ endpoint: didcommConfig.endpoints[0], - routingKeys: useDidKey - ? (await this.getRoutingKeys(agentContext)).map(verkeyToDidKey) - : await this.getRoutingKeys(agentContext), + routingKeys, threadId: mediationRecord.threadId, }) @@ -246,17 +251,27 @@ export class MediatorService { } public async createMediatorRoutingRecord(agentContext: AgentContext): Promise { - const routingKey = await agentContext.wallet.createKey({ - keyType: KeyType.Ed25519, + const kms = agentContext.resolve(Kms.KeyManagementApi) + const didcommConfig = agentContext.resolve(DidCommModuleConfig) + + const routingKey = await kms.createKey({ + type: { + kty: 'OKP', + crv: 'Ed25519', + }, }) + const publicJwk = Kms.PublicJwk.fromPublicJwk(routingKey.publicJwk) const routingRecord = new MediatorRoutingRecord({ id: this.mediatorRoutingRepository.MEDIATOR_ROUTING_RECORD_ID, - // FIXME: update to fingerprint to include the key type - routingKeys: [routingKey.publicKeyBase58], + routingKeys: [ + { + routingKeyFingerprint: publicJwk.fingerprint, + kmsKeyId: routingKey.keyId, + }, + ], }) - const didcommConfig = agentContext.dependencyManager.resolve(DidCommModuleConfig) try { await this.mediatorRoutingRepository.save(agentContext, routingRecord) this.eventEmitter.emit(agentContext, { diff --git a/packages/didcomm/src/modules/routing/services/RoutingService.ts b/packages/didcomm/src/modules/routing/services/RoutingService.ts index e46757209b..5286de469e 100644 --- a/packages/didcomm/src/modules/routing/services/RoutingService.ts +++ b/packages/didcomm/src/modules/routing/services/RoutingService.ts @@ -1,8 +1,8 @@ -import type { AgentContext, Key } from '@credo-ts/core' +import type { AgentContext } from '@credo-ts/core' import type { Routing } from '../../../models' import type { RoutingCreatedEvent } from '../RoutingEvents' -import { EventEmitter, KeyType, injectable } from '@credo-ts/core' +import { EventEmitter, Kms, injectable } from '@credo-ts/core' import { DidCommModuleConfig } from '../../../DidCommModuleConfig' import { RoutingEventTypes } from '../RoutingEvents' @@ -25,14 +25,16 @@ export class RoutingService { agentContext: AgentContext, { mediatorId, useDefaultMediator = true }: GetRoutingOptions = {} ): Promise { + const kms = agentContext.resolve(Kms.KeyManagementApi) + const didcommConfig = agentContext.resolve(DidCommModuleConfig) + // Create and store new key - const recipientKey = await agentContext.wallet.createKey({ keyType: KeyType.Ed25519 }) - const didcommConfig = agentContext.dependencyManager.resolve(DidCommModuleConfig) + const recipientKey = await kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } }) let routing: Routing = { endpoints: didcommConfig.endpoints, routingKeys: [], - recipientKey, + recipientKey: Kms.PublicJwk.fromPublicJwk(recipientKey.publicJwk), } // Extend routing with mediator keys (if applicable) @@ -74,7 +76,7 @@ export interface RemoveRoutingOptions { /** * Keys to remove routing from */ - recipientKeys: Key[] + recipientKeys: Kms.PublicJwk[] /** * Identifier of the mediator used when routing has been set up diff --git a/packages/didcomm/src/modules/routing/services/__tests__/MediationRecipientService.test.ts b/packages/didcomm/src/modules/routing/services/__tests__/MediationRecipientService.test.ts index 611f62cd11..610e37dc8c 100644 --- a/packages/didcomm/src/modules/routing/services/__tests__/MediationRecipientService.test.ts +++ b/packages/didcomm/src/modules/routing/services/__tests__/MediationRecipientService.test.ts @@ -1,8 +1,8 @@ import type { AgentContext } from '../../../../../../core/src/agent' import type { Routing } from '../../../../models' +import { Kms, TypedArrayEncoder } from '@credo-ts/core' import { EventEmitter } from '../../../../../../core/src/agent/EventEmitter' -import { Key } from '../../../../../../core/src/crypto' import { DidRepository } from '../../../../../../core/src/modules/dids/repository/DidRepository' import { uuid } from '../../../../../../core/src/utils/uuid' import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../../core/tests/helpers' @@ -178,8 +178,12 @@ describe('MediationRecipientService', () => { }) describe('addMediationRouting', () => { - const routingKey = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') - const recipientKey = Key.fromFingerprint('z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th') + const routingKey = Kms.PublicJwk.fromFingerprint( + 'z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL' + ) as Kms.PublicJwk + const recipientKey = Kms.PublicJwk.fromFingerprint( + 'z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th' + ) as Kms.PublicJwk const routing: Routing = { routingKeys: [routingKey], recipientKey, @@ -192,7 +196,7 @@ describe('MediationRecipientService', () => { state: MediationState.Granted, threadId: 'thread-id', endpoint: 'https://a-mediator-endpoint.com', - routingKeys: [routingKey.publicKeyBase58], + routingKeys: [TypedArrayEncoder.toBase58(routingKey.publicKey.publicKey)], }) beforeEach(() => { diff --git a/packages/didcomm/src/modules/routing/services/__tests__/MediatorService.test.ts b/packages/didcomm/src/modules/routing/services/__tests__/MediatorService.test.ts index 98b04fdf51..da297926f3 100644 --- a/packages/didcomm/src/modules/routing/services/__tests__/MediatorService.test.ts +++ b/packages/didcomm/src/modules/routing/services/__tests__/MediatorService.test.ts @@ -1,5 +1,6 @@ import { Subject } from 'rxjs' +import { Kms, TypedArrayEncoder } from '@credo-ts/core' import { EventEmitter } from '../../../../../../core/src/agent/EventEmitter' import { isDidKey } from '../../../../../../core/src/modules/dids/helpers' import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../../../../core/tests/helpers' @@ -65,7 +66,16 @@ describe('MediatorService - default config', () => { mockFunction(mediatorRoutingRepository.findById).mockResolvedValue( new MediatorRoutingRecord({ - routingKeys: ['8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'], + routingKeys: [ + { + routingKeyFingerprint: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), + }).fingerprint, + kmsKeyId: 'some-key-id', + }, + ], }) ) @@ -192,7 +202,16 @@ describe('MediatorService - useDidKeyInProtocols set to false', () => { mockFunction(mediationRepository.getByConnectionId).mockResolvedValue(mediationRecord) const routingRecord = new MediatorRoutingRecord({ - routingKeys: ['8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'], + routingKeys: [ + { + routingKeyFingerprint: Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'), + }).fingerprint, + kmsKeyId: 'some-key-id', + }, + ], }) mockFunction(mediatorRoutingRepository.findById).mockResolvedValue(routingRecord) diff --git a/packages/didcomm/src/modules/routing/services/__tests__/RoutingService.test.ts b/packages/didcomm/src/modules/routing/services/__tests__/RoutingService.test.ts index fe50fdbc7a..0fcfca6061 100644 --- a/packages/didcomm/src/modules/routing/services/__tests__/RoutingService.test.ts +++ b/packages/didcomm/src/modules/routing/services/__tests__/RoutingService.test.ts @@ -1,10 +1,8 @@ -import type { Wallet } from '../../../../../../core/src/wallet' - import { Subject } from 'rxjs' import { EventEmitter } from '../../../../../../core/src/agent/EventEmitter' -import { Key } from '../../../../../../core/src/crypto' import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../core/tests/helpers' +import { NodeInMemoryKeyManagementStorage, NodeKeyManagementService } from '../../../../../../node/src' import { DidCommModuleConfig } from '../../../../DidCommModuleConfig' import { RoutingEventTypes } from '../../RoutingEvents' import { MediationRecipientService } from '../MediationRecipientService' @@ -13,29 +11,18 @@ import { RoutingService } from '../RoutingService' jest.mock('../MediationRecipientService') const MediationRecipientServiceMock = MediationRecipientService as jest.Mock -const recipientKey = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') const agentConfig = getAgentConfig('RoutingService', { endpoints: ['http://endpoint.com'], }) const eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) -const wallet = { - createKey: jest.fn().mockResolvedValue(recipientKey), - // with satisfies Partial we still get type errors when the interface changes -} satisfies Partial const agentContext = getAgentContext({ - wallet: wallet as unknown as Wallet, agentConfig, registerInstances: [[DidCommModuleConfig, new DidCommModuleConfig({ endpoints: ['http://endpoint.com'] })]], + kmsBackends: [new NodeKeyManagementService(new NodeInMemoryKeyManagementStorage())], }) const mediationRecipientService = new MediationRecipientServiceMock() const routingService = new RoutingService(mediationRecipientService, eventEmitter) - -const routing = { - endpoints: ['http://endpoint.com'], - recipientKey, - routingKeys: [], -} -mockFunction(mediationRecipientService.addMediationRouting).mockResolvedValue(routing) +mockFunction(mediationRecipientService.addMediationRouting).mockImplementation(async (_, routing) => routing) describe('RoutingService', () => { afterEach(() => { @@ -44,12 +31,12 @@ describe('RoutingService', () => { describe('getRouting', () => { test('calls mediation recipient service', async () => { - const routing = await routingService.getRouting(agentContext, { + const newRouting = await routingService.getRouting(agentContext, { mediatorId: 'mediator-id', useDefaultMediator: true, }) - expect(mediationRecipientService.addMediationRouting).toHaveBeenCalledWith(agentContext, routing, { + expect(mediationRecipientService.addMediationRouting).toHaveBeenCalledWith(agentContext, newRouting, { mediatorId: 'mediator-id', useDefaultMediator: true, }) @@ -59,16 +46,15 @@ describe('RoutingService', () => { const routingListener = jest.fn() eventEmitter.on(RoutingEventTypes.RoutingCreatedEvent, routingListener) - const routing = await routingService.getRouting(agentContext) + const newRouting = await routingService.getRouting(agentContext) - expect(routing).toEqual(routing) expect(routingListener).toHaveBeenCalledWith({ type: RoutingEventTypes.RoutingCreatedEvent, metadata: { contextCorrelationId: 'mock', }, payload: { - routing, + routing: newRouting, }, }) }) diff --git a/packages/didcomm/src/modules/routing/services/helpers.ts b/packages/didcomm/src/modules/routing/services/helpers.ts index ca375c886c..a98b02c1a0 100644 --- a/packages/didcomm/src/modules/routing/services/helpers.ts +++ b/packages/didcomm/src/modules/routing/services/helpers.ts @@ -1,12 +1,10 @@ -import type { AgentContext, DidDocument } from '@credo-ts/core' +import { type AgentContext, type DidDocument, TypedArrayEncoder } from '@credo-ts/core' import { MediationRecipientService } from './MediationRecipientService' export async function getMediationRecordForDidDocument(agentContext: AgentContext, didDocument: DidDocument) { - const [mediatorRecord] = await agentContext.dependencyManager - .resolve(MediationRecipientService) - .findAllMediatorsByQuery(agentContext, { - recipientKeys: didDocument.recipientKeys.map((key) => key.publicKeyBase58), - }) + const [mediatorRecord] = await agentContext.resolve(MediationRecipientService).findAllMediatorsByQuery(agentContext, { + recipientKeys: didDocument.recipientKeys.map((key) => TypedArrayEncoder.toBase58(key.publicKey.publicKey)), + }) return mediatorRecord } diff --git a/packages/didcomm/src/services/DidCommDocumentService.ts b/packages/didcomm/src/services/DidCommDocumentService.ts index de48ccfadc..07efe22de9 100644 --- a/packages/didcomm/src/services/DidCommDocumentService.ts +++ b/packages/didcomm/src/services/DidCommDocumentService.ts @@ -1,24 +1,28 @@ -import type { AgentContext, Key, ResolvedDidCommService } from '@credo-ts/core' +import { AgentContext, ResolvedDidCommService, findMatchingEd25519Key } from '@credo-ts/core' import { + CredoError, DidCommV1Service, + DidRecord, + DidRepository, DidResolverService, IndyAgentService, - KeyType, - getKeyFromVerificationMethod, + Kms, + RecordNotFoundError, + getPublicJwkFromVerificationMethod, injectable, parseDid, - verkeyToInstanceOfKey, + verkeyToPublicJwk, } from '@credo-ts/core' -import { findMatchingEd25519Key } from '../util/matchingEd25519Key' - @injectable() export class DidCommDocumentService { private didResolverService: DidResolverService + private didRepository: DidRepository - public constructor(didResolverService: DidResolverService) { + public constructor(didResolverService: DidResolverService, didRepository: DidRepository) { this.didResolverService = didResolverService + this.didRepository = didRepository } public async resolveServicesFromDid(agentContext: AgentContext, did: string): Promise { @@ -38,20 +42,25 @@ export class DidCommDocumentService { // IndyAgentService (DidComm v0) has keys encoded as raw publicKeyBase58 (verkeys) resolvedServices.push({ id: didCommService.id, - recipientKeys: didCommService.recipientKeys.map(verkeyToInstanceOfKey), - routingKeys: didCommService.routingKeys?.map(verkeyToInstanceOfKey) || [], + recipientKeys: didCommService.recipientKeys.map(verkeyToPublicJwk), + routingKeys: didCommService.routingKeys?.map(verkeyToPublicJwk) || [], serviceEndpoint: didCommService.serviceEndpoint, }) } else if (didCommService.type === DidCommV1Service.type) { // Resolve dids to DIDDocs to retrieve routingKeys - const routingKeys: Key[] = [] + const routingKeys: Kms.PublicJwk[] = [] for (const routingKey of didCommService.routingKeys ?? []) { const routingDidDocument = await this.didResolverService.resolveDidDocument(agentContext, routingKey) - routingKeys.push( - getKeyFromVerificationMethod( - routingDidDocument.dereferenceKey(routingKey, ['authentication', 'keyAgreement']) - ) + const publicJwk = getPublicJwkFromVerificationMethod( + routingDidDocument.dereferenceKey(routingKey, ['authentication', 'keyAgreement']) ) + + // FIXME: we should handle X25519 here as well + if (!publicJwk.is(Kms.Ed25519PublicJwk)) { + throw new CredoError(`Expected Ed25519PublicJwk but found ${publicJwk.jwk.constructor.name}`) + } + + routingKeys.push(publicJwk) } // DidCommV1Service has keys encoded as key references @@ -61,18 +70,23 @@ export class DidCommDocumentService { // FIXME: we allow authentication keys as historically ed25519 keys have been used in did documents // for didcomm. In the future we should update this to only be allowed for IndyAgent and DidCommV1 services // as didcomm v2 doesn't have this issue anymore - const key = getKeyFromVerificationMethod( + const publicJwk = getPublicJwkFromVerificationMethod( didDocument.dereferenceKey(recipientKeyReference, ['authentication', 'keyAgreement']) ) // try to find a matching Ed25519 key (https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html#did-document-notes) // FIXME: Now that indy-sdk is deprecated, we should look into the possiblty of using the X25519 key directly // removing the need to also include the Ed25519 key in the did document. - if (key.keyType === KeyType.X25519) { - const matchingEd25519Key = findMatchingEd25519Key(key, didDocument) - if (matchingEd25519Key) return matchingEd25519Key + if (publicJwk.is(Kms.X25519PublicJwk)) { + const matchingEd25519Key = findMatchingEd25519Key(publicJwk, didDocument) + if (matchingEd25519Key) return matchingEd25519Key.publicJwk } - return key + + if (!publicJwk.is(Kms.Ed25519PublicJwk)) { + throw new CredoError(`Expected Ed25519PublicJwk but found ${publicJwk.jwk.constructor.name}`) + } + + return publicJwk }) resolvedServices.push({ @@ -86,4 +100,31 @@ export class DidCommDocumentService { return resolvedServices } + + public async resolveCreatedDidRecordWithDocumentByRecipientKey(agentContext: AgentContext, publicJwk: Kms.PublicJwk) { + const didRecord = await this.didRepository.findCreatedDidByRecipientKey(agentContext, publicJwk) + + if (!didRecord) { + throw new RecordNotFoundError(`Created did for public jwk ${publicJwk.jwkTypehumanDescription} not found`, { + recordType: DidRecord.type, + }) + } + + if (didRecord.didDocument) { + return { + didRecord, + didDocument: didRecord.didDocument, + } + } + + // TODO: we should somehow store the did document on the record if the did method allows it + // E.g. for did:key we don't want to store it, but if we still have a did:indy record we do want to store it + // If the did document is not stored on the did record, we resolve it + const didDocument = await this.didResolverService.resolveDidDocument(agentContext, didRecord.did) + + return { + didRecord, + didDocument, + } + } } diff --git a/packages/didcomm/src/services/__tests__/DidCommDocumentService.test.ts b/packages/didcomm/src/services/__tests__/DidCommDocumentService.test.ts index 6bba8b8cb9..5bd7bc784a 100644 --- a/packages/didcomm/src/services/__tests__/DidCommDocumentService.test.ts +++ b/packages/didcomm/src/services/__tests__/DidCommDocumentService.test.ts @@ -1,9 +1,9 @@ +import { Kms, TypedArrayEncoder } from '@credo-ts/core' import type { AgentContext } from '../../../..//core/src/agent' import type { VerificationMethod } from '../../../../core/src/modules/dids' - -import { Key, KeyType } from '../../../../core/src/crypto' import { DidCommV1Service, DidDocument, IndyAgentService } from '../../../../core/src/modules/dids' -import { verkeyToInstanceOfKey } from '../../../../core/src/modules/dids/helpers' +import { verkeyToPublicJwk } from '../../../../core/src/modules/dids/helpers' +import { DidRepository } from '../../../../core/src/modules/dids/repository/DidRepository' import { DidResolverService } from '../../../../core/src/modules/dids/services/DidResolverService' import { getAgentContext, mockFunction } from '../../../../core/tests/helpers' import { DidCommDocumentService } from '../DidCommDocumentService' @@ -11,14 +11,19 @@ import { DidCommDocumentService } from '../DidCommDocumentService' jest.mock('../../../../core/src/modules/dids/services/DidResolverService') const DidResolverServiceMock = DidResolverService as jest.Mock +jest.mock('../../../../core/src/modules/dids/services/DidResolverService') +const DidRepositoryMock = DidRepository as jest.Mock + describe('DidCommDocumentService', () => { let didCommDocumentService: DidCommDocumentService let didResolverService: DidResolverService + let didRepository: DidRepository let agentContext: AgentContext beforeEach(async () => { didResolverService = new DidResolverServiceMock() - didCommDocumentService = new DidCommDocumentService(didResolverService) + didRepository = new DidRepositoryMock() + didCommDocumentService = new DidCommDocumentService(didResolverService, didRepository) agentContext = getAgentContext() }) @@ -57,8 +62,8 @@ describe('DidCommDocumentService', () => { expect(resolved[0]).toMatchObject({ id: 'test-id', serviceEndpoint: 'https://test.com', - recipientKeys: [verkeyToInstanceOfKey('Q4zqM7aXqm7gDQkUVLng9h')], - routingKeys: [verkeyToInstanceOfKey('DADEajsDSaksLng9h')], + recipientKeys: [verkeyToPublicJwk('Q4zqM7aXqm7gDQkUVLng9h')], + routingKeys: [verkeyToPublicJwk('DADEajsDSaksLng9h')], }) }) @@ -108,14 +113,16 @@ describe('DidCommDocumentService', () => { ) expect(didResolverService.resolveDidDocument).toHaveBeenCalledWith(agentContext, 'did:sov:Q4zqM7aXqm7gDQkUVLng9h') - const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) - expect(resolved).toHaveLength(1) - expect(resolved[0]).toMatchObject({ - id: 'test-id', - serviceEndpoint: 'https://test.com', - recipientKeys: [ed25519Key], - routingKeys: [ed25519Key], + const ed25519Key = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58(publicKeyBase58Ed25519), }) + expect(resolved).toHaveLength(1) + expect(resolved[0].id).toEqual('test-id') + expect(resolved[0].serviceEndpoint).toEqual('https://test.com') + expect(resolved[0].recipientKeys[0].equals(ed25519Key)).toBe(true) + expect(resolved[0].routingKeys[0].equals(ed25519Key)).toBe(true) }) test('resolves specific DidCommV1Service', async () => { @@ -174,14 +181,17 @@ describe('DidCommDocumentService', () => { 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id' ) - let ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) expect(resolved).toHaveLength(1) - expect(resolved[0]).toMatchObject({ - id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id', - serviceEndpoint: 'https://test.com', - recipientKeys: [ed25519Key], - routingKeys: [ed25519Key], + const ed25519Key = Kms.PublicJwk.fromPublicKey({ + kty: 'OKP', + crv: 'Ed25519', + publicKey: TypedArrayEncoder.fromBase58(publicKeyBase58Ed25519), }) + expect(resolved).toHaveLength(1) + expect(resolved[0].id).toEqual('did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id') + expect(resolved[0].serviceEndpoint).toEqual('https://test.com') + expect(resolved[0].recipientKeys[0].equals(ed25519Key)).toBe(true) + expect(resolved[0].routingKeys[0].equals(ed25519Key)).toBe(true) resolved = await didCommDocumentService.resolveServicesFromDid( agentContext, @@ -192,14 +202,11 @@ describe('DidCommDocumentService', () => { 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id-2' ) - ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58Ed25519, KeyType.Ed25519) + expect(resolved[0].id).toEqual('did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id-2') + expect(resolved[0].serviceEndpoint).toEqual('wss://test.com') + expect(resolved[0].recipientKeys[0].equals(ed25519Key)).toBe(true) + expect(resolved[0].routingKeys[0].equals(ed25519Key)).toBe(true) expect(resolved).toHaveLength(1) - expect(resolved[0]).toMatchObject({ - id: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h#test-id-2', - serviceEndpoint: 'wss://test.com', - recipientKeys: [ed25519Key], - routingKeys: [ed25519Key], - }) }) }) }) diff --git a/packages/didcomm/src/transport/HttpOutboundTransport.ts b/packages/didcomm/src/transport/HttpOutboundTransport.ts index 38d662cfba..c19cebdc82 100644 --- a/packages/didcomm/src/transport/HttpOutboundTransport.ts +++ b/packages/didcomm/src/transport/HttpOutboundTransport.ts @@ -78,10 +78,8 @@ export class HttpOutboundTransport implements OutboundTransport { const id = setTimeout(() => abortController.abort(), 15000) this.outboundSessionCount++ - // biome-ignore lint/suspicious/noImplicitAnyLet: - let response - // biome-ignore lint/suspicious/noImplicitAnyLet: - let responseMessage + let response: Response | undefined = undefined + let responseMessage: string | undefined = undefined try { response = await this.fetch(endpoint, { method: 'POST', diff --git a/packages/didcomm/src/updates/0.1-0.2/connection.ts b/packages/didcomm/src/updates/0.1-0.2/connection.ts index a8144ad78f..95672df5b3 100644 --- a/packages/didcomm/src/updates/0.1-0.2/connection.ts +++ b/packages/didcomm/src/updates/0.1-0.2/connection.ts @@ -166,7 +166,7 @@ export async function extractDidDocument(agent: Agent, `Found a legacy did document for did ${oldOurDidDoc.id} in connection record didDoc. Converting it to a peer did document.` ) - const newOurDidDocument = convertToNewDidDocument(oldOurDidDoc) + const { didDocument: newOurDidDocument } = convertToNewDidDocument(oldOurDidDoc) // Maybe we already have a record for this did because the migration failed previously // NOTE: in 0.3.0 the id property was updated to be a uuid, and a new did property was added. As this is the update from 0.1 to 0.2, @@ -215,7 +215,7 @@ export async function extractDidDocument(agent: Agent, `Found a legacy did document for theirDid ${oldTheirDidDoc.id} in connection record theirDidDoc. Converting it to a peer did document.` ) - const newTheirDidDocument = convertToNewDidDocument(oldTheirDidDoc) + const { didDocument: newTheirDidDocument } = convertToNewDidDocument(oldTheirDidDoc) // Maybe we already have a record for this did because the migration failed previously // NOTE: in 0.3.0 the id property was updated to be a uuid, and a new did property was added. As this is the update from 0.1 to 0.2, @@ -330,7 +330,7 @@ export async function migrateToOobRecord( .map((s) => s.recipientKeys) // biome-ignore lint/performance/noAccumulatingSpread: .reduce((acc, curr) => [...acc, ...curr], []) - .map((didKey) => DidKey.fromDid(didKey).key.fingerprint) + .map((didKey) => DidKey.fromDid(didKey).publicJwk.fingerprint) const oobRole = connectionRecord.role === DidExchangeRole.Responder ? OutOfBandRole.Sender : OutOfBandRole.Receiver const oobRecords = await oobRepository.findByQuery(agent.context, { diff --git a/packages/didcomm/src/util/matchingEd25519Key.ts b/packages/didcomm/src/util/matchingEd25519Key.ts deleted file mode 100644 index 54926fca6b..0000000000 --- a/packages/didcomm/src/util/matchingEd25519Key.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { DidDocument, VerificationMethod } from '@credo-ts/core' - -import { Key, KeyType, convertPublicKeyToX25519, getKeyFromVerificationMethod } from '@credo-ts/core' - -/** - * Tries to find a matching Ed25519 key to the supplied X25519 key - * @param x25519Key X25519 key - * @param didDocument Did document containing all the keys - * @returns a matching Ed25519 key or `undefined` (if no matching key found) - */ -export function findMatchingEd25519Key(x25519Key: Key, didDocument: DidDocument): Key | undefined { - if (x25519Key.keyType !== KeyType.X25519) return undefined - - const verificationMethods = didDocument.verificationMethod ?? [] - const keyAgreements = didDocument.keyAgreement ?? [] - const authentications = didDocument.authentication ?? [] - const allKeyReferences: VerificationMethod[] = [ - ...verificationMethods, - ...authentications.filter((keyAgreement): keyAgreement is VerificationMethod => typeof keyAgreement !== 'string'), - ...keyAgreements.filter((keyAgreement): keyAgreement is VerificationMethod => typeof keyAgreement !== 'string'), - ] - - return allKeyReferences - .map((keyReference) => getKeyFromVerificationMethod(didDocument.dereferenceKey(keyReference.id))) - .filter((key) => key?.keyType === KeyType.Ed25519) - .find((keyEd25519) => { - const keyX25519 = Key.fromPublicKey(convertPublicKeyToX25519(keyEd25519.publicKey), KeyType.X25519) - return keyX25519.publicKeyBase58 === x25519Key.publicKeyBase58 - }) -} diff --git a/packages/didcomm/src/util/modules.ts b/packages/didcomm/src/util/modules.ts index a78714f282..a0269a5ac9 100644 --- a/packages/didcomm/src/util/modules.ts +++ b/packages/didcomm/src/util/modules.ts @@ -44,8 +44,11 @@ export type DefaultAgentModulesInput = Omit +// TODO: we should reduce the default didcomm modules. E.g. you don't +// need the mediator, basic messages, credentials, or proofs module export function getDefaultDidcommModules(didcommModuleConfig?: DidCommModuleConfigOptions) { return { + didcomm: new DidCommModule(didcommModuleConfig), connections: new ConnectionsModule(), credentials: new CredentialsModule(), proofs: new ProofsModule(), @@ -54,7 +57,6 @@ export function getDefaultDidcommModules(didcommModuleConfig?: DidCommModuleConf mediationRecipient: new MediationRecipientModule(), messagePickup: new MessagePickupModule(), basicMessages: new BasicMessagesModule(), - didcomm: new DidCommModule(didcommModuleConfig), oob: new OutOfBandModule(), } as const } diff --git a/packages/didcomm/src/util/parseInvitation.ts b/packages/didcomm/src/util/parseInvitation.ts index 0651d209e3..008dfea8d4 100644 --- a/packages/didcomm/src/util/parseInvitation.ts +++ b/packages/didcomm/src/util/parseInvitation.ts @@ -14,8 +14,7 @@ import { parseMessageType, supportsIncomingMessageType } from './messageType' const fetchShortUrl = async (invitationUrl: string, dependencies: AgentDependencies) => { const abortController = new AbortController() const id = setTimeout(() => abortController.abort(), 15000) - // biome-ignore lint/suspicious/noImplicitAnyLet: - let response + let response: Response try { response = await dependencies.fetch(invitationUrl, { method: 'GET', @@ -96,8 +95,7 @@ export const oobInvitationFromShortUrl = async (response: Response): Promise - let responseUrl + let responseUrl: string const location = response.headers.get('Location') if ((response.status === 302 || response.status === 301) && location) responseUrl = location else responseUrl = response.url diff --git a/packages/drpc/tests/drpc-messages.e2e.test.ts b/packages/drpc/tests/drpc-messages.e2e.test.ts index 538fc068fe..c6cc5dfe61 100644 --- a/packages/drpc/tests/drpc-messages.e2e.test.ts +++ b/packages/drpc/tests/drpc-messages.e2e.test.ts @@ -3,7 +3,7 @@ import type { DrpcRequest, DrpcRequestObject, DrpcResponseObject } from '../src/ import { Agent } from '../../core/src/agent/Agent' import { setupSubjectTransports } from '../../core/tests' -import { getInMemoryAgentOptions, makeConnection } from '../../core/tests/helpers' +import { getAgentOptions, makeConnection } from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' import { DrpcModule } from '../src/DrpcModule' import { DrpcErrorCode } from '../src/models' @@ -12,22 +12,24 @@ const modules = { drpc: new DrpcModule(), } -const faberConfig = getInMemoryAgentOptions( +const faberConfig = getAgentOptions( 'Faber Drpc Messages', { endpoints: ['rxjs:faber'], }, {}, - modules + modules, + { requireDidcomm: true } ) -const aliceConfig = getInMemoryAgentOptions( +const aliceConfig = getAgentOptions( 'Alice Drpc Messages', { endpoints: ['rxjs:alice'], }, {}, - modules + modules, + { requireDidcomm: true } ) const handleMessageOrError = async ( @@ -113,9 +115,7 @@ describe('Drpc Messages E2E', () => { afterEach(async () => { await faberAgent.shutdown() - await faberAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice and Faber exchange messages', async () => { diff --git a/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts b/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts index b054f1841b..83e1f44619 100644 --- a/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts +++ b/packages/indy-sdk-to-askar-migration/src/IndySdkToAskarMigrationUpdater.ts @@ -1,14 +1,16 @@ import type { AnonCredsCredentialValue } from '@credo-ts/anoncreds' -import type { Agent, FileSystem, WalletConfig } from '@credo-ts/core' -import type { EntryObject } from '@openwallet-foundation/askar-shared' +import type { Agent, FileSystem } from '@credo-ts/core' +import { EntryObject, KdfMethod, StoreKeyMethod } from '@openwallet-foundation/askar-shared' import { AnonCredsCredentialRecord, AnonCredsLinkSecretRecord } from '@credo-ts/anoncreds' -import { AskarWallet } from '@credo-ts/askar' -import { InjectionSymbols, JsonTransformer, KeyDerivationMethod, TypedArrayEncoder } from '@credo-ts/core' +import { InjectionSymbols, JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' import { Key, KeyAlgorithm, Migration, Store } from '@openwallet-foundation/askar-shared' +import { AskarModule } from '@credo-ts/askar' import { IndySdkToAskarMigrationError } from './errors/IndySdkToAskarMigrationError' -import { keyDerivationMethodToStoreKeyMethod, transformFromRecordTagValues } from './utils' +import { transformFromRecordTagValues } from './utils' + +type AskarAgent = Agent<{ askar: AskarModule }> /** * @@ -25,18 +27,16 @@ import { keyDerivationMethodToStoreKeyMethod, transformFromRecordTagValues } fro */ export class IndySdkToAskarMigrationUpdater { private store?: Store - private walletConfig: WalletConfig private defaultLinkSecretId: string - private agent: Agent + private agent: AskarAgent private dbPath: string private fs: FileSystem - private constructor(walletConfig: WalletConfig, agent: Agent, dbPath: string, defaultLinkSecretId?: string) { - this.walletConfig = walletConfig + private constructor(agent: AskarAgent, dbPath: string, defaultLinkSecretId?: string) { this.dbPath = dbPath this.agent = agent this.fs = this.agent.dependencyManager.resolve(InjectionSymbols.FileSystem) - this.defaultLinkSecretId = defaultLinkSecretId ?? walletConfig.id + this.defaultLinkSecretId = defaultLinkSecretId ?? agent.modules.askar.config.store.id } public static async initialize({ @@ -45,23 +45,17 @@ export class IndySdkToAskarMigrationUpdater { defaultLinkSecretId, }: { dbPath: string - agent: Agent + agent: Agent<{ askar: AskarModule }> defaultLinkSecretId?: string }) { - const { - config: { walletConfig }, - } = agent if (typeof process?.versions?.node !== 'undefined') { agent.config.logger.warn( 'Node.JS is not fully supported. Using this will likely leave the wallet in a half-migrated state' ) } - if (!walletConfig) { - throw new IndySdkToAskarMigrationError('Wallet config is required for updating the wallet') - } - - if (walletConfig.storage && walletConfig.storage.type !== 'sqlite') { + const askarStoreConfig = agent.modules.askar.config.store + if (askarStoreConfig.database && askarStoreConfig.database.type !== 'sqlite') { throw new IndySdkToAskarMigrationError('Only sqlite wallets are supported, right now') } @@ -69,11 +63,7 @@ export class IndySdkToAskarMigrationUpdater { throw new IndySdkToAskarMigrationError('Wallet migration can not be done on an initialized agent') } - if (!(agent.dependencyManager.resolve(InjectionSymbols.Wallet) instanceof AskarWallet)) { - throw new IndySdkToAskarMigrationError("Wallet on the agent must be of instance 'AskarWallet'") - } - - return new IndySdkToAskarMigrationUpdater(walletConfig, agent, dbPath, defaultLinkSecretId) + return new IndySdkToAskarMigrationUpdater(agent, dbPath, defaultLinkSecretId) } /** @@ -83,10 +73,15 @@ export class IndySdkToAskarMigrationUpdater { */ private async migrate() { const specUri = this.backupFile - const kdfLevel = this.walletConfig.keyDerivationMethod ?? KeyDerivationMethod.Argon2IMod - const walletName = this.walletConfig.id - const walletKey = this.walletConfig.key - const storageType = this.walletConfig.storage?.type ?? 'sqlite' + const storeConfig = this.agent.modules.askar.config.store + + const kdfLevel = storeConfig.keyDerivationMethod ?? KdfMethod.Argon2IMod + // Migration tool uses the legacy key derivation method + const keyDerivationMethod = + kdfLevel === 'kdf:argon2i:mod' ? 'ARGON2I_MOD' : kdfLevel === 'kdf:argon2i:int' ? 'ARGON2I_INT' : 'RAW' + const walletName = storeConfig.id + const walletKey = storeConfig.key + const storageType = storeConfig.database?.type ?? 'sqlite' if (storageType !== 'sqlite') { throw new IndySdkToAskarMigrationError("Storage type defined and not of type 'sqlite'") @@ -97,7 +92,7 @@ export class IndySdkToAskarMigrationUpdater { } this.agent.config.logger.info('Migration indy-sdk database structure to askar') - await Migration.migrate({ specUri, walletKey, kdfLevel, walletName }) + await Migration.migrate({ specUri, walletKey, kdfLevel: keyDerivationMethod, walletName }) } /* @@ -120,14 +115,16 @@ export class IndySdkToAskarMigrationUpdater { * Location of the new wallet */ public get newWalletPath() { - return `${this.fs.dataPath}/wallet/${this.walletConfig.id}/sqlite.db` + const storeConfig = this.agent.modules.askar.config.store + return `${this.fs.dataPath}/wallet/${storeConfig.id}/sqlite.db` } /** * Temporary backup location of the pre-migrated script */ private get backupFile() { - return `${this.fs.tempPath}/${this.walletConfig.id}.db` + const storeConfig = this.agent.modules.askar.config.store + return `${this.fs.tempPath}/${storeConfig.id}.db` } private async copyDatabaseWithOptionalWal(src: string, dest: string) { @@ -218,11 +215,14 @@ export class IndySdkToAskarMigrationUpdater { try { // Migrate the database await this.migrate() + const storeConfig = this.agent.modules.askar.config.store - const keyMethod = keyDerivationMethodToStoreKeyMethod( - this.walletConfig.keyDerivationMethod ?? KeyDerivationMethod.Argon2IMod - ) - this.store = await Store.open({ uri: `sqlite://${this.backupFile}`, passKey: this.walletConfig.key, keyMethod }) + const kdfLevel = storeConfig.keyDerivationMethod ?? KdfMethod.Argon2IMod + this.store = await Store.open({ + uri: `sqlite://${this.backupFile}`, + passKey: storeConfig.key, + keyMethod: new StoreKeyMethod(kdfLevel as KdfMethod), + }) // Update the values to reflect the new structure await this.updateKeys() @@ -336,7 +336,7 @@ export class IndySdkToAskarMigrationUpdater { for (const row of masterSecrets) { this.agent.config.logger.debug(`Migrating ${row.name} to the new askar format`) - const isDefault = masterSecrets.length === 0 || row.name === this.walletConfig.id + const isDefault = row.name === this.defaultLinkSecretId const { value: { ms }, diff --git a/packages/indy-sdk-to-askar-migration/src/utils.ts b/packages/indy-sdk-to-askar-migration/src/utils.ts index 167cbced53..3fe1cbe427 100644 --- a/packages/indy-sdk-to-askar-migration/src/utils.ts +++ b/packages/indy-sdk-to-askar-migration/src/utils.ts @@ -1,8 +1,5 @@ import type { TagsBase } from '@credo-ts/core' -import { KeyDerivationMethod } from '@credo-ts/core' -import { KdfMethod, StoreKeyMethod } from '@openwallet-foundation/askar-shared' - /** * Adopted from `AskarStorageService` implementation and should be kept in sync. */ @@ -41,13 +38,3 @@ export const transformFromRecordTagValues = (tags: TagsBase): { [key: string]: s return transformedTags } - -export const keyDerivationMethodToStoreKeyMethod = (keyDerivationMethod: KeyDerivationMethod) => { - const correspondenceTable = { - [KeyDerivationMethod.Raw]: KdfMethod.Raw, - [KeyDerivationMethod.Argon2IInt]: KdfMethod.Argon2IInt, - [KeyDerivationMethod.Argon2IMod]: KdfMethod.Argon2IMod, - } - - return new StoreKeyMethod(correspondenceTable[keyDerivationMethod]) -} diff --git a/packages/indy-sdk-to-askar-migration/tests/migrate.test.ts b/packages/indy-sdk-to-askar-migration/tests/migrate.test.ts index d2e880d0b4..25d462a129 100644 --- a/packages/indy-sdk-to-askar-migration/tests/migrate.test.ts +++ b/packages/indy-sdk-to-askar-migration/tests/migrate.test.ts @@ -3,10 +3,11 @@ import type { InitConfig } from '@credo-ts/core' import { copyFileSync, existsSync, mkdirSync, unlinkSync } from 'fs' import { homedir } from 'os' import path from 'path' -import { Agent, KeyDerivationMethod } from '@credo-ts/core' +import { Agent } from '@credo-ts/core' import { agentDependencies } from '@credo-ts/node' -import { askarModule } from '../../askar/tests/helpers' +import { AskarModule } from '@credo-ts/askar' +import { askar } from '../../askar/tests/helpers' import { IndySdkToAskarMigrationUpdater } from '../src' import { IndySdkToAskarMigrationError } from '../src/errors/IndySdkToAskarMigrationError' @@ -14,22 +15,29 @@ describe('Indy SDK To Askar Migration', () => { test('indy-sdk sqlite to aries-askar sqlite successful migration', async () => { const indySdkAndAskarConfig: InitConfig = { label: 'indy | indy-sdk sqlite to aries-askar sqlite successful migration', - walletConfig: { - id: 'indy-sdk sqlite to aries-askar sqlite successful migration', - key: 'GfwU1DC7gEZNs3w41tjBiZYj7BNToDoFEqKY6wZXqs1A', - keyDerivationMethod: KeyDerivationMethod.Raw, - }, autoUpdateStorageOnStartup: true, } - const indySdkAgentDbPath = `${homedir()}/.indy_client/wallet/${indySdkAndAskarConfig.walletConfig?.id}/sqlite.db` + const indySdkAgentDbPath = `${homedir()}/.indy_client/wallet/indy-sdk sqlite to aries-askar sqlite successful migration/sqlite.db` const indySdkWalletTestPath = path.join(__dirname, 'indy-sdk-040-wallet.db') const askarAgent = new Agent({ config: indySdkAndAskarConfig, - modules: { askar: askarModule }, + modules: { + askar: new AskarModule({ + askar, + store: { + id: 'indy-sdk sqlite to aries-askar sqlite successful migration', + key: 'GfwU1DC7gEZNs3w41tjBiZYj7BNToDoFEqKY6wZXqs1A', + keyDerivationMethod: 'raw', + }, + }), + }, dependencies: agentDependencies, }) - const updater = await IndySdkToAskarMigrationUpdater.initialize({ dbPath: indySdkAgentDbPath, agent: askarAgent }) + const updater = await IndySdkToAskarMigrationUpdater.initialize({ + dbPath: indySdkAgentDbPath, + agent: askarAgent, + }) // Remove new wallet path (if exists) if (existsSync(updater.newWalletPath)) unlinkSync(updater.newWalletPath) @@ -49,6 +57,19 @@ describe('Indy SDK To Askar Migration', () => { }, ]) + // Ensure the migrated wallet keys still work with the new kms + await expect( + askarAgent.kms.getPublicKey({ + keyId: '8b8S451U9Hf4iZFdYJRuvPBBVbwW3jH8J1BH2CGEEDZD', + }) + ).resolves.toEqual({ + crv: 'Ed25519', + d: undefined, + kid: '8b8S451U9Hf4iZFdYJRuvPBBVbwW3jH8J1BH2CGEEDZD', + kty: 'OKP', + x: 'cL_1liG48WAFSltbtvsi4Os2of3DNrqCkg4WOu2xAnQ', + }) + await askarAgent.shutdown() }) @@ -62,20 +83,24 @@ describe('Indy SDK To Askar Migration', () => { test('indy-sdk sqlite to aries-askar sqlite fails and restores', async () => { const indySdkAndAskarConfig: InitConfig = { label: 'indy | indy-sdk sqlite to aries-askar sqlite fails and restores', - walletConfig: { - id: 'indy-sdk sqlite to aries-askar sqlite fails and restores', - // NOTE: wrong key passed - key: 'wrong-key', - keyDerivationMethod: KeyDerivationMethod.Raw, - }, } - const indySdkAgentDbPath = `${homedir()}/.indy_client/wallet/${indySdkAndAskarConfig.walletConfig?.id}/sqlite.db` + const indySdkAgentDbPath = `${homedir()}/.indy_client/wallet/indy-sdk sqlite to aries-askar sqlite fails and restores/sqlite.db` const indySdkWalletTestPath = path.join(__dirname, 'indy-sdk-040-wallet.db') const askarAgent = new Agent({ config: indySdkAndAskarConfig, - modules: { askar: askarModule }, + modules: { + askar: new AskarModule({ + askar, + store: { + id: 'indy-sdk sqlite to aries-askar sqlite fails and restores', + // NOTE: wrong key passed + key: 'wrong-key', + keyDerivationMethod: 'raw', + }, + }), + }, dependencies: agentDependencies, }) diff --git a/packages/indy-vdr/src/IndyVdrApi.ts b/packages/indy-vdr/src/IndyVdrApi.ts index d8b67c18d0..5cf685872c 100644 --- a/packages/indy-vdr/src/IndyVdrApi.ts +++ b/packages/indy-vdr/src/IndyVdrApi.ts @@ -1,11 +1,11 @@ -import type { Key } from '@credo-ts/core' +import type { Kms } from '@credo-ts/core' import type { IndyVdrRequest } from '@hyperledger/indy-vdr-shared' import { parseIndyDid } from '@credo-ts/anoncreds' import { AgentContext, injectable } from '@credo-ts/core' import { CustomRequest } from '@hyperledger/indy-vdr-shared' -import { verificationKeyForIndyDid } from './dids/didIndyUtil' +import { verificationPublicJwkForIndyDid } from './dids/didIndyUtil' import { IndyVdrError } from './error' import { IndyVdrPoolService } from './pool' import { multiSignRequest, signRequest } from './utils/sign' @@ -22,7 +22,7 @@ export class IndyVdrApi { private async multiSignRequest( request: Request, - signingKey: Key, + signingKey: Kms.PublicJwk, identifier: string ) { return multiSignRequest(this.agentContext, request, signingKey, identifier) @@ -61,7 +61,7 @@ export class IndyVdrApi { * @returns An endorsed transaction */ public async endorseTransaction(transaction: string | Record, endorserDid: string) { - const endorserSigningKey = await verificationKeyForIndyDid(this.agentContext, endorserDid) + const endorserSigningKey = await verificationPublicJwkForIndyDid(this.agentContext, endorserDid) const { namespaceIdentifier } = parseIndyDid(endorserDid) const request = new CustomRequest({ customRequest: transaction }) diff --git a/packages/indy-vdr/src/IndyVdrModule.ts b/packages/indy-vdr/src/IndyVdrModule.ts index a3e5f10f4b..b46bfd1d0a 100644 --- a/packages/indy-vdr/src/IndyVdrModule.ts +++ b/packages/indy-vdr/src/IndyVdrModule.ts @@ -29,7 +29,17 @@ export class IndyVdrModule implements Module { for (const pool of indyVdrPoolService.pools) { if (pool.config.connectOnStartup) { - await pool.connect() + pool.connect() + } + } + } + + public async shutdown(agentContext: AgentContext): Promise { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + for (const pool of indyVdrPoolService.pools) { + if (pool.isOpen) { + pool.close() } } } diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts index d383a4e885..92dacf596e 100644 --- a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -57,7 +57,7 @@ import { SchemaRequest, } from '@hyperledger/indy-vdr-shared' -import { verificationKeyForIndyDid } from '../dids/didIndyUtil' +import { verificationPublicJwkForIndyDid } from '../dids/didIndyUtil' import { IndyVdrPoolService } from '../pool' import { multiSignRequest } from '../utils/sign' @@ -177,7 +177,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { schema: { id: legacySchemaId, name, ver: '1.0', version, attrNames }, }) - const submitterKey = await verificationKeyForIndyDid(agentContext, issuerId) + const submitterKey = await verificationPublicJwkForIndyDid(agentContext, issuerId) writeRequest = await pool.prepareWriteRequest( agentContext, schemaRequest, @@ -201,7 +201,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } if (endorserMode === 'internal' && endorserDid !== issuerId) { - const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + const endorserKey = await verificationPublicJwkForIndyDid(agentContext, endorserDid as string) await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) } } @@ -407,7 +407,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { }, }) - const submitterKey = await verificationKeyForIndyDid(agentContext, issuerId) + const submitterKey = await verificationPublicJwkForIndyDid(agentContext, issuerId) writeRequest = await pool.prepareWriteRequest( agentContext, credentialDefinitionRequest, @@ -431,7 +431,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } if (endorserMode === 'internal' && endorserDid !== issuerId) { - const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + const endorserKey = await verificationPublicJwkForIndyDid(agentContext, endorserDid as string) await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) } } @@ -657,7 +657,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { }, }) - const submitterKey = await verificationKeyForIndyDid(agentContext, revocationRegistryDefinition.issuerId) + const submitterKey = await verificationPublicJwkForIndyDid(agentContext, revocationRegistryDefinition.issuerId) writeRequest = await pool.prepareWriteRequest( agentContext, revocationRegistryDefinitionRequest, @@ -681,7 +681,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } if (endorserMode === 'internal' && endorserDid !== revocationRegistryDefinition.issuerId) { - const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + const endorserKey = await verificationPublicJwkForIndyDid(agentContext, endorserDid as string) await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) } } @@ -890,7 +890,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, }) - const submitterKey = await verificationKeyForIndyDid(agentContext, revocationStatusList.issuerId) + const submitterKey = await verificationPublicJwkForIndyDid(agentContext, revocationStatusList.issuerId) writeRequest = await pool.prepareWriteRequest( agentContext, revocationRegistryDefinitionRequest, @@ -913,7 +913,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } if (endorserMode === 'internal' && endorserDid !== revocationStatusList.issuerId) { - const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + const endorserKey = await verificationPublicJwkForIndyDid(agentContext, endorserDid as string) await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) } } diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts index e8647b8693..13d8fe959c 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidRegistrar.ts @@ -1,10 +1,10 @@ import type { AgentContext, - Buffer, DidCreateOptions, DidCreateResult, DidDeactivateResult, DidDocument, + DidDocumentKey, DidDocumentService, DidOperationStateActionBase, DidRegistrar, @@ -23,8 +23,7 @@ import { DidRepository, Hasher, IndyAgentService, - Key, - KeyType, + Kms, NewDidCommV2Service, TypedArrayEncoder, } from '@credo-ts/core' @@ -38,8 +37,7 @@ import { createKeyAgreementKey, didDocDiff, indyDidDocumentFromDid, - isSelfCertifiedIndyDid, - verificationKeyForIndyDid, + verificationPublicJwkForIndyDid, } from './didIndyUtil' import { endpointsAttribFromServices } from './didSovUtil' @@ -65,7 +63,11 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { } } - private didCreateFailedResult({ reason }: { reason: string }): IndyVdrDidCreateResult { + private didCreateFailedResult({ + reason, + }: { + reason: string + }): IndyVdrDidCreateResult { return { didDocumentMetadata: {}, didRegistrationMetadata: {}, @@ -77,14 +79,10 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { } private didCreateFinishedResult({ - seed, - privateKey, did, didDocument, namespace, }: { - seed: Buffer | undefined - privateKey: Buffer | undefined did: string didDocument: DidDocument namespace: string @@ -98,106 +96,92 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { state: 'finished', did, didDocument, - secret: { - // FIXME: the uni-registrar creates the seed in the registrar method - // if it doesn't exist so the seed can always be returned. Currently - // we can only return it if the seed was passed in by the user. Once - // we have a secure method for generating seeds we should use the same - // approach - seed: seed, - privateKey: privateKey, - }, }, } } public async parseInput(agentContext: AgentContext, options: IndyVdrDidCreateOptions): Promise { - let did = options.did - let namespaceIdentifier: string - let verificationKey: Key - const seed = options.secret?.seed - const privateKey = options.secret?.privateKey - if (options.options.endorsedTransaction) { - const _did = did as string - const { namespace } = parseIndyDid(_did) + if (!options.did || typeof options.did !== 'string') { + return { + status: 'error', + reason: 'If endorsedTransaction is provided, a DID must also be provided', + } + } + const { namespace, namespaceIdentifier } = parseIndyDid(options.did) // endorser did from the transaction const endorserNamespaceIdentifier = JSON.parse(options.options.endorsedTransaction.nymRequest).identifier return { status: 'ok', - did: _did, - namespace: namespace, - namespaceIdentifier: parseIndyDid(_did).namespaceIdentifier, + type: 'endorsedTransaction', + endorsedTransaction: options.options.endorsedTransaction, + did: options.did, + namespace, + namespaceIdentifier, endorserNamespaceIdentifier, - seed, - privateKey, } } const endorserDid = options.options.endorserDid const { namespace: endorserNamespace, namespaceIdentifier: endorserNamespaceIdentifier } = parseIndyDid(endorserDid) - const allowOne = [privateKey, seed, did].filter((e) => e !== undefined) - if (allowOne.length > 1) { + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) + + const _verificationKey = options.options.keyId + ? await kms.getPublicKey({ keyId: options.options.keyId }) + : ( + await kms.createKey({ + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + }) + ).publicJwk + + if (_verificationKey.kty !== 'OKP' || _verificationKey.crv !== 'Ed25519') { return { status: 'error', - reason: `Only one of 'seed', 'privateKey' and 'did' must be provided`, + reason: `keyId must point to an Ed25519 key, but found ${Kms.getJwkHumanDescription(_verificationKey)}`, } } - if (did) { - if (!options.options.verkey) { - return { - status: 'error', - reason: 'If a did is defined, a matching verkey must be provided', - } - } - - const { namespace: didNamespace, namespaceIdentifier: didNamespaceIdentifier } = parseIndyDid(did) - namespaceIdentifier = didNamespaceIdentifier - verificationKey = Key.fromPublicKeyBase58(options.options.verkey, KeyType.Ed25519) + const verificationKey = Kms.PublicJwk.fromPublicJwk(_verificationKey) as Kms.PublicJwk - if (!isSelfCertifiedIndyDid(did, options.options.verkey)) { - return { - status: 'error', - reason: `Initial verkey ${options.options.verkey} does not match did ${did}`, - } - } + // Create a new key and calculate did according to the rules for indy did method + const buffer = Hasher.hash(verificationKey.publicKey.publicKey, 'sha-256') - if (didNamespace !== endorserNamespace) { - return { - status: 'error', - reason: `The endorser did uses namespace: '${endorserNamespace}' and the did to register uses namespace: '${didNamespace}'. Namespaces must match.`, - } - } - } else { - // Create a new key and calculate did according to the rules for indy did method - verificationKey = await agentContext.wallet.createKey({ privateKey, seed, keyType: KeyType.Ed25519 }) - const buffer = Hasher.hash(verificationKey.publicKey, 'sha-256') - - namespaceIdentifier = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) - did = `did:indy:${endorserNamespace}:${namespaceIdentifier}` - } + const namespaceIdentifier = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) + const did = `did:indy:${endorserNamespace}:${namespaceIdentifier}` return { status: 'ok', + type: 'create', did, verificationKey, + endorserDid: options.options.endorserDid, + alias: options.options.alias, + role: options.options.role, + services: options.options.services, + useEndpointAttrib: options.options.useEndpointAttrib, namespaceIdentifier, namespace: endorserNamespace, endorserNamespaceIdentifier, - seed, - privateKey, } } - public async saveDidRecord(agentContext: AgentContext, did: string, didDocument: DidDocument): Promise { + public async saveDidRecord( + agentContext: AgentContext, + did: string, + didDocument: DidDocument, + keys: DidDocumentKey[] + ): Promise { // Save the did so we know we created it and can issue with it const didRecord = new DidRecord({ did, role: DidDocumentRole.Created, didDocument, + keys, }) const didRepository = agentContext.dependencyManager.resolve(DidRepository) @@ -206,14 +190,14 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { private createDidDocument( did: string, - verificationKey: Key, + verificationKey: Kms.PublicJwk, services: DidDocumentService[] | undefined, useEndpointAttrib: boolean | undefined ) { + const verificationKeyBase58 = TypedArrayEncoder.toBase58(verificationKey.publicKey.publicKey) // Create base did document - const didDocumentBuilder = indyDidDocumentFromDid(did, verificationKey.publicKeyBase58) - // biome-ignore lint/suspicious/noImplicitAnyLet: - let diddocContent + const didDocumentBuilder = indyDidDocumentFromDid(did, verificationKeyBase58) + let diddocContent: Record | undefined = undefined // Add services if object was passed if (services) { @@ -245,7 +229,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { .addVerificationMethod({ controller: did, id: keyAgreementId, - publicKeyBase58: createKeyAgreementKey(verificationKey.publicKeyBase58), + publicKeyBase58: createKeyAgreementKey(verificationKeyBase58), type: 'X25519KeyAgreementKey2019', }) .addKeyAgreement(keyAgreementId) @@ -261,7 +245,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { // create diddocContent parameter based on the diff between the base and the resulting DID Document diddocContent = didDocDiff( didDocumentBuilder.build().toJSON(), - indyDidDocumentFromDid(did, verificationKey.publicKeyBase58).build().toJSON() + indyDidDocumentFromDid(did, verificationKeyBase58).build().toJSON() ) } } @@ -275,29 +259,39 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { } } + // FIXME: we need to completely revamp this logic, it's overly complex + // We might even want to look at ditching the whole generic DIDs api ... public async create(agentContext: AgentContext, options: IndyVdrDidCreateOptions): Promise { try { const res = await this.parseInput(agentContext, options) if (res.status === 'error') return this.didCreateFailedResult({ reason: res.reason }) - const { did, namespaceIdentifier, endorserNamespaceIdentifier, verificationKey, namespace, seed, privateKey } = - res + const did = res.did - const pool = agentContext.dependencyManager.resolve(IndyVdrPoolService).getPoolForNamespace(namespace) + const pool = agentContext.dependencyManager.resolve(IndyVdrPoolService).getPoolForNamespace(res.namespace) let nymRequest: NymRequest | CustomRequest let didDocument: DidDocument | undefined let attribRequest: AttribRequest | CustomRequest | undefined - let alias: string | undefined + let verificationKey: Kms.PublicJwk | undefined = undefined - if (options.options.endorsedTransaction) { - const { nymRequest: _nymRequest, attribRequest: _attribRequest } = options.options.endorsedTransaction + if (res.type === 'endorsedTransaction') { + const { nymRequest: _nymRequest, attribRequest: _attribRequest } = res.endorsedTransaction nymRequest = new CustomRequest({ customRequest: _nymRequest }) attribRequest = _attribRequest ? new CustomRequest({ customRequest: _attribRequest }) : undefined } else { - const { services, useEndpointAttrib } = options.options - alias = options.options.alias - if (!verificationKey) throw new Error('VerificationKey not defined') + const { + services, + useEndpointAttrib, + alias, + endorserNamespaceIdentifier, + namespaceIdentifier, + did, + role, + endorserDid, + namespace, + } = res + verificationKey = res.verificationKey const { didDocument: _didDocument, diddocContent } = this.createDidDocument( did, @@ -307,9 +301,10 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { ) didDocument = _didDocument - let didRegisterSigningKey: Key | undefined = undefined - if (options.options.endorserMode === 'internal') - didRegisterSigningKey = await verificationKeyForIndyDid(agentContext, options.options.endorserDid) + const didRegisterSigningKey = + options.options.endorserMode === 'internal' + ? await verificationPublicJwkForIndyDid(agentContext, options.options.endorserDid) + : undefined nymRequest = await this.createRegisterDidWriteRequest({ agentContext, @@ -320,7 +315,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { verificationKey, alias, diddocContent, - role: options.options.role, + role, }) if (services && useEndpointAttrib) { @@ -329,33 +324,56 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { agentContext, pool, signingKey: verificationKey, - endorserDid: options.options.endorserMode === 'external' ? options.options.endorserDid : undefined, + endorserDid: options.options.endorserMode === 'external' ? endorserDid : undefined, unqualifiedDid: namespaceIdentifier, endpoints, }) } if (options.options.endorserMode === 'external') { + // We already save the did record, including the link between kms key id and did key id + await this.saveDidRecord(agentContext, did, didDocument, [ + { + didDocumentRelativeKeyId: '#verkey', + kmsKeyId: verificationKey.keyId, + }, + ]) const didAction: EndorseDidTxAction = { state: 'action', action: 'endorseIndyTransaction', - endorserDid: options.options.endorserDid, + endorserDid: endorserDid, nymRequest: nymRequest.body, attribRequest: attribRequest?.body, did: did, - secret: { seed, privateKey }, } return this.didCreateActionResult({ namespace, didAction, did }) } } + await this.registerPublicDid(agentContext, pool, nymRequest) if (attribRequest) await this.setEndpointsForDid(agentContext, pool, attribRequest) + + // DID Document is undefined if this method is called based on external endorsement + // but in that case the did document is already saved + if (verificationKey && didDocument) { + await this.saveDidRecord(agentContext, did, didDocument, [ + { + didDocumentRelativeKeyId: '#verkey', + kmsKeyId: verificationKey.keyId, + }, + ]) + } + didDocument = didDocument ?? (await buildDidDocument(agentContext, pool, did)) - await this.saveDidRecord(agentContext, did, didDocument) - return this.didCreateFinishedResult({ did, didDocument, namespace, seed, privateKey }) + return this.didCreateFinishedResult({ did, didDocument, namespace: res.namespace }) } catch (error) { - return this.didCreateFailedResult({ reason: `unknownError: ${error.message}` }) + agentContext.config.logger.error('Error creating indy did', { + error, + }) + return this.didCreateFailedResult({ + reason: `unknownError: ${error.message}`, + }) } } @@ -386,8 +404,8 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { pool: IndyVdrPool submitterNamespaceIdentifier: string namespaceIdentifier: string - verificationKey: Key - signingKey?: Key + verificationKey: Kms.PublicJwk + signingKey?: Kms.PublicJwk alias: string | undefined diddocContent?: Record role?: NymRequestRole @@ -411,7 +429,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { const request = new NymRequest({ submitterDid: submitterNamespaceIdentifier, dest: namespaceIdentifier, - verkey: verificationKey.publicKeyBase58, + verkey: TypedArrayEncoder.toBase58(verificationKey.publicKey.publicKey), alias, role, }) @@ -447,7 +465,7 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { private async createSetDidEndpointsRequest(options: { agentContext: AgentContext pool: IndyVdrPool - signingKey: Key + signingKey: Kms.PublicJwk endorserDid?: string unqualifiedDid: string endpoints: IndyEndpointAttrib @@ -488,14 +506,21 @@ export class IndyVdrIndyDidRegistrar implements DidRegistrar { } } -interface IndyVdrDidCreateOptionsBase extends DidCreateOptions { +interface IndyVdrDidCreateOptionsWithoutDid extends DidCreateOptions { didDocument?: never // Not yet supported + did?: never + method: 'indy' options: { + /** + * Optionally an existing keyId can be provided, in this case the did will be created + * based on the existing key + */ + keyId?: string + alias?: string role?: NymRequestRole services?: DidDocumentService[] useEndpointAttrib?: boolean - verkey?: string // endorserDid is always required. We just have internal or external mode endorserDid: string @@ -504,20 +529,6 @@ interface IndyVdrDidCreateOptionsBase extends DidCreateOptions { endorserMode: 'internal' | 'external' endorsedTransaction?: never } - secret?: { - seed?: Buffer - privateKey?: Buffer - } -} - -interface IndyVdrDidCreateOptionsWithDid extends IndyVdrDidCreateOptionsBase { - method?: never - did: string -} - -interface IndyVdrDidCreateOptionsWithoutDid extends IndyVdrDidCreateOptionsBase { - method: 'indy' - did?: never } // When transactions have been endorsed. Only supported for external mode @@ -536,31 +547,38 @@ interface IndyVdrDidCreateOptionsForSubmission extends DidCreateOptions { attribRequest?: string } } - secret?: { - seed?: Buffer - privateKey?: Buffer - } } -export type IndyVdrDidCreateOptions = - | IndyVdrDidCreateOptionsWithDid - | IndyVdrDidCreateOptionsWithoutDid - | IndyVdrDidCreateOptionsForSubmission +export type IndyVdrDidCreateOptions = IndyVdrDidCreateOptionsWithoutDid | IndyVdrDidCreateOptionsForSubmission + +type ParseInputOkEndorsedTransaction = { + status: 'ok' + did: string + type: 'endorsedTransaction' + endorsedTransaction: IndyVdrDidCreateOptionsForSubmission['options']['endorsedTransaction'] + namespaceIdentifier: string + namespace: string + endorserNamespaceIdentifier: string +} -type ParseInputOk = { +type ParseInputOkCreate = { status: 'ok' + type: 'create' did: string - verificationKey?: Key + verificationKey: Kms.PublicJwk namespaceIdentifier: string namespace: string endorserNamespaceIdentifier: string - seed: Buffer | undefined - privateKey: Buffer | undefined + endorserDid: string + alias?: string + role?: NymRequestRole + services?: DidDocumentService[] + useEndpointAttrib?: boolean } type parseInputError = { status: 'error'; reason: string } -type ParseInputResult = ParseInputOk | parseInputError +type ParseInputResult = ParseInputOkEndorsedTransaction | ParseInputOkCreate | parseInputError export interface EndorseDidTxAction extends DidOperationStateActionBase { action: 'endorseIndyTransaction' diff --git a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts index 61539d19c1..8890ce16e8 100644 --- a/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts +++ b/packages/indy-vdr/src/dids/__tests__/IndyVdrIndyDidRegistrar.test.ts @@ -1,4 +1,4 @@ -import type { DidRecord, RecordSavedEvent } from '@credo-ts/core' +import { DidRecord, RecordSavedEvent } from '@credo-ts/core' import { DidCommV1Service, @@ -9,8 +9,7 @@ import { DidsApi, EventEmitter, JsonTransformer, - Key, - KeyType, + Kms, NewDidCommV2Service, NewDidCommV2ServiceEndpoint, RepositoryEventTypes, @@ -20,7 +19,7 @@ import { import { Subject } from 'rxjs' import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' -import { InMemoryWallet } from '../../../../../tests/InMemoryWallet' +import { transformPrivateKeyToPrivateJwk } from '../../../../askar/src' import { agentDependencies, getAgentConfig, getAgentContext, mockProperty } from '../../../../core/tests' import { IndyVdrPool, IndyVdrPoolService } from '../../pool' import { IndyVdrIndyDidRegistrar } from '../IndyVdrIndyDidRegistrar' @@ -31,17 +30,12 @@ const poolMock = new IndyVdrPoolMock() mockProperty(poolMock, 'indyNamespace', 'ns1') const agentConfig = getAgentConfig('IndyVdrIndyDidRegistrar') -const wallet = new InMemoryWallet() -jest - .spyOn(wallet, 'createKey') - .mockResolvedValue(Key.fromPublicKeyBase58('E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', KeyType.Ed25519)) const storageService = new InMemoryStorageService() const eventEmitter = new EventEmitter(agentDependencies, new Subject()) const didRepository = new DidRepository(storageService, eventEmitter) const agentContext = getAgentContext({ - wallet, registerInstances: [ [DidRepository, didRepository], [IndyVdrPoolService, { getPoolForNamespace: jest.fn().mockReturnValue(poolMock) }], @@ -56,7 +50,21 @@ const agentContext = getAgentContext({ id: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg#verkey', type: 'Ed25519VerificationKey2018', controller: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + publicKeyBase58: 'DtPcLpky6Yi6zPecfW8VZH3xNoDkvQfiGWp8u5n9nAj6', + }), + ], + }), + }), + resolveCreatedDidRecordWithDocument: jest.fn().mockResolvedValue({ + didRecord: new DidRecord({ did: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', role: DidDocumentRole.Created }), + didDocument: new DidDocument({ + id: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + authentication: [ + new VerificationMethod({ + id: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg#verkey', + type: 'Ed25519VerificationKey2018', + controller: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + publicKeyBase58: 'DtPcLpky6Yi6zPecfW8VZH3xNoDkvQfiGWp8u5n9nAj6', }), ], }), @@ -68,23 +76,43 @@ const agentContext = getAgentContext({ }) const indyVdrIndyDidRegistrar = new IndyVdrIndyDidRegistrar() +const kms = agentContext.resolve(Kms.KeyManagementApi) + +const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') +const keyId = 'the-key-id' +const privateJwk = transformPrivateKeyToPrivateJwk({ + privateKey, + type: { crv: 'Ed25519', kty: 'OKP' }, +}).privateJwk +privateJwk.kid = keyId describe('IndyVdrIndyDidRegistrar', () => { + beforeAll(async () => { + await kms.importKey({ + privateJwk, + }) + }) + afterEach(() => { jest.clearAllMocks() }) - test('returns an error state if both did and privateKey are provided', async () => { + test('returns an error state if the provided key id is not an Ed25519 key', async () => { + await kms.createKey({ + keyId: 'no-ed25519', + type: { + kty: 'EC', + crv: 'P-256', + }, + }) const result = await indyVdrIndyDidRegistrar.create(agentContext, { - did: 'did:indy:pool1:did-value', + method: 'indy', options: { alias: 'Hello', endorserMode: 'internal', + keyId: 'no-ed25519', endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', }, - secret: { - privateKey: TypedArrayEncoder.fromString('key'), - }, }) expect(JsonTransformer.toJSON(result)).toMatchObject({ @@ -92,7 +120,7 @@ describe('IndyVdrIndyDidRegistrar', () => { didRegistrationMetadata: {}, didState: { state: 'failed', - reason: `Only one of 'seed', 'privateKey' and 'did' must be provided`, + reason: `keyId must point to an Ed25519 key, but found EC key with crv 'P-256'`, }, }) }) @@ -117,93 +145,7 @@ describe('IndyVdrIndyDidRegistrar', () => { }) }) - test('returns an error state if did is provided, but it is not a valid did:indy did', async () => { - const result = await indyVdrIndyDidRegistrar.create(agentContext, { - did: 'BzCbsNYhMrjHiqZDTUASHg', - options: { - endorserMode: 'internal', - endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - verkey: 'verkey', - alias: 'Hello', - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'unknownError: BzCbsNYhMrjHiqZDTUASHg is not a valid did:indy did', - }, - }) - }) - - test('returns an error state if did is provided, but no verkey', async () => { - const result = await indyVdrIndyDidRegistrar.create(agentContext, { - did: 'BzCbsNYhMrjHiqZDTUASHg', - options: { - endorserMode: 'internal', - endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - alias: 'Hello', - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'If a did is defined, a matching verkey must be provided', - }, - }) - }) - - test('returns an error state if did and verkey are provided, but the did is not self certifying', async () => { - const result = await indyVdrIndyDidRegistrar.create(agentContext, { - did: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - options: { - endorserMode: 'internal', - endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - verkey: 'verkey', - alias: 'Hello', - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Initial verkey verkey does not match did did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - }, - }) - }) - - test('returns an error state if did is provided, but does not match with the namespace from the endorserDid', async () => { - const result = await indyVdrIndyDidRegistrar.create(agentContext, { - did: 'did:indy:pool2:B6xaJg1c2xU3D9ppCtt1CZ', - options: { - endorserMode: 'internal', - endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - alias: 'Hello', - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: - "The endorser did uses namespace: 'pool1' and the did to register uses namespace: 'pool2'. Namespaces must match.", - }, - }) - }) - test('creates a did:indy document without services', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - // @ts-ignore - method is private const createRegisterDidWriteRequest = jest.spyOn( indyVdrIndyDidRegistrar, @@ -224,19 +166,17 @@ describe('IndyVdrIndyDidRegistrar', () => { endorserMode: 'internal', endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', role: 'STEWARD', - }, - secret: { - privateKey, + keyId, }, }) expect(createRegisterDidWriteRequest).toHaveBeenCalledWith({ agentContext, pool: poolMock, - signingKey: expect.any(Key), + signingKey: expect.any(Kms.PublicJwk), submitterNamespaceIdentifier: 'BzCbsNYhMrjHiqZDTUASHg', - namespaceIdentifier: 'B6xaJg1c2xU3D9ppCtt1CZ', - verificationKey: expect.any(Key), + namespaceIdentifier: 'Q4HNw3AuzNBacei9KsAxno', + verificationKey: expect.any(Kms.PublicJwk), alias: 'Hello', diddocContent: undefined, role: 'STEWARD', @@ -248,278 +188,27 @@ describe('IndyVdrIndyDidRegistrar', () => { didRegistrationMetadata: {}, didState: { state: 'finished', - did: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - didDocument: { - '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - verificationMethod: [ - { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey', - type: 'Ed25519VerificationKey2018', - controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - }, - ], - authentication: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey'], - assertionMethod: undefined, - keyAgreement: undefined, - }, - secret: { - privateKey, - }, - }, - }) - }) - - test('creates a did:indy document by passing did', async () => { - // @ts-ignore - method is private - const createRegisterDidWriteRequest = jest.spyOn( - indyVdrIndyDidRegistrar, - 'createRegisterDidWriteRequest' - ) - // @ts-ignore type check fails because method is private - createRegisterDidWriteRequest.mockImplementationOnce(() => Promise.resolve()) - - // @ts-ignore - method is private - const registerPublicDidSpy = jest.spyOn(indyVdrIndyDidRegistrar, 'registerPublicDid') - // @ts-ignore type check fails because method is private - registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) - - const result = await indyVdrIndyDidRegistrar.create(agentContext, { - did: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - options: { - verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - alias: 'Hello', - endorserMode: 'internal', - endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - role: 'STEWARD', - }, - secret: {}, - }) - - expect(createRegisterDidWriteRequest).toHaveBeenCalledWith({ - agentContext, - pool: poolMock, - signingKey: expect.any(Key), - submitterNamespaceIdentifier: 'BzCbsNYhMrjHiqZDTUASHg', - namespaceIdentifier: 'B6xaJg1c2xU3D9ppCtt1CZ', - verificationKey: expect.any(Key), - alias: 'Hello', - diddocContent: undefined, - role: 'STEWARD', - }) - - expect(registerPublicDidSpy).toHaveBeenCalledWith( - agentContext, - poolMock, - // writeRequest - undefined - ) - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'finished', - did: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + did: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno', didDocument: { '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + id: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno', verificationMethod: [ { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey', + id: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno#verkey', type: 'Ed25519VerificationKey2018', - controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + controller: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno', + publicKeyBase58: 'DtPcLpky6Yi6zPecfW8VZH3xNoDkvQfiGWp8u5n9nAj6', }, ], - authentication: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey'], + authentication: ['did:indy:pool1:Q4HNw3AuzNBacei9KsAxno#verkey'], assertionMethod: undefined, keyAgreement: undefined, }, - secret: {}, - }, - }) - }) - - test('creates a did:indy document with services using diddocContent', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - - // @ts-ignore - method is private - const createRegisterDidWriteRequestSpy = jest.spyOn( - indyVdrIndyDidRegistrar, - 'createRegisterDidWriteRequest' - ) - // @ts-ignore type check fails because method is private - createRegisterDidWriteRequestSpy.mockImplementationOnce(() => Promise.resolve()) - - // @ts-ignore - method is private - const registerPublicDidSpy = jest.spyOn(indyVdrIndyDidRegistrar, 'registerPublicDid') - // @ts-ignore type check fails because method is private - registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) - - // @ts-ignore - method is private - const setEndpointsForDidSpy = jest.spyOn(indyVdrIndyDidRegistrar, 'setEndpointsForDid') - - const result = await indyVdrIndyDidRegistrar.create(agentContext, { - method: 'indy', - options: { - alias: 'Hello', - endorserMode: 'internal', - endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', - role: 'STEWARD', - services: [ - new DidDocumentService({ - id: '#endpoint', - serviceEndpoint: 'https://example.com/endpoint', - type: 'endpoint', - }), - new DidCommV1Service({ - id: '#did-communication', - priority: 0, - recipientKeys: ['#key-agreement-1'], - routingKeys: ['key-1'], - serviceEndpoint: 'https://example.com/endpoint', - accept: ['didcomm/aip2;env=rfc19'], - }), - new NewDidCommV2Service({ - id: '#didcomm-messaging-1', - serviceEndpoint: new NewDidCommV2ServiceEndpoint({ - accept: ['didcomm/v2'], - routingKeys: ['key-1'], - uri: 'https://example.com/endpoint', - }), - }), - ], - }, - secret: { - privateKey, - }, - }) - - expect(createRegisterDidWriteRequestSpy).toHaveBeenCalledWith({ - agentContext, - pool: poolMock, - signingKey: expect.any(Key), - submitterNamespaceIdentifier: 'BzCbsNYhMrjHiqZDTUASHg', - namespaceIdentifier: 'B6xaJg1c2xU3D9ppCtt1CZ', - verificationKey: expect.any(Key), - alias: 'Hello', - role: 'STEWARD', - diddocContent: { - '@context': [], - authentication: [], - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - keyAgreement: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1'], - service: [ - { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#endpoint', - serviceEndpoint: 'https://example.com/endpoint', - type: 'endpoint', - }, - { - accept: ['didcomm/aip2;env=rfc19'], - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#did-communication', - priority: 0, - recipientKeys: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1'], - routingKeys: ['key-1'], - serviceEndpoint: 'https://example.com/endpoint', - type: 'did-communication', - }, - { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#didcomm-messaging-1', - serviceEndpoint: { - uri: 'https://example.com/endpoint', - accept: ['didcomm/v2'], - routingKeys: ['key-1'], - }, - type: 'DIDCommMessaging', - }, - ], - verificationMethod: [ - { - controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1', - publicKeyBase58: 'Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', - type: 'X25519KeyAgreementKey2019', - }, - ], - }, - }) - - expect(registerPublicDidSpy).toHaveBeenCalledWith( - agentContext, - poolMock, - // writeRequest - undefined - ) - expect(setEndpointsForDidSpy).not.toHaveBeenCalled() - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'finished', - did: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - didDocument: { - '@context': [ - 'https://w3id.org/did/v1', - 'https://w3id.org/security/suites/ed25519-2018/v1', - 'https://w3id.org/security/suites/x25519-2019/v1', - 'https://didcomm.org/messaging/contexts/v2', - ], - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - verificationMethod: [ - { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey', - type: 'Ed25519VerificationKey2018', - controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - }, - { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1', - type: 'X25519KeyAgreementKey2019', - controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - publicKeyBase58: 'Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', - }, - ], - service: [ - { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#endpoint', - serviceEndpoint: 'https://example.com/endpoint', - type: 'endpoint', - }, - { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#did-communication', - serviceEndpoint: 'https://example.com/endpoint', - type: 'did-communication', - priority: 0, - recipientKeys: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1'], - routingKeys: ['key-1'], - accept: ['didcomm/aip2;env=rfc19'], - }, - { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#didcomm-messaging-1', - type: 'DIDCommMessaging', - serviceEndpoint: { - uri: 'https://example.com/endpoint', - routingKeys: ['key-1'], - accept: ['didcomm/v2'], - }, - }, - ], - authentication: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey'], - assertionMethod: undefined, - keyAgreement: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1'], - }, - secret: { - privateKey, - }, }, }) }) test('creates a did:indy document with services using attrib', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - // @ts-ignore - method is private const createRegisterDidWriteRequestSpy = jest.spyOn( indyVdrIndyDidRegistrar, @@ -550,6 +239,7 @@ describe('IndyVdrIndyDidRegistrar', () => { method: 'indy', options: { alias: 'Hello', + keyId, endorserMode: 'internal', endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', role: 'STEWARD', @@ -578,19 +268,16 @@ describe('IndyVdrIndyDidRegistrar', () => { }), ], }, - secret: { - privateKey, - }, }) expect(result.didState.state).toEqual('finished') expect(createRegisterDidWriteRequestSpy).toHaveBeenCalledWith({ agentContext, pool: poolMock, - signingKey: expect.any(Key), + signingKey: expect.any(Kms.PublicJwk), submitterNamespaceIdentifier: 'BzCbsNYhMrjHiqZDTUASHg', - namespaceIdentifier: 'B6xaJg1c2xU3D9ppCtt1CZ', - verificationKey: expect.any(Key), + namespaceIdentifier: 'Q4HNw3AuzNBacei9KsAxno', + verificationKey: expect.any(Kms.PublicJwk), alias: 'Hello', diddocContent: undefined, role: 'STEWARD', @@ -605,10 +292,10 @@ describe('IndyVdrIndyDidRegistrar', () => { expect(createSetDidEndpointsRequestSpy).toHaveBeenCalledWith({ agentContext, pool: poolMock, - signingKey: expect.any(Key), + signingKey: expect.any(Kms.PublicJwk), endorserDid: undefined, // Unqualified created indy did - unqualifiedDid: 'B6xaJg1c2xU3D9ppCtt1CZ', + unqualifiedDid: 'Q4HNw3AuzNBacei9KsAxno', endpoints: { endpoint: 'https://example.com/endpoint', routingKeys: ['key-1'], @@ -621,7 +308,7 @@ describe('IndyVdrIndyDidRegistrar', () => { didRegistrationMetadata: {}, didState: { state: 'finished', - did: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + did: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno', didDocument: { '@context': [ 'https://w3id.org/did/v1', @@ -629,56 +316,51 @@ describe('IndyVdrIndyDidRegistrar', () => { 'https://w3id.org/security/suites/x25519-2019/v1', 'https://didcomm.org/messaging/contexts/v2', ], - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + id: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno', verificationMethod: [ { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey', + id: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno#verkey', type: 'Ed25519VerificationKey2018', - controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + controller: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno', + publicKeyBase58: 'DtPcLpky6Yi6zPecfW8VZH3xNoDkvQfiGWp8u5n9nAj6', }, { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1', + id: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno#key-agreement-1', type: 'X25519KeyAgreementKey2019', - controller: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', - publicKeyBase58: 'Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', + controller: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno', + publicKeyBase58: '7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE', }, ], service: [ { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#endpoint', + id: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno#endpoint', serviceEndpoint: 'https://example.com/endpoint', type: 'endpoint', }, { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#did-communication', + id: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno#did-communication', serviceEndpoint: 'https://example.com/endpoint', type: 'did-communication', priority: 0, - recipientKeys: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1'], + recipientKeys: ['did:indy:pool1:Q4HNw3AuzNBacei9KsAxno#key-agreement-1'], routingKeys: ['key-1'], accept: ['didcomm/aip2;env=rfc19'], }, { - id: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#didcomm-messaging-1', + id: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno#didcomm-messaging-1', type: 'DIDCommMessaging', serviceEndpoint: { uri: 'https://example.com/endpoint', routingKeys: ['key-1'], accept: ['didcomm/v2'] }, }, ], - authentication: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#verkey'], + authentication: ['did:indy:pool1:Q4HNw3AuzNBacei9KsAxno#verkey'], assertionMethod: undefined, - keyAgreement: ['did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ#key-agreement-1'], - }, - secret: { - privateKey, + keyAgreement: ['did:indy:pool1:Q4HNw3AuzNBacei9KsAxno#key-agreement-1'], }, }, }) }) test('stores the did document', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - // @ts-ignore - method is private const createRegisterDidWriteRequestSpy = jest.spyOn( indyVdrIndyDidRegistrar, @@ -704,6 +386,7 @@ describe('IndyVdrIndyDidRegistrar', () => { method: 'indy', options: { alias: 'Hello', + keyId, endorserMode: 'internal', endorserDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', role: 'STEWARD', @@ -731,19 +414,16 @@ describe('IndyVdrIndyDidRegistrar', () => { }), ], }, - secret: { - privateKey, - }, }) expect(saveCalled).toHaveBeenCalledTimes(1) const [saveEvent] = saveCalled.mock.calls[0] expect(saveEvent.payload.record.getTags()).toMatchObject({ - recipientKeyFingerprints: ['z6LSrH6AdsQeZuKKmG6Ehx7abEQZsVg2psR2VU536gigUoAe'], + recipientKeyFingerprints: ['z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz'], }) expect(saveEvent.payload.record).toMatchObject({ - did: 'did:indy:pool1:B6xaJg1c2xU3D9ppCtt1CZ', + did: 'did:indy:pool1:Q4HNw3AuzNBacei9KsAxno', role: DidDocumentRole.Created, didDocument: expect.any(DidDocument), }) diff --git a/packages/indy-vdr/src/dids/didIndyUtil.ts b/packages/indy-vdr/src/dids/didIndyUtil.ts index bf0f1043ec..b064e0a836 100644 --- a/packages/indy-vdr/src/dids/didIndyUtil.ts +++ b/packages/indy-vdr/src/dids/didIndyUtil.ts @@ -10,11 +10,10 @@ import { DidsApi, Hasher, JsonTransformer, - Key, - KeyType, + Kms, TypedArrayEncoder, convertPublicKeyToX25519, - getKeyFromVerificationMethod, + getPublicJwkFromVerificationMethod, } from '@credo-ts/core' import { GetAttribRequest, GetNymRequest } from '@hyperledger/indy-vdr-shared' @@ -152,7 +151,11 @@ export function isSelfCertifiedIndyDid(did: string, verkey: string): boolean { const { namespace } = parseIndyDid(did) const { did: didFromVerkey } = indyDidFromNamespaceAndInitialKey( namespace, - Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) + Kms.PublicJwk.fromPublicKey({ + crv: 'Ed25519', + kty: 'OKP', + publicKey: TypedArrayEncoder.fromBase58(verkey), + }) ) if (didFromVerkey === did) { @@ -162,11 +165,11 @@ export function isSelfCertifiedIndyDid(did: string, verkey: string): boolean { return false } -export function indyDidFromNamespaceAndInitialKey(namespace: string, initialKey: Key) { - const buffer = Hasher.hash(initialKey.publicKey, 'sha-256') +export function indyDidFromNamespaceAndInitialKey(namespace: string, initialKey: Kms.PublicJwk) { + const buffer = Hasher.hash(initialKey.publicKey.publicKey, 'sha-256') const id = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) - const verkey = initialKey.publicKeyBase58 + const verkey = TypedArrayEncoder.toBase58(initialKey.publicKey.publicKey) const did = `did:indy:${namespace}:${id}` return { did, id, verkey } @@ -177,23 +180,21 @@ export function indyDidFromNamespaceAndInitialKey(namespace: string, initialKey: * * @throws {@link CredoError} if the did could not be resolved or the key could not be extracted */ -export async function verificationKeyForIndyDid(agentContext: AgentContext, did: string) { - // FIXME: we should store the didDocument in the DidRecord so we don't have to fetch our own did - // from the ledger to know which key is associated with the did +export async function verificationPublicJwkForIndyDid(agentContext: AgentContext, did: string) { const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didResult = await didsApi.resolve(did) - if (!didResult.didDocument) { - throw new CredoError( - `Could not resolve did ${did}. ${didResult.didResolutionMetadata.error} ${didResult.didResolutionMetadata.message}` - ) - } + const { didRecord, didDocument } = await didsApi.resolveCreatedDidRecordWithDocument(did) - // did:indy dids MUST have a verificationMethod with #verkey - const verificationMethod = didResult.didDocument.dereferenceKey(`${did}#verkey`) - const key = getKeyFromVerificationMethod(verificationMethod) + const verificationMethod = didDocument.dereferenceKey('#verkey') + const key = didRecord.keys?.find((key) => key.didDocumentRelativeKeyId === '#verkey') - return key + const publicJwk = getPublicJwkFromVerificationMethod(verificationMethod) + if (!publicJwk.is(Kms.Ed25519PublicJwk)) { + throw new CredoError('Expected #verkey verification mehod to be of type Ed25519') + } + + publicJwk.keyId = key?.kmsKeyId ?? publicJwk.legacyKeyId + return publicJwk } export async function getPublicDid(pool: IndyVdrPool, unqualifiedDid: string) { @@ -246,7 +247,6 @@ export async function getEndpointsForDid(agentContext: AgentContext, pool: IndyV export async function buildDidDocument(agentContext: AgentContext, pool: IndyVdrPool, did: string) { const { namespaceIdentifier } = parseIndyDid(did) - const nym = await getPublicDid(pool, namespaceIdentifier) // Create base Did Document @@ -279,12 +279,11 @@ export async function buildDidDocument(agentContext: AgentContext, pool: IndyVdr return builder.build() } // Combine it with didDoc - // biome-ignore lint/suspicious/noImplicitAnyLet: - let diddocContent + let diddocContent: Record try { - diddocContent = JSON.parse(nym.diddocContent) as Record + diddocContent = JSON.parse(nym.diddocContent) } catch (error) { - agentContext.config.logger.error(`Nym diddocContent is not a valid json string: ${diddocContent}`) + agentContext.config.logger.error(`Nym diddocContent is not a valid json string: ${nym.diddocContent}`) throw new IndyVdrError(`Nym diddocContent failed to parse as JSON: ${error}`) } return combineDidDocumentWithJson(builder.build(), diddocContent) diff --git a/packages/indy-vdr/src/pool/IndyVdrPool.ts b/packages/indy-vdr/src/pool/IndyVdrPool.ts index fc476b6d21..435fa3f102 100644 --- a/packages/indy-vdr/src/pool/IndyVdrPool.ts +++ b/packages/indy-vdr/src/pool/IndyVdrPool.ts @@ -1,4 +1,4 @@ -import type { AgentContext, Key } from '@credo-ts/core' +import { AgentContext, Kms } from '@credo-ts/core' import type { IndyVdrRequest, RequestResponseType, IndyVdrPool as indyVdrPool } from '@hyperledger/indy-vdr-shared' import { parseIndyDid } from '@credo-ts/anoncreds' @@ -56,6 +56,10 @@ export class IndyVdrPool { return this.poolConfig } + public get isOpen() { + return this._pool !== undefined + } + public connect() { if (this._pool) { return @@ -103,18 +107,20 @@ export class IndyVdrPool { public async prepareWriteRequest( agentContext: AgentContext, request: Request, - signingKey: Key, + signingKey: Kms.PublicJwk, endorserDid?: string ) { + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) await this.appendTaa(request) if (endorserDid) { request.setEndorser({ endorser: parseIndyDid(endorserDid).namespaceIdentifier }) } - const signature = await agentContext.wallet.sign({ + const { signature } = await kms.sign({ data: TypedArrayEncoder.fromString(request.signatureInput), - key: signingKey, + algorithm: 'EdDSA', + keyId: signingKey.keyId, }) request.setSignature({ diff --git a/packages/indy-vdr/src/utils/sign.ts b/packages/indy-vdr/src/utils/sign.ts index 5a98beb355..f629d8d1af 100644 --- a/packages/indy-vdr/src/utils/sign.ts +++ b/packages/indy-vdr/src/utils/sign.ts @@ -1,20 +1,22 @@ -import type { AgentContext, Key } from '@credo-ts/core' +import { AgentContext, Kms } from '@credo-ts/core' import type { IndyVdrRequest } from '@hyperledger/indy-vdr-shared' import type { IndyVdrPool } from '../pool' import { TypedArrayEncoder } from '@credo-ts/core' -import { verificationKeyForIndyDid } from '../dids/didIndyUtil' +import { verificationPublicJwkForIndyDid } from '../dids/didIndyUtil' export async function multiSignRequest( agentContext: AgentContext, request: Request, - signingKey: Key, + signingKey: Kms.PublicJwk, identifier: string ) { - const signature = await agentContext.wallet.sign({ + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) + const { signature } = await kms.sign({ data: TypedArrayEncoder.fromString(request.signatureInput), - key: signingKey, + algorithm: 'EdDSA', + keyId: signingKey.keyId, }) request.setMultiSignature({ @@ -31,7 +33,7 @@ export async function signRequest( request: Request, submitterDid: string ) { - const signingKey = await verificationKeyForIndyDid(agentContext, submitterDid) + const signingKey = await verificationPublicJwkForIndyDid(agentContext, submitterDid) const signedRequest = await pool.prepareWriteRequest(agentContext, request, signingKey) return signedRequest diff --git a/packages/indy-vdr/tests/helpers.ts b/packages/indy-vdr/tests/helpers.ts index 29ae6e0b43..6d90952d66 100644 --- a/packages/indy-vdr/tests/helpers.ts +++ b/packages/indy-vdr/tests/helpers.ts @@ -1,13 +1,7 @@ import type { Agent } from '@credo-ts/core' import type { IndyVdrDidCreateOptions } from '../src/dids/IndyVdrIndyDidRegistrar' -import { - DidCommV1Service, - DidDocumentService, - KeyType, - NewDidCommV2Service, - NewDidCommV2ServiceEndpoint, -} from '@credo-ts/core' +import { DidCommV1Service, DidDocumentService, NewDidCommV2Service, NewDidCommV2ServiceEndpoint } from '@credo-ts/core' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' import { sleep } from '../../core/src/utils/sleep' @@ -27,7 +21,12 @@ export const indyVdrModuleConfig = new IndyVdrModuleConfig({ }) export async function createDidOnLedger(agent: Agent, endorserDid: string) { - const key = await agent.wallet.createKey({ keyType: KeyType.Ed25519 }) + const key = await agent.kms.createKey({ + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + }) const createResult = await agent.dids.create({ method: 'indy', @@ -36,7 +35,7 @@ export async function createDidOnLedger(agent: Agent, endorserDid: string) { endorserDid: endorserDid, alias: 'Alias', role: 'TRUSTEE', - verkey: key.publicKeyBase58, + keyId: key.keyId, useEndpointAttrib: true, services: [ new DidDocumentService({ diff --git a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts index ca7a9fd044..c39e4eb961 100644 --- a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts @@ -8,7 +8,7 @@ import { import { Agent, DidsModule, TypedArrayEncoder } from '@credo-ts/core' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' -import { getInMemoryAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' +import { getAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' import { IndyVdrIndyDidResolver, IndyVdrModule, IndyVdrSovDidResolver } from '../src' import { IndyVdrAnonCredsRegistry } from '../src/anoncreds/IndyVdrAnonCredsRegistry' import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' @@ -20,7 +20,7 @@ import { indyVdrModuleConfig } from './helpers' const indyVdrAnonCredsRegistry = new IndyVdrAnonCredsRegistry() const endorser = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'IndyVdrAnonCredsRegistryEndorser', {}, {}, @@ -38,7 +38,7 @@ const endorser = new Agent( ) const agent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'IndyVdrAnonCredsRegistryAgent', {}, {}, @@ -82,9 +82,7 @@ describe('IndyVdrAnonCredsRegistry', () => { } await endorser.shutdown() - await endorser.wallet.delete() await agent.shutdown() - await agent.wallet.delete() }) test('register and resolve a schema and credential definition (internal, issuerDid != endorserDid)', async () => { diff --git a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts index 4c9b61b231..7cbb33cdec 100644 --- a/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-did-registrar.e2e.test.ts @@ -7,29 +7,25 @@ import { DidDocumentService, DidsModule, JsonTransformer, - Key, - KeyType, + Kms, NewDidCommV2Service, NewDidCommV2ServiceEndpoint, TypedArrayEncoder, } from '@credo-ts/core' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' -import { convertPublicKeyToX25519, generateKeyPairFromSeed } from '@stablelib/ed25519' +import { convertPublicKeyToX25519 } from '@stablelib/ed25519' -import { - getInMemoryAgentOptions, - importExistingIndyDidFromPrivateKey, - retryUntilResult, -} from '../../core/tests/helpers' +import { getAgentOptions, importExistingIndyDidFromPrivateKey, retryUntilResult } from '../../core/tests/helpers' import { IndyVdrModule, IndyVdrSovDidResolver } from '../src' import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' import { IndyVdrIndyDidResolver } from '../src/dids/IndyVdrIndyDidResolver' import { indyDidFromNamespaceAndInitialKey } from '../src/dids/didIndyUtil' +import { transformPrivateKeyToPrivateJwk } from '../../askar/src' import { indyVdrModuleConfig } from './helpers' const endorser = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'Indy VDR Indy DID Registrar', {}, {}, @@ -46,7 +42,7 @@ const endorser = new Agent( ) ) const agent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'Indy VDR Indy DID Registrar', {}, {}, @@ -79,9 +75,7 @@ describe('Indy VDR Indy Did Registrar', () => { afterAll(async () => { await endorser.shutdown() - await endorser.wallet.delete() await agent.shutdown() - await agent.wallet.delete() }) test('can register a did:indy without services', async () => { @@ -165,26 +159,29 @@ describe('Indy VDR Indy Did Registrar', () => { }) test('can register an endorsed did:indy without services - did and verkey specified', async () => { - // Generate a seed and the indy did. This allows us to create a new did every time - // but still check if the created output document is as expected. - const seed = Array(32 + 1) - .join(`${Math.random().toString(36)}00000000000000000`.slice(2, 18)) - .slice(0, 32) - - const keyPair = generateKeyPairFromSeed(TypedArrayEncoder.fromString(seed)) - const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(keyPair.publicKey) + const privateKey = TypedArrayEncoder.fromString( + Array(32 + 1) + .join(`${Math.random().toString(36)}00000000000000000`.slice(2, 18)) + .slice(0, 32) + ) - const { did, verkey } = indyDidFromNamespaceAndInitialKey( - 'pool:localtest', - Key.fromPublicKey(keyPair.publicKey, KeyType.Ed25519) + const key = await agent.kms.importKey( + transformPrivateKeyToPrivateJwk({ + type: { kty: 'OKP', crv: 'Ed25519' }, + privateKey, + }) ) + const publicJwk = Kms.PublicJwk.fromPublicJwk(key.publicJwk) + const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey) + + const { did } = indyDidFromNamespaceAndInitialKey('pool:localtest', publicJwk) const didCreateTobeEndorsedResult = (await agent.dids.create({ - did, + method: 'indy', options: { endorserDid, endorserMode: 'external', - verkey, + keyId: key.keyId, }, })) as IndyVdrDidCreateResult @@ -196,14 +193,13 @@ describe('Indy VDR Indy Did Registrar', () => { didState.endorserDid ) const didCreateSubmitResult = await agent.dids.create({ - did: didState.did, + did, options: { endorserMode: 'external', endorsedTransaction: { nymRequest: signedNymRequest, }, }, - secret: didState.secret, }) if (didCreateSubmitResult.didState.state !== 'finished') { @@ -265,25 +261,28 @@ describe('Indy VDR Indy Did Registrar', () => { }) test('can register a did:indy without services - did and verkey specified', async () => { - // Generate a seed and the indy did. This allows us to create a new did every time - // but still check if the created output document is as expected. - const seed = Array(32 + 1) - .join(`${Math.random().toString(36)}00000000000000000`.slice(2, 18)) - .slice(0, 32) - - const keyPair = generateKeyPairFromSeed(TypedArrayEncoder.fromString(seed)) - const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(keyPair.publicKey) + const privateKey = TypedArrayEncoder.fromString( + Array(32 + 1) + .join(`${Math.random().toString(36)}00000000000000000`.slice(2, 18)) + .slice(0, 32) + ) - const { did, verkey } = indyDidFromNamespaceAndInitialKey( - 'pool:localtest', - Key.fromPublicKey(keyPair.publicKey, KeyType.Ed25519) + const key = await endorser.kms.importKey( + transformPrivateKeyToPrivateJwk({ + type: { kty: 'OKP', crv: 'Ed25519' }, + privateKey, + }) ) + const publicJwk = Kms.PublicJwk.fromPublicJwk(key.publicJwk) + const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey) + + const { did } = indyDidFromNamespaceAndInitialKey('pool:localtest', publicJwk) const didRegistrationResult = await endorser.dids.create({ - did, + method: 'indy', options: { endorserDid, endorserMode: 'internal', - verkey, + keyId: key.keyId, }, }) @@ -351,22 +350,22 @@ describe('Indy VDR Indy Did Registrar', () => { .slice(0, 32) ) - const key = await endorser.wallet.createKey({ privateKey, keyType: KeyType.Ed25519 }) - const x25519PublicKeyBase58 = TypedArrayEncoder.toBase58(convertPublicKeyToX25519(key.publicKey)) - const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(key.publicKey) - - const { did, verkey } = indyDidFromNamespaceAndInitialKey( - 'pool:localtest', - Key.fromPublicKey(key.publicKey, KeyType.Ed25519) + const key = await endorser.kms.importKey( + transformPrivateKeyToPrivateJwk({ type: { kty: 'OKP', crv: 'Ed25519' }, privateKey }) ) + const publicJwk = Kms.PublicJwk.fromPublicJwk(key.publicJwk) + const x25519PublicKeyBase58 = TypedArrayEncoder.toBase58(convertPublicKeyToX25519(publicJwk.publicKey.publicKey)) + const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey) + + const { did } = indyDidFromNamespaceAndInitialKey('pool:localtest', publicJwk) const didRegistrationResult = await endorser.dids.create({ - did, + method: 'indy', options: { endorserDid, endorserMode: 'internal', useEndpointAttrib: true, - verkey, + keyId: key.keyId, services: [ new DidDocumentService({ id: `${did}#endpoint`, @@ -477,22 +476,23 @@ describe('Indy VDR Indy Did Registrar', () => { .slice(0, 32) ) - const key = await endorser.wallet.createKey({ privateKey, keyType: KeyType.Ed25519 }) - const x25519PublicKeyBase58 = TypedArrayEncoder.toBase58(convertPublicKeyToX25519(key.publicKey)) - const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(key.publicKey) - - const { did, verkey } = indyDidFromNamespaceAndInitialKey( - 'pool:localtest', - Key.fromPublicKey(key.publicKey, KeyType.Ed25519) + const key = await endorser.kms.importKey( + transformPrivateKeyToPrivateJwk({ type: { kty: 'OKP', crv: 'Ed25519' }, privateKey }) ) + const publicJwk = Kms.PublicJwk.fromPublicJwk(key.publicJwk) + const x25519PublicKeyBase58 = TypedArrayEncoder.toBase58(convertPublicKeyToX25519(publicJwk.publicKey.publicKey)) + const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey) + + const { did } = indyDidFromNamespaceAndInitialKey('pool:localtest', publicJwk) const didCreateTobeEndorsedResult = (await endorser.dids.create({ - did, + method: 'indy', options: { endorserMode: 'external', endorserDid: endorserDid, useEndpointAttrib: true, - verkey, + keyId: key.keyId, + // keyId: key.keyId services: [ new DidDocumentService({ id: `${did}#endpoint`, @@ -534,7 +534,7 @@ describe('Indy VDR Indy Did Registrar', () => { ) const didCreateSubmitResult = await agent.dids.create({ - did: didState.did, + did, options: { endorserMode: 'external', endorsedTransaction: { @@ -542,7 +542,6 @@ describe('Indy VDR Indy Did Registrar', () => { attribRequest: endorsedAttribRequest, }, }, - secret: didState.secret, }) const expectedDidDocument = { diff --git a/packages/indy-vdr/tests/indy-vdr-indy-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-indy-did-resolver.e2e.test.ts index fd8f3ed3cb..a0f2409bf9 100644 --- a/packages/indy-vdr/tests/indy-vdr-indy-did-resolver.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-indy-did-resolver.e2e.test.ts @@ -1,14 +1,14 @@ import { Agent, DidsModule, JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' -import { getInMemoryAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' +import { getAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' import { IndyVdrModule } from '../src' import { IndyVdrIndyDidRegistrar, IndyVdrIndyDidResolver, IndyVdrSovDidResolver } from '../src/dids' import { createDidOnLedger, indyVdrModuleConfig } from './helpers' const agent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'Indy VDR Indy DID resolver', {}, {}, @@ -32,7 +32,6 @@ describe('indy-vdr DID Resolver E2E', () => { afterAll(async () => { await agent.shutdown() - await agent.wallet.delete() }) test('resolve a did:indy did', async () => { diff --git a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts index 34046aceea..ddbcba5001 100644 --- a/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-pool.e2e.test.ts @@ -1,21 +1,23 @@ -import type { Key } from '@credo-ts/core' - -import { KeyType, TypedArrayEncoder } from '@credo-ts/core' +import { Kms, TypedArrayEncoder } from '@credo-ts/core' import { CredentialDefinitionRequest, GetNymRequest, NymRequest, SchemaRequest } from '@hyperledger/indy-vdr-shared' -import { InMemoryWallet } from '../../../tests/InMemoryWallet' import { genesisTransactions, getAgentConfig, getAgentContext } from '../../core/tests/helpers' import testLogger from '../../core/tests/logger' import { IndyVdrPool } from '../src/pool' import { IndyVdrPoolService } from '../src/pool/IndyVdrPoolService' import { indyDidFromPublicKeyBase58 } from '../src/utils/did' +import { transformPrivateKeyToPrivateJwk } from '../../askar/src' +import { NodeInMemoryKeyManagementStorage, NodeKeyManagementService } from '../../node/src' import { indyVdrModuleConfig } from './helpers' const indyVdrPoolService = new IndyVdrPoolService(testLogger, indyVdrModuleConfig) -const wallet = new InMemoryWallet() const agentConfig = getAgentConfig('IndyVdrPoolService') -const agentContext = getAgentContext({ wallet, agentConfig }) +const agentContext = getAgentContext({ + agentConfig, + kmsBackends: [new NodeKeyManagementService(new NodeInMemoryKeyManagementStorage())], +}) +const kms = agentContext.resolve(Kms.KeyManagementApi) const config = { isProduction: false, @@ -24,24 +26,27 @@ const config = { transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, } as const -let signerKey: Key +let signerKey: Kms.PublicJwk describe('IndyVdrPoolService', () => { beforeAll(async () => { - await wallet.createAndOpen(agentConfig.walletConfig) - - signerKey = await wallet.createKey({ - privateKey: TypedArrayEncoder.fromString('000000000000000000000000Trustee9'), - keyType: KeyType.Ed25519, + const createdKey = await kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('000000000000000000000000Trustee9'), + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + }).privateJwk, }) + + signerKey = Kms.PublicJwk.fromPublicJwk(createdKey.publicJwk) }) afterAll(async () => { for (const pool of indyVdrPoolService.pools) { pool.close() } - - await wallet.delete() }) describe('DIDs', () => { @@ -88,13 +93,15 @@ describe('IndyVdrPoolService', () => { const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') // prepare the DID we are going to write to the ledger - const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const did = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const key = await kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } }) + const publicJwk = Kms.PublicJwk.fromPublicJwk(key.publicJwk) + const publicKeyBase58 = TypedArrayEncoder.toBase58(publicJwk.publicKey.publicKey) + const did = indyDidFromPublicKeyBase58(publicKeyBase58) const request = new NymRequest({ dest: did, submitterDid: 'TL1EaPFCZ8Si5aUrqScBDt', - verkey: key.publicKeyBase58, + verkey: publicKeyBase58, }) const writeRequest = await pool.prepareWriteRequest(agentContext, request, signerKey) diff --git a/packages/indy-vdr/tests/indy-vdr-sov-did-resolver.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-sov-did-resolver.e2e.test.ts index 813963a762..be450568b6 100644 --- a/packages/indy-vdr/tests/indy-vdr-sov-did-resolver.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-sov-did-resolver.e2e.test.ts @@ -2,14 +2,14 @@ import { parseIndyDid } from '@credo-ts/anoncreds' import { Agent, DidsModule, JsonTransformer, TypedArrayEncoder } from '@credo-ts/core' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' -import { getInMemoryAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' +import { getAgentOptions, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' import { IndyVdrModule } from '../src' import { IndyVdrIndyDidRegistrar, IndyVdrIndyDidResolver, IndyVdrSovDidResolver } from '../src/dids' import { createDidOnLedger, indyVdrModuleConfig } from './helpers' const agent = new Agent( - getInMemoryAgentOptions( + getAgentOptions( 'Indy VDR Sov DID resolver', {}, {}, @@ -33,7 +33,6 @@ describe('Indy VDR Sov DID Resolver', () => { afterAll(async () => { await agent.shutdown() - await agent.wallet.delete() }) test('resolve a did:sov did', async () => { diff --git a/packages/node/jest.config.ts b/packages/node/jest.config.ts index 2556d19c61..93c0197296 100644 --- a/packages/node/jest.config.ts +++ b/packages/node/jest.config.ts @@ -7,6 +7,7 @@ import packageJson from './package.json' const config: Config.InitialOptions = { ...base, displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], } export default config diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index b73c3bbcf1..c46dc43d97 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -7,6 +7,10 @@ import { NodeFileSystem } from './NodeFileSystem' import { HttpInboundTransport } from './transport/HttpInboundTransport' import { WsInboundTransport } from './transport/WsInboundTransport' +export { NodeInMemoryKeyManagementStorage } from './kms/NodeInMemoryKeyManagementStorage' +export { NodeKeyManagementService } from './kms/NodeKeyManagementService' +export { NodeKeyManagementStorage } from './kms/NodeKeyManagementStorage' + const agentDependencies: AgentDependencies = { FileSystem: NodeFileSystem, fetch, diff --git a/packages/node/src/kms/NodeInMemoryKeyManagementStorage.ts b/packages/node/src/kms/NodeInMemoryKeyManagementStorage.ts new file mode 100644 index 0000000000..c58fcccd19 --- /dev/null +++ b/packages/node/src/kms/NodeInMemoryKeyManagementStorage.ts @@ -0,0 +1,33 @@ +import type { AgentContext, Kms } from '@credo-ts/core' +import type { NodeKeyManagementStorage } from './NodeKeyManagementStorage' + +export class NodeInMemoryKeyManagementStorage implements NodeKeyManagementStorage { + #storage = new Map>() + + public async get(agentContext: AgentContext, keyId: string) { + return this.storageForContext(agentContext).get(keyId) ?? null + } + + public has(agentContext: AgentContext, keyId: string) { + return this.storageForContext(agentContext).has(keyId) + } + + public set(agentContext: AgentContext, keyId: string, jwk: Kms.KmsJwkPrivate) { + this.storageForContext(agentContext).set(keyId, jwk) + } + + public delete(agentContext: AgentContext, keyId: string) { + return this.storageForContext(agentContext).delete(keyId) + } + + private storageForContext(agentContext: AgentContext) { + let storage = this.#storage.get(agentContext.contextCorrelationId) + + if (!storage) { + storage = new Map() + this.#storage.set(agentContext.contextCorrelationId, storage) + } + + return storage + } +} diff --git a/packages/node/src/kms/NodeKeyManagementService.ts b/packages/node/src/kms/NodeKeyManagementService.ts new file mode 100644 index 0000000000..0aa970e8d5 --- /dev/null +++ b/packages/node/src/kms/NodeKeyManagementService.ts @@ -0,0 +1,428 @@ +import type { AgentContext } from '@credo-ts/core' +import type { NodeKeyManagementStorage } from './NodeKeyManagementStorage' + +import { createPrivateKey, createSecretKey, randomBytes, randomUUID } from 'node:crypto' +import { Kms, TypedArrayEncoder } from '@credo-ts/core' + +import { + assertNodeSupportedEcCrv, + assertNodeSupportedOctAlgorithm, + assertNodeSupportedOkpCrv, + createEcKey, + createOctKey, + createOkpKey, + createRsaKey, +} from './crypto/createKey' +import { performDecrypt } from './crypto/decrypt' +import { deriveDecryptionKey, deriveEncryptionKey, nodeSupportedKeyAgreementAlgorithms } from './crypto/deriveKey' +import { nodeSupportedEncryptionAlgorithms, performEncrypt } from './crypto/encrypt' +import { nodeSupportedJwaAlgorithm, performSign } from './crypto/sign' +import { performVerify } from './crypto/verify' + +export class NodeKeyManagementService implements Kms.KeyManagementService { + public readonly backend = 'node' + + #storage: NodeKeyManagementStorage + + public constructor(storage: NodeKeyManagementStorage) { + this.#storage = storage + } + + public isOperationSupported(_agentContext: AgentContext, operation: Kms.KmsOperation): boolean { + if (operation.operation === 'deleteKey') return true + if (operation.operation === 'randomBytes') return true + + if (operation.operation === 'createKey') { + // TODO: probably clean to split the assert methods so we don't need try/catch here + try { + if (operation.type.kty === 'RSA') { + return true + } + + if (operation.type.kty === 'EC') { + assertNodeSupportedEcCrv(operation.type) + return true + } + + if (operation.type.kty === 'OKP') { + assertNodeSupportedOkpCrv(operation.type) + return true + } + + if (operation.type.kty === 'oct') { + assertNodeSupportedOctAlgorithm(operation.type) + return true + } + } catch { + return false + } + + return false + } + + if (operation.operation === 'importKey') { + try { + if (operation.privateJwk.kty === 'RSA' || operation.privateJwk.kty === 'oct') { + return true + } + + if (operation.privateJwk.kty === 'EC') { + assertNodeSupportedEcCrv({ kty: operation.privateJwk.kty, crv: operation.privateJwk.crv }) + return true + } + + if (operation.privateJwk.kty === 'OKP') { + assertNodeSupportedOkpCrv({ kty: operation.privateJwk.kty, crv: operation.privateJwk.crv }) + return true + } + } catch { + return false + } + } + + if (operation.operation === 'sign' || operation.operation === 'verify') { + return nodeSupportedJwaAlgorithm.includes(operation.algorithm) + } + + if (operation.operation === 'encrypt') { + const isSupportedEncryptionAlgorithm = nodeSupportedEncryptionAlgorithms.includes( + operation.encryption.algorithm as (typeof nodeSupportedEncryptionAlgorithms)[number] + ) + if (!isSupportedEncryptionAlgorithm) return false + if (!operation.keyAgreement) return true + + return nodeSupportedKeyAgreementAlgorithms.includes( + operation.keyAgreement.algorithm as (typeof nodeSupportedKeyAgreementAlgorithms)[number] + ) + } + + if (operation.operation === 'decrypt') { + const isSupportedEncryptionAlgorithm = nodeSupportedEncryptionAlgorithms.includes( + operation.decryption.algorithm as (typeof nodeSupportedEncryptionAlgorithms)[number] + ) + if (!isSupportedEncryptionAlgorithm) return false + if (!operation.keyAgreement) return true + + return nodeSupportedKeyAgreementAlgorithms.includes( + operation.keyAgreement.algorithm as (typeof nodeSupportedKeyAgreementAlgorithms)[number] + ) + } + + return false + } + + public randomBytes(_agentContext: AgentContext, options: Kms.KmsRandomBytesOptions): Kms.KmsRandomBytesReturn { + return { + bytes: randomBytes(options.length), + } + } + + public async getPublicKey(agentContext: AgentContext, keyId: string): Promise { + const privateJwk = await this.#storage.get(agentContext, keyId) + if (!privateJwk) return null + + return Kms.publicJwkFromPrivateJwk(privateJwk) + } + + public async importKey( + agentContext: AgentContext, + options: Kms.KmsImportKeyOptions + ): Promise> { + const { kid } = options.privateJwk + + if (kid) await this.assertKeyNotExists(agentContext, kid) + + const privateJwk = { + ...options.privateJwk, + kid: kid ?? randomUUID(), + } + + try { + if (privateJwk.kty === 'oct') { + // Just check if we can create a secret key instance + createSecretKey(TypedArrayEncoder.fromBase64(privateJwk.k)).export({ format: 'jwk' }) + } else if (privateJwk.kty === 'EC') { + assertNodeSupportedEcCrv({ kty: privateJwk.kty, crv: privateJwk.crv }) + // This validates the JWK + createPrivateKey({ + format: 'jwk', + key: privateJwk, + }) + } else if (privateJwk.kty === 'OKP') { + assertNodeSupportedOkpCrv({ kty: privateJwk.kty, crv: privateJwk.crv }) + // This validates the JWK + createPrivateKey({ + format: 'jwk', + key: privateJwk, + }) + } else if (privateJwk.kty === 'RSA') { + // This validates the JWK + createPrivateKey({ + format: 'jwk', + key: privateJwk, + }) + } else { + // All kty values supported for now, but can change in the future + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + throw new Kms.KeyManagementAlgorithmNotSupportedError(`kty '${privateJwk.kty}'`, this.backend) + } + + await this.#storage.set(agentContext, privateJwk.kid, privateJwk) + const publicJwk = Kms.publicJwkFromPrivateJwk(privateJwk) + + return { + keyId: privateJwk.kid, + publicJwk: { + ...publicJwk, + kid: privateJwk.kid, + }, + } as Kms.KmsImportKeyReturn + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + + throw new Kms.KeyManagementError('Error importing key', { cause: error }) + } + } + + public async deleteKey(agentContext: AgentContext, options: Kms.KmsDeleteKeyOptions): Promise { + return await this.#storage.delete(agentContext, options.keyId) + } + + public async createKey( + agentContext: AgentContext, + options: Kms.KmsCreateKeyOptions + ): Promise> { + const { type, keyId } = options + + if (keyId) await this.assertKeyNotExists(agentContext, keyId) + + try { + let jwks: { publicJwk: Kms.KmsJwkPublic; privateJwk: Kms.KmsJwkPrivate } + if (type.kty === 'EC') { + assertNodeSupportedEcCrv(type) + jwks = await createEcKey(type) + } else if (type.kty === 'OKP') { + assertNodeSupportedOkpCrv(type) + jwks = await createOkpKey(type) + } else if (type.kty === 'RSA') { + jwks = await createRsaKey(type) + } else if (type.kty === 'oct') { + assertNodeSupportedOctAlgorithm(type) + jwks = await createOctKey(type) + } else { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + throw new Kms.KeyManagementAlgorithmNotSupportedError(`kty '${type.kty}'`, this.backend) + } + + jwks.privateJwk.kid = keyId ?? randomUUID() + jwks.publicJwk.kid = jwks.privateJwk.kid + + await this.#storage.set(agentContext, jwks.privateJwk.kid, jwks.privateJwk) + + return { + publicJwk: jwks.publicJwk as Kms.KmsCreateKeyReturn['publicJwk'], + keyId: jwks.publicJwk.kid, + } + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + + throw new Kms.KeyManagementError('Error creating key', { cause: error }) + } + } + + public async sign(agentContext: AgentContext, options: Kms.KmsSignOptions): Promise { + const { keyId, algorithm, data } = options + + // 1. Retrieve the key + const key = await this.getKeyAsserted(agentContext, keyId) + + try { + // 2. Validate alg and use for key + Kms.assertAllowedSigningAlgForKey(key, algorithm) + Kms.assertKeyAllowsSign(key) + + // 3. Perform the signing operation + const signature = await performSign(key, algorithm, data) + + return { + signature, + } + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + + throw new Kms.KeyManagementError('Error signing with key', { cause: error }) + } + } + + public async verify(agentContext: AgentContext, options: Kms.KmsVerifyOptions): Promise { + const { algorithm, data, signature } = options + + try { + let key: Exclude | Kms.KmsJwkPrivate + if (typeof options.key === 'string') { + key = await this.getKeyAsserted(agentContext, options.key) + } else if (options.key.kty === 'EC') { + assertNodeSupportedEcCrv(options.key) + key = options.key + } else if (options.key.kty === 'OKP') { + assertNodeSupportedOkpCrv(options.key) + key = options.key + } else if (options.key.kty === 'RSA') { + key = options.key + } else { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + throw new Kms.KeyManagementAlgorithmNotSupportedError(`kty ${options.key.kty}`, this.backend) + } + + // 2. Validate alg and use for key + Kms.assertAllowedSigningAlgForKey(key, algorithm) + Kms.assertKeyAllowsVerify(key) + + // 3. Perform the verify operation + const verified = await performVerify(key, algorithm, data, signature) + if (verified) { + return { + verified: true, + publicJwk: Kms.publicJwkFromPrivateJwk(key), + } + } + + return { + verified: false, + } + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + + throw new Kms.KeyManagementError('Error verifying with key', { cause: error }) + } + } + + public async encrypt(agentContext: AgentContext, options: Kms.KmsEncryptOptions): Promise { + const { data, encryption, key } = options + + Kms.assertSupportedEncryptionAlgorithm(encryption, nodeSupportedEncryptionAlgorithms, this.backend) + + let encryptionKey: Kms.KmsJwkPrivate + let encryptedKey: Kms.KmsEncryptedKey | undefined = undefined + + if (typeof key === 'string') { + encryptionKey = await this.getKeyAsserted(agentContext, key) + } else if ('kty' in key) { + encryptionKey = key + } else { + Kms.assertAllowedKeyDerivationAlgForKey(key.externalPublicJwk, key.algorithm) + Kms.assertKeyAllowsDerive(key.externalPublicJwk) + Kms.assertSupportedKeyAgreementAlgorithm(key, nodeSupportedKeyAgreementAlgorithms, this.backend) + + const privateJwk = await this.getKeyAsserted(agentContext, key.keyId) + Kms.assertJwkAsymmetric(privateJwk, key.keyId) + Kms.assertAllowedKeyDerivationAlgForKey(privateJwk, key.algorithm) + Kms.assertKeyAllowsDerive(privateJwk) + Kms.assertAsymmetricJwkKeyTypeMatches(privateJwk, key.externalPublicJwk) + + const { contentEncryptionKey, encryptedContentEncryptionKey } = await deriveEncryptionKey({ + keyAgreement: key, + encryption, + privateJwk, + }) + + encryptionKey = contentEncryptionKey + encryptedKey = encryptedContentEncryptionKey + } + + if (encryptionKey.kty !== 'oct') { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `kty '${encryptionKey.kty} for content encryption'`, + this.backend + ) + } + + try { + // 2. Validate alg and use for key + Kms.assertAllowedEncryptionAlgForKey(encryptionKey, encryption.algorithm) + Kms.assertKeyAllowsEncrypt(encryptionKey) + + // 3. Perform the encryption operation + const encrypted = await performEncrypt(encryptionKey, options.encryption, data) + return { + ...encrypted, + encryptedKey, + } + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + + throw new Kms.KeyManagementError('Error encrypting', { cause: error }) + } + } + + public async decrypt(agentContext: AgentContext, options: Kms.KmsDecryptOptions): Promise { + const { decryption, encrypted, key } = options + + Kms.assertSupportedEncryptionAlgorithm(decryption, nodeSupportedEncryptionAlgorithms, this.backend) + + let decryptionKey: Kms.KmsJwkPrivate + if (typeof key === 'string') { + decryptionKey = await this.getKeyAsserted(agentContext, key) + } else if ('kty' in key) { + decryptionKey = key + } else { + Kms.assertSupportedKeyAgreementAlgorithm(key, nodeSupportedKeyAgreementAlgorithms, this.backend) + Kms.assertAllowedKeyDerivationAlgForKey(key.externalPublicJwk, key.algorithm) + Kms.assertKeyAllowsDerive(key.externalPublicJwk) + + const privateJwk = await this.getKeyAsserted(agentContext, key.keyId) + Kms.assertJwkAsymmetric(privateJwk, key.keyId) + Kms.assertAllowedKeyDerivationAlgForKey(privateJwk, key.algorithm) + Kms.assertKeyAllowsDerive(privateJwk) + Kms.assertAsymmetricJwkKeyTypeMatches(privateJwk, key.externalPublicJwk) + + const { contentEncryptionKey } = await deriveDecryptionKey({ + keyAgreement: key, + decryption, + privateJwk, + }) + + decryptionKey = contentEncryptionKey + } + + if (decryptionKey.kty !== 'oct') { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `kty '${decryptionKey.kty}' for content encryption`, + this.backend + ) + } + + try { + // 2. Validate alg and use for key + Kms.assertAllowedEncryptionAlgForKey(decryptionKey, decryption.algorithm) + Kms.assertKeyAllowsEncrypt(decryptionKey) + + // 3. Perform the decryption operation + return await performDecrypt(decryptionKey, decryption, encrypted) + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + + throw new Kms.KeyManagementError('Error decrypting', { cause: error }) + } + } + + private async getKeyAsserted(agentContext: AgentContext, keyId: string) { + const storageKey = await this.#storage.get(agentContext, keyId) + if (!storageKey) { + throw new Kms.KeyManagementKeyNotFoundError(keyId, this.backend) + } + + return storageKey + } + + private async assertKeyNotExists(agentContext: AgentContext, keyId: string) { + const storageKey = await this.#storage.get(agentContext, keyId) + + if (storageKey) { + throw new Kms.KeyManagementKeyExistsError(keyId, this.backend) + } + } +} diff --git a/packages/node/src/kms/NodeKeyManagementStorage.ts b/packages/node/src/kms/NodeKeyManagementStorage.ts new file mode 100644 index 0000000000..f393c11c2c --- /dev/null +++ b/packages/node/src/kms/NodeKeyManagementStorage.ts @@ -0,0 +1,14 @@ +import type { AgentContext, CanBePromise, Kms } from '@credo-ts/core' + +export interface NodeKeyManagementStorage { + get(agentContext: AgentContext, keyId: string): CanBePromise + has(agentContext: AgentContext, keyId: string): CanBePromise + + // TODO: can also require `kid` + set(agentContext: AgentContext, keyId: string, jwk: Kms.KmsJwkPrivate): CanBePromise + + /** + * @returns whether the item existed and was removed + */ + delete(agentContext: AgentContext, keyId: string): CanBePromise +} diff --git a/packages/node/src/kms/__fixtures__/jarm-jwe-encrypted-response.json b/packages/node/src/kms/__fixtures__/jarm-jwe-encrypted-response.json new file mode 100644 index 0000000000..8a2a3f0e9f --- /dev/null +++ b/packages/node/src/kms/__fixtures__/jarm-jwe-encrypted-response.json @@ -0,0 +1,40 @@ +{ + "compactJwe": "eyJ0eXAiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiRUNESC1FUyIsImFwdiI6IlNLUmVhZGVyIiwiYXB1IjoiUUcxWHNXVlIycmhyaDVIUSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IllSS25mMWRFMXlpUm1aTkVWbmxkeDM0QUV1SVhVXzdrUmcyRGRGNUQzcFEiLCJ5IjoiVkpadW9iU0RTNHVnLWZEcmhDNnlMSTRpZ0lYalFSeWJnbUxxR2Roc2NpWSJ9fQ..reU-KrtHcdiSu-zB.6FIiZtXyKSMI5vW2jhojGvcSkd4TrWYpDxrFlv7cD3HLac8m7ZIJ1gfeKAKk2MW82A2jvBFHSeAT3e_UT6cGT90wNJNCDMReIpOKLDyfwxSCe3GqumK_kGZYO5v19X9Ru_daOUzeen8RssU-PUhpA3tQCxvrs1_CZXxioga3qCeVS90EDImwPGzZLmWN7izQyIoeuSSLgLKQ-ZsQe31NHbZs_wvscVlkjWxfBUt-fZ0Jra5Ui4VYsmXwvO5mCdLo4pFJ_HDgv-NAh-gzGBaLfAMvvUkt3227aRy9wfVRxumUhswLCnV9TdffSX56vUHxs3oSeOp6o4w9MN5Lmz6PCxwk8rAEN5RIc6RabGvWCL_s_ML_0XHzOeHZObbFAGizZ2oTx3btH3M_6cVJo_EhdI38ZmVGUU3MOTODiwXfm0qhVd2Vya9EvZP9mDz7c4rVHX0rZS3hq-oePvV--1U57n7ztRu-fUALmHbJSIjUmumx9NyzstnwJCdtQqe6-1Z3PUIjsBiXrTTzUB9VvRfNul8khkVB6t1wf0bmPe9pUzeLJoMA-JpsVdvu5dPVZN2o1CBjKSuTXd98rcJeaONcGWAYK_YAWgCNFLhsrAJfvvxpjRDOZRVepzUKeIDaErvDnQj59euW4NIAhIt44vL-JwIyW2K5d8nk1urp394ZCGawU6wPGvHGLwkGCi6lyb578X3IZ7szzcWc98DRi2fHjsJhcwsqz9B8sceBywSE_OvKpq_B5TamgwXSOvL0bpiSca2ot4N26zXBNcP3jyz2lixSnEAA8kN1jC_f0teRMd_y-8Ix_1V7CrdamOXk5SWk5cMnv8BgQ7Ddkwm96zIedr7d29NPlAH43T6J8Y6FsS6KTLRdMjsbcv1rwJJbKLY-Y0vkhvmJJLqyo4Ew3sl-EPOAH_nkUEAxr7XlsG8ANV3K5Tl78Qt1gUXsst4t3Bl21Xc0HpCcaT3aNlb9horrDGROz-d9ogazjKlZ99-TDHNd_xVRk7N5l_F74hR27Qtx-lo7icCrQTq05I2RqXGzmxe5XZQBZ1b9Lahiyo0LKIj7brvL6FA88xrrFyYNRpQArv9ROjBLn1NZP5HvELJmmiHfq3QElvOsPb7tzGS010s73OmNBjpTjfUuVpBTvK5HtlTer7KJbduOInO5JkSYmxuB3n4TFfz-gpgIUuZpiFb7rcrwZNwVzZZ5MBJvBWTLRR4Nm4HfwPjdpKr39sYyPb79e33pES4AD1IHkt7XzTMuPp7-bTHWJsRORD01sTOf0Cw5IxG9I1IHqWYJRV6Xwmz8XC65h-wjloFddBSI6v83jauFTz0yhyI9x9IHh5akd5-3fTOjC2at37lxUIfOJz9_MMuQ0e-1BQ_UD2R0Nx3_14EjpqkdVcCKDSQPEwmNjpvZ60GgNPYLFakc4Ox4Ej6PyH4vsChNcdkdX9rGNK5hbvN5pu-Bm9bVlTYu0998V0uhfj2KUUBfhqbJIBSGjn98JuuBgvaLUxkWwEm7XolVvojhITqi1dTVzcK5U-uJkBCHYIKBwJD_miSaKj6xplBxbdvlySpi4zDpdv5PJKoFTCHckIkcIfbgbCq5MOGNyDToCmsx6FV4v56mSHKFwIHeMEATlMVX03EgKVpa889iEwjd191WzJg7VYADXpIEFdP2Nkhaojpg-_7T4pqdA4Nc4pr30-vmDD4ujgMduY3LudOSYtIv0uLaAoIWjR371RKH7EQd6jLXNwuQlJuaA9wxYmwwMCZwCLiXGGUMDYI4iwtESU6k1F18QpGaEQV5XwjEaWWUOVAqBj2KuTW9tqARK1TJXgpqtjUJ_BSLvpOAM2rnZjZkxfqB5eVBbZnV1x7t96mTaXQTHG5BMOqz8_oNt1MBB0jExGsxm5I9aKSZPZFgaeAg7V3JnGbJNFjgRp_N2SvYnqzAU4hgjlqnRMhwBwfcgD1JwKgyVJKUps9mD-K7VY_7vPBNf82WXg4dNdnnwRMRC4hXscnMV8xuLy7CJfbpq6d1Qj3l53DLIkHSMBozadahfBZyP9kdUD5v58Ie1WrEAOdibgFSmB0ra3e1zMOKFqbA2d2nsmS68R5L6e6s9hA2wSWQf6JmYwX4EFUG_h0nAqA_jZpd_FoZ6mc-Dqu_WAOFj61f2CHt0Buk3BVOY3ymOmZpfUt3ATfvN09moIs9sCTcYt0UvTD1dKnecqECko9WaxWO-Z7tiVapQmSeoexF5IgHGOuUY2qLSvEO3zJAW5no7ZwqrUyNcd_bHk1uyZr4t518XjmEsv7BxGZ55weOpg5cccddI1UF_xVuRgbTbx_MgAgI6zpGPwdtCuTppvxyihCJpbWzThuvJ3vGzwWVbmIecswB9Kvkup7daUOg46wNO9RyFPHq2QFTX38mfok3LOnqCI7OBRyq4V5_SZpuujyBDcslzyghKHsySpSjIbElYy19FMJzqlwBmOz0yeNvMcMdd60bHzf_WZMcipQCWJNJqk2GdrMvtViPJKCbAIj3gh_otlVU2aHm0FDGMKKdaUOHE1NzxsL-f0u4qcFQ1zl8L3nmvISDrzqsGWGeFf9h8oc-lK_8zAPeKbwUPccg3jU1_9nKy9RixpQSDzkowMs9rvJ7YOtXAWwNTpMtX5he-LfJOp9GcbHSb4m3YG5S6KiJ8SqqnQ-kVOyqe-xIkOBbco_QtxZJtAiYThOSHItfDON7arnpaw6SvRkpLAw5g03TgPhCShbRkNvTl02NKB34dKSYMakfDmdr2pIPW6pyahHjSq6nhM34FRYMde_MXQM0C55imlIJHDg-wiIk3JzDSSyPggqKFo4AJ3pLj0WaaGLOB32zSilpMq1OptQJKKyLpaPl3uSvjq3nGGzmFzvMLTdjTTixst_I9RP_IwLiRuliKHUnvRNPS6md8xWwZZQYx-NC58y5YQ4ztx0n66CWwGGbaHU17Xr6MExiPzp8OfWgcfII0rbDBVhDH4Tak2EUQaudnDwLofvNA1JTIu07KbXSlmaMyWkXgZcYAD19qwUjjTptstMMNaKNiN1q7RUtk2qwPGUi4h1Yf6AEwJpTwY-E21uT17WPsRACi1eRFFLvIqMlc_DdxVNjfnDAlqzIlghLGKPUMY7dRcAyQu4m3pwagU9gJ0svgv9ogWCqoPoYMbkgDvZlgce_g6JIACYHhUs9zpDu8sp-j2ymUXK3RSGM19E4ixfKGjKNRgluHxSNIGio5kU_cKLoetbO6zPpjoU2V7wXrIkABg3qd3FPxAoKfgIhxeeD80JX9HN0-Jetgd2UuRYY2b4eCb-dQeSAW7FVRA37Qm9t-HN060ynY0uxkjQVU_ADoxc3NhM79V8UbsRm3-MaygWsGkWcSHkcDNKjDf4F_JXxNggJmR0OR91QyUcqqT1bC3OvoNyOYaWBH1-KpFlPUMCIm4dTR2ohMEf7ep4E3UQDUZLa8kqC3nhZPAVrjRGZ7nMs9XNSqrDCxinunL_ad2Dj7p0P51na6fvZXDFNXMUCT_zSNQYNGoODYXmb2m6t0SwMseuoWPyb2zhIuSDKp--z20cSEBCGZ7UJDwq-gYqwQqio_H0b9obUOpnBO9SQ2G99IW5xClSBool0Z2HQA7jjkC6p5ONhNVYr7gJOzGf2G74NDNB8_xZzSj6ti5i0oUu2DEMmiP1OPR37EMdi_pAAH8iWQfCKQVutK3JGQnrypjhgNyoKfisQ-kqTLAHyilIQhy6-BHaRRngC2VCufdUGF95p617QB6-V4DC39juPq4K-5k5WD4npHdMRscqP7AwQpRqL_paW9h3rN51EqHXqglfhFeMBZ6MU6xRIbPAPrgP-yx9Dc-XPv44jdPZnzXOBC-3Jz8yRGlO9aLXv4A_EU6S2tj9jMb-7fo3iEnZdJN78YrVaNOglUhj3JVUbOk57d4yqaNu3uBH_moxxOvGWXKlCp75GjeWh7WSmOZ8Fd0uhtJI-KAo3iODNBBnTZWEuVCqSA93XkEwaur2EhkH5BxSy6k_D-o6YPkSEowst5ctUL0zQXFiDqSOzK4uppdrmLQJJLxdls38Jx6plQNNAjrJ0ax4OP3VSgfW-zDe5zlrvm9FDc7AlAuF1B5HyTFVh1wBo8gIqib4yvmng7WLdXffuo22ugINMxZiuOG6JCGDJ1JJvCnps9PUzF5YHG6T6SaYarwhf--BatSpgFxXxZURDYqbPCedvvfGDq9dzfkdFcCPbpBf-gVi73VjiWERuV_7LW4yg3Z5_qVSE8BpGYHcZhjWtl3v-gUFok6dMGF4rcs3k9etHegP-Kp39hdfQq90O-sui4mmWVAh1LKiAAwHlAFFvowt3M8M9o6SerrTT_rOe536oxa0tBPYfKt6cXNskPkCb7UPdVI0RLMZ7GxN4sbgZ2apdkbtmXS2vyGnct_S7ZDB4gfInOrx6hcBgZQEVANy4_jYx-Mx84XJK4nfuXhdeGgMvCb3Ne51h--owMpoh_Ieem6AL_9pZ0ZxLQOx5LsP-1VJ6dyYOkfQbnCH3zUC2gbxrtlS-QvdQKtL35fyTUjuHQV5NvhuJuW2k-4jLRHDWiu7_1XsOmBVXAib7XNcNEFzGnbmYFzYSQXH-QWC0A1ihNtlYT8tcXfRKXYBFzvKJy6VXCG8VCZJIqmiuLBnKLAw4X-jwjH8d-IJmJeQXk2OvdQ4ivzzsW6bcF2DdBFh90Vj_ofWvVbareqCheiLOeqPIlVmNOiSQvuEkfSB7N9Bk0-Zlx-v3_UHSLrWemrfdsfdi57uO0BDlIGFmBQdpedQqYEv6SUISiHqxVqYMxB0_mmHBsAUx_qOCCsIRTUGUdIeHqkSCJjT89JjsTzeOyVGssYqPQb7u481OpmfxCIRy2blERfSlUj1HFb2UIHDsgw86tYf2I0_MaXYaoCxYHMhIwjB3VwQiDx4s4KOnntJrrxUZmVFZCWSgWQJErrrNdViagEL-7KdDKsaPQVNXYnE9TpkI9tSDycn8p4jY_38yXR3lx0KKuzr6WwsgE9fmOiBOBZ5Nm0JK6IPXdAoYlUodFdDGBQ7TakfnzJthn7iCZhI9lQoeZQGikYe5XVf_uEQXm5Lp3MF0wq3_A6j6YccQ4H4Ls_ggKsa9A_C7HW-ahJYvMDKlCglxUMQw_WMuRv5vcOBvkmz3SGAc8wh_GBQvC4GQLvmOaznUVlaQEviniFWiCMZIKXnKd5sqQsgjDYj4CQHT2nNh6hIYlHFvRA8m7RAy0-MhoAxdNfmepL6vg1ManonHvYADRGoxRVLDpNKdENCzw8V9cRUAHH9S7suNgdkSR2p5zAOSNl4zmKSE0bWVqZa9bfL-wBLyx8DLTAR8LBFi0_yfcG_WWu8FO8W-uYgJfQ6EysDwyzUNzv2PwhsYyj_H2QapH5pchuNOeGVJBdR0FisflDgYK8QmXgm9xifPP6kI1OiwE-uaA5mjgNxjuq9FYCg3gWSHWp5eJqPZFzoKwOvADiy2x_-8VFgSwcB6on7i3_t44VAlYm3VHbM2Q1rAhsBOYBF3fgdcUVaghpIG8oHhOGy-UOzRjub3cQTM_kI1kicaMrnNf-olqAAZyCUAq-U6j-D3KMFYfIe7naUkKhTuoFNc9ONJzHRI5lCRB5WO5GgxvKkMZ7KhrNdkgzfkkT5q45mO6IeAESEvgfL2Cw1xP9kqDIeMgiCuUKYuxmHxK73XtWLmfgXmMg5AeDqeYV6gKOa2OdWAYlUWlMUWFOTcaUE_cuWRBhO6Q-mOalvU--YqdCIcsgJh1NrFNvccWSq5FgDYegKCJ5MKUq6lcToJm1S4ZnRF4-LdgXsSfvK2NZeTEtQfFoTCyBK-kTh40zTmVjNY4OLN4u9-Zwl6JYCWc8eVZN05-hOmXHsiwRzwpCfq-_QhO46t-N1I03dJ_yQFCkbLecLtyYMEtZ7EFWaTJL048rQ2J0DmgCHGEQeFHxolmU3sqPtaBKSmeW0sMBvHfWZR4BYXXT2tpBQfajcJZWzV3WkGW7NHjjp4CUljx87to3uw6d0ui8sHI2xAZ2d-wu6wj45LpvVlOMDQCXsJYhIFaXiGCgESR8nJbHr3YGD5QEL1YNJaCoWZG5rPN6bJ3C1woKlyfJrX8yO-UpKAWWwKrtHSXpW5viZb9U6xnez6x-EDuXXFr19APj279YS7kloPWaVZnBUQH3D3HrNB3-QcpCXomaQ9V5offbnV47Dl4ukIkQb1zA8l48LoZCHUlN7zZvzqhjXNhB8fAzI80Vzvn9LdXLboik3KGNdQEdG7cJHttAUxDQzDWIoTRJ5yySYWr2_wD1CbWieqXEoAYaP9rE7BIcDzxwzgLF9WBCvbP1l16w5MgMnqMbo7ezzDED1LaVPASjwH6s15IrPoiZwRfHno-4oC1a7Yp9EKsQUs0ZWRKZL8DgvAst6Vt1lTh6YN2yKL6eK_BOfdOciKTN1w0IlC817JyEmDMl4NFCOiRWD3LpMwUtskdWp7QnciIUBDJJAC4IHlcOxazQ5OZhSiBRrROSC56ZIzGuRqUx95DLdYzi77ADqH0Gim_rdKuGdcPnHhFztrc0WXonMDdtCIeGAEFR2de23HdWEGO5UiMkJjmHNGa3TYw-vdlEWOjVcFwnVAad2kRXO_5gyLtbQxs-asFbyjiI4tfahvEO4-164TvW--tWC3pEihuyIGzy0GRDE2Ah3-hJAuiio4i0OBl_xGvbP1AwNAz40vnq8Gslw5YqSsF-wkGzSva-IAqv0PLTo9dX1P081CBru6DgwfAsNGoJK-Uy6aHC5Uyu-wSoob-_zGSNW81K0WhaoV75DnGEIVOn-3GDC7nae5_wwgwdWLLvrn_6XM1MfbVtbyoFcXoD2P7ZnjUnaF6jm5KY4c76ZjLCUiNMjbsvbrm6SMgVKmL6htM6PJygPTlmqy_q3968JeCUoxQEErWljBD2od0tC2_Vd1jmyEYuCCGIBzDUYxfX0ZEOI-_Fv1YZ0zdyR4YcfUDlvoZ16MfIGyDI21WCGEK2XlnvM2Qsl9L8EY8NRB9XBn7A9WlDN-AJeHbuMZO6DTUoVOBREQhPKDYwklPtI64ya3rxQ4p0luObgbAGsTQIb9xcNWireGDpaVdoehyOAUcGOa-i7qGVuZRde_d0m_APFTev9tnNVvvGXrCr09_Fjciek1b_2zwGedSM_BO60qneNilGsep6QXoccAVhFCAUALmE1uxHEREnktUWtveTfzXcDgqScMwMbaOmN080CwJ1hMdgPq60wqnFPUYXLKNMHgwl7oIsq0ZoKDENtfCErhtv2_jR-ncxQKDpL5bMIUjDA9IFijd9aqv6AGmB3XjJUyHuzhHyiprVNfFIqUfvr3sv08-MuonNaH-nkeL_KAEa0jb7wXBYrEaWX8tOxKeuk3_TQSnoesSeqB9bR_OiT-F0D-5AHPV_cw4uYoU2spxNjkZph3-tnl8DV5nw147KWun83AM4PqWe3g6eTdsCg-rg1dZQBcvSMNFsWCja6Uudc1JYhZN4hpovZJeWwJbYDwzBF1siZc_jikbv8_00h6N0IlD4cAUraoXFFH6G07TraFT-YaBpnQGeWISKSuUNShS6emJRZAPjmRqjQFSpaAP3tA8MkFNWdW-Tsle834gFTcmVbbbDSMwd0RfmIqOtjgtHrfVBI07F6dF0_qRBycalkufKJ82A_m8-lFrMCssi9iO25YxzeuJNCMsm8P7ibF66IKC3vdIo--v4Slv0_5fDbLD1smCmnXy3GvxK661RVEpcSV7Q40UoSUdNvSZHGKfkRhF_477ob5Ib5NDgDg5-qbBndRXUUJd1ybQZPoeNtG1xIr3jMRoavHrU7qKgxu4HDpW-JmfFaBMaG1PfHhr6J4XpthYwkJwIgdIoFy2wPW49RJrPRNBa1DEtgHGG_qyTFMoFCTYM7Y0fvPyx1VIM0pZn9ieV42wA3sE8pWykThmXwK-cUkOsVaTQxyp3aLIiyppjGJ0NqCyugsse1cm2jh0NgsIZMIM1OQpUMcJ2xpJOlTrbKEXmLTfe71GJaPjkXF-LPawPV2JK77Yt1JK9kLza1ZkLLhole3YTDNu6JzObeabLBTEGydcQT4hcoa-CLLLIjIWUkMaAPj0E62IOKRIEQSDXN7nx1K23nOfTZciYF6WDqFNuUDbfM7pygz1Grh-4cDpyxYYwM8LckE4s2QqqGndYmITUQ7HTkDzBOxQC2Es9fR00cWMQ7jqlVmNu7cjFsOVOVbMUu0Athwy9AjWO8ES5-WJijxdY1GLTZoUoJOCjfUEz0wkpYDOBDW5b6VqbMwvIuLjpYGmle9yknMAwb724s2PxJubonDgkLVTz-_sdol4xFpkWgREJpXrzAe0twQa068RjcMF_uTkKQhL5MNLwerUSOUhMi2xeIvVLFOtLRKnIDxxF4WnZpCgdVnXhAr16-ybelrEs5DfksnNtaeg8uOWczJWQmKgtsE7AuxxK1LRKI8D5KRej51WekSLTI-Duw-D3quHmfAHd9FZQ_5ds2qvNZvkjRgnTzOndT7OEuzychKfxXbhSDu35EjvoNvpxFQrJIO-W1rAZCgnvttE7xbKCktX29X7M5WX6WW0LzFsaE21snp7ylm092yLE2dt7p6TbBkwdhnQ_2R2nT5Sde_VYQF92mYZWgo4DqfikB7nYiablPEoeRdcOpYfunUJFYiSEUYUKk81OzoSAIrkiLnBZeAwxMTtvvuiFa8dBXlZJZN-Uvw5P9YJSzP3xyIGQ1SSjbx5KKFkw1H5e7K6pp24jH7EuVOSuhvFxN2tzjH5f30cgIY53AvI7QyVORuKnaxeRJwpbXVkkT-_gABcoWLWtzu35aW8DWP6PQjtlq3-6bF3h8bOVE0V09hlHZqSsPfyHurqbGB9rPH2aee9gyNirzhhi8Jalb75w_xAteqw8mMnD9h7ZFL8L2IBH-f3pWzJgqurmWxV8wEpI5Kr7u-EcOGy7qWC0NW8O1L3gZ6zr2lt8dL7l8XbblOiGMC-s0gwQ_7khMbsO2XdpcHz0lUKiOBbtxc1FxvA1KZDeQrOs4jpNIlsikiyPRx5xuTVkHoONbbUlGFYYyk_SSg6-YDOBg4RZ8u5II7WJqY5UCB-gZySvzp7KYkSQWiMTQfiFOSgd8EPN3moqJsTTP58r8NuI9jkY7hImZGrjTo_FTsVl_s9fmmoTSG0pPifbLTNEAhylDckCnSa9-OXI_HXyu2drmHPng67VLIFw2pTD5rwofu9oDghqh1hmI3kbtu44xXgUfvk90XSUd0wYP4uIVmpgivww49vkijLyJteeILs7EA8PQHA8MAVl4fc9dJ9x8WDBrBTuxSnzHSPhT5TChntl6qqDfEDloC2Ro3KVJWjYXfCtzkRY_SFRo_p40C2xYHyo_3sVYjlexrsaWjuPLnf_v-qWnZ0tvJiBI_t6P6nXja_U8KMLIJwvH1Z-gdQOS0uPYRMy2RJcrA2obsczqkilTi9z3tbmABw9lgeMHqywQi6URLDkRB9NTG7gCt8-zV6RDS2ZMYdyLVha29JTGn6-IrXye5uzPXtj2PRWM3b207AoHQR8DBvhtALkxdU2b-Nzn3zPheQacQbtew0nLmWVehxJkDPiyAOU9dItTakbvm3q2bTCBBiEFdLxblGETo15Cw4h5U7xST91SmY2XVkikxeRKfFMz9_XMjeVjq2l3eBTULI4TLhdai1ktP4FYshlOXGdroVAkKj4d3M9UvXt8ow0bH_iK29odey1q5VuanB4etOU8jtaF5uaoKX8yvAr35r1bzcpYiKw0fjkg2tqCKMNBeUIJ6I4uyHnGifJ9pLiVEdua_-jcXvLquyoYfQCQEReMRbsbZFleK9C4ZzaUFfL4u2V_bwq3GpGgCQ7Nq-bzBEOMVSiYvvaZS8g5MJWkzu7S0zFyFdRumYdf5NDRnaMfXQ5F6LtKpcxU4A1LO74Pldb-emF0g0ykxFd9qcRUkG6aBQ53xv_dGevRCldqLMnNwDyGKZuTQlIdWbAG85xIrGCwQBMfWb4SSiAjedCeqaTSKC_T7lKeO8v5u0fgELOyUQSLDwdTExXz9urRTmcQDh6QRPO6tbEMVzrL6cXNZhgsIIwnJA-E4leRS9qvjIetFMYSV3J2C3mehTHfgo4lZ_R3Bcs5jgzhamKA3v9iC9PZASC3mTpm0R5WsXkRXp6Y3Qb3h1PGohfAUf3wO_dLSmCEdoHCdHtH4sWWoGf_aP6kK_NiMzSyGldiKC0O7PC8DRwnAyUfeNxzHYnos6MIuZkSIFkTRtXxIl39p3Tcli7J79H-F1URMqUOsk8LYvRteaRki0WrqX7k5HWmAxXktXlhAQ7Qq0Se6CJ0BMhf3Zd8-m2dEJ0iZfgQwqMBefuq9AlYm2e1vJfTIsUw7MAoBxMc0xNvDeU-fzsudLQ3nfVYXLawUWw2hWsWkiPJ6H4hizUXFzf1DMtutNuU-0UdMRDzQg-hFA7bP4h2YHDp0lwYHjCY_XTvaM33BMgmef_DnnMPiUj_Vet4WDh9EkC968YRDq_VCdD-YMuWvXIfN-9mn73xcd4wF8vcCAFoZCtmoHdwgRjgbik_Whzd_Odk-LiNtKCayu5b65Z8X4IiWNbpPkWhyN7L3GQCNkn0U5_CuWRhbjLMZw9Sg7dmP9v5YWAiB68ndZoWTGYUFPqi3sNIjxY0ublzdYl7dD23gYLHH9lygp_v6qhUSJfkbkHZrhA_vrVU8zIXHXDcueBWtG4jdkFGDzcIv5G94GOR_xtq1SxaSZtSs9r2LX24u4fCSRjiwqpIj6lYiQFDDOYbTB3jhBQZ86Kqzaa2rNzdZd4W6LNAtamj84mRiCYP288TXJsp2UyIRvJvUreg2nmGTTx0ndmEOODm-ewwXqSXpIuUWyG3rYVqyZu3spdVnA906rPPsysnEUzDGWS-5tAnhu6wDdiXhVpB_At7wBrSuRTOyw5ktYx7kgIaGoqhDR8G7ykXUfVaZahXSfWjcqekjAfkEtsu1yuqeUmj1N2AAn3dmy4BC9BqSU_YaXcEGklXBoNqgM4S5LRsq2iCzMRV5sPDFFNzI5NRTFP4oAibgxvdtuTXXRfj-PL-mDbXWp7qXK-q-_Y8UPqXR9-_SrRH1mDO8hbVReZHWkmw_2fE5I1HKeyc5KcKfDT4oRahnoAJ6ocSYX1yOPovkF6h10p_8lgsSwt-e4HZWd4BhYBQ9X2nwpF-CSmel8wJbFiY6ZM9pIBUdKXD-qZv9XmMFyyApQNVORa-UHUmGZrwGBqgirZTxl5wyidO_3qKYXadyYn8hYj_5UUxNo1Omgg217IE-zKHLOUSDFssOHNEPe32anKKvUQyIV9FFuHS-shP2DvUEVpyMH05Mqzw6iNtI06J5zC7ydabHJtUGC40zFuVlazdr0wUJhg52nuU_-H7HHl0erOabfeNuctz0szVVJL-mH5S0BaXOBspS8QH3bKFLg6j-Wy-b0XGhk2Mzyzy8ROT9QtpVv5YA-HwQKXtWIN87t4wIVn8ItSIqdRWQYRxfa_Y_XW_TEd6mkIJMzsNukEnyDQ6FFOR3EQ7IZU2FOvDJSczRiIN-rFioY3cFCgH9CKIf9K6Rw7Nx1-Jv6VryJaLE7biy1-VPFA95YgcfM81-iQmQ38UBeMUEUelUpvOfKpiRHJeXnhvUsGu30DKNtwaXxIzkHhQdMj_3JxRJlZMq1YsI823DZ0E5DrjBwcGGqa_dC9oCPotgvxIvuhpDSvHRzDzgo_H0ShBTmmiJTpYmtkwwcqtC4JvrNuPUJYslVNqCOyexubhYOlxAIBRvVKHY6gejwpWUkCJyWNsbqgVI47_91mWXKLGaEG4lGjtxWWhQWDJI4LARY3Ah1Tg0qKdCXdGMNmxkOhGnajJqLgxuVFrnXlNyaCXBnBTtTyTY03n7_cwsJNg8Wx8hd271r5us0J1AZfELiWgY0RhtQvmyzlYQcIf_eKLu1a-GtWWIHcHYwYVmtVNhl9RDMk0F9sz155s6AciSBc4n-_b_ffEdenkK03e8IXQS9HCwIBiUIXTlFuGpJikJTRYrnjkPoW2OPKw9taqV2uVQbNdQ8m5z-r0JpNjAHaKHwYWg1tqD8nrVzAQic2nhSVpCPi0s_KMMH0rgg0vd75K_fGIqTmVCYtFh3w1-4i7jaybqpWjSHe67ZkZqm10m69jmG8PFQra47vWo1tqHm3SXhGmJyy14_WK08Ty3hXKM-9bTupFflOQD7cf-QrUJYbcBJABeP0nOSd8dsbXcvWzhnrgjg6pnDPSkyFcjStC_RUQd5HBxMy0-l-RvtGImhf_CyUsOuD-kJzU_MKvMYPR1LUou8s1VwZHks4DQ599jaiCqNMw8J2_ZSH63Mnw5BanPaY83h4FfJabpOU4-G6hXLdNWsneXSQidzmPgOm417BROIKbmgd8SeYCQk0Muy-wxTXUSsozw7SEIGUyNtacwtf0TIrtLG5nfruxlARZwXVtwekKySrbGovC-EvN6cZtfHMPzGMMRl906D2UVeP_EbxfqhsA87-fi0G5DTwqErqzfND-N7eSU5A9l1n1xgwknEAV9WRaEFqSgmHcU_u2y_Afq2aP6ILvT6Xrvbq59l1oiS5aWtsrDl66jnMNFblj-MlhrXfRkER1PM78Aelr3t38xC8y7rXl_pucXd8qzw3gvVpBqWULZZoFY9PCXRBwdDN7lSR2Eaw-biajbYydaesgP7I3K1LYItGRSSWFfOllAIs83bzdeWdYnDuc_sv_Oboc04v8NK66iR3tbfD7vcFDqkhnvvcjKItjgrTXYAP8oFVxboydJT8WsK19W7GhSrhMHkuTAIPuGDiIUi7T_STS-t116Yw70eOsTypOJVcX5GoIiAFYPDBYc3PWvsigjnbchX56t5u98ym775G9clI_Gk1IMg54icpVDOjmcs-SCIO_b9TF06o3CfIqXTfwtGMleTqVhHhXxI1qg2P_gR5w_98ahKFCQTWCSS7brIieZcn-FQAeS8T-uLEEUGylGEpIcgsx8oiRA3Bz5Y6eJwzZeDiVz652_feNqpUq51fZY9k22W9-cDvGGe9OrbiqGFq1-g_jQKWKZ5HCOjamAAkGb7_z3KQmX0wwAt9uCtR1GDSDQNll08lwBIjWQ9zgJpmOO8bRGfc5x8m4imclN5f8WuajmxOst9QkYisjnj4h6S7ytp5egVhvMAdymHjpNfoB1LyB8K2ReHXDooqskAeWlw0r3mpnjpT8MBQX7dQHL6FXIHFfH8fsUxK8pJIUUbPtCYr0bHW07HbVUr3bqNegqpdrshp23rIHHWXgVzUBPF_Xyh6Ef2khb8oXHQiJnnH5_4s_Az3fTk5g7-Kn8NHGG84zYuKl-xvoDpUH_mvi-8wHq7QDgKof6BIokhs1IPBSoXF0V6Q6tcuGFeOcUqiDvzlRoWS9eC0ARaCeGBNeCecAJ0Kvo8aGSIJxOZa2vTsbSyxim2CBTXVSVIwYtuVl5hSNV07trE8Shao2B2vYnz6McCDssjboL0Q5Vl-bfURIZHB2EyRdohWaNqI3YKMXggfYK8xWBhD8Izm_X2BBBwCleVght9cuUWcBTNLt6_p5TyoxPtRWDhKBgM5bQjshAxQzt0qcf9djqRc-63xxmLMFdUcG0JJA_BVcIFKa5koBq-P7LRTgEVJLgWt-ZcZQF8GEh-rIxmCLThPDXI-_slafT-reGc_W1q59aF7572Z7N415Mw3O5G08pklcpE6bhkVSCVNhmtiE9Tq1TTBs9rCaxMMqBPELnXY6vh8i2LZZrPbxYhKMrXStE3jITe0cYWBHKqsyDQSUxc-ipvwzzMUeSzXY2xeJqJb0Rcq05iOhQM2n2XIyxl8T_pS2SKGaTiY-0XbjpiQpe183sOcmDXVqUeAiwxm9HjXwKKO0rYSFKPNcuWIfW9tVTpw62O4-hhjJx57O2uifFH3cGqw99jd5GtmZeG1RTEhApo6EZYgkne3HPzyLe7PSNAhdW1yNAZcUVpc-SrvCAMj3fVYcsy9sSBjmIfXGULP5HuITotHpI1WMjImITDHSARToOxZbfjDQS2JWhHrvdH5KyrYw6JT_EviqxUKsBXR3KfpYBB2G6bnScNyFBBeE0tywHNUnKQtcVHlfgx55sMZ5mxYBQQ4_trZ53kH401HxxoOdT2858PiNoXUwrWz1m0cY54Q1ABlunXfJFuBVCcqIiUyOh5i9aGmwy5C7FMD8Y0HMaEsU27PjiVJjA2uMg-U8i4xs0bxEEWI9h7kfDpNUKdcFGv-M9RCP80n-VKMosfT5TUl4yjF86W3qVlOFvB45b4vp0XRCeFUDXuH-JAnMm90pE0XiC38kuZnrwy2AEIL-GLhkB0vtc6mZDUg-IJDhb3sSfb_wWUviWtNnB3xdWf1kTawnfbQaR940Q_a_Fgq_ojJDyZHKA8lIczca9bRqmyRdqj74I-KCgGoixjYSxqzg97AMiYOPovYkJypyF8I_4bpcRdnkEPxXkCUnalAP-r62iZo6ffVXGi5awiVzc2mHKJg8lPzJlLhfw-ZGPvJQGrLeJR_IoR0ssTVpOB9j0LQkcY0H4LnfKEhNOLMTZjfMr-bO_yN_6J1yN3S07wv6Y-AT4aYFoZ-jiAUlrX5hEtjAWw_Sb7jMVyOGOaZtl6qqTuVlCFFBLBU0n6otYiJKcBs3DZzrQPvZDHPZa6LJ5IaXotUZqSEQYBehGIA0DYHYansrFv08RMu0PbdiIBqm57PzHkkLyNJXNILGdqkNEom6kQGIPRXPLKt51nMGAyTlgpsNvxFMfExz-LR1taDDdg6xK6EZBRCqQyvlF6TAOQaQMfknmGmKTOXyyZZNFpVIt5DDdwhdLjWSV_Mnmg-dW4OAgzicRrbbLEIYSK-5q77cWn8dYqrImsA3KpzI616f5BrZlOxu35nTFURLoijsEwyIuWtPkl_IauRZqlET5EhNnTBc6fa8Pvi8iq_GYU32dkniJikYNtLGC_J8pLMkd7h9HsZ7Qnl-_vfVVUmWv9D24SDBJ3cyzFee85lPmKQxlxqkPVelkvH8LGUUH1QJtfwXsmEYJeip_owQc4c97KwwWPZ-kliQkIN-Vkqz0PyogYfsAw3e_Guu2O4Pk3ysLzxLC3hpBHe7lacVYP9L9w9djhREWDVKoxgIpHuJA3UTVPyl0CZQ0pSWPcCvdkdQliEZN8lTRTc5NhIdAur3tohX4q_oeKm_vj-NwuEPhT5RKGunJ0CxT6JnZyn2VWwwtI9oFHFZfo0ibzUSaok73AnR4lzoyxEvFGRClY-a6iwy4Ldw214BlPOaP8Lv4hJBzEBstqhPC0XfRMQiFkfNIJeoH3mj1htWUco1pTHNzXELvcbHU9qRQzC8Hsr3NUZ3MsTKIZNXzqyvwVQrr6X6nBnyBDTYzkUUkGNQWERQqEyFu8n5enSa1mcaNYWX5LZXdWjXtLr38CxVQn9OSIpdWBAobUs_qr0D9HU6IuzDRx9imFWdQSStZF4QS2FmQ869-6TPKKw_ZghYyp74R8UVrIgAqNEe6tlf_e5zjOOgH28oHJS4D1qZRzsfwMrfKwY31ZuIjxxG9zoJF8z2NjKbUetfX_I7Il9viWb2ZgS8ZIY0hYx5o1WQnsPNsEB83sN_gLIvr8xqbSC98lW18hW69UVLe1moa-FH2l1cZ6nlbdeARv9eSnHuRrx3L8ywtx5lSwG9_LyNJEiDJfzamdhVZXpi5eGUJDuXoYGxwuG3EdG3lyuzSVmSlVKYFcAL3IbjTyR5hvj0RfFhtRMgJ3VIySQPLx_I54t2tTqgYOz4wajLJWQqFZWE0gOb2khqYTnBwJyODvhxrk9odip-Zp1vhTGavtQZxao_oBU35lgb9oRSsMolkPHFgPD5GmCv-yCNFcPxt81zGQiPTyYLZRwDgt9Yyjn40xSmQVP0a1q-LjCtdieRm9_Jpday6u4pZERFGnazcOj0wf1M90HnpsSkabL-GuelD4D8wI3yMg0KV5rhYFEKuLh2-HPX_TYKPMb0heDyBId40YbtOcN-J_nUo1f8pm3XUPpu-AhKrxDTlvBKX-gfLcRs4nKAWymMejeYkXB9lRkive7AZirU0FXJdIFSd2-lUsW2zMTMta56SPOgXt8IYTm3nQJABn4TlO9_q_t2VoqNWJuSVdmRb_ZuincO4rewjNVm8LjmZiZ1Xjztx3va-jWm_B58AClYggWA1EbaRX16ejxF2QdKVMg8tibVoCpaym-lpXQj_qXp7Ri94rozUE7kUdUdkvGH1KC898F3Ards1YH7-V84pxQCH885JQHdvkoQg-SAsr1P2O5VTU6zpovhpaKnPqzA_8n4w5z5nfUTSy2vh7b2y0KRnyNvqJTaCKVFwtkErn5kQne9T2BGe5jVIzTcNlrikqJLGJrIkTFmp4x17N4ZxOvjMRfF0GW8PyY8-MzxRgMFie0NibiuXHOeJwreDCu4yIdh3WyiEpIh7uuEi3AsfbDRcKgQdyWHlrCPHd7bUDPH5MuoM2dc3Vv6fP983sOIvVOVQNj7tpUotHcWZquc2JYXQzbH9HLVWdCb75c3hd3feDWzF39lj0gPGOyWYI-EctLHCoFwXtDGaBT_TeHh7WXSGBJ-ogZeZgDT-rk-nIKsrJCnoMwAItOtL9c27z69P92MxLnjNl15GMtiOOeqzKK58xh_blIoisPeZTXsTXxNSEfHLFPjKSNAutkZRb2_NKD0kdjqLx8orDe4618hgzHswL4iZow-b8zgaRvdkGuQ_a-qWL5HesfL0x-R4qvEUkSsb4a1W-U9bS5vzsWhuO4_TwpY9dIHJKQJ-mvK6ueB5VYpx7Bsi-tvXefvc3hRzPJkTR5ulXDWahCWyz7AiMc2-5pQLq85rZgKBS7twO-rHnFPMJL1myMrTx6DthFWwyyPiQ0kFOPTGz9IrG3T2H0jmrqlSntZIOUqkNq716eTw8GJKYkPvQDipeTxAl_zRu-ygLwx2H8Ve6JKivnwzwl6DFaX3U2uIRag0SxQbkJMdVeNAXnXO1hxC8GeIWvYQaS0_6Mh_fzBdixJd2ntyaY1oGXas1s2WTpA5DEPLrGmc6z5-xWe3_8588mGMWlHspIQ2wNueTzQH1Qo6lTIhmfR6DqEpBQn2IxdTdGBz5o12Pns15TENK3J7qzaNnNqC60Utt1p4TCZxHSKSf4mEWXyOW1TezNTPPZ1rDxs8P0VQxLLOhuMF8tLwW3Ui_2aUM3wnziLC2jZr-XvcvxxNHyKniDQcg-JOOtMwA0i9IHw4bwu4Cp-Np4Em7mc7jkg7FHjnpThPEwNrx47o6SMo-kkETr51oMwUSe9eeNqEpc8vDWOEi7DMzwQGf7cbNUHDD7tMOWdO0B5idvdoyfNe4ngrZ9cwHdBDzhwAWR_zsE0_eo9T8iJKDRGteyf3ilSYCTAdQiFz1Q8zj5HgTJN3sfF6KnNZb3S8WDWbA97bZxC-MOdiqxXZR3VtfRRYECdcorVU0EOSH9egwcxq8e3G5lXO69Bf19wUx-Vz-YTF_bfWmla5Bv4XW8QXkODNeqo1sGzoR-1mIab0ykMj4JZ7PT6J7ajSE6fM3Qh4QEc1L6hLs6dv1Uu8wbTEZ1MJjkPGhqxbB9W7kDAIQEkbhyXhStFBDNarOQfZbUiEdt5wlHsia84MXpQpGzzLKsblJS2QZ6H0iRE9mqJ8DmgWdvpo8mHQEHMqdZTiO9kza3NMeiB8R5iu3lpZFGgdErX9FtjxcUozQJUrvsz_snEQYfMgSSFnm_EEbVvhx0oXkeM10S1SbqLM__tG1DhCjxGi0UvZ66mpyRXj6_RbF0f0BsCeDAgZKtUMNb7Xkm27uJIQWaXaGh_lVrCXTTHUctoL8c_nMmj_FprdY1KzmiW3fxYmYQCL7mwAZbbQbJIJFOmn_lKFMjKLPbm4lGmVJCFPoT5TivJZM6rUKoGnb2xqC8MX3RuZtqqcu1xOpRa8XzGhIO9Mz8DDChkovuVz4OMi3E6xiIBGkjznauNMGu9FA8W7jARe2oVl_bqs1xOHwCJ4j4chY3tiJEhy9QEBnZLcIztrYGsRK6mXFNRN7ou7SpkHVgWDNWvZ63dsQznoX8qooio7pyXKKEomcZBBlxA6yuMfgoFB9Ew2_hQBkB7HsJvjiRHpGzjfNObd5K7OrbsEH1pKIlumc52oC18J9CnAE_1Hv3jFyKL80BUzJxlGJkSUM5ObSkrRSGM37OejQb1qpJfM-Ww9CElyQ12VxzykuapdvYWM1Qdowj6kTVkS2nhn1q39AOlrwUXUh4HFd8N0S2iR6_XBZosgxd80iINe7KtzWVPDn5Lst6UKuJBLDswknZvASrHbVLXkTNQKLR115TdY6x28wGdOLE6cgRnE7WnpaL3jm_7JfWPrWMn6IsttKltMri8CpOPGOPJJ4WDyAlQ6cc9pQatLNnAigO8T8c58ZRZNZ1RQqe3kl9184E3FShpoH9lDlCuWqQkLNKa0-clvFDpGUCflBYd8KI3WvUnVOfru6hTlb47UNXLSJ1TQh96YXoEf4p0zUFSOi8Tech2LU7Jck6g4zS1GRi5KTsxTRIiK1AbjjHKimCA0YBfvjcR2kAKt23ibcpNXe0DOICPcfizuz6a-qMGKk8WRzpDhVk5ONnXVHclQ96DCS0qblbuw6IeAQP-sB27eiA_OmLbzFDL9rUYDGAZk6ZQpUuvyhzJ0_vHLS2PV397aEpXDsCWbBXoHj4kmcVLQuO_1OA6ArWpbTL8oiqn5thbHi3FQTtnCs4tFwr-9dRBVq5goOeIb9qVPw17s6y6-m_cpI-2lK62c_dAspzrJdq7JVFlUCC2J_mJ-nVq-e-9L_Gboe8yJ1qBVphm21Q3oMtb2uBsenSxlj-WtP5bRrEvNfotdjESiM9TOlHDdTNDUf542eX4n2NcQYDpwLEbLFeI62mcMlmbbyHbqmiRFq5ZrKuxO_0uSwNzxoPhHpS2ZzcnpFocA-CE0ABa3mK91A43n49qI0w4s-ygWp4pfc1uQhPqW5SVMnn1xquCGkT69uVBy_Czt2_cSrjNktnIMB7o1fD-5OHb8Y3HRxh5WnBQIxcY5-yalVU0BsBvsjdvzRBSWA6FFUkhF7sC0m6Lh1y1puimWyd86quqGFXowRtU_BSvyGuxDM5TKsHPgELnc58nMYOGLANmZ1pihmo6TFUb8kHyb20Kwbrr1aUpIsFue3Njcx8MvebbXH18IDxXGNOh2LBAGQgq3_ZWkATfflhR8p0tSYR9HfagPnsqR4JgXY6NZP_gd-O9KKnuJVMQBf9FN0f4DysJhhO5TLxmFgCx2T5LpZRwUEMpXxYChOgbB8w6gGHdM5g8AO9wHHHg_yPQuODTwsYyH0iTRzZmaBLpzNgy9uLbcW02ye0-kI7YfK4KCO2I1DLX4HpsWiSjqIHn1qpwkcWtyK-JNcFZ0aTF_3pZd1xkbO-QvX_-Dlx5Ajnqj29G3jfBPxA7rKovbM55zD01a07xc5BnDEZWN8bD_z0nXFL0BRKbqxoIXM8o-tTXN_rmmDtIfvKi2e7rUzktW4TSGtvuqya27fleCdT5-5zbWg2X8qmWPui9ZQKvt13MJxeHiPn4RRn9HZKR307Nq-QCZliUbliVGLRLJHRs3_r44HFROY5xk6DX1H1gUD-QMNcj-_6thB2vAq4FZ0D9jCf-aCC8liwIe_p8JveLYCbfSATTBgtLu0-1k-ZMhpxiSc9cGo8osXvI9eG85sdVoGfyG6lMnAeAfYFL5Af_AgR1gfu6ahe77bMjEkFtwUf4lMOJD7JJHz25QUViAjCK48wJ8TqSJ5UOvh_ymN56biLnpVjmhOHuKAdwd6Eq2_Yq6ROmuLm4_ZQ_SPVERG5kRGJJUUEx3UHfV92Hy7xllGVgoWvHaW0nAcE-uwMAk7sMHANOCe1Pp3J5AjVE1EG5iV-QfxR-rjuN3m1oytwwZ44thNNnYNJzHm32lCk-hx-mhjaLYkhk2kmhYH9dSR0qzey4Xs0sTfva-7MY3OQOaPFsfF28werw387I26H939q2jDXVb6XogR2-ZWiqK6pmARAxB4-iLoSVx5eV9lrB00KsCpmfwIuIGB3wS12vqfCWpFSs_io1IHraZvxFCyh7Ff34HngAk5msKoqY0hQl36loHDypDlXUURJyOjND1rEGnP00x6BRu_r7k8kPGcvN5JudTlr-Wb7u3-bfBo4-iDYBWQAg-DenUe9NObUbxsTu3TFBzELJxeZwUpocelGRCs1JTfnRxcz_7VUuSMi1tIdjy7zvJoEd3jMsPE2mbnJKiMFVql_0P5CYiiEnrpCMOq-2MLcmIWq4LiC95fIywUzBWHR-eUCC_W6HMud0f02RouqMSXu9_g9GblDPlYtAs2UlEN0Ang8KxRFhW_bf7PJSLkR4XM1kZfEh9jG_9sbEA334L6qIMoaIJrUfPefYMJTkTUc1zVCmUlhfklV4L9ETO5eCqPJKagGpg3Gg0IyckP9OuTJzwtMLhpLwmQ8a6FAbS7YOYAOqmY2JwsXfWBLLl8w6ZaDf2jEvgMFURJhb1Ex9d861T_KL0aUurCpNXE51ixEqrszbp0_jaoKM8zeJgOV8az4kwV1q7pYskR34uuEzsTrr2gwtKlF89s9N5Kfek0ZZ5IGSL7YYWoKplL_6DCjdVx0uYGJXjURcT9K1QyyosCM_o9FRlM2bTKGMWonQyFj3EWItavS7h8VFoZlMSZwvHTAkPRwIvhktWZXDFrIvtO3KK_WMnvf2BMgY2R0TfZe32JqmVGGaURSsGVuT0zs9hSW6GgkRky7P2hDjIEpOFjYvKVqXLloz8t_yMg2NxuxBLslxZ4ceS9JadsXuxcbSnlRbGJGK7U8DYhbUjU-qSBlNO1_34MZT1Lsb09STHjW-Lvgw3zAQumB-Gw6KxPQW5LrCZM8AHknZAOt4NwbXdIlFc0cUBj6fWuU56VM4i2C6henOegRBIf9bCARtQo1vQK2ZAuz1KsFn1CzZVrly6dimqnDbULfPuiiIxwWsFbjORpS5yvk-frfS5kkMD8npCIqYr0d5S2Jl_n2A0fUyiAQMX2DNlJ2UwQBvbVNqZbkhONLcIFdIIzWJGHPhIeR45qTxET2e4-7fU0pP0_B6BXLs_Tyr9cTbGhN-kasYa92qAFA_cCJyX8Au91cofayH0zh0Ow2-IuHLbBX3btDZTmUjACXLrl3on1osCfljFX4dEH0aMwf7NPC1aYfdEV9WUWPJ7UbKCl4uxt2AknHHKVJZB6zw7JKJATlmILbgJHC4EgiZCwDkE-l_zkBKTxpzzrvJSKA423bu-YuGab9mkasFJWWKmgOg1c-4g8H6XvrquFmyxDmgd_j-nMB2yUL7OG5Rgtk4npBzQOkH-a8pJh-G9hk62-YziIEuooLDnN3R9zqf9mmczLMlSNAny67hjLSudIyOSznVMVMnt7-EGhKM-EoAP0wsygy4NGh7xTrgPP_z5NDfToTeu9mWATQQxTi-IhlLqj5P8L36igttQWdw6t38BvkvG4mS47maQxfZtZTGeyeIsYSMGd9_cf3ECURpE7d6AIwJPvNGjvqzQ6eHvtW1Nm7jqhzWGGE78hmwBWxlDAye7MjMaTSEj-fH4rFYA5RGzUtoVx46lra_vryeaZyKeQI-eHDRrI4edLubcVBsnMJNXNG3Ex9ZKnWhSMcQkCNsm5GD4s1BaHgp43cf5icLmlsDvcag-QISLJMhytJxRpN5be_SGWM9MvQC_BN9fHUtyxwGmq4PLLgXp2GeV43mSBP-MkaTG3WiNjmY0w8OkXAiciOZBOQJtAw4sdbs6VawSC1EJhf_ve5M9x6hj6V_VRn13F4NRCx85Yqt9ULwjcs6IyQHMhalYs3paPjcpqTPLVw4KTY4TIEVzFWQG4lPy9L1bEY3LmWIMUmKrLoJmaIP__xO9VC4N6oTwaUgiSME8LX632sSuH2NJvhiX7miHCQMBS2Fxts7h76YHhUeXjf0JsvEVHUm81593IqwoRNWgJ3DnT3Fzy27WndGA4FoI8ndHHSE8Gp8ZtiT5v0Ci6wbMHjzlv042KPIgsKHN_A7FjA06PTwvrHjdo1zaS0rD-TjMj-yFjf6NtNeCOioDoQpTt5Agov34radRt-70A1-FiXKImd5r73qb7NuYSP6-QlsK6jFh4t7kr4MFn5cyVnvaEBGv1GC0eiRRGnF0yuHbhh75cZHOFigXhEm4o2WvuH_1oh8PFOQXY54lQl4npLBaZkNcYUXZUzbYSXqp7dG-32o0xrU22Uuqf7wQrJfLpb3f2dLkYQFyjI5pQ5uElV1cRjQ1nqfvBLSNuAyxRkI9zT3WZs7Zgfl6Q5-0heFqdTCVAtfK6-8fcUoC0Ka2ivoKUfEZPm85Bnx03PH_MOSXwbY5zzGHJoc_XB-wX2EGvKEX7WQ1OnPcSpBK8iDajjPxfU23ZWtVERhOKmnXO04BOzDAvGVwd9__E0ybjnO2iWLiwwEJc4Skcga-py87UugXuGPly2bCS6Zgq-GX8cl_HoEV9zvzmS0Anq58VAc5xJHA5ikkkXMzPfStDI8hfpIFJqtaLSDh5bXPifr_9hI0ecTEaEgFajRfTTeoODRr1g3e49zORa2IJcnaTbZMRliRlA3OFtwrdbGxSlCmNpDzn4RZw3FSDztaukaUNMHPiRKH_9CXkeob81shpfpxHXUK8IAswjsIViCmA4rHOIRre9j6Z2DFFBNyfEelDub44JlZa-b7KHXcj7thJFjfLb1Rc01ZrLIEFN0D7tsbKmeP_ERyfr6uhnj1viqGC1sHzo8Gm2yFFrgDw0xd0deqcT2UPt9jrjZOpRcer6nsO88q0iwpLmbZWjDKake_ZTg6GzGVwFOru959LWbnk36_of_jiLmSqm6jPUybBFp42d45VO8hC48DjFW04ZfefpNZ2vXIOuGOyQBJ1jVn1ZYTaSpM8kZ82X5NxxCYNfcKBARULPhxpV9KtZ9U7acfIl0vNur9hLZ7lqS-Kjb0OtzL9A_x-2NoJSL-te0eTg1nTgjD5g3KRGGWgClvAK3ooEyooiXvPyv7eXhcu8RXTWFcH7lKcJTkwKum7HhwlBNIezziVmGuxZPjMNZ2tFxOt9pSPfaXndDqSckz2zPD-sN2DFQEVZyOhlYpA8vqkvx6LWNZi1UgFUreHWaXfUtd4I29rwLAXm3UASAWlPeJvvF_k5NudqkJol6M8WdSbhqMUYqh9OLLsV1GWOLVbUii-42xZAcj8DaYGKzcy231Om8InsjOcorSHbBLVWU78MLsbhr40Xyasc55J0zSV2lWWliA9-iKytUpO1sqIR3acRQHWiWoZD2J8VKPodxEU0x1WcbVho7dbFcGHP7Z1EN9ptt9f90ChFiNZSe_ejF7k2v4d_VImcE7AIh2MZoJ3enNZD8T9pcCm-GaB16auosM767I6lM_IYquvYIRgqH8rJE9GggRBdvuvraP1XF98mnhIJsPXuD7ecKagQ6vRedaWO4Yqq6FYwR6CyzgGPhkqrhxR3WYZBACS_6g-UPfqXtFXRISSbmFwKFsg7OJg7pW5uRllLdgrbZ7zjDkLphaYDj4ALrWBrBbzGww50xZQsHtPotYMV8KCL6XwdyOmM_u_cIVTxPUk4qpgP1KEq8GlE-GMpij8Fw4w9CdPxp6CMS6UMTlQq3YJYTYzEk-kb-Ddg7m45TApO7XG8CS8dcmQasL6GhMSShkRPuP5exEiLlzdyK9LHBCWOZNM8Xv-K1zPIQB_Z_TePHjwFmuOsBZoz1p3f5Ac0cvD0f7YPckw0j4sauIGWpAhBIVkehp33i8InzLAZ-5q64TU1ZdP4Dc0UvTgH06Y0wV6RwMw0RPxJvSxWGhjj_rX2Zu-QbrkoUVWy_eyvIXo6WSgULXSXoFtADOh3mLPR3ceyydaYI5MjmqtjcjPWVNN3dfKY8w3F8rb9vxzwPbv1yX7MBByw0gsvo27n45kQWAwFQ4rkBClY5eey9CkydPyLf3S5ADEtLbiM8SDxwqv93vXag6_etlmvEcqhYoE7oBsKGy8GZ3_fdHLAb01ONFVLqt8uzoVkGhZnCiu-wiwDkMocvQ2mzSX44--nc_ij-q761o7HBboBpBK8vsoRIjl5kGobXvRgVuyqUTveyy00QpWhhBX1icrka2Pdnqo6qlm-Odr8igRA68f3ZLxCA38jd309iQOndiAOMcOgAJE5YcllCoNQYuJurOti3VdThV5g7ugPiG8DRCBXMZjo9YyDJviB7tU4vpDFOpgi0BPWl6nhO3bRoXR_hQlHszcgw-Vwt2h9DiN5msi2IkgSH6_AzTygNOT6Lr8WCyPMG5Ai8z9jvazsSZWcPBFmUR5sbE6V95-nV5iaECb1vE50Gmt4MJO6Yx9sftisFcxPF-izUr_3rtWiLzwIGP05KZnQRHnkhlR_O6rMvcbB83x9IHkrprBbsoH0kvnZ5nCmGf1oiKWcM9JF1FXBH4xa9Lrz4MjTIpTpPGh2BSYeE-iNoHRvsVtooAwvHrqQ5xnLUZ6QiykjF6BkW9lWFR4q_6giq8_DlZc-GTToIZxGgUvpRUDIjh91sX0qxdC7CXbVw_MPbd0FCMt-P2bOeR4rwUgqMkg0k40Jg3lL-Lv8mepiABL-7tc7O2NUZlmImrFW_3T_Fs-lXsiYXyWZVWhU80OnX1cma0HnC6_XMZl-Q3bURtMH5mMTDD9iqGe8dn6rFsR4vBY632Ld4DNYtASqMeS_taTr9DLrNWOB3lElS-mlLAr8i2SDnBBVGRAVeofxqHLGuGIUabdgd3WQ5vaNanQ6eAm_9RPS_e8MZiBvZOtoKLMSCfTQEk1XkRC6IKmB5Kds28T2nKYF183xrOJoNsdXe1D5ylAkKYkwsA2uEvOZuOx7yABJ-W0bHD01moYYVeEsHIjoSUosEQUznpL2OKjOhPrG9YkefTn9uq6DqiAV-R7mF9iht7FDNCxBMk7WhSV404VMfASY-5HJuylNA_L-xGvpJgAfeJtxjIHN5O_MOktuQc42V8AW2sYuu9ITu_lai9iHnoL-g3_HXsgxkuU85wAUfdkCrFBftxFPsAOaKjXERoqf625aHVQaWi4tHTbPR0K0jzckfwTlCGbOvpv0c07NuohD0kc3WaMpw6vyHUKWU9PKSS0eMHzQG03P50tFWQzSDsnL51TeSiQaY_e8ppkTqOTwAta1qGhL0ai85Q0sTj4zXVNaAiSqNFYAkQI39hhwzvCM6dzTKbzHN1Jk6Ne4tO32oNvok2-Q5qMG_8ZUbF-mF6g29QJbBlE5qIsxxXlGufgqtZLwzACOtH9-sEB6NIKM6eC8-1olWVZZLkwa-X-gXuJH1ZN6rxJ1kWKbHUeopp58pkt_8_08sUMN9pBXCx6at8-vIlkLebfZtPlbTNXPIOMOVSWl9JZljENVcRZpGY8hJHPIj03pZCKW0ldr-ujPNW1JW9LTZNGEYHacBpQXSqUOPWaKh2l9oCMEAjmtA-Vj_1PFzLvEVokpnlkqjcQ6Lf67pLS6Cp948WRcEzGmwlMyEDxBwoX1eZXrB7pbGMlf8jGwT1870aPYXAcdMkpOsqBy3bNm8fStvalYGg5rSiqXt0kfJoeWt2DyIr8yaQ8zqJdQkSGxtlTh8wI2qFTVNtD3OhDl0eIHnwZOQxgsMGeQhDKllh43qVOB-IJrIAlmyq3X4T0ZJ5m-JCtAt5mG8YFn13kyPq7m7wdDEUdcIdh-YScXikqL6ncnjb8qhErMUhZwWIgBVsJbIiuaQczQ2Wg54Dv477JavdEy_ON0W8rs8lFv9P55VBzVO70omJd09Kf4QnQQZjCutJOkVemgR5zv3OjjdqDsjYBqAVIPg7PpQ37TtY4Ee9yh3iueSoLN931NOBK82MSVFWAcKhS5c-rlWmfchFguJQZANFdqcoDUJ6uyQ2Va-KYqd0y4JkNceRfrByKwFA16M1XKxTNECuQV5LI4mv4sTt7gspb3OEyWhc49ofWTJu4Ww4LBjKKw0aN_U181e3z_YojoQcjjMNyEp45PITLyYHWJcVT_W5uvFLvNxG-c_kHGBDVc2At3lfvxxauqId8MyN0zMgBlMH29yRaT41AoX-orx1DDATP6xf0XBKU_qQ_c0j7ku-2t8V3xRJXyIdquUTxGQTiyXNmgjmCAwNeQcPnHJH4GpK73fMBu4Z7krXVwXdjpQa_hdIVBGEMs6W2nQxSvJiT7ekQmEZfd0LfDZ0zW7Ek9zYtZ62zKaEputldiq-tfURS9uLQWCDNCnCOONuHY37Mph785nDi449RLFM_piJG9FtaKP9yIz7nlYVGqLoLXpJekjYcHvn7gHJHRtTPbUeEmT8JRfgtXQWWWUKF3q_lhdwdrZ9Ov-xYG86jQGb9u3yN_yywzIwL8q3EcF2eBhpvwVFegz3h0udjUCuKByXZZrUvA3KuZ0aUagb1zBSBMnyxTK6FF57sn8oWj9X8KGO1WpuJbhpR6JTkVAvn90MhqIbK4ZuLqbebD--_gW8Ni5PyBecjoy8oN9BbUTE9BYxR9Eb5-ALGgi90cDqj_IhRByxRidIK07hQgCtSlYoEKq2vphiY7dJwTiCFA7xRACvOh7S7PLT1sbh7lFZ0FwFQDmo0tqtng5KsNUzI-l-mWsPl1sZI-totkHlgf-JbFvMXSHi0y4fHDzTsnqtTB9AWTaRs7R8k_rViT5rzYmKTZ_5Rh9WQIb01o6rh-7qr2nMDeALJnfYOmznCw4IBnBm-h_WOh8Zko2wKeBvP53PIc2B5U6Mi6aPWZdrO2oYTSkGsKkwq6x4IIiTa0565nndDpAFlvx-R7sC70WMv3cCdEngBwdSsu1pFZcdk9tiicbua6iMYZLGz3BX27le7Pr-3mUXSTVptuTseMOEkOjmJY7hkQto85Qoiy2n8Ejtnx5BjocIWLF-gTgpD_-ZdRjzs9iG-RF58YJF0ij_LP9-qMhL9hkL7-UNIvQK60IL143yQGdHDmm3nOYYyboa6MeFRhm7ZHUWImsYfLqeLuMQfNhk6PKH5K4Irck4VO-R5UOHlFT1JypsbOPsumHWLABTFx928v1Tah_MREt2nK9b-X4FMSrKJukt5EjcZ099COHhaG0-DIOU2EcQNDq4bfzhyB6pT2kCzw3oHexNd19mm2MiD4k09cJDGkFmdAAZfRxiiNjDNL_oRp64H20I28xn2-xSYFoyCrzhZR0sOHyD79D7m7KXrT4S3ZvrjVT-SeX2OL86rsxMbJ6BFwVRr9sNwKNCWTnSqhVJKT6sf_uUYBxA7U5u1G-4zpYOZd5YOl1YvYqABTcXHjL_eAt6JkiESAEAPfzWWpnOIztJif1y680QP4kx9UbZMKiAXlYpRJu_Mbgyu0lrXJxy4vpK0YbwtXuHI2c2jyUM0yuJNBLkrcHyXR4_xs9NoMlv65HATPkHIDF3Y0MUjt6B_qoVT00mMESRXZTG56m8aO_uV5kvT78o3qWhUm6vNVxQOlDK8rrPxIc6auxhuD49OfXrWSCH_SMJB-VRfoX5BN56KBhH6_OrpPfIM7xgQmGrYDh9vU9eW7xK-XvGAbsYHSk2JmWfK_MbrjlnUd1d4jNgjHzfI2lFHfuxMRAJJN0yFVU41exG9Q5wdeSv2aSTI7mkA1MHINhqNVmfzgp81fHuquoIlM7a7TSOcU6rJ0ehVW4LpbCr1NiqnLfYM6Oc9FwAMTf5Paihy66eOP6EP71SfBbUsCJrFD70N2zWwKOPPhSgB8z85l_Lkf2TARueV1kyDXM_kKIyeYQszOJYhI4WE2Ia3Jl1dcEIMFdWZ97v-aqZtaSgAI6VxFE-o39yE7U1l9gWuq_BNAPSahP2gIHhUXPorPPzDqYeZdcPo54xIzhwu1nTDxZaAoJL-bSg3o4sJDB8Rk08jifriFEBiFo936Pdok26Rx6PEnDPleGKVH_qUo7_7phgz-kzKrTbNGU2nBaPxCu3QZha0JFiYn5OplS2q7lGdxf5os4p6RjQL-JVF2bw0H59dkAL2jmOOFoIeEzq_3hSFgdHfN0UMfULh59Hmsem4_ZZC08zWhGvxXC0FQVhJuxHjMEaa2LinofbxI6rHQnbV1bFbLplEaJVS_FDddZARCFjSjvKac4T_7ACbXGsmIDcZRMZNpyywLtW6zXLUlgSse1_bjYHFPnWkvIs8hSDS9ePormA9irE-_oDTYgruFOZNN8tkvF9FAXIjlk2ZJE8iehXvp2c6FJSlgeqbJ4gNyn7sUg4F3ktZ0B-fCDfQpxhsNrqixzLdYtNBNm5sWTMc25a1giS2-viLxCw9W8EZVdxGS6rrIF2kv81U2YTmZbeUPgmrUPMbvmhAcKI2cJkBOYdq2_IiGraelpoqdDA1Fu-J31Y-Hxo8BASGSVAC83qdB7GpvLEaYZv0PVlQpzsI9QkmDi4lrj8APOnCMXAFFUJ8QJRHSYH91VIEEKHbJPg-e2R_tp60pTiHjwlATk807HubuQxVIM595K-Ttl6iUgtMgYAuLT_Fu0YM64mWpy7YwgHvDYJAyktwWyPxr-a5r4c7z6lYq5CaLxxvNM_4Ansyl8XojIPUnOyWSQXvsM7eKKrvHo2FNUauH7gEyjHbhztk8rHcY5X0FxXDpZ9pp7itn6rzcKrwgfXvcMAONZeEgp0yYfLPv_KANpAeGovDMFnfC5oZqBrS8TAHooXIAlJvVILeo1f5giNtJ23KmFRqL_C9FFbUh5DDfqVZu0yVwJsMi1oeXURs5Lq-jSzWkPAeelBcsa64usJbydJ2gwcPxkJgtYaO5ahZFqp9ll5INWedP2aQDeCInl0E7BSRJDay3AyXo2Pxl1QtcGNADOGigVHuVqeMtMH9ODoAwjzMCCQDzwxOWrVHbKKW_0CNUpZcbZVmv63L5zsn_-ZUu6JxO7_5YnzBiXpNTUSZqnIxogux5AReo2PF6DASIic6IXuRJN5m7SLQIcRlExNhb-vypLKMjy4JvFw1bPzauP9CzlKj28uZsyUyuqTgrCIs5LdV-wFawkBAwVn43EQrQzWQ7eRV7-bSKQJvMFaQL15I4W5VX_wKHucscnKZ8hC-gRDzAA7qqXjOaUBDPoX9-yAsML3itskpjORGTyEpC6XzvyDODss1TobIy-5URFTzhfUTe42qaMQFMLtHNFByL_cqiDseGuflMA5S--WVQIdKor8tsmkQqf_sRP-MhzD0v2fjShJV_lYiKIz2yF4plTc9jqZj267xywz5QI_gX6NsGd1TiscNXqnXUg310xSPnp_Tu_wRXFrrkIKzHD0Rxs_KUYZFtMdiVRsv1clBZHPprW9-o9Gt8Rb0Hvr9zwZpPPtaI4LyovW2RZPGZmJN5hK9ooYwn8hDJArq39VOmeDXMZLiV0GCYZpva-J41J4Iyl3aoMDvIQPjUR9anBd12BDGpCXQvttiJnaQwzcBX1IfvcawvdEjPxVoWpfyow68Fv8JRb29fq2fsgOqTwxVmJBNF34JQPo9l91M4HMs4vSwevB-id8sIL7HG25keWOMdVJ_BKk9Abc5zMTxF_dqXVFkYjKQrikL2zymuWHZ7oKJk8ayRRwPHhbzlJw4FlFDCiODbhLsQ2B8Q_pSkhm1SMVtaFoqpFPXr5MVY9Pg2MusqdpcBQjSe2Trqq1-1p6DePOcnUZCWpYD35xgmNd16opcfIUii35H3rxMrdM2DtjzC9UIUdfgs9lMtSfGYNNMyQGwIrpf2RFrhXOxgEBzCaicG_VX3FvfFxvcupdHCMk5HuTYamxejBKbjHVAqH1sPu9Hj_D4hQVRN9eOO0mFFq3U_VYqlWlKn-cSXxaMmG5qkPheYGW37yhkf0BRIPb4rB-9lF2c6wsom2YPEoyx0bVQM5Z_Ze0-NO_6X8kPONjikye3ip2TBAe-ZXxWG-DeCUFTQlIyzaeef9ZR-IL9AcyhD7-tsDOZEzIaN9vsw2r4MvfuZrFLY1iSWNNIkoqBfBVSyA-BlbKnL21y5asqy4AFNBPGu1P0-U5rXLrYle6PuPBzQA4oWBIimV_e2ZyGqm85k5_T9fMkRPFWBJpsDaoEEcMme5OdYatmc6jFsIFr0rOWyLiONnsQA1LgBtKqYYVz9iLiFjBFR5rLyKHaNTvO7G6UVNHUPk_hdcRXbYmKkwovt55LN36XTMf3w4Xk2VMPHewgUjd9EEbzDyyOqTlZPqzVv92xcUywczL1QVvOuqTfgRbfErcournC94zVOjMPWnsaBcVEdeu_jQieeik4QH2Sh0bvlDlXhsN-DY75WA6yW2vhflgv-OF9ErJjEvIrAGhqX4GBh7qFdCrgTdnOZRrvdcBDd-Z6PU5gaT-tXUm6zU_RmZEb6xy3bkgtm8XatNBL5eMRY94K-zn0vrBhSLBWQQG4wNfsndKMnwI6iZ0Tm8_X7QJSgYY8CU6LwKMKOItPsQeZeGOL_hBaamj2Nl-cy4_heUhS2TsId4STce9t7-FivxAVOeiivjyZWAYekMQtLR0mV3Y4qYf7VJ2rY5hegjS-kibTJQcRksVIHcukKeJT-SYNFfaAVN44S9sC0zrgqQPpi5buoLbVhIwwx7wHpR4rfxaWVxZcugqF65BjYqG2tRBjVZ53McQ_-BWOhvfF7B_ibHm0lh4ypYJZ5rQbeiBQYvX-gGwMxgFvDvDpxPoqJqBOJVjlzzFjTmmsJ2JH9VenW6pl4wrkgcfJrQqlWdw95vUx5xrjxNnlzmfvBXVn5CvskfyigwUy-FYzKc9dWDWtEeOpe6wtEkH37Pv9INCao0Y9aVRR_V-69FxITM9hLaXZNcYlN1vcjAd-FvGmXVI6Hn_VpTHqg_R1HF5pyJPbvl1f_4xtFylr0j8dwgR9BN1ObwQ-1ZDilONdZL76Ptnlk83B3OK2ixC4ex3rirDxJ39Jn1IV-4-A2SZ15Oc9tPkzFlo5FuZ60bfapxpnCTP4xuXxwoMub12bIYPthDhuAcMYQy8TmGWRlaKMCBvqmpC58H3UrCd5SGyPAjwQzRyeUGF-a6FAsHhvFma6YHcMY38s8f5fyXHOk5IE9Tw3Rh3NlNawSPNT7W5OKlLw4I02YOMITC8TbaLkbm9vk2BlMmwVX2xXkBI8EFfJt5Y-AZsCBZO3D-mRyqy0oX41fgGTpUJaOhuIMk1rCA54D29soN41YhLTPkGgKJT2iVSqByzN4kQ9RV4ka3FjqKsC_yt8U4G9T8gFGzgCFuW8dpUWaJGTuVhHDEVC1G84OsnvFU7Z3yKSYPl3_a8xE7KTL5WHNACdILNXjVEU5YOaErLm3x3TQ0GIv9MtsT0nYfjnxZIxJu3m-qtcARcwcgoFNdLCKMsAV-rY8-z-QS4sZ5fB5XdUYEggArh5IghuJaepIQw8xmloRYWJvgyvMwb1Ss_O_A9DByjqVWFQ7sX4cePbeCTBkRqUqO8jPX8TEgBeQi7i-bMlu3EB0N9QfOhnXrHCveVmObwvoLmZ3VDAzObVM3iwgqW_kNYhXBG4I0m-P_LIZ_bekhJvS67Tj5vTNbtTJNWX5p88_kDFxho_VE3OX2Emzcqy18l78aCclEPa0gexfaFhWgGH8LzDW3m9XSTXlJrwqRoQHK56w8ZdkD9y2YskZZp_tYmDetgoZolMruHgOVo6W_Iv7KtJTiwOxYT7d8-vDvr7kIglVCfWuC_RPXRU4-S6cTNcoi3N6SLod5OcmrWvZE8eCX6N-uRXIpVarKOMlygjIuGX2LDLq73uGsuLlvomyg7mZsJqocAP7lcyi4jt6RDuWMg8LXghcXY-TJXj9paOZUnK3YAYQ6z_RzsX4a9tXyWGQWD12Eb8i_OApIZxeMbg0cFbJW_doodOzhvEIm1qP8xOLyCWRFcpEyvn_Ncpg8eO6_TEvULnXcPnAL02O7Cbwz5g1xlNf0pQO-3FK_IuGw4S_SNp-GWM1oH6yW9CkuCYbk7AFOQfsl5wbW5oCWS7gzcGjh_2GU34fWj5DhegLZRuhLguagCqhPi0AxcDHko60ekCbtYzSb2Mt7s2L2XBXzogydtuNqF1UwLk0YS8CnOfYjnc2Mu0q4rITxcxpyfw6IKjCg3EG5pUCqNeK1Xt781A2rBRbQ2bNL0I6eT8LRbSAePpDFaMRtiSCqwYzUUTbfV-9yXe-gPuNgFtesonO1v-yEf-qKrIWUvQTF1myJ9ZRXWby4QFcFUmB_14HGjvNFjIXUl0AtQm5LA6ne6K_aiqxxjMP3cUjEXEdAI5ssBZHsgFnWy_KB2c46R7tFJvnFlHMYZMyLXDnC4s5CjECptkYaMfc-ppU9xNx3xtU1TC3XecJPDBuavWks9nRC0vXzR_zyzHUPye64HtRC10S0pWUv8uaOGPmL15-BMlyRb15_SuQ9YJiF32rEVEiiOK3p_qIfl2-GTrFfW4UjaLGbRoa8rBvC0mFyDIsjCArizXq3G3NCuUjEWmtQC-y8s0oxv2_Wq-JpqfFOd4_FAdbVfcDtaQbmffJiE8KiUHNfLqa6HEl1Eeuav-8b-y_R4WoM21vkspcliFg8hsXokojL6pdznQE2qjRucv2nQbmuU4jSQ6WKVPe7ZuTF9Mmr9KX13EfbXzyHx2los1MLYxM2wyVrb2nyWP8EtgrSblQB-gcwuSiuRnd_9ZwJPfFV4tlVB_HO0iMUDSuDgTuewfZ5x9oTABvzOES0QKJHjWhIYDJMOn8QLhwA2GCbKJhay68hZ2nt7MpFx1foxN7vRqTtiWuAd3Y857f_cQVkjcE50kw9U_1-KzTN4-fM9uSJ9EK-ULaifH6H2ZSQLoDf4VaG4oaboUNsoN4nzSREcZMgZMP8XUQNEkqAsPkBii6_jkIiiLSDKDUi8A8-N86zwkwRoJiJ5TnHnna0Hsxh_a72Dl7u6VqpI0EPug9W9OwuxRyetrWAacL2EONNx-8SCoJUG0BgB466wgGgfXjvdzr7dr8a4vjey7ERr300JJJgxeXGAJtgPuK0DeIOaweeVYJ3E0H66Qa5jw4NRVxKXjVI-VL5Bbp5yqlRhxdYKTYI2CiSN0_PZDX3V8o3CJFigbuK-thfbFnJerVRqF4B8-cCh5D_3laFYwQJJPY49YUIHgowM7NrztUKJRTnCHF8yJluu6zaxB4ciuK6U_ov7P0Vl0zo6F2yjnfROzb0XdGZTISJ8AO5B-XUEQWad5ms1eN0OJ7nERb-ZvDXI0rHYVDgQe61uCuiTKSIN-xGnpxgBV9QHsyYcg9ytDr0MeMjssrusdFLoAbCzLpybUpqRMfLPlj275vGHaXZaTmuziaeJy8qxiBPNVJE54ZXztVBo1T6ct0InpBQFQuoq5t8wdBmuC63qlD4izJg_dJIpT57aXwhc9vamXTTEqIslaruGGWSyLIG5FIMWMsJHLg0UqxPkCqISLkow2nHjW6fDcfIgd_O9e0AQTkTODz3mcO7Ii0WQa86kb2M4gK9cC_ImUQyYo_k8uSowvMaJYmbUUSD0C7G3YIkUXj_NNYpRQAUDvaeLRFUWUoBPumXhB4E_9eezfIv7ut2eO0aGThXn_RzM7kljaophW9RHrLCruAskhAQqZeXKE_Pd2Vc3HruLcu57pBjkC_LN1pI6fB_2V8ZNLTYFHM6XuyCgLmkWorLOr8etkid60BQmTzr3snZK57nhQ4ExGBfeuJxyh-vluxuzRuekhZXOtpyGzzheY42Hja6nEFpTJdmBPYiqWUu8CibcYwNK2Nz_8lHk2xf5r9EibHllfA9jSr9t4_tiACSePeT414SUQxny_DRm22JEWsop9ccCgRBoN5e511hGIt_aImzbeNVgLtn5kHht-sknInyIBgw7QVQT77vAkAHoOH_POLANBOlY6M76786pOgIQS1gDKPQOLs20_Ccet23oeOTZ34ZE9dfHSgenefiyTX81u8VKjmhslwAUBdeHbaOq6kZ2BCbrRb2vJhsh0SbAa2Wwqs4E4syzXcoFlcWf5MXxXMcqlFfxFZlWwhn5UffczI29nKeT7WAn60NQx5HNCBxLHR18nfY3NvSzo9v4WtCVs9eKGbBSZFSp1R9VBlW853bwzoziEvTP9zslzEJdSzfhDsil-hCUu6gK6uwbZMEFVKRxiy6M4nx-OVYlyvX5DYtaRmhqAnsT8Phh-wTcXhblmH24oAFroy2sGA7TZ5nZ_8IEh0vPc8J1cMT861HfMzx1g6FucVQ8AipzceH3IVLTH6bzuVKXh_QWXzEHnL6sOPgQ7FMRzGeOtsxh8700iR51iSRe8pwWCSQ54Z5EPXT7q_3zGaYOqxz76hn8JOEc7pCByX0em_dwshHSgxCo2Ki3eUTqw7IkL0wQqRAW2Rv5ognopEikXt9dGdub8C8MAgP3_e4et317iGEAkZ_Lgz4zViDWp46oBCr5ZTA3NODA3M8wIJSPqOUWhVgsr3ShsWJ5wghXaDZAv3ewN8f-0YTGnMLfARdV8fgYqIGNJd88q82WLp7yKN9LznDARgeS4iljP84z5KNFYLuiOyJi93Bxj-1amorm_vgwg0e5XoittJW5iB3Nzcu2w9VYe7mKEPtsQGAlmxDF2uu3nZCyFFdh5BCocb3esUrx-hOCefJEYAJTRHi6j4XSV3OiZqePI77fg7WsaHA2-2hpJkmcs-nro6Xvs3avY-L9MNkb6J5DfWuoO3iLhbCXGcFwcpIBlYz1vDR7Z_nMFTLMHLcBdD2rHkHab7u213pnpmv4vzZ-9jhWvzJAgpER81k0PwLRGiuYLvQwUha8ytfaHadNgWN-AX_ix254hlziyw8ILAu9pHxNQhr87PpUWJHd7ka6b8m9qzeY4TbEyEqjPSQRR91jkY7cyUXFnU2rKKEgTvX__AZMmkNFS2E3mQwZcKObGgEgZ5-hA-8--Mzmh_S_1s1ccJ9NmA2nCIjVSi9JNHXI5cK7jarI7QNxAPSZ2tLFlwUEEONVFgBSUxbkqtesFwWjOFLp0AFY116DQZRV0CGb7j6_Ehi6yGlcM0MhSvGOziwm0H_LLWg40UVP0GkLMjJBKzkv_1X6JnlSJWqhx2oqX0P0MVID-rYS35eOvzqQmVyuj9VzaCy37R3Qr7TiFeW5MXJ1Xq_p_DiQvCbZ07EWGkSv2w-wxDecRD3tiN8rKjURp3u3QCmj-mdmOs86lfmqDDTAoOask9Xeiq6IiZ_Ohp5hoyn1wdyxWTwcL4voYhv_l0y4a8Q-UAUMA4spXNUsS48Kd8j_M8SQPCXqG6uQE7Lu9JaosY5xu9ucCiHd2pbyL62A3JXo80aVmTeGH766E75FOVjbam9CQz7Z3P9loRHKXczBtUKTdeF0Jx5Ddf4dHRuyke2G_k3NZjK-YYwvpwCOGkELkdNPprAvhGIpGpKJ1Ga8R9S8_aTsKBsVy9DJkEr1VspuTB7s2faz0f_5DhoY1iRLeGkjAtN_akqghB11yeqaMw-D9EGKeq1OT1FUuG6pkglCBhLFnHnQvzj_Y-76g7aVB3z7mHw0Y-ZhUrjfaTsyuF1U73zWRPdmNRGLiyLiUPAqNvXodpPT4rLCFs5cMqN_MKh3JeMR9gDGANRQff27dTuicWERJe2UIjhnkHLouCE4OMcZOICLhCehyBSRELYTOWczZD-BOPhhZq-ReD4zjU1PxLO6uU0kq-tCndkyu0jgWRemhs7zcUtJ1JdG4ElbiQtWXnQGzu63eWgMZoS48Va2KkxXsPkfmXaCPZF-woPzc1B6SgvOBhZuqFdT_Jm52RzhN4zXOLgDPlZNR5XX-rRnOMahQn1xkXfKqW5qDFWQycndzh3apxFojRlE4_7btNFMR_ps7_KtbUuT42hKiT61_iBsWKCAnZlWF16cyEXv_khbX_J8imS5r70AsZKX1iY__mWvPFqneUa4nyS7h3PUL68v9pwMaVvV1IQbIJ4nZYbABpkc0AlRPzzji3H4EORGlzEET_Gg_kS07HAQZXITZem0zO-N0UFO01oGIf7yxlK8P8c6qHOgPs7ZmnGJWkaNY2RDih8xSn2IeO6gPKpuyWP8vRhlZOVfig3ChbMA0uPO4o4-GwpdOma5Oqn-YDkR3iyrupF6McjGs9cOyoqFKSr8H4_2vJcI-CRokA4483bWkzD_j7cFaAt-QyD3SqIwo5oKkNi9ANAzfQg9IWtxrc1JuO6jdfNuRrVPtt1gk_yn9weUJ9FbJAxJM9dnZvrRDsmefq3vGrfdhm5pLNLV5JInaKpL7SVJPS3XnGJvk8ZJDz5LnrHqBU_1RN1AdnqM766O4VLKDjtnLGNgzUi6LXd2bg-Cd6dmAMHxUSwwQHTDkE20ool9z8mQTfU1wGxfaKxCoZHvz4LA4erQ15IUitlNV3z7FQiMP4QYSJgMePfMJarXF8JZi55nSWldGEmjUIGw7PBNXW_mGzFp-L06BeFnM0FBuuHt5_Tudrr4IcW9-OEkOCTyCSjHLxWVpgF5buUI5GVcJPZlkws34ZdRObO6PAtwnOWR-2Zk-YohSWrM80Lwj6MoZyd9x8Ze5FixHNgUb5Cu7jjVk77280aFh8OoBTOXaTgfXeF69Ol1EL1HreS3-eoXI6pujJ-IGXc2HcUsQgA8HtGqv4AS2DQiKcQzwx4_kteItNuN2hQvulAnanBcWn2AZAOWNwOz9ZgRyRHVYnJ8C-ZUdigW3ffbgzSwAN7p8SD3RnC2swfT_4EB021-qF5EZU5kEg7vPeGOrXPMXqrQn7gleEeOHQYCSdiBTRZP3D4h72ARYAawhA2kj27XkA9TRIExpKcir-G8DSw7USyi7VY2u83nmU2_CJ9IyF_AdPa6DywGs40tIRxQcIEFCG7RLFs-aaSR_-m-jkSVret3qEiVUESBOOo1y959XCCQYgJlS3zl06_dMgd0ns3S7MqUyMPIzGAICc1XYuf8xdNPqNEnYuzbQc6RDiOL5Gl1Asz4GcvyBoAfF5s9RhiSA7xUUxYi9OeNFIMMNf3dnjtJ7rbhqvsv9YPZ838pkIklmsOlzl6RMMTV94rz2eJ87ypanHSpwvtuHMjwu49ITZh4nvhn8MuAB5GbfjIw3dyypE1amUyj9PhSljXg1tfNoFQupKdBYsKKwphDEICVQnnYhMKvNY3dP_S9d-MiejrTQYmr63eV3u6Jw3hdf8t1r2J9AF5hFBIjYMgHDRyCUaGbpCqqZZY1R1N2B7zXo8kLo-wxZNCvrRWFoZGVwAqLVRt87BeFOh2fNd2ZwsdPdbIVihsuLUj6vCcFEbL5O3jMQRqxI3_EH9nXxjvk9vR78zK3vhcae29-H6R61Dh2nGTPRlabpzhUHsb7iOJA14noEf9wdGKlFzHF8lvCii-zxHczINhAtHnOaAlS62fvRbuTAPDFtJMkQrJIA7HkSHSQ2Ij8N_DHjH516TeYYgVEEB4WPnKVN_oqZXAbkkaCRrNn3DkUX8b1_QwgO04CkzuJPlOyxb7p8juwB7wR9meMiPdZcvRSoiH5dem_DQJNs1QiKCVOWtd2XLglLwQvl6Mr9nCncHHD67_t4gxDbnNeaUZ0bZ2qjugOgdd4kpVdk-OCWCqJQoJtCMCvF3r3ZkBk3spJ6JKP11vs03UyBxPZ0fwBy-47E1zgVqmft8Eya2i4nnc_HCK7K4NKRvX2mkGy5YZEWRngKob5-wDC9H9GoCFUr5QYsF6lz2HLGIW_4XDxGqTquqTuKy8RYxcIR3Xi2aCw6vIPgvGUzFkPSws0NlC-H7kshWmum78e94T2jjH9OwAUqR-za6SWX8m0ss1YStgp0m9WnAzfRkVFQjFv3BamsJfmN10iGbY-uMLJsExnsX_QFEwgVmzEjP4mCv0gkKZyMnn_Hl89iA7XakW_2iVmF3B2JignGA78mHaIun2PLdi-L4tLi-JenasLLgO7M7Qj8XEySf0T-at0HoAW8YjJtplkgB1vAWwmNgQYNWAihsEH-17ujlD3pTYM4VH72J0WwdJ7pusNmA7u-ACCdUuXgZvm_b-3Q7Ehc2pVr3cbjND3JLC7cIsvfgSSKf88oWi84MoNH6b5s8Ndb6_JUDVph4YApVfCYr82m0Y03nHLJ8OlPdOogWmU5nNck4CRg31DoSj1WDkrcbFWEw7cH7a6CerNHd6PsRrKtPsPIZLRPi0-yK2gqkl9JUH35uuxnnQv8sEDdmGxH_EPf_GA2HKfW-bc43kcuJmsh6zFoyV5pz2qCC3lqi23C3_6QlYlh3XVl5rWeCuIv_tWjE9ZWabf37zUE7gvzqQZ23efEWEB5GQRnaJLVKiIfEBIsUz6QoYToCid-cYEjchBBgPSImN4mSbFZZRj94DVbtXgsjb9zx71RwKAqkTjpLN2mBBt5iQiny2xn28fNF23FED1lKhLHNCrAW8WQK2_9W6mwCOvENT4S2ZBjVA8-elhFZ-cEdusHY1RxvuMtvEt4mErurBG0M0bdIfdrSfYysmgANNINRh4MCoTvf30Y5b9ZZ9PbTQfOF6aN0PbARnGZ_Se4757S7vFbrllyLIk_dFvAbT6touxjDlMqiDBNnGpao50awogoGeslPhiPnpBT2fEY5sLETTH3c7wg8njdo8_o8EpwVUTGKPJEtdniebwKzozrFkYeCEEn7Rad5M3B9LMbomkvh9gAw4F96HEWqPpbisjTpG8_oMqYrZN-V2Lbc71j3lTl_E0o8qYoPKTku0fPl3ZYdkkl6elnrs1rmaGpJ-LLbKS_3brW-dHVS2ofm6uQW1Dt3kBflGrcVLvUCoeCktPEOhtVm7B9-THqMNWAnUG1-Px-aPccXaHk_nks2DDhmDIuFU_r2Uqlj4KfyqHxWZYjY6n73eVzx5Ju9S0NSmGsmk6dnLPBMYABldP1y4jVhfXCRPu7pTB36GArC3fR7Z3sets0rYycCrKX0uFkUw4wJN_w7K8GbSoQQKuECFagqhlA_E07RTkULDcZp-gviPoKt5wX5zkzNl-xaIAl09IR9KJON-WYgrx5CRlKg3lF9Jd3D3DXwK8H9Q85XUbDNKiQpYjMDxIaZuavGp_8AKZ6U9nUnmktFiCXpo3Kmi73Bu-UGp6zF9h7pFtHz92U5uNiJlhg3egKW7xbaXuUgwaAASmgPZbeNaW719pZjtheEWz8XNsO6fQqNu1iB2ucntrmPIyPn6a5j2YgIC2sxvmvXXnCjUfwu0COfbz3S4avR_EqSBXmf3DDxKvuhSC0v_SsC_73MIgvCZ6czP8KSAXDCmbZoYrdiAvfbABj3ul6wRSNbg28aQtN-H37gazzNQcuaDvTVroJHF_VrrkCgZZoeuBAHdcdQQ5-BiyG3nQwa0e8aZJOwJ6HZmpvmNeg3rGjfdwwWKtch1nhfdZeytKhsMKG-SQnX-V61w-gTNduAUdtHS-g-wcPsr24TfVnQ25ZgvvBeWdoOVBGvrtyVgK5A4hxIUBdRpoLJ5qlFYoV7F6BoRZgeqRimbFMwEBkQQfAsM9QP8SxoH2yN5VJHh7vShu4EWlek7JG1X3DupT7F8D5AGEuOLdaFGYm4R6p9KCyNtW-2zEPGGIX-pl5i2hbwL3LAy7xHXz2szm4x6wyMxLA51w9BHlx_JFheotRLXRsRmakF5h2YiEw49rcDDjNtlp0fUhnb9C2exHd6ouHCPI14XoGgm6qcjFGXuwSEwH9EVak4iSrykvxwk0OtszP9P6kg1orcCRT7zVvhQokN50L4DFSEUh8vs5DhJ8cm7sOxF__oopwtzmxfTujI4utbUZER96vMm-YgPezW2is-N_D_DIMqvnuLpm1SFX-Lr4ykJ41bGuWXHb-D4h1MBpuf-egM6BpjDUQu-AiYqxomu0dmIU7NYDYnO8mWJEWLG0yN1GKIZbRRuAn7PjffTEfYuKVlSma4Ere7cSFWkvdtSclm-xeqDGrLIM8Ku-xzQVzKjRrZFgTKorW5-SkHU0tykhxjUy8scXv9drgclLnNHVvH28v0Ibi7w9f8jwXV-HeoMyE0o3Fn-WOA9qHTGCsbDNW1V1PjOl6z0Q7w1vkUReA8bULHXW3r87thCDvm_rwaNO-K9c1e18ZKLwnXGbGfcoKbvgvBwJnN_1pAtVpZuUjhWQAQ8t8u_mQBWm5W8_8IxIia_8gG_bPmbho_FtoqTqeCG3_42iVSqKQna1gLf04TKUypQHEFF3Lpyp0yCNfGj25UnoC2ShbsruGrGl3bMgEjgJQwR1xj8OdzS_5BuDzOQaUTzkXmpR3EQJx1hLSzMDWti95Pe-K0pC-y7mq9ErOH_Csd2KDYFD0odTUCDRkHQj8tiYW5BsKFjQcUteLiLdWIKObiN8wfb8vm8yNPpaqqCgf_2mcuvsfTRUTHdQAjml_XOZ0eITN-UaYKPSClutN42Mo55pl5OmONZmo0ZEeTwtRfRqH9NLwFnyTxMvn1iGP-o152Nlxhh6qyLfdS6QI0i9rfZYQXOye0HwMb9xhje0MdbDPHcoq35G_yLbAKdi3CRY0rGGywLYLvXtZc3E3sBke7O_O6SHrO_rBrwQqEZ1MXrPff9BEtUBY_bvyAbxCbN2l_VESXoXeBNF7-62auAHboFvhbo0GUligcV168xW5dSeQixwl6fgGfo_S3pl3LFgNFavIGxzsXuF82CK8Ni2S8nfuEp3rVLSAZEnD2Gyu--fvxONY84l166bdb9W7rjrudQJt3pKt5JYAdi9xT7FQmrfpPOPXJiOgAbC8dBRQQGXzwNoi1G8gGA3A7RKDgMMjsCqHubxNARPlQaJZaMAqIOfXTQGoXaumy6WsMSiRGmTQ_oNAYSXbkd2ASSppQ8tQV2EbrfpL5QDQZDRMWysNw7Ro2D5aToOYU7Vn39OFhyG9_Vko_9kj7EFARFSsSSf88_ehNmmDjkQ4U2xmCiJSamx5kRIOznfzZSb8rmbdiR2iZ4uodStvdet-xXPiGwVboH_cUlM9LZK8bGrolaf3FclXQkvvrZT6FPOYejUmBRa1OVp31lN4Wciw70XkzAyRZ3ca8iwTzrZsG8vbM7ckkf0G3Mtv829JaYW4vNMwJ_TKXUimkICzLXxt2i3b_6d4wq3NBQDiY9qJ3IZgX3_szL9NHggJsCLk9e5X1tWiv9D_xrMYwKjv8TMLfoi4lJ6VmOCnlctcormv-oxy1djdGU6-XF5A8ygeDqF7NGwDNAN3V-UupxOE41Ic6lTb7hRBmN5ibKp9zFS5Ll1clG2L4v7LFDoVBBl2hVxb94QM6pxLSedTCoeQq1R7izX3fI_JDZfCsGqmiMb-sHe7zrr2etR8n5zrTHPQYk_3d6gqCRfhtzzGLP-DydukXuvT5tTqD3VAd0eCI5tD3PJ4U-_RRvBmrP_RusW4UqT1PGYzGbKiaMV8RopYGAbz9ZSXTk2YEVGTuKNIIdr9DmltsZB7A-Fx9tKWnJ1Fn4lG03YpITukAqS5pHIyCtfyJeHKEtAgH6uyGzvr_nOwBuPCa5i7KAr2EXgg3Z7AODHDQ38Ev_DKLimVnkueE-SvyRkxriuv4EE1eogPI5_5CxTdNZ4gpO_XGP2ecxB8pGzr4fHiavRLBru1rGBOmX_ATwHSD83FMuSrjMETYIPJ4ZaoO4lOk6SEHEj52b7AWH5DMZR7eJM5yZqG3xXj_jHGxQMI75VwfqqNcOq-2-XthxVwInmradRymUYIskUmuSd7_cHFSTXYdcKgYgakgrbcObF2O75v-dadmcqQ6r72NAn0psXH8p0qaUL7fjwXpox1KJG_RLknMNAduPgqb4qSLd6tBAP-lm4-NHgNr6bfoeixtsQvIyGFSK_20pJ3wkveNSvOAZ13TUTIE8WUCZmjvyevKYz2KmqFTZtWUV_UtxGU-0js6uu0i3te3X_lk-NEY1gEp92FzpHNIyK89fwAy-dTXGAqsGyUE6NQ0T0HMee-wwz--eU_oS-lsV7WvZL56v-Gbu8rPXTunWXCyiPBc5c1CkHYR7tnkPXT64BCfJHYLGM-wZjXt9SBCjsYJVmBAhUc-VcnPU96d0IDcoJD278qmrD5x-eK7_cD9v4YAvHlOso_ob2oXXI6KVKkZ1Bk2WKRqDvejLa8NT3BXVX7d3QlQWcIWZF9Z-v4AYCKuU5wuCIgdj5KEU4nUZ8Lj2cAeUVd9v2KU4CVwZqrf9s9EaIYt3bQNkd9LXq9jL51i81VAJD2kjCqNCc5yGIrFPd5KRhW4Azz4isZTOv_xrf7BsrzAHMWIoQ3II4N75hLXCld5dm82OGmWEOghmocbkPG670IJJKE_c3Jad9RwUWgk_4-0UIbchnweWa2pRUN3-5vtEyt2uZtv2sxp6F3nTMh5AqYUxcNzYbt26xrUrZgr1PVuvIMW4Jm-X5U1v9Y41FmxAsY60rzpLmBaELk7oFtdFC0O82sOpecLTibtczOgdRjTaDREsvT1BoyKs2uaYl75vTLhUXRzAB6wxsl4_qpHWmXTq7_YZzuvIbE3cFS0PDoZeg_dxdk0DOB7vTVtSpH02BVQM-QYmF9X2PEFIzPaYZ5IL25il9D_lDQGQOgIvRp-lp-uq034PS4sz8dBUGHWxICHbNQLS_Ui-bbGVQlzRGPKbXvjIH1uYLWInkf46HZ18f_XK80H29o5viFNwOa8A2OO777oWIe_hjYiAKE8vJCT4GwaL23iS62EcdBmFLwLJxb-ZNynALlKJQLQy7WGAJ3l95UotRIWjF7SbOqIbtMg1x-0CPrFdovOJtFqxJmmZZXyVNqXShKnpzxFuMhzVMZZkRmcR0Uri0S-Yeu1YmvMM52oupgoD8SJja7olNOAT25FNpFuCZyWrg3DTXjj3ZAGG7XZOg-0ii1S6HdFCCKQ266kbzGQ2y7VcVJ-GQFGlRwxgYo67HaFUy8mS2xTV7Ee761MPjDTAjLkxCVY3skb46EUyIiyVSgcpVzm6jZaRFgHZwN0d274ZPp2lPDmSSDXFb_scve2-bvAuNZHIP7xDkUsh3BwtfKfTx_LFgjVQNe0OplGcXxh8I15nm1CWJOcxiwtxwPRDeTCG6ubxoLSj5mDN0z7FPhTesvvHrSuwNypvsDiSPcZmuCWR4javplPdCPcArPxEb7OI4JgYUNCj5_v7RHkLyd3c1S1EcBkJhUatVuqX5Hu0tpzrBbGBU_he83fY7NeCtV-TCfOgFuDwepaAXnB5vTN4bMBKv2MkP78B9VA7ZdBiwf48tDR0wmgK6w8C9CCP0yD31_zSu9qZd2WhUUQP7IdV36EXMiUcRh0qb7mWppGcDC6NyczX4tqlltMpUDRKet7OICUwJRk8TOQVZo4xjUuZNGsVcb0yN_v_IH5zSZN5O8vr0IJWiTxafKwwtTkgZTE3L3OBmDdzWwG3t0wEJO9yh1NFfsU1eIKwWv75CGjqrh9aW2iA-CaHgSidQKWgpnMocgFEpou34M6u4cbnd5MuW8jTQ6D14oOyrmIrRiiyt6MBLixmmrtr43EPdv8CpEUBI5FjrjvSW-HRA0pN7zd6zso6Zoz3iopNr4ssM5_kLPy15TBDfMEG4TeXhlYp8KpRAs_xskzxSGmBQhKjKOILtAESIjBD7IchxtXTnUtgPgQRnckXdz_GdvUloGVjFEANrw3PHWDAtyDbOy05Z2I1xYm4goFWWy61eY-2OoVG41a7rHCk9Ubj5VevMpRmXtLfNBwERAG-B3sJQcQCyH9Nyd_Iq51vMTozKKe9QFOf4XLQ1LK3eb0aB7TER00ue7VWjMIuWrN2pbTDxhQdYR7HD7Y1uXZJOc48ejND5XXPzyAv5Uch0DmVHVuR56afnMwb5VmL1CzKSg99XSLlm4fJe8dF6U6Da7Jz5IEM3ofdVE-LWJkHRnWKbg8mvXI1xuN-YBXEzuKtAu3X8mDpTTPhmAsEaRY3fQatsAkbbhuSC9zK1qYF9Sio4qz-__nalHK5r_3hzlVeWFoU0M-ri_oGTK_QDhq9zDsgAznD3K2yHSEXXb9SjCkXCRF13ragN95XqgCXoxSjUzehb6GuG1DFO_r-Gncrc_nHf8t8qM6CrtSqAY38hoH7B_0RFC-E_d-FyOkb0lWKReZpSvbsb8eBcQd0bzNUkfvyk7gWHMwsItiQeN_vHH_gAmrtjfAj1SlfzemWj17cfbyHSC-tdXtWQMIXRmLdQDciPkvazVlyhcwpk7vJsMLxm2FVoWs6wEzyfAzx3a_HHvwveIQXfTE7irjVI7Mr-mxx1ggvCAfiAiQ_rKhh2OzQCdgs3glHwqtXHOfBkvJt_O2J6-1e06MmKuzO7YLzO5aZdqsdTBgozgU8pmP2xYBPjnY7Rdqr29pp2hCptvyMq4NAN9YvgzJunQDJ011_6CjO3pucbfxZ4iK_6AO8M1ucMM-ZlxVTZRxE-2NGEiEc0Dc4fPslW4JiISkSWt24TRHX0hFUTgW4-3rZVeBQaYZl9iM6jbXwL9UcOhO8pivANmQuIu2Zer-nu0Fz7qf6jwKtu4aRGfmHpZy95l0u9Y0u6pbLwR354qUupH6ztvFX48MqH5O3AEyriOlbEE-UPilSx0pqn0N197E2hDFNXteLQnD-hw5QBJXON8CYX4zK6IM2ONcPXceRKPD_cmDGsGJ4QEv1F5B5vPdGCJ46PDUYOsKcSobXUku8bpo98kpyelL09bpOYJh8fjqE6F5a1zeNkc1x30WkDKAHEGS2zrfPGFE52wRXSy8L5ifwJa74WUCJ-d5HcvPlk_jhmP41qKmw9ilCVdJUsx7ncasZ_R2JIA01E9x7wEtbRMd7mFKac18a6FaiojiiyJ463a0vIbEmPGESIx8HpBrjwvvItNhyiGgBWDSg7yxrYOCBbmfMcZo0p1JsSqlUUzJnnvV6gN-H5OWtTSVAoiDJLF05kUp30lnlfJpWxum0XYpswy2mKf59uW0oK6oqxnlubgOmHNFSNQfTJBTMzQMz82YHxr47KpZRakoi3tzo2dPlqa-19cT98OMni8V498WB_i6sEZUF4o18ZqdPZKJjHTXGD7KdxlxNEfNfUnh1RejIZ_y_wPDKdmpcl-KRpvm99YpQRTnieOgsHcMUUxpUuin_uSz5XoVQZlCjh8igk_JoAB3CwSxQYRL2UdQT2julD6uyzvzBIG_CCtGTinDMhbHfzKtCp6ZxNwnNqvsTfxJkZpZkl4fhaJwUj-_Mqh8yAIXZIh8LBddgSeBA5FLPRd0vz05qS8f1mIYXjHcWT6-culz946wRo_BvAIBasmvqDrER9OdMTQ0VBkdFhiuNiGoEbPosDHiMv_uK9PiPjriyjy-KtlyrTJIi_l2pRl7Aob0iVMizov-CJtPYktd8CtejOC1UHRaAyTwqwQB8e68bKdrMhy75M9byAJjD4twO9SnaZ4PD-obvaGdCJpTmdiPiY9UrVRJqmX6vWFbabnrL9EG19CyeZFRgCex0n6s4pD3SfCxYiLuC0w6ewx3YHNOihA5x-KUQnY37YCdKHL42mrT3tLVu9Z1rMDgGBnpU6073jRtk3bSdD9XLpatVY9CBpyLfSMvnxKQFM_SE4QgX6mOEeC6jHTHJhRl7pZltRqq76O1jfRuvcGtzZnhUV-UhDzqA3gcPnYOFzDWMQkIgpguOm4OVDkEtLc1YMP_DJlXypulNgUc67x94CRGRw4V4LNwFK8xuNORPaSwAQAYZ5daj3D7ZK0hfqyIvio63CF-elaKq-pj224xyZebmGGeiyiYGLiKRnb8hbefyy4WO2Qfej53sRY5dl4S4wVu2K8O0kM826Yw58POs4lhuLkK2lJcC3NswP4DulwKq2n92h3iiJIeM6fmgRAwHr2XjjZTT-mM-BvSwTJCvvZupBlMnGrebwozYlVfUE9bVridfJsgE54HMrBhCVPdIUZbOeqLjw1QjAv67EcppoLrggB5a0VErwzROIzHJ5xg7AqajzSLAzWHh5R3sinLVxmdpMGn_aPJo9-CP4tja3e4Q7JgxIApDboXkGBrl0JeAX9fnJDpCPOUmFWBeSKvQf-aW368MnisL4PyovhBxYsvRmXW4ddT-4Jl0Bni0Du1QO95fHJUmfdIJJHZcpNQ6RBMb_nXYHmoxgKt1NAyMld5jb5bqi1ynW1MvmsLMZmlLB3z2901RBTrxdD9Ul4YJedzYbOy78wpYvQd5o-ln_SCV26er-aoNQAaVQmAwvpvb_P6mHUVe8XLgqkyuJKjVgAj61D0RLMcz2ZI4Nxoj107B81bLdJAOJsffP0iGTdbIcSdjJ6EaaXD2Ump1NCmkRxdqXP3aHjXMaDKNSx8uRReghaKAqPXTSmGqMWLS9dpgNunsTSvZGFFZ0b1Lbk8UW2v-vOUqdrJsKPUCISaOWZguM72VtJY43iwSpjzI9VksJS8NVgL4QTRBzAGG36-yan9dU-TuAVW5SFX5kpdH-ZMJbk1wRn0tJxwY1ZyTrn3rcTZ-61vD4fEofbj_nr5m8gNNqfC15ML9PV57l5LqNz5dnnqfzIz2awm8HX49o7mC5pyxF8sfKNRkCKBz5QatubsQwe6RRVWZObr_A1te7npG8zOK4xe-BEInjV0X8m3UuV2HezJUwoVSYdn-bXmhE93p4Dssqz9W9BhrF5xaGWXiAUUKJajT58GLXw2CXHVig3YflTgZEsI-UyA7FUhEwFq1FLmv-yyCw-1CiUliGLC9AruV-XFiOhe9PGowfRW2y7dB9OyzPdkPyvI3a0f1l0wQDyVPVtpdTgmBy53r27ZMC3YBitu10py3P_NzddqXsUZKaZXWCqzd-TFTACcPNnHTRpaPUSmV96U_T0pVHUYzAifvLo6Tc1QDPGxdfaImKT0eK601yGSyuLNe1fgig5cSoORi7jc851W8USzcbCyU7wZot6dxEzEiPAhBnYBNa563nu5cbWkSsSuQWwB47yJBTKA1nsIM24fuKca8qw0jSeL8HLuBzJT3PHC6SZqB83JBr4XYxHUCM9s08HGEIs-_dIiPOeC9CCxk73etXdaF0lMqef1SLsFWeLLuOUbZtQ_eCHNI4OtlAMLv1OvSsxTTlKZaP37fUdvF_pPVHoCh-pWEmcDuzB26YifzT44oTM1riy8Ro4-inUSQTyuVzS_JkNIzZeXaeEZrMmJDRRo1YmC-KVW2qx4TZzqYEfrLyGPJU01H1Lbp-B2Du9cGcZEfTBJeLZ4UUueKdrAQCBM2liTnHPAnnna8SVR7-XfDVT8SL7kq3M4GGSuObs8IGvwU3PEw8AED98aHt1N3kEfwvWJRyteefglMJdSoIq9KomFz7qMjNC2qrzKwUcpiXbqFk7VexImEcIXP-QW2QwArS9P-T_PkKzYqRFRyKp9Yv42m76xygmYRowNNXgKN2cQFGc-YeDlolj3PXe5aP6LP42mLqeXBU_DgtNHso4wq83DTW478-dmUD5sr0h3tOAyVbIOWE2NkKzINHFfDgGXik6QGKYkqZH5ghq44SyeviRPD_lFil0KcWGQqcEbBXK4t7zZZkK-K9CaPLB4-oN0WjwAjRy9VcPn-GiXk3RQ-joBIzc7aaveTvakclamuYKj6_bj-VBKdxDSCjTmxbOf7_Zl9NgOldvBSJFxq5ARcxUsAU3-bgAROLXxblTfLGTu2kR2ci6zy7VVaz5-c3MpOM1fUrn04GG5Qe8jsQqu7v3fac8IzCAoaEslSaiTWao2bktrA_KQQyepSmZqMlTfbIeakRXPEvN0NG2aXR0x6uX7Es8XPp2aviSz3nKXQLdyXNsUnZ2dQzZBQ3q3XcMFLzcalvM4WTanuRFEpxq4E68gm98HRfTUybWvXwrTgKUHZ4fmN10K28yh6hUlRehOhyRS1UwyPSWVAng9dufGefiTpiK88tq1OrjqFHNXpLLO3ddqhPqISPI1YI2FSRzqkniE889Iv6NeHflAMU6lta5pqFaVwMmCZwXPkQGHWQR1I3tLcwORhhR7Ltd6jzLHnLn_sp1-6RBNkhVmZ9qTbX_LFhtGp2scJqGX2zetGKgrDuTHYgZmf2-WFWKwvCy7TYETEa9_WQnk540kNRI5lpdbTJ-vyJWVp4vx3GbxWadcoi_GjaDmKvB9tEf_0HIDPLvWUDwBjzPObTfe7Pak4jbnR4ebKYYrPZ8J5QZgbAHDg3Lx_Xe6Ejrmzw2en62MbhVw-OXpW72NNCvaDNjapm0mQFaskH0QvIVmnp6OlmT6SrrSLRagBnjhyKjoEfjyHr9oAlwV-LYCAasu7RCwe1cwO5gsJ_SDWamtKXHl8mg-QkhGWwAXI4UtNM9GdUqL9D-bH6YizPdERHGo5cUUaEXhw6H_RYUNgmcm8NnL7hNeNhlVY83X9OZ2TGHWfKXbBUE6ULmbd1ZmhkqyO7J8LX-MkfMs_Wq6u5c6PPXLnKSEbOdagVhoPYTHL-D9SDY6C57CWaCKUOKOr7DyQomz-FFu5fo8EAmgZQXm79kt3sDkQ1OBqOw1JUuOyHFo11YP8fsZitmWM7jLxIePxuCv8XuCD1Hb5YVLmGTeX66byLXvysMUoMJoxWm6Sd_hicH0snXqKjZ30XO_RPdJX6vlhPggzMl4jH6yr39jMV3ZeiNAN3Bj7RssKnfVT_WdS-BO09AYWMbruq7180LOSref8l4hu-h9HyRxw_XaiOYEubpHgx5LR6N5YMSbnnyzHESYHdakVQMMcUxtg7QEqpLRF--xJplnQRqhhKbqR6Bd-w6MJGluQQpV4jxNPxsT5vCOzy-lImUnTruWz8PKxHZPlW659xJkEeztbTei8lsquhjqHEHuy6L_yDDAI_-YT13xFgfxACeO_gnH89h77kbRAVBs2DCctCGek-_Yh3MgqsABdA4v7SvQnODWKksmsE-UROb3mFDxFy2k5-wS6Mg7Hs3OdPioehF6hLSJ91HEFvlyyNZiwzjsqXZRJx1i6UNt0_IFxJVqMGgsLv1pGid1uy93dGZ1zfzHPjWlaRIkkZPGky_1kDA46fyGxqamHd-tUiV8lSSOaZzSgJTQKAwt6PyLAjgWoGY7E9EYY_DkViALvhF_2mUJL2zi4c8OGqXBVX7AlavG3ymF5F1FIwPMnzdDQhl5umd5sFnMn-vmYxxn8syTK4Lw3MjC49AqLT2jMdBGYy1ziR0QLseS6TjDJG6HdKKOcB-YgtNJ-egJ9GTS4udeWEOGUegw-rL_vGlJzWoWifwbQgeBcZJnZfd3MAF__bsSJB3dzhLxK4RB-a2mf44vUN9kpPHukoihxfQB4FzcHgVYqvizA_McxNL7XjtpF7O04nH9uXWK8SC-KYUpcI8i3FYT4R2EQihBRa5J3Scmi1ft5yeTQOT67KqMK0Fb10NfjsgGYUVzW84UiAPhP_2TcD2ut4eI3GyjK5uyLwYVYgC6lo4OVd6UdSObdh_9CTpvHfVAv5EcrfqbfQ6hAd9XOU5-owMH66mblSFvYd1zgMR3haxdRuu6aYinr38V4lAHmSuTCXR-avDpmWQ4MCfMjRdemFBxgaYZ6WLOapmJ4ii6JAZxCat1lyoCNIfqv04EKYO5ViMVRLtJ0AYhdk6Amph-4QlbzzGQX-htu5nH2CV4T0GZZfZiXoynyfI4FenoML_nSObnHZp7AGQZRLrxZD4De1quHO7-brJvmrPqmJgYK_4Ia0VZ_ZDTBEgUNKAIK7D-j-j2eOTgWOVFGLDXSPtUCpWFROUrGJ8jm2EeMbM1PhQAjS7LDwZ3-VAWdmjRa05c578qnqzj-j8T_jNVpbcxOJIZSPvS7MvjD2GPcVaFYUPR3z_wBtpzOlkkRmggKreUMrEOItANzi4X3hyOJpKyOwNtRPg_B_XfBDkJRLifSl0NP02xdHyuq8kaRnsnCVXmUM9Th3Km4XahUYQH13IS2pt_szAt654dtk1dqh4pQI1O9ydLD7Q4_NvBykS-AihIwJBipUnfag11ryM6HLzsP2ylumd4Y9iswRSwFa_39Xevp2Bh9ufPyZZUY3IMBhR-l-EdptYSk0X5jVdN-VIiMe7m12mrP7RAgG7-Y9-MDytZ6ZSsvxM8sdHv6mC2ilIgN-tEHKXXEl0oNkH8_kixpfQ7SgGTnbPz2xBA5gg4jKWHR2VKG2zXrgvw-yjEEx0xV5i4kbtUk0MSwdh1CVgQ4VlWANMMsUlHWmvCiflLtCqNDYfW5JkG8-f5atVMC_0W3WqzwcVd-ZgrZEkcBpoe_-3igmjI1kVoGXGVxR4GViZtyYyKRaKgO53ezyHEZbS8eJazJDNA5neUHokDOyYttuRjV1jjfPLVuZJf6qHbLpB14F1X2AzSk3ntUxORZ6Snsmx0FFQlpQRxChFnYl1Rd6hNcRvZt_Uv2YepN6XFkehNS1MMzyQfQh-F1P2WY11zevEEOmFVAdb-NwU9KuBEI6mUTjknZF9hPDm7bW5q_jUYsT4GbAOEcHldSlvULSWzNT2c_RNqnAwJKizchK58tORpHSPRaUNAu8lJp_z8fwxPNzPXMZV4lSMwhSN-5vN3nMfaSUkh5EjQ1HmeGFkKJl7Co6-S6564tXtztnf6uITv1mECfv1K41w8hi8EWvDh5m4g_2rJ85k8KxwrJ_-Ivu-RhV29s_Q9tcuzkElqLnb4D66cJwpnmxZhftvo8NXUurxfylJo9P5laafIQVNmwsqoZBnsMATOwomDuUlhJ9V37MWRQZ5BOsER3JoQdroGE_VmMYkBeCZv34TQrJASZRubTBCZ7e0LulvWxtmOz0NEy4HIi-kJIjmcWeslG1pjGmy7VjJju6CzcNjzwdYw21Ww2P9DOXOA5VI9eU9lNCeqma1QT3y5s1S9YhGhRfyv0_aMYxGM9H76c6jB1pUOtDQ8TnAdqrHM7YWQzgTCI0u70NB3Bsiw39FcN-tKFFwiSzqpFCyaSSlEfhVWqdPXjRzKAkLU4v5FqdfXPRaLSm_EevUO0Op3dl_jMSDftrpDlLpAaMLamtAQpYQLkQlCWpX6KLCYB47WPkABTJNcW969VzIB4WD4QChGyIz2G8zMaYdZ1a2GS-2OWpUMd3zhDuG53McerwCcfv95CmSoKY3zXMbwQDf18ae7OCTMaqpIk0rWSW_Z1adcHuIxURcENO-acxGuLv_hnRFO3qAszDLoNGpVCeMj-Y3Jwq7tHORQCXhQra4oTYG3oqq8eCmwWe2IWp1b_JhxRQotSJGN7xq6zXAL9Fd7TvF-_MrbkCwstPlHrTGJB61NKzutOyce16sdnc85Qz8wrxVGtvdNYlAqFDwbCzLzh_-R4DuXAHv5rFmCWT6Hm0i5Mt2CZf1fmuwYoBZvIX8Ec59vH5pH5nKF369eQfWX9nfBf8jtNBOZJjGV1HCtl61RVgE1TrINX-I3Hxy5sKyHCaEgqQUwkdtfYWr_H2YIpX59ky2CbNr1945G2_FlYYJ-IFxJNoaU8S8IAITLkCTv7pxqLUxGN2DvjUjoiY6J05RNvIz4MJrYG3b68nR2TMNWzNr6Dg-I_3md_CmEIa0PWcKh5K470eMXHFR9h7O3BlmvWPBD1p_k6QXQjS-ymGOkhCMXsaLvV1m3JIGrP8EoJCQs3SdoCQ6X3LQf9W8kgfyaIIOm7uSHXh-nIqxwUk5BaZWjekd09NaN73Pi1nmpesT616sc7LwLR0jRVlKeSOPvb6qQVM0K8xqTRs9J3K0tYf0Obk1-GZP-jXVj340HA9JSXi8fZxHNws1mxVpSWVXxK1L1iUx6_o3v5rSE49sTEhl1NkfPCKQK2MrylqH5PZfRxj3t86y0mj9J_t9HelCqAhlxHYuqhWUgJQtKF05MqQBaEdvO_BHt0C_NLOHiACO_rHChMC6iM5cOHaYGECd_oqh4lq6PSFb4X9dgErt5URtj1AdpyLoiIaPdaO25l-6kyp-Wvzq6VxeBu7S00zWIoYj7Uz-9det1a5lGXFoHcfPFT3mF-81OMkJPxnbWzcq2DNvSzvd6OA7LYSngc3WjxcAf1WkDpMEdaiMRaClX8s-7Fnhgy8tEZQVidIGyevGjIAWhcgW-D7fnN4f3ihY5s0hGjfZHbpfI_8zaNnKeoa9OINkf6hOaGJyFqz9M8Ufx-VGJqqvdaWkMYcKl5YfPjDH8rqfwHqZSEzzTEef390MzBfKWjIhmDKQMkals7lffUQsgoytxhw5d0kc_uZZ4uH-z20ctafLLXrz1mgKzZq1ZK_og75nfEhaOTQLTJrnOITLYKx9PuV7krL4-h2SBotv_oH3v_n6AFOAmp9vA8lkbt4Dq8emm3E9hDC0FPAQmgKoue8tb-_yGhLvNddPItgzgWaoQ_pJCC2-RP0itL87gNmiJb2rnUHHLjGRUg1T5B7iP2DWA1NbNpLNtZhBLz7KGjd3w25lwOc9TwOKwtcLA5PmVfo2-k9hREgBUJCZYWUb32AsLLSRy9p5inE_bT8Iil1s_FDgfYu58nFvhzJznzj0bpqrih7gB7Q5_75hTeb2DIv4korTVZpNFFm1DVbDlDPZNKB-_CyXPO1Ql164PvAVM6v68i6Gn0VK2Q7zhtXrDo0DJfEmGJHU4lqv7NdwtIp4cIzz1sMo0xEuHFbocvte18fRkJ7HWyjt2s1CudcZt0_3b9FKo2n3hAckIi9To8Tqt_S3yJengY_eCFSxOG1cmfqpDvrYxQTOV1UmMSgYmc1gGN9mc1EHMUSkX9qq7b_QyIqc6gltDyNy209bbrvAaM8Q5HPcKHnZeaLbG6FNiBJxUBGNQpvKm-gKlTzYdAeyMfqTLB4-6DV-YQUpAb3eZEMSh7FCTvQGUvjgcKw9DTBfGHity3r5LqY-IrBF5ZW0MBBeXCq-b78OL--BqJinT0LDlevN6Uz5d6fKhbnvDnL6b4G-LMuIRwjJ9VHu3Gu-cbIbQ_-N0R0K-FZU4IUBuV1ZaYeU_6zeMuma3hUwphSzh_Lz2xNI8D2aVI0EveMgndoO2Hy_OwEv90FN7cT9uqxl-4NI6D76Q3nWow-d4Y2HQbj8uhSznQhABtHuufo4lvnTtWnxDgcdCO7GlHepKKSsEn8lUJ8RSjPibn5L2A11PxCCsdtSU-acd_RsDK2sb4mnZFJEnylNumCeGPmHljAUUiUjmCVJYVo22a3X2kkVsxAChK1BR70P9-ZQJDlx3e-WVdr9g8DCdRHAq1kMUmCrhfBMV94a0LKCmaqwFh2RkOXp35XFOrvM-Vgt7twbmK6CG4xyjGYc7pIa58-U1IL5Qz078OmNqRbjH4d88QC78dkqEeIVvGheaDFElHNbRCFRwapF4SUZJxeekbGDZXuZ8mHdM9c_YC446V-IHsL0BFbBW6b4Mq_vHXAd0gXHa1R5SsR_pDcd4257DKQx8iFGupgwlZ91dqlV4aHYW0vGPxazx1aXOP7CRg1vblybZu47CAAXy_vFP19Ei2DxiuYZ6dz8DPUZysPjTO79o5Qgs9hnqwXJLuGoZy3e5SP_X0-T_AaEZgFnaeMBJ7ey4Aq3z6OkXUCBrCNLYbjOnVHDMm_tRBCQy5mvhL14SDpEM5gXDRkLP6dIisy8hqr8MiPsWFcR2n0k4LxbmqIOn8v7Js6h9mOU2oC0DolhVijSHocfuWsrprfrs3wTSQDdtqCs-pBo4Iz01PVGKnY16VisRCiZS_ISn4WyPytgrBZ8jz-1TYiq6AAiQW1s9nktXS4xhAkyOP2mcDjvcaWtq3op5PrtlalH4B5Qn7RfZ0mbt2leiHzG2kfRCuReXElRIbdHj3u8JExRMeTVj6euNLLykhkBYhcfsilrBk3Pib4vhkdFQxvBq8lueqABjrggvh243Fv4p36IXnJfCprHaCT2EzJiajqjwgiij5JqldRAORxqRRrHtgMvE7PU7TPLFfA8LlfEDPqARf7vm2cBzKb3a3zL-tFrumRErEhFQlJuoZrR-wrs6orzOrCerGgiI4iXn_8XH4SNr2FbiCQgoOkldwPgZIefd2V3bpo4vhuJvRr0CD1Y_kXCZ3mNuqsoMR3PjcSEL7vjKDC0fumddEU60iQqmsr1KWG29MkDkgwdcHs64KLWZ4YiTH-XIwnwBA12C1fgKWXrmQbeBs8qMvBzxAqBVW0QfKX1fHCzbJtWr3JTCZPtNbC9eMY4sdZOZ_YnSYIcSabN39hT5p2Uq2drkDJ8muvvj68LLum_Ss_hghk8xhVjFu8g0DaYUWmxae9leFSUeYxyyrL-wyWQnip3O7DqEDorIAtHQ0VS8Uba9pAWNN0PCrCvY130euk5p3R4qLYiqMBEYbme5NoD6dw8pKKmXQF6KPpV0iUvQrS5cccHTqtwVDs7hdOYAssXaYPyXaMd5dTTd57HEpIKEzZ-e3XX1i0xhIEh0s5b4V39llnR2vlFKWg34TwJHw8xaVTJ2ztV1_Jq_8InO75HJlkXOx6ULhu-YZ0UGY-L0rE1iQ9D-D3UgJf-myn6owDmlCWhFRm-wa5erlKAiK_dN_J8ktt91Xn5XvQBDWalIo8LEgYv6IIX6vM7sDe1f7dHhbJ1cscapDLObffU7WAvtKEwOEQdwEFfybi_wvcEB2k78bGDVxHPrnaY7gAgk7IZUdloQUThSKr6VeFT0ss21Gb3obP9f4KWd6EYFO1XzjnjcKQ3dJC5-YRvH8wDmnOff-GesQVWGi0rftH8M7dM8nXh2eJ-nFsg4EDSL74EM4cP9lw1WlhhD13kf-3C_3cmbkM6pFNMTxTpjGmL55JcmmIdoARDwJW4ccS60quGmICZ5XXvluXssrZeQHlczxhOj6AWL6yU3ZbRPDF-92NTwO_FjxNU3bPT4BjbI2MzU_Nh51jovXhcCLHFPb-Y18t2Lm8ZRnjYytUBRP91b4xCqJHts9aQggFbYlQqc2O0u6BAYbfRaKpduIrRu_LB53xLt4Zm99plcwflkZP03Qn5McXXhJ4LBc8HxOoMD__cCflRBM5lpXhR6NZRrX0LgcwgX2xdec-pZe09_f3FZTN9u4BKY1P8Zrb6g-wGwX2OZ_OPgEZDExajpPdX5MxM67rnr9eRAtDC9Qpno3SwdO-4p7ioPuh3yQvqcN79p0glEyCz4R2XU2b8XP-IMsevS2ou6AQTI2PpY6vWNMH2f6MgliHx_Bt4rwvLFysZ3v54qTWjZM7_boNuprprBvrU65YfFIew6FOX3ZUZpWSUkOqiYZ3O7WB1XdBqiorqpwWc0F6D-mqloIRERHdFVHur7YpVZ6eN0MmQ7B9wlBovSqoM754gCJ8iH_UQm3cI73SGevkZ8Tzpc_okhyUfbMTQvy5jmQu1Uy2ptEtQDP_Wz9t7Mm_PxmLdZst6RK4xCts0yr-CjXWPrW2ubDulP9Z3XF1mZx8al3u-aH25jfLeWDnRv1ZqH9WF7-FAq48JvYi8DB_AC1GXoL2xRYcIiiDG8ukrb0_wXkxHoP8m9TbcF2lnTGza4tL6_WuInhf7X33hdcS4GEog70XOl8HuY9YZ34YZdBQtfQ0kWEQ6jwdoVH09texbFiJ10YMZS88Zia8q9Cmw9lwvg_ex43MICaGhJBaoQszsHe8KsuEFQn-_thl2jOZioPt3a30ZmRj5PHCZoiGejFcF1c7FJf7O52Kh2RmnDHaoQAccbuZQhyRIlEUDbNN5TbwF4ocmReGHuV1o3y11UWULdiluvOLfY3zjS6e2sW_O7sdiWqrCSVhu5Qktpd8ZlaIQTdpUHbMonV_jSIkMVoVymho6qjFYN5kR_A6UXeCRoWF8EeQJ6h7RscDL2s9MHcBeLlzPpIXupvx0uld0qCWTTqxX6zztxkNQLEexnXHJmmqPV2aL9VHg62nA01SiB4i1XGXmS4e7ce7ECnI1ImCHRH3xkFkpGjb2yEvfebPu5dqPAS2kdXPs-1l3ZmxXkP_GmvS7WDjtCArD8lz9SJeuIHf_PgYAnYs0C4jIkkrd3R4d8dmMoUWeT80zy3YTKagFq5V2IwuzgySkNupSqhDjUqkOH0wDKZGpIWStGMU8U_Tws3yW5jPXBMmfZSiWBr4ObUVDAPr0kY9fIGOnpxNf0IdqX0wqlF3K3VSkNfILX5ZE3sXJGq7UaCq8DnNZ7QqDcEHd4xIcO4lCwSCBkTlyqKz0HxCsk2IA7THQXDldzmBFCvQrqtAiszkxrGNLPo5olaNUqOkZ1ahza2xZuQe-ODzqqmQCnweHulyAPk5oa3pdKc-uHzWVif5WFEizybCwuLdFuKgb3RAxLa1q3kLb6xUM2vVmOd1a1mTe62shwfZkvAzz1JZMu2nZlCAYAXgld7wobvcRhT8qKSppbpyhmde7DQnUGeq2osFwQLoFO66PwWmJaIAHe2laRRVwVqL5isEQi7zuIHkk_bGUKIPxwplEBVh6DCPnYjGbWa93MPWvgvLH6v06yq9JnF_Pe9tt9sSvWMhx5oMcR9JmTYB2z-fjEpwkpdQ9k70p3IxQvAxVno-QMBLOSzmhsBAzZEjgoT9Kgs5uztERyoU9fxGD-HFnMVXIwx7s9w5EaIhl7SHwnW66mWWfWt6UzHckaLRasdw3EjilPmRBskNLhzE2TkED2CA4AVwZ0aPxz2uHXl8yIhpJJoqhb340d2pYmPvgDoeR5GUYoprJiLwzUomaGJdekCkL2ufJThNIjXsvPx4DZ89qIhFVQmU83sBwP64QZBVyoav7T4KwEi9E-QUkl5Zdy5JYqln2FQQvGsYwkBCZU_hOJzy1cbwzmP0vKBzhplO3IqBUOS8F_A-F3oOnHQHga65OFFwPjcWAz9U5lRI5UDCnGvdSbiUrHRT40EhhisAJ4Ke98o0ZCXubKHrWJFzBzEaMkogwK-SaE7A7PMSFE6cTBhdbi_PM_VZzfjEYafWGjG0MaYACVqeEzt0V49Uja31lyWejGxxjxKQfONzY7SQvJnxRa-IZqJM0VQUR7lEg2zJQz0FfIKVbPyU862zJYMbsWRtvgEpDzKVFX8IKQMAxmmMDZqyjq6XFo6KQywu8QwQ5K0sCfW6Cuu3E5tsHxs9ImwZ92iIud2ipk3s8b9bE-x3n5-gSU-TrqNuDfesuVD5YIvaG3JDIxvmFqjk8s9NAxLU3TPtKuCwzOnBXmDUDxeP66YP889BtZFQtBayKiUz3TX6i2rCcgJZDZMpClX8SsckKFB8Y-VtsRGt0kWbZbvCTKvp59RdQFVxAOPwaITRl6VCoGIz7buL3ycAoLlccWhZWMZchd0cMsl3V8waHziRxSoxKxZWYh10wAf3BO1RNhOD9f6adfUwZGg3ZCdoBOLgHfwl8pFQo6XfInB5f-51di9nnKBvKT30zDT_qHXUAAJpIoJtA9B4dhLP8oB5o9Jdy2MFhAXCM_RS_5u0Xz3D3_Si0vJrGFrBxJu2ZpQgznnqhZEmeEKTn4Ckjw4vwTF1MFc3VBeEH9LZhmnahWcL3D2BB6mKxh_7gaVlFuHZqGhTOcmkdmGtpPet0dIfVGoL53ZeBsnn5Jg0ZWtvJazCO4Anjnobgigc9ToIUv1DSIcY3AzYtqk1kmn3hkgEqDknlEVtpwCcXoOytzxJMlCmsX0mJBPyUzdNnU_rzd7Ho6o2mgT2tNM6tcBcq2g-lQd-XAVzUxW1zvd-xgXiuFE5EVt1nxReDz3CqOo9e68NHzd7uCx7YKZ6p2yFJAvsJDI8Irba0udk4T3GE6BNzqdkDVBjQee1KZ_BVTJLQwFDlH--aNk0WwpNq4tvRTctoA5Akh1ZaIIHPKMWHJyOLtke8IwGjvk5ydvSvYD38ir39E7i6rx-sdYVQKYP_muRsfyRQtyiol8Ro3MwUiDlrS-Mo8IGnILAD2Q6tL557umsorICB0_xIYRLD6OiL8I3w4JGp3IwQ4ZsbFbRoGdzQgJ_8MBARCg8UQAdOwgIDbJHZPGXS1lVA3siZ7uBBPSxPftaTC2UBAS4tpQl0PtQEOeXtchwUFVuF2NWbuF41yIqAkbwbIxL0M5C3dEy46J70ZUvjbqUfUCuK1EhCUtDBg8L8Cdk3AdDYOXvLDvAwCmS_0c5BmgU7V8Cn5Bb8swDKUxtd6PCGmzJlcwyqAY_EYtTNjnAMwSW0Vy70d4mdufpPCarimrQYHw_1vCap6F6q-kvZ5n37N4q50uglYVL641MCeQdj-A2QmeE-1UiR_e2npKaJKAOnbMrCP-YCScJuWkhHe1WjID0jHOjyYLg18Gl0S3Pe5rkJBiOGJHJS0QtZWdXl9WZW8IXAKb78Kz7gM7y6zrLefrzwSQPGQ6VEU__mhkJNZ4DBT8oc95WvFYQSeBeC_N7CCgBTyxBDma1f2U9utdqXU-qxG0LPFS0jlnUAbsQrMAaTfjC4NIPWENansXMO-NNvv-Ok2hxfIFCs9Qd0TBeg-1mJA1bKH6VFNvU5fzms1U0GAxstkSiNS5_Ez_Fh1mu7_6aARN_HwQkzfLfQqjePD3-OfwGL98TVcX2xwNrkCnZO2PWbrEkwNS03ZZhv4QhJkYqlCF821Ft8ZXoKaLPnkRRaVVLkSSWTyeS9tw_6DWjYZoHKsmTyEsMzkQtXINX5ZGwLCU3-_mQ6FbHTRBV3WNLmHvgpQtfh_5vf0ZnAGbGrjDmgzVZ_XKiQFhz_HAjL59RJU7lBwE_rBNpCdXU2kywAkhJuxK9D-9JwQMGuzbQ50zgPLE1gsQ_APAg5nHiQ7gPyQcYfX6zWN_ePypt-1_q4KipNTTXG0hyXjh4xPcuIBHR7pbnL9c9BM82S-8RUpk3YabfffV9iX4ZZwHnPZEk3as7FHsDFKag_dH44DI7KWKiOqQHapWcDzfX1oMQ6ViURBTS6Sh8-vBogdNznlGR4cmvhOZnUdHd7qg5fPoVp8uP38z19x8edbFvglXKFRvax-OGZf6bnAWVSZ_Y-1KSMh8i7mGSHHKyvJrIOBzPLpVGVhBW67SBUwrFb0UE-Zm7USqhA3aeRqefxt3a5sqhcW_D6bCedCS6gNRD-NxPjHq8i3JaW_B2oZn4dUGSTIir3NEuL5rVynsDFjJxqX18AOmhIhceaJoKLjNMQDdXa98eQK6FhoBXyy4t-uJqvSK7Up7tSkQpuaTfe0txTmRKaS_H3XeuT3i_mcm9YO5IvsF8I0rsOGePGGa-bXEqFhRfUR1LbnlyBqWhBLQjiu_BVtyZlx3-Y37s-CsdEMVCB2xgi2X-65ixOFvseMXWDqGtRXmWMI9wYOtEsqcpwHas5gmNSvqv47UMXQB_B8GOdIDDB3atNRsXmdxaT5gjKyF2xWpHItliGkJzDHULwQSyBqDuWhPoLJV17L7HEabY4g08bKXCOwwvCM24eCf3kcnlt7WfugpNdaoiTARihf5yonRgeydnfwm1ajSlZVbjVvp4TvvaxN6TU69S_fdkvaSXRmgcTTeSOci0as8jHW1rjX7TeABMhzrsgdqpplPdZsQZV7qK36CBcZzoChSeLTq6qf6xTPKvOxN_VAkYSCieYTIBfqmFPANfiKi_3uXnDruVicu8SvIFyWig4xvnZrp8DfDrROe4P5i9tU76MtPrvve2tL20B8bf36f4QMwCWGLpdDcCnEvbMRWiiui9cr6ixjrqufmOHdE_GakSvWUV1C0tdSaZiYRO7n8xxqJfszhpoWxwuiovwNyVM26bYkPt1tErpmZ4QPUx-KKTuSETfWCG8XSX7u1EWny7D-UBlzA5CCysfhpmttpUFiptPmm5cYsaZBUrpMvJumUBQl8R2TVqxlxZ0pwI7tyyVHpYL-h3SYkTjBHjMR22w9G67T4Yiint_cEv5kRIOqCrET_FR0Vmx5viEdNDkNP04f1t2XjrUljsHFuF80QIVzYWr-PEqhbZD2aLBjKqhGqJjfkqwAK35dFDq7qOmd3OZwQzbyrYkynJnDR5wRfMCyWvOvBu_OQ-Ca1ZnzYhxDnKlAnTak-MEW5hVDhvij7V6dIinCPi24HFPz_OGeMP-qnowLV4V9mEHuzOpPrV6N28bPZK4prLQD8nZkVRa3_vFhqP8exTNv7jdPL6rROsq5u2jrijJbzCKJob8ell0v5UTFmAKL-y3rVvef04U4u-8B864bSTb_1l0I8nbLbi1qe1ppUT1AONW-rDghUzDFD9nUg5nCrqXMfkJinSKdfjQS3piLUuhtXaSyFUYIZqyw3SCF4A4mhoEO7gds5WtA7IVqfbLIf5LpKkr1kb3cExX98Rm6IQ2TuieN-swZvkrkXvNIQkeNAPOhkLOPbqQXHvs0bS8S1BboK2fEF94Yw4xwbbQde-wjsrfpbKTGqxV-5K96yEoUVnUCXtHOip83kEMhd7tIw7rHKgfvmdbcYuZax6gQqbPtvaQrdTX4ujSM1LR4WcSX65sqSdQHttjuEUPOIZqRIRSYT4UWAMgS9pEOfraeUHaiUEVjlznfSrjsQSGa452QS6Cjqyok_b50qS2ePjubd6etVJ4WEvsP-u7aCiK7AdLuP4M-76YNCBKqStPqQT6sidbGnPVk_lqvalTrhZkqAEbsC9EpQRorcHJdDN4j13EfspD7XM-pVcKTtUs2WwRAQmT70HTejyl2c8NfLAb11GxROVcN4fEOaqRcmvbUaDQEmz0VGyQOLW2tTpxTHJD96AseSUrG1NZenz6Ptj4rWO1GimVlIdpRiVdxxdyvjt0OOZx3o6cbm72RPThRrnEguOlJTCpYd_jKwXJNiXBL6SntapWjjYvBUMD8uFKyPcu6ojAEVl4eWUhsnmrFgtO0HxW2Bc3HOWmm2uoaBbTtyHve-dW5MNnA9Nb_qeflBEosIqrPvAsyhX4XuOXsmgBBzCCQG29ZqtM7U8GjoOYQd8hk7D2N10LyN_HeuaNIK8AJBekyuy8UsHw-bE_3ybX8wnMT5zzEuZoSHpJkvCOJAHqYYvO2N3nK7VreztFLWai2lU-eQeQd9OdTpomuO9y16VQ8F3giQ9PAFfZNl9g2CtPJHizG9V6FAVuNr9mLep1flsDYh5gL9p6wf7prvzo8ICwLYkOYHlRyYZt2V7uWtQ7WG96KKGh-RsDEjGvFtubBjuAgD7yIiLgStFzcOy2HTSvPYfOGemcNAJlHjPN95TaGNRZFMRsp16VyJ4Gu8BLscU5B-wovH2TUc9mZYA7DbybxdDL8ia5wj0wBq3dEuzwky8nJ5E-BgIi9e3T2OwzQUahLQOuyuzEWCDssXSsXM0p4KJTyK30uRjoLelj2jzluAo0V0Ax54ls7GxKiG55VH8u6QnxA2kFIVesdVrC3e1aa7Bd0_lIGmmni1Ur88h6hSRG_uqbFInj2Y2LEzVpRUPyUikf_UvJh_eDlqvedEwV4gkW0zu73TUehzFs4lDJMhDUjFNHO3-WVE6RlVT426AXyv0jxGIZ_FY7Kvl_6ZMSxtOV1lUne9u7x4sxJW5IjOGL48p6GkRnja-4pEfUfhI04O4S31Ldz3WrC--13iFtgyfadqjElLMc8AfUoVD_WmS4UdFW7ZeIhOsuQEqWKlWlW485w2JoZJvHAUTUwdrj6N_oFTlx38vMucpXvYQ6-K1oED6h8O8LWJ7pw2AzTTcC0fMqNUHE6XBRo97T_-vl--g7NQAHPNIdDxOrAGP0vTTQ7LGcfsWoqNy23pYAyxoCnEvKi_8U5mjdbbMGKtDg1DPP4FreJiLUWskEHbfkULyXkknSoDKY3uYZojZTf2JU4PCVU-FTFG9kQcpQDpCoAliPLdjT0tdhJkwuI7an4J2aoadw38aCcs7EXIarLg73HXVpmwekouXiob1aZhOUwlTB_rXC-6XaDcfXQZNEqFCUydr428Zo6-DHOjzO8oSoI0fqZbiNSqA2xkiRuYw3Jvx0Fg7KaJEQtGaVHC5iv6rDLw6zIWPkSKi84SCuoD0i_Orfap19bGne5WQ-ZdnnQrMrQaUecRiMOTGn3b5KvPIUzg7tfMAmMtQCx-M9skchwzHhIys1mWn1OIl2CHGVMq8lTvLapr_EjfpadyKPfx_rSuvAQdQlvbdcUXHLYQkEnphEVzfeyNBmWeI8NiDwF1_YJHV5Ue0Vt92J46NJgufEABWihlyckTaoyDti7CqnEnrtLrklaAm954JfXlal64hSH19B2VCHnfbLMrdhfUnId0duse_zwpO6c7ZPu8_VLLB4Q2nUyMxmey8mHrTYI_IzkBxVovIt6Oms5k8QFPSO_A1UMbWJQ9W_qQ_sdZ_TpCGs6ZASul5kf3Nnt8SIx2yjXW24IAfBznaj_Lrwz9TYmg_YOitpkmL1IzYHY3anW_h2oQcO6YF1N41MZDF5cy9Et1XfQGKVaB5Ivvtr0f_fb3IfVLq8leXvT8Q2OVpIO-cRw85hzMXtzi9U3ZY-mhqtVjkg6YV2GkPJ1xFulSGwWYl7rFNvGTgiRMAiRzMSR9H1y1HfgzjO1Bgg6LfuFSpUcVH9y2RpE1MiZUFhjJ-9620b4ayloYVoz4KDg2vmXFJFG2GAn8hxXzU0-XLmx7qXrZADrwRZD4qVh-KNBqs6HiMBVr5ymTfIxpiCp6Np8QVE3DfwjOB7hHZwZPUF74ljfhitKQH_pnr_h87AI4tCtemd1wUhRWNU7B4W1PbYj2RssmSu-NG37U4_S_Ej93MdZALbfa0ofdXfFV-T5nMJhphj0BWijrPbeILSndcalwF25RX67daqZy1n-X6ESeCngA-o3eeo72jeF2NKIJQ4yiEmc5k7beu8XCdtlVSfBMwhlswIISD83tUPOt9gDSw3UmwEAXypkMqCMQ8Qx44Msjv7gQxd9k1UnvtraelsyfpEhfJ3XKM3yc4lYbbD3A0WTjGJI0FTc7pJ4K4ouM1NTpncMTDJYiT8gfdD3fY3Qh8Z-L8QPCKsiaP8eReRKYc9P8lISC6EvoOcvplAzEDWdUOgrLp5lmd0WPmWE3hctPIYqfMwwyiJoGAuOQaOXm0W7VwOA0zHv2eizo1pBA7FhenjdnE95f1PV51-bvzddBB0DPfPUWo2-UtUATN0mE_WlFC_DDv-VCzO-PrK3W86k7146y1ucKsarXCNsRp2HN6K1OP6FcdbQT-VnlCcJmhKNYNgYUYq0fkCsKo8kBeNdFXTyguoG0rL0zxtwKiwiqcnkeI9CKIqYPhkyCfSeLT3MWMESXZ7szloHVDOdaUCrSa7k-7abkujKpPn8gPmEafRKKufH64TYAgm4jSzwXjPIIBcFeqUnx91NgoF9zU4AEoki_diJo_sgHI8KhhcaD8o3gWpOZbCXQhtJoUjFLBr-m0aMZebK5yhHZmpOwCjHlGpisHHZtP3wodeu_bPhfpoPf7PQFt7l6xqZ9Y6vbL3qOMZHDkmAlwiES8BDmQktxVPmqXtmDrdd_zXT7YLsuqQJl9JRVpo2lsrnj0EQ-a5E33_X8TAt9reYpO0qRB_CUiinslILwYSLC6PLR635pBqZTGajZtc3XgJUFEFQ5Abv_yIh-QhXFtf0RR7pUpfxjGnQ4lSxF5SELl3YjbcqpeNUQqLrC4pLkhzTpvMGkJ3ilN811wCJvmhBRzD_PUfr8drIPwoJ0Ov_j8dSK6p3Znw3RBcquOgs5jR1iMtGE4GXMg_I0ldc90K2TYr_YspC1QE0z6tS0X3Dbvfx5URg42Le1OlCHpMil52UX0vFE3HD6reX0zFUq53poSNvrM0eFzpNXFzX7UvihMsKtUSHqRyoZq82tR0vAAtVVR6jXvKESar50WkXq2YX2fPQPQoEjwTIfOQygRJBdzVqREeYaubXH5275DXE2MZNiAPJMDX8VMyWDEaHzFIILKfH9zMAyxdVYivn2-8cOu8rXKDV2VX9V4SKXEKy7LHocpAz7vLbUPhBULw-qaBDpZ9faCZMWgSVbr03aC7pTV7SmAdHBn1U23HSH8YsqZu5IZC4S60zIFXGfNlpv2fh4IQZn3ZDtyBylgDMHFjv2PQthok_jdsMUwHkMg3ojzDN39JkBQ1bAsGou-kxb4OIwN_ioCkpQVTkenQkrFA-RJqZDSmTwEdA4soeOpFE-wvHpYeDT-Qhd24npVx9bTBx-QJlqg7p6wzOCUYbOvU-ABf7Jvcl1mfUCHNOS6EcgYbVAJpcRpzTrgKv6XJPZ_-FPAwgK9zUCinzisXIP3e3jZM1prVvJgJqCUn-gNDZP6EXAAwFFOlIaslOBMYT6Ui_27Sh4LhoB9ds0tF5tqCRX09WZsINBRoPJu1BkKufyx2I-HuGctyWhQSCyxQZp-5XuUZTy1-xKmlC1nTkf6RWY9DDzySxEx1kvQhBzfnee2PuRX0Q31JW24O4ejo8R_0c-gAPgiKCXwHxQhzGgGhWp56WdhPIfWpZTqKFMzwckPABRhxHyPAnxUQaPunfle-kDCuevmEejKGvHYHbqv9vq0Cb5QlxVIULmSoQgYqleP3tHqEhRMP4_1Dxpvd4CHccxxxLyO4VKDgGSThxSmTcXBx3vKho1emAWNqAOWDjVjr_SC6c9WgvvqViyCDIIocRi7p4b07dG4ZXj6R4tflIItF7KRFiCx71ObnNba-mFGYWd-wgK2HVC_sRbWUK05kJoGhjQQ2AQZBRHxM1LnsSSn5zMi8t6AxFxRtgea_a-G4yvDkHv8-h-do8Xg5FdSA_u-ItpRtnfAjkq6F1sWXEXVBK9QsnC5_9sKu7oWLeYxFxwtCRSIKYRxsJU0P_Vsajr5qb8dKymurrLF-Nk1r0VqDvUW8ya6tu00jc4OBAiQxY6xBEdJ2b1L5Gq-Kgubtqhv0qxbS11WW23vf3khd35JdTCjPm0_LbZ323zfq1kcX9QWhXSfJ1DrpUIbqTPefVI_cwYAYflQDvrNMdgqbRYt6wExW1qcTxib93fOIOSVe_m5lECMaz-rO7lqS2YkCUxQShSLy2pwLp5uvgZsajVaQk-OWl0b-kXerj3_Is20jbUX5uzF_No1U7kaln3Lu9fjn9TqSgzkUT2SjETce6DRBjnbh9IVJvy5fwuWXAaZ2OcIWcUDoFPgm9iHCrojjBbReqWvK_yKFRYj6lUTuLXSMbn8g6AEbrHYXZDtPzraLgB-XVmQqIy9tsb2q9Kfu0UWa5S7sbLZyRYbM7rBhwr7noeGczLl0XYwu2DEFZtTRaojHlEahWKznyB3OOZHrpmQ_gtKIr_Doi6p4ZXk4qaQyxzHmnpBepyrCMD0hWOmfFUjYST2pcAURZfIUGprr5be2hk4WZuvokzZB-P5xVYwKMS8iu_T15TEcc_e1IzLFQKGb-oyR1dLIsugl9LIkvMuGfZvwTWR4O1lxU6cHm4gM-Wh5LmfAmYPEXUyRcmGswvW8qs6dEajfx8ZBaAwhNNUVe5sZtCRs4ZslTXOc14xiMjnlXB49u8fSodEaEgUd2TP9IXsNv62xFR-R1aMDi_Cd3GJOKpjUYMvBBqUFrjf72j5I2Uh6SP2j2kg1NavPiFa51ccPEvWA1Dn_w4dLpNzYij40JowAkkCkPhb1otGEFgQFIO_it1XLVaUC7CkN9spmw17AYe450fcqVtzeq3VyAU9u-dhPND7touK7IOQh3XM9hSJBNP7Yl9o_jw6Kq8VQPHCqwihyBI78up0MTRnYLmIZ-1OY11N9JvvH4wUtb15DuzabdgUgeBq_N27y_lW7ipu_4ATEUB4-tmsOFIrtEMEVtbORsQ2OaeWC0xmZBtYBtPZ4xRAO-ItIT5u5K6bic30qz4p15XX9-copjivwJtvWISEdVydYH4zM3eS6r80ShD3jLAEDeRNcbEh6NUT9BUPgRuIpQUNA4ZN_s6DEIbQdUr-atB7Txi1MuBdhJsFuTQ2Mhjc5aTKinpkIKV-YAtGfJoMOJHIPQzFZF-zGiC1RhQ_ay-gV0rsDjqRt37EejSDBEItNqgb444Tv28okvY6zgjSaJqi8pgAB9fw4yV_iMiHAvxWfFsRf7o1GFq3Ec8NpsuF9VGsR-TNPcpGutd0TYlQdKD35uUlVY662u4igk3WszGxbLnsAyS-TJXFEDjh0yY-XyPnbOxNCnnKqNrKAl3usztLB1ffOP6B3DzjGwNtJ2-ZPsNzSS8N70RHVKBnb4q0hF3bnESAC6SPHXyB0m5wYxKybL4oGbn-NxiNXpmT6euc7Pj2ga64zUtgj-GTxqiRDuNzjfqDXXAcdrf70uv4Gd-qySRkCK3gQJwQS1nSa1Q0GpM5GYFrOQ_NyF8W8cIN9T_DfVPQWaXtNBkkLrejly5i2LtoV2EfVvMwZp5xrHQOztph323fVBohp3PUVsCArobTupmK094Cw4aflhyj5QKWhxeiIUJW5t_7J1gnl-olc7qsQAAOQhG84HotEYO9gWFdUQkSqz5pqer8Xnc869aveY_G-6edVk7iU6YCnDESOGgSu-rV0ZOS3wtPjR-Dhuo4vQXOQowl2cgksS3NH6VLHH7E5TYRzcXdb19dNhIBw1ZiMLMS3WK66TCAud6FZPIXYCuXG5jAXUKjo7qxCKjAKKAWtqUAl3UpwlKRijSVEZDgX8APwzKr-FbaUqG3nHBV7jzBqLw-hxC5T0fukanyQyN3Yv3fr_HSFbNnNEKyK25q_2m1rlRBQ-RzR1BrZhGvAd0sKYf-W5p5RD2JB6zW66yRBrjk-j9hNi2xXBF3vRgWA1IwOgMuVoeJGHJSbt6TlyrYIzh7hzCLLlzUMdKuI6VP2AD9v4Urhvi1IV5rHqO7Vsi3AQEX8BWnf3wZKEP8j2ldLnLx_G-fVtMPLzR7n6JaXpCMIOmL101_l2o5uLEdtiIUVAo27K-MFTKVwKNzPhIoNRWVM00xUnEDSkCiZX7_6gf367uNsSpXD2v-5V60OBHZdWB08cUPLtAR8TxUdv5TmIYmDjU4IYsElJfFE-s2-liPxXDnao4SoSNhMZEfmUlwKpc41SEBD9iYbhMdYgHV06w3sXyz6cndD16QbHJzyazs-yAioYWk4_AQtvaS6OKLdJ7st6rkT9l6Y-DpNcSe6XWXP5BhLA8xD7DVXHobx6NK6frBI9VJxxSGkk1iFkNEzNeklsohRDMYeN8o6er8mG_wQnh4aGdkH-zRdY0YCyd8gCvMMv-8X2UWVzCS8XKdZ0WskhOyTx5qFaupGQy3lrO3MDCb1RFcF4gFUEwtNgwSnrNqxBbuPTKECkMAYldHTRFkFPuP01V9UBBMcwJChcTdLnCiyDReK1p_a6B_0QQQimGbUyFuYOoIEtc9idTnE_SOOZMMJ4OPF4FHXIin_kxgDLHk117WXYHZxkCQFq5tkatf50YKHu2meG7VztmmHCcBIIoQ1ZWW7Sx9Ta5SmOLUiQNEJXSq7Gf9SJW0erhk1SdOnQQ-3yiDM1iCrHSOTLSvfa0IV0GGPelA3S9Tj7oCgZhNp8byd3jMua9x6pOBgpCK-th-90DzJMXy_IMX3vXz2FfKLqgdJqiviJRer0QXFCXbKIpo1BLCp_6zlBgzr_q7xFIGqxh2N7JaNzmumlBYsnXYZzkuQFy55rpHr8kvIt4I06kOxwRmeXH7UdiThJbTofhD8oVU1lS7TN5QiEGBfdYRLHFIkjZZwLaCFrCW85ezTxd481WnyW7vtvI0XHRBkcIKxDs5sMNoZffUIunSMvmc1PkVB-_U16LXLB1GhjDesVgPPUGjO8H79hZddGbLwRexS-i37QnFayxJp-cVly10C5zP1z28v29Z7Hz4_Ba4CehNCMDLOquI0sSlvN3QwcWNgNBnq4WPkBvfW3-BUipzZzuFb7rrih6LgFCony1FJhm40CGThIsMSs7Dd_6T7iT2oLiudaQbdZ3JN5gwtklqndTSbx0nmh19D6w4NqHxQsDNy8w93QsIUfWzc5vTTK6FKD1Wha5Z8dC7tzwzqZ0b7NgbEz140TYasURlrfD6Q3EBvq4msRZYfBo6aC-dd4jDXIHwC9TcszfapCHvUUC7SCQaszlMIIkXOWTX4MbEvGQ-sf3jcY5vmuMhl5M-OxX29mAkkhAIQ2iTfL6uRuo8pkmPScfq4DFlaaGgrUixrgshg8Neux4y_zaebq5wDHCPEGdJOev8c6MFkSSSZDmd76dch4-_RjYk1AtWrrSanQs34OUWg7x_XfSJWtrSU0knvChOUqIv2UfpAiIfXF-8gPVvuqb3Y4IT8nPdgRWRvd-MgjANy3ljHvhWRmi_A4ohpMPLXF5WZSQjt1juWg5kR0Z9ZdFmJR-AyVvrXcvoFGWA3VfCG8NNrc-uLYfcUbG9JKkkd9iZsgtcJnVjWwKRUsGKR-wtIt8kDgNujyqhVNQvY5b9f_UAsnGCrujp7PuP2JEEkRWK5PnAsowSo97qYoLJc4QJR-_Ist_XLDXYWlIcTYvNUGsWY25pW6Y-bHOL9FdvKxtpffKEPhUrfhWqJfXHWmmiEejaUh6qxHzUB1_gAqd_4YYv_rRehFW4P1r9J4ma2vSwBc9OJbP1k-A2kQF_-TvuiNemYLLg8a90E6SrlK_9bQy6x0CmG2mf493YhBV6tNUqp-YT0rURlr05qOiupzttslSdYruR9CdYJev8pcPgtusSYgaqNhXbPzrOCtJrIKfH2Rp5iEftFQ6WGE0UOBy_dbqbj5f71PHGOMx42BPQRzSmg5Kwc5ZRzVE5d1eTaZiUy6SaBY-LBOD3d49yIyB78svTUzX4_xjsu1IjAvkbjGjQv-Y3NJiSET_Lwta49Zl2HUE8rPsaemQy7LNmVocdu4SELt3-uaMdHnTPYIxoBOkjrNg0D66Z8idvT0EtQObnPknxVN6arE6k8blM8IkIOBv4xD-vznCHM8YFQamQ7GremtXuaRGGTEWXFTq2157B5PEHEtDbH5bddpZTjFdlwyrJjay4x4wPZGL4WNecd-3WIyQASSdpUIPuHX6is1_IH1471dzJM4Pdu34U5NFXoQ5cctlsAXGaJnSg5UAp2imBjqYibEWMzTj0gvqH7VEfgUfcIRdrcbOjTwgGdLmL9tBNkVhQI640PcXVYHNtJBcdaTqSFWQqPAO84rZDE1hvogDZqTGHYTLS0bSrjNeKcZAU1BSoPGQ3G-SK-050B9kjQg2aIwBIgHRiycLMPslKY6qc1khTIAtBZ-sWV_2OrZPsNzjYg-tL2neeVNzJ83Go2CSW6WtweJAixoX4MKsPST1Aid-dZIQzudLPJW9A9tktgfGIjb-0HOd0sNMEYqtBBQF6-2gCnDMlrLYfCWOEpfvab_QMCxsXLla2DCuXkBxpJY7zBHSqDeqp9o0podiTXWzIc1NEwZG7b0FW0MFWzbEc1UYt1l4tCtYc5A-ph1-PsKbraALpWaZL35vnkkjpgXtV4NF-JReW-tTM9y-LDW9IucDKGN-OM5_qR5ImkpGq1li2k9vlyo2AmLzMtAXrQ1fXmZatTpNRFKX7jhldmsFB35Sexa7s3NxQb4Fc3oBqBfW_AVxnyXbEF0x2K7onfrjPxsYLf3dsD6Rs75EMPop80BSlsh5UDv4EqrfC1jAmDTNZ1hfrvwWNbphrjYq5egBNIkNfQutKuSSYH9K3tQlMY6bWB6DP6sKixmivRXtPZe1vy0wlsGqs8XJ2F_dADGOC55V3U3kYGxeFqqJnxNcx_HFw2rEVcTuDN6AlYZY4dd69fm4N8qU0dYpzOQk9t8eRp6w_2XSGe8WYjGVnqavQh-Kn5gVsA4qJv-AMo7qhMfKPNyfprO0KjgUucfhd5VA6LS2wEWLcOuboKQTRAqHg-C4YLmTAnWM45dPN5-2xpSI-E32AdV1s2y183LGk5Boq68fohYfD6Oa7fGebpLtgBpAXLVALfiQdKEj81kNVisMlcvS8NAlVVJG9XB45NQghDH8eA5fjCCCzJMZltT90XNJ50Koj8xFsIl3IWhhqgFgfkkeHBMR-WeBDUhhQi8eTNFnqtGld95QY9NIcKEWW6EnDwfAV7qgicmbczWanHrxyfVHzhqzcT7ws8ZVGZCrL3ale-s73aVouTOV9AikZQUq2349h7M20XmEOhAsNuCTLZWkDGVWEDTsP4hnDr5JXTS4jmJgsDS88vcPot5suwLGMt4M9saU1bg8QStts7CtcncN9ENcqxDkwQ0prX5ghmT25TwDEV8rnOwC0MDTUmeP5GGJYDuIBmSAui1gVD6MWXUrkU5OWxBi5M4wJ7kNEcfx838jcC_KAhYMClZ-WUevYQPXPumyJOHQUTyd02SeuPlst8dPKOM8qsslfG7tyqwBByRvh9omOWD-MC1MQi5tSbzBW9A6a8fsCQJ4021c5o8_9HY893aXd_qz9iqEFcFqnFOekOxg8L6pjqkrJWynddJqTG5G38h3qIDJyYhlzO5xYr_h6zqF2z6rCaSeBoGZYppO9Byg3dAnMhT_pRdK_iZSlKfdV7tW3S41-h1edB-wQj2wPG1Hs47GkEBtOp61zsxZXh9nLknsD95Y1XkqdpqKBu-_o56QJpWog94PAYmbjHkeks8W5k8aDR2dhZ2lXOrOhjjBB0EFypEX4IA1xCtcIaJKoLANxl3JfkapWChU_y9gpis8ATOlOu6Mm3PqtKkvyXPdFrpBjoVt83h8fv7zvVjxEWrdntAlhd5avjkX38o11fd0ZknvjOnetx-PYf8uh5Lo3IXtT54hCI1HdGzK2V6-5cyaJCC0x2ARmQ814g_DnaBrypOs4gOaT2xZep2EvBBh6mztk6sVX1RDMCfsBsAHBSlFYGRyqSG8Istieo1QOhBoInHW0PxQpOEsiV6qwsXrJnJH2zsfM5aGGwuLy8ppb9Js7fx5Ce2AGrpvIomlr1Io7GoUgcawR28TeJ1HJaJcBiagJet_fON6f0sGcviH7MDl_IaoyF9hZ7s_s3ZgActfWUneNBawB7A_2GyD-c_FeCQuaDz7ufPPFWmGHSRSYlCsM8cyhirnCL9nVU1usu-Hc4DnEtNncYaG5S0lDIo40gTtyPAOArYywxU8vkCG-ijsvFibARCSMubzV08sxf4dcA-dHvVRlVbTS6ii6IVp-AS6TCJbkD1vKXdZV97pb70OLECSz6VM0z5zdzCUKk10oHBPdEN7jFTs9KsHuHHnGhPX9LbHRw6tLwXJHDx5CwjL8XnWNgf2uUxdX7u7auJRza04LaldG3x8J_9OxqVjPJ4nn9NJYcZvOCk25upTdozR8sRlbkShfnWuwbUPXjBzGXm8xc2p3u-xdetH7CkZRWw1TGN7sa3zvk1YBEAFjRqpJaMvIaYJBWlu79k7f2ic0IXthCburab_4B2epVxT-JrFLdM1w6rc7ThVtes9zE2YTmKYCt2KkmEL9bGnZTrrW_42GlWx-Bx1ljojzhGgvU012kgAVv_Gr3_xYPQmu65dg3X-sXe9lmy0sujePkCwOvIU7FH_cnO1sLeL-1IMUIsPBqXkD4l8hCSFlQYZcGN8ZUkfQuiWEN9d8jVXTVnSxBtqDll4FO8kqCTVgOrEzA5QjqCilYc7RcXRKlYPjr8mXurhteTwmHH1BHbIbXpjXhkLiaimBN9E1dpwR47hsCGzhAAcdBTySOYJkdde5Ln8u7IJMQTyoD12rVTbFUNGlWLvX3DbNY6XCkygwt895gPZ6kFMStkhZfkanllaYE-_vtcGkp_PaxH3baR7JI1GZRG1eEIAm-Fkl0VOjSLrdtBhbZ7J_M7A6wur122blXiWZTfIEujTcXde0b0IHVV_zfrPPlqCxUHZXg_Ova1x0B4zOeiZ4Slswmqr--4bxZ_eqrwAHxtdadowmFZ0GQHgznpj2RQxQhHe8om-0t2WKKokg3UQLLRw4a2h0QspsAE0_D0omOh3Cbu57P3hPCkyN4UF-O_5KteAtvj8m1J2hdlXcj4LIR9EzhWzQU2NImQMS6BA0Uhq4B-TA2L9pwj4yMl4GZ9tRWrKw--LH6Sfb8BNP0We9aCliY9VDOtnqGVlfCP5CDgFm5fh0k4wJBzwwDJEb4421c3_qRC9JNJafFTYBChb0EYORs7t_iEXG3d8sj11Z88jZVNY_FMa4MT1U9VCqB4YEvV6oY51cxHs6NwRPkVsrliO7sldj30TyveQzuKEgI8RRsO31VpL95tLCVhvdBD_GUGP23ialHWwduCyNbJoeTj9iLQ_FcaLgx34jCGX-8w3WWvZpM4TxwZ9Cavp74cpZCQYOODqLZOu_fF-nGV6U5ESOw2o8VsyW8g0A_2h982nlggZebyBmXUUa3i2ogvlEL1Y9XrXEo6OPF_BJ9D8qbWoDVdvmHnCNOn1qUexECbJSo64mNY7jORxbCjY7SCJj0LozPsjsErjNLbKvDHKDu0Ro4yCGr7AOVvEnfVA-GHdF0KSQpten0DXgV9kaYSi8i-PaBjV6qBk6XFjjis-svNK17WLY9JrsgoLLA6NWCUm0QTUowDYGDRy7c8sc3cWI172W3_sJ003N-gzFnnlfY6bnSD8U4pG0P_m5W6md8A0uMWD1UaNGZbfeKYWVGA_JfTMxHX2hMOXY3dwJJMEsFVHbZiR8BBQ_V7ODm01mmKOjG-si9Prrx7a9dUwZDXjDsAnxwsUJl3KXHAHpDf5Gbqz2Mm3wH7fYSyyb8TNtrexyb7kfHMryLK-UIt-ZO88smmpdTWG7lbdREZoaSyLCivs6iIqXY2eDsiDxOygez5RrMXidvDbY7A_CoknZPvEi7nJWTSNdcmk5y-bpgaixjNVkgDfpj8HO8vOnoXm-3xmwRfJ6r3WsDzU9p-kX0pkkjyZvYgktVgAHWWxoMiW98KJ4i2luJfDuv98xxsrIJeCfgGijTRG2hXYADNZnLix4crmaOkAEygw8nd2Q9BBHBbyQYt27pHSIVWsqi8kiwlWh4oHYOYNrUlOhk1MDuUhIDutEoAlP1oTy0d69X5SXJdKmOxfqhP4N8MOq2XH7NiSkWmnqJL_O0DmznLWF2P5j09HxMLUQpIxsjND5vMRdzbo1KtHvZWlSFd0n7zRaRXq0CQmqeIaFh7wdJP-SaaiNN8Sjejr6HZmde86B9kFJIAkdsaWApZxWDNbirDcrusjsh5e0eeLNDlOyLzlmqh2AfIJrMAE5zb65pcZ6dwBrrZ-p8Sdcqu7V6j962aD5VY2A17KVRWVj4EYnJFM6AEKv-a9weKRfLfJO2MtuZIZ8gC7id1yabYxGM2zbhdR26yQt1SQzmilE7BLBj-MBVck0tGF_BPz7_KmDe_cga_QAYaJJ4xLLHH2bLl1JhZIL3Gl-KUGRN9NlfMb53QJjqF_xZZCk3AOS-hqtmug3SfIs0xFcBoVtyxjhvM69ao2O3KtEnZiOcJ1grKAv2SPVCNrgY-x00B0cwD81dt0IkVK1qZ_claJ-y_ttUN9s0gxJNE-cl6bKf0TnG9ZVUXtqAIPsm-zg5_LMTqru5aoN9mF1yvdyzVf7lQr-JxABjo94F_QoBaIJ7Z7Pf75XAR5g7jO5-xuU0lw8XyEpRRsaeiQejaWCFBMn6zWtE8smp19UXI3TBKbmUBTkpV2eEOTqZytbB4QST3roviCkwd58nPHhNffrsQhKZi6i1zzZw6orH_Q5V03DL3pWNRbLiZpqQa_NvqwmMJdSW29rZN5Hz-djwDJ1MyUOPIrH1MbaGofK7ppf0yz7ny_vrv-NcR9Tb4DibFeAu8wOdcaCa02SOwDdXTpMbPkUjCCsyxbK8tBhfANEehAKWMjHJGElKFORjfNVGgMLtPOzIT2V_VkDHt4H9vR1HfzVXeeYo5STpLhXPYumyVv2L87uxATSK0EHPkQcn0iZpnTKqPIVDROfkHev7ZUs8IORh4jQ0oY0N2cf02q9QA3WoEkRXmkl-q83sUYX2yjsChKTbslcWD-52Fk_IMeUcrba6sVWHv-3zH_5dF6X3FUhfyURmFVOJwW_XTTzBKYoqrfPUo4A4CfJcuifwYfCkF0QVeFuZjF0hbb5BrtRx9VjNQFdJnONbuyd2_ntCtVJ5oVo_jeMwGPr0IK5vcPjfwJCM_3IXkexSNBVywWjowSAMT51h9Su7YRHOTnDFvgHApdXSKL5bcYoG124a4dLM-2Mt3i4ZDcFwOELxqIxkzZ9V_91y8ALXsVxfKQKdf7aYN9jhFJGKDTcYUKa4FmDkW9F79TBIxaacGdZJc2f6ibUe3VGMd0SoDg3B59JO492gkKTRzYDgJcJ2ub_Jc8LGXFvnb0UIOAZcB8_nv59mhh39IUEoqxKbezGxF-PQnZoMYMihEJyrW1hG-HumPZVefmFA2kZJaXHk7_af3PcN18nDg9gD0JG0DxHPKFg60XpN5WLjRkfyR34zqT2oASaKvpkpDMaaTXlFFZItbuB9T-xXxZjkl8iYAN14TuZnzzVR6mrnSKDUoakdutE3B_y9GP34dWSjHChGa6IZ7ie1OqPFhXMhjwzYFvU78X93xBje4Ea9czEPdnBAPzq7u4FtcvorREimJal8tCxboRYA4S38yoK24_P-gBsrk_H-Xlx_TSfb-tJldKSn9N4xZ5f9r3ANeNxupQKjUJI3C20-gEet7WMHL1ywqubDbJLo5JJWijF31WlspRiurau-vU5QQGLPzM8K_1r47h4kEdrNGk2oxRPXehHoQu4QRmVLaWlcdc9xh1c9z_3AmiF_KlhPWuen7YTwbc3OJJe32ObPbaoaRQoMbM0RDzk_zRs-Pwf_NrJLyhMBZWh9D6GaDVZ2v61DYmMEY5ttfBZddcT8WtBJz091n36urQF4RKQrYatOgGYXlupVD7WI_2pWGjhwBurwUCQpZJdJQSUCP23ouyeGkB4H0azFIxFn31YLw-M4efE_5MFhNZl-UwcmUqj5QWTknGnVhYpAMBCbe4804AzxutcksWoiFzNqdzMIJn-48dRibzmF4Q-4BtE0Rgfg2Y6vz8XPneg-FKn0nVrzqoOGcKmy0g61ei1SIjMUrhDQlJhaOvb7rJO52bV7f1-APW0TzlnJJev0hLnx3GXizkY5WKNYAKK2k3_A342xVkbBoRwQBTRdY0iVmSg5oldmphMDDc7d2hMoteKnahrHem9igwYoSH-VT_Sr9RMbU97vFM0AuE69DrbFi4spQYhPCyUxyfOcUtnTlyv1JMqHGAUIygIvbqIC8JIn3f6MZD5_bxCa_rPuLbICbgk6kOPmxPi92IubeAmU3lJJGmfd__kBqP8MuSws9wqUB4rapzPFSzjfXNwlmtmYfEIDFOzM4YYutyuGEiyarUdVcMR9uokmrZq6jIlv14E47H8aiH9wG5aXc56WZfIT9Zl37SR5rtfy7Bxd6iBfR0ygumtqpuXkDdDiYz3EYnuHic2yIrPggEClPQc_yYCQYAkrBaWntMrdiUoQWXcNp49Ntg1Tkk31CgNwZJgRRF_myI-0YDAQI8vxcNU_92KfEz3fIhEz7Obp2L9JKLW9kByuFzQw06dUOIHmGUW66pT2Ffj95lbl7pZendXv13UKnAZhSwO2m6KssAu_ccenLYwr8TCNbqtbdVkMiRUX48yknIrMycaXNC2CFQWRU_TYAVmUcp0PaJNvdViz0Vs9UaQ0Rjd9i_rDGzQ4AO1WFyGdkaj8WthWgg3RNJJwmn5K-OSMKTAl0hjRXQU76hnrVgOWAQkIwE6Flt05PoXO_BZwAqKa1E8nU9Ls7xhL7qGDE0pcfWEvpv5S2xHW6-dkYqocZeD0ovmTjvNFZOkytNYDJOmHMB9ubO3wJ8rCEfNEUTT2k2nSD_ct9zVyaE90LFZUphZkHp9GUHywErlnMQDS5dwhbp0ZJTMlDMp2N0OotH040ycyO7JKgsDHoRFzX0v4cbwXpwdyn0KkmqG5wTAkdsXKkwWnUHf5uKzaKM9oQNENu2lKAYshj5HDKFdq5rLzWbLBW8Y3N3t_GzEARYlG5R5nqMpQMoEbx9jbQeEko1crm2S7Y4aNE_khUx8HG5p1ObmOuPlwliyGQeBS3KVxggMqcGccRh_smvyvSHJpgLwur6FLSr8oivgBwinpdmB6PBPOl99egvOHNdpoeifZTBsSzZw4dlc-b9DYY52IJ3jQGGPsD2YCnfbzyaaSl5Lnbjlc1xhDd4J4dCa9YDgXo5eB3mhuR1icnT0ujSy9n5sBJxMf3BMUwRD2eGYk8YCKMkczoQZ7n98RNkHAiR_6_b9qfOrg4R4hfvWE7iFvwjSH8SfamUq1nAC0WuzF6inNRUrOfN0Qk8cKnP55-h0vqJ0brr_azgCZ8LbYL78ZLx7mvn75cmNX5Zov5JJsPmR30lsJZ7on2RWwe0fq66NhIrpmtVRRDH1_XL3Sxab1uJPAXV0EHPa-_WMekGPqlQyVKzvGfZl-IN7yq_3-AraWIADG5K0kM106M7ZdUszbbZf-p-_LvVUCjdhb9jQWbpgDXP7GjOys1vIMZmagxDEz8NE7u77KRVcFHIlY-cC6rTYzJv8Hn4onb3ka1Uyl79Y0UeZwaa7VAHbqH2kFiQnFlPmu2V4n-uZa-LN2yQH6ZFt3wTsq62tUyQNo5C-MOj9kcECdRLfCm4xmx49EhVVIIA2OmFFWiZT865gLwTlfbqoLBAICosHZVg8xim8VyENVbAtYkqZfU6M6AiHR3hB5zsPPb5U98Od5qvtClqS8GLTZXyKM3VWmUyRExwnmhGKwzvGWRwWmDBr-er7e1TBNiNeAMy08Kyw-GvuoYgVkt_pCYnhhKkLDrC1MqFreGNb7IFfD-9w0_8-ABwuFWHCEtlaXnsQX1DOGU2mdn2I8Gd0pKOH1cZx6QIJFbiyljO3OrtPcBVxXYY3K9Da4uxoVL5UWLmLDlIMgN0xQOKwincRSsHvOlax2Q3dENWtCNa4sJrCdT_62xcydMBdtBruYKqib3lefqq7Yuey03ng_amSLc7RDpz9Feju_exKDdsgl2_v-QjAcXQ4dBhYToPBgsihl0B2O-NIO6GF6vh0fIr41tZK9LTtZae54CXsLsBqgaNsIOJ8kSRIcU33zXvJAfCRbZQe3HHlscgWG4RRKQrAWoonVwHG0DZWXpfH_uOCGQ7JS_4q5Gk9hAgDk5XCsYkhxvsauCCeCbdsYi-lcYmNRLxsD_9ofEeXxZZGF4xw7tsukfJOBuEQfdh1YvrV9xjkoR150iOEDtixFswQjkml1EHz28oIzVmo1TEQmhCEItweZ9p-N4B5I-okQf7pylQPjrup9bMW8yp8UB3o4WCvLEy15NIXnOyBY0cNVyp6P4TXjcvl8FN0JV604KqB5t3_wfec6gvijr0xfQFegDTq7RsRUk3XJVavHbbIuyqmNacSKMufJ6V7ZXGthY5QGopC8TOrmwzx_PmkJbVSnkXaSs45IarrjsVv2Xd80pDKcSRpUqJUbKq-Lj6XR5krt7Caxbh0IMDmtQIXjYIk4JSPkn-4KseruJnkecCFt5t9w5fsKc_ZVeg6ilSXSDViouItJYq30ZEimu32i4skBC4mb19OYjNyezXTHeQPf1IJd3KgkCluxRm9jpybcnzmtjS9-EpOJgvVB2c42169Mtfzmrr-hpscSLvy7OSUbafr-M02DxdAQkr6HTtze0sHMPqhExByI_FDzUVt8JSnwujVSzQ7MOF5UcZHq36_vpiEa_MToUexZkdqjbNHinZWuVsSU549F_uexQOY1HyxUY2FZ1okLXCKvIkNTDbfszNVlK8SAjGyRSQ420goQgu86QPGvuPKu_KuM1urUouIS0QxKhqIUUme8QNS96hU0m49cBAaxVcT8vpjz2hXrnfIBlQ8lth_Md7NE3t1CtxF7UaIu7UlUbqwor47hruq3Vn5c28xKKWE3nCoYQI5ovpQsJrtEwCNoR88kdJMGoTOJQ9-rVL-n_3zYWUoLsl4VY9vQoJoqbWKNA1iWhNsT-DrlPQH14AYfocpYgyhuF2gVISxpCVG6G1OBwy3qYHkFWjYG7BzVhZgLiu0xETmyjXhRKhShOLrcCo4wRedNMxdtseb3N8q-Ui6ETGtPbhEiKI3o2YY4HtzWLQxObyvLuH_JDzKHdJ7auolrTbXIcEBnIumklTyM2zKccus1wJ6Ei-dgq8ovJEvYVdEGJECOO4BvB8-qVeFuWit84GvU3F4E0Ar4B8m-nFgikfXrcAlR1eKlimIcl6hz1bsnPcH_gPllkq4SYa-m_KqTU22LNeEm4UNkkX280NDtNJO_GToN0Iz2wr6OmRaBwLFKclXxSPeACCImCagT3Fc6CfpttlQubBhK5cImp7TuZefyLBCKQ8MQeU72zHQ8bS-iMDu25JdqaE6D-E4taQQY-nNyNYEhOg5BdggO6klIy01pvQ_bEWL7yBkZ6GOxj72REk4ncaNN5pxV5kEH1XH5gQG_NlSACsoFGVUYfr4hbUqo9BdvWZmJWaOcch_IoN82ByGuI6cT5kyRIr_gpzcfacF5wGFHB0NJvqP15b19KUO58U8WcrsEN9YRqG0TKZkpYu9CMA1_23TnnD_4qW0iAPNYWfp0j5VE4O8dEsh6oFeap-5UbVCl07xeW_2-04fI7zNyFuCHXlt13XCDidG2_1ZHrkR8u2FjkiT1YCRR30wAVAFv-RVOjomgU8_XjkJ1ef6rkbgo8PxZPaHjuLa8WmIj_SPZenxDLrjeTGALwCTuzDn_8Hi066dGidjBcfO2iJRH1PvHNPVIuOmHApCXRgVCkCwIIdho1BG66ZzULOsSz5TCcS5yJaSbNWzn47gPKvHu-1zc7nYcJLAJwkHJtjoh2EQvqAs5-_Sl6L7LHNlr0yfsXOtF04bsfOtfYBBCiqRvF_BCcj5u2OG_d38yZBt4rl-tCNushRG7nSUdRngm7LZ7nsuzLuriTPXRcmZocZGRLlKA0Jrbre7W8u5Ng6QLRPp_k7uYJVaXVpbglDO9R2fzPahpL6t2FmGhMzrPrj7R1mQ00EPoYHnXt4eL-AMDTLnCJoDRThosrntwCM_c_F2_HTDDkGz7ttgNqSOMv_NTF80CCLHefVXLOoCsIzA_t4XVV5ycGeP3jbr_wO-8VQVbTbOaNvynVBBngVKAfbLHrryPd_BlRQm3SRsyUb61zxngC2FME0iVHH4zp-zqUUiQPNdJwxpEyaGcOno8pz0h7fbhUYBUijHvxlwf-RndiJz1KPQ_Pr0mf1wf2UHfRjHxQkhw4sERDmyGdBW1_qCNTzupGFgy2yAX7571w2W7amJtiq2CeC_36pAyjxyDUqtdFoMs96U2FxFvldGg-iAO_3NObeV0GDFt5J-yMW1f1rh9a29sQLvZURgSsjfSCVQEYzRO9SD-O686L6zvozSz_XWWbK6pxOUPLRVDZbD-Owg-qyDa933lIVELe5RwnlRQ_YvQIvISx0yr-G6YDirzTHOzfVBhPxvoA0F0l5eG-l1YJaZTMg9Lbuoh-TnPPMR_R2zNMOF4KOy7FKUWJ8GzudMOIt3pw-0DgQ2zoxngtEKDOZJCOnb78WerC5DjXs3ZjI97Daq9G58Ecewfk4uL9gfSJOBLK5iE3Pf5yW1FbcdV1akqpSrjCxnAkjylnSLFf7teJdYrT_iifu5WQnKiQGrcsMAi65ExgSJW8ceu10CvSc4QgY3UhGCDYXmvLSYMIxkiNwAME_V7g60fx9upWIol2M5KGFsStlVnjc-5qVRL5xksHtEWzbIxXL8gUf7vo2l6GlSmzLurcWAP4_9maKwa0lT6annp7EYQYA9fyEqGOgciqBsgP-xG69esGUFN5wkm7N9UXokvJCultVhJVznPNp1lF-shE6oPmaRAUT-Xd-TkeOzR_O8rwwXz66QP70WTIjpBwtwmCrVi5vlmEi8MGKSi1tIQ4DPpM3zzffwbzZZ-9W4k-_-WNkLqWa85_F_hOemN2KNeqmGNt4DvzuLCUEcvMA4p7CtF8PA9xeNLlXiF4cP_7gQBNI3v5rhjoqatVyllmkAwUNIFHA_4XKB9BS15l04RqTVPrjsUA2eECkAcGyKBZ0hN-Nt04wAdSvHMPyNPNbQZueEP0U3rQvfuZhlOHOBVOzZDUovEwhrfr6ArmAY0TrxL2jiIasgWCL6gcUooN1nklytYWbciJiOuG2u5gTz0QkOrpp-gneQftIURXyOFT3D4dqY27HF2WGvDMeJcYxWmzOrWs4aPvFHyz4yPuVU16VvGkVrp1V3PMJkUPWI_COW0_rXdzVNIkOYwvcSZ3_FznP2aOfHx6HyY7Pa9AOHnYIhg7EPiH8zxJ-71ebGlvF3hvYoN7yaw5_pKNbMlImBN70R_HDlG18_TamVA6zbjhXmQ99HiE8kl4v2CaM8C4tEEGaCfDAqY_sS25zrU_Q2i_9Z4wWs9Y33hpsIrnKOLGQVBeMXADFE2OIsTLWwrTzI5xyEsopYZISzlj52kMKafwrD2mjH4OW_no7q53jB8xFpMtjmuNi9kOJV-lDweRqPykg-GmPxaFYMZcrfW9RNqSi7gntqbh0jDtwYThzLjsZKO_CAe9_-OVNFLArkXq1196pNrEFti9jALJ7-hiz2TXDO_2xjDpbzCSQnmNxLOGGjGQzx1S9Htwx66xmOeuRuTNo2YSDSmVt7BGrL3cO3PMB9XsCgigamrkvIW5kfsIEweCd3DHOF8g9leHwn6U2tKYCYvz8_WKVGvKa-lORGmMiMNAxAiw6O8jRWKnatRhkC_vDkAw3C1E3ByffVR7a03mkQaVCl55MKFPIDn1E0LHqc99nRmpPPdYm-sbvTnV-Uj4oGNpn0Wy0YGeO83qDlvLOMuqytlOX6TyZGQk5Rp4OE3FLXd4CoE8Lf4L3gq0QtWcSleZx5Kjzod0SgoTPGI-1VcPs-fNTWGlDSwBvEyjkGXahnlf6XpwoCmlkQbsdrCEoGZ_DGNUXBNc77B5in5O7QzvRZy1wFCnL9jYbVDuzUAm_omy2F53EcK-Odj5c0cVn_Ccub9-ZRhxjW7pA86X3a6eDz6ylY8J8UpjcxiNV99r1loKj0lZtQ6haAz6Zz_XSHoRj3L-4phtrNOVRxYebFGuYN9y5qtjAGYQJKZ2aNvGe8VNDuM8QOQlBjDByeP-lT2L4nuAmnbArZkfyGBKdhmCCOs7V7F3pCsBVr3LkR6QbV6-BSKSYMRXAD2KhOpPNW8ITw5et3W04pLMmTbtmpPRYA3HTGR0oRtxvtFrgvlKfhTd908zjCrKm1C-pin8uFPJG3THznceNv_6InPkvRoWzz5cnPgZfbYAkqcjSWZCdN5lWgkNaiQ4gRS4fBX85hyaW6JzAWYZADHou3mqqMSW0N75kiYjgyYWgp6jKX8zgcJfkUnMNGzzQ-sF_coH71X7Pm9VMzxc4QSEoqKRka2jZjRagHXEJA3zaeZVsrmumDg6UZf3mwiQ_gEWyV2_qM01ObXnGqYZbn8xUbNKfsKXIU_dxORCN4FEoBMPXkTSns9LOfQs5h5QFn3cK2XuHTkRG7ZkPIcjFp7DVrRY9Fz8C_Ca0212si9zd7rLB41qhE3QnBc2iMHPufSh7DLBqLfXA_uBrzXAu7C_ZA306XLTwXbaT8m-MMnuCqdShTB_0F8Yjyfcrz9FeKPANZsp-zkVq1ug1fg87bRyGbEN0YSF5Y9VogrXkHyYNL5tppg4ELduOaxOFTizbH1ih5Rb_G-_vjfkRc8uGweqwcBJuQe3nxysN8Vh2kK1U5pMMdp_svbGTC6wj1SdAKLHHGfbVptt829BxT9-ZcoijwBtmbdXz4ITeIEcAgq9EEgau4nku-sOukxGu4Qz_MPZuio7Ql474r-EOMcPdgPCVpgRiVghlWvQKrADzLvai-5KhewjowsTmJKF8Z-7u3FDaW1BXuUofAOdJnuerlHgnaInkzGs_3IjbnQWq5Tsfg-7_hELG0_j8mrg7z6ovTCsXIT6v0n11L1wiOIK5aklan2hhiw1VaB8sJKJFO6-tK6i8voEDsvkn9rWVUh4Y20egj0wZt0CFx9wYFvWr4YfSVygIZ34Ha4u3JA-KkuQwDDMDMUHm8UjEsZmQIuIUBGfFJLqol-LkDWoYxdHABQ0jidPfgAXBjKHDzF86ERKbJssNcGXDP6PP7noxhEQ_U142DxhaukWydewERveeeBlMDhymqQLcKImntNQjeGpZPNDd1Bj3Y2TsTBaEzhcxmxqKw5GOyDAD-PlGoIJY3h5hBRVHHNw8w8coakPZy_Pp1caegLa8pkAl5C2Pk4oy6UjF5IB7M5sY951Xm4Nrl5PaEVkc8zeVq2JFWToRcARbCCmzmjmbyb8l7ys3-Mzx8EnLGPGpdFmC0ktKGwY6qQgq7LZ_TWJeHJag7If-UmnmWZkrcZ7XHluhePqbZa9stUoapcAA_Ba5GKKVEM91lK_tVx4zxfErktvp-OJ5vbcsjcxcOBnunQSzh1dMDvGwLNKlBM68VJMA30hOC2NCrb0o5vxjKolOCfUd3ms3ZYpNUKnx30VaiTbGCsvf-iJriW5A1DAU4rQJeceKAx6_fPjNAxmmAbuavYI97jllLNUh0jESe8ouiJpRDPjL5DCcTcy_HoyZSPDAklnpMTr_6sSaS78L9iUjL5yvpl8S53n_H_8I1-Z35KuNmLiOjPTC35RqNUqYy7RBgFCX5-C21G95btUZZG0N5He962jWUEDA5YS6RRlNZsYLmHsCwh9RLC4C0rU4hNbxzmxtTwPbN0_fyt9tP12gfDDLj1Cb27IeF24CHozC2N4nGmEDtao_fimryTg835pvIE8wtfFneFRB6F-xBiGnUxSUpV8EbWha1UMDRCqXIInH2qXYSuVI61_RuRyZA8zqFTJqeZosycXBj8YDBMlAavsrrt_7-OjYSSvASKWrmn0XeCO9-SxRBbYPsTvxjSKSynuPgfAAwV5HZgdbSk4mw9-Yx22xS81Ugx8SmIozf3kKLySJ0ejg_0ZRQqLdfEGpd2IJjVghDe_ZrwiPyozJaugm3NQ4GTjDwMh0SzQA9hhcOHVphmV8-rG30BVsyEBfJsNKMCwEmA6hITPnzOlNEx8XNbs3ldc4L9pMdtO8k74_8FDrTGAcFqs3ixwejJdPLC4lvmsI1Nk7YLxDPtbB8oyqezF7fEfESsPYTf8hzdorwUup5T9cQn1QFSact5wG1yPrdGagaGO-oPjJ0xdjSqQrDUhXsOvE0ThaCOvJwUxOpatSHs99L5IK7k02-YoWFdExLriZN7c9MXbBOWb4PGKUbBGXMBSKCbxbVYOlt71ZylRN7CXmuQIbw2DlOn8Swf4gDicljoFYcM7SR_SG4ZjxkGAL5nwH_s-EwP-etTc0WW03uiDD9W9yWZw2FIFaPzqOBAL1Q1_Xj6Exth2XauYcoO4AhuLD8jbq0utyRT6wjyF3voVNcrDCAyJDmQpO8t9WYoWgmUVaaJsIWBbBIm6UqjOyd0hKcM808PH21z8ppg5ab34UDrW8Ea7WcptqLWxbOijVO9_KOz0088fxxA7SJdx3M9FvL07blxBmlYjHygI172b-ZsJjqEM95C9HCmV5XEuSzwhTZn4gwrRvqJ34UZbl5RHlsWL3-uMfARhpeGul63OspvPU3tBqEYzfAuPefTcG3ygQNoH8lhQr1e8leyKMhlOnIPdk5rj-fajCyKyvPdFXYv7NsfzVnKr6Ik6EEnvIH9VpXDiZNVMrQNhLnVPlngQmiKN-tinZ9ToFRK5NTv9R-E6R98ATRdN_szoIC49612JsidFsp_iJUwfq5qF-2Bg73OrSIIO3YR-MbhL1QwESl8QmhzVSROF11ymHLe0um1vw7Lg9UxLn6XblResUMnz_JInkYlA4A2XTM4FwMC8vbTvKkWA9SfhPxydedPSgjSInmMJ0ZtLJPKrLuEzAN9rAZPFIomnwKaQziYs_oKSh2gMfAyg8FI3ohHJw4XN9UCxV0ji1_WGqXhJ-3RTe-BE2XKeYyaPDg7FxGB0viMi-CPxqPtJJubZr8Fs_-4ohPS3AkphWPo98kaSducAStiS7toZR1CkJpP7q8dUqt1ZuXP9Jyxx-emnbBstT9YOqRNLq9oYfXdaZRZH6QZOoZgo6wGbl3hXy0ayuqkjuqTmsS4M2XCVBGWDbqGgx1-7yHDrwu6K9rKQE2WDXOC7FbVskNPceWIuzKxzDUpWAGMiRbVXV0OuABebN9slwgBOvoCZXdSH2dewIUVQ3hsNMu_2IJZ0sNQdrMD5cwlFaGFIASW-iVO5xViHwQdRF4jBgKraVA1YzSqNwEE9nWg1tBBkWbgWtzzSkFrcwZXtjetsPeKcDXASl5fbUdIc8k0EGPjwj5CTY3VRY45lbVrPCACELVYJG0HwUbohmYyZNED0Tn3KngziF14BKjKRZmwLR-1BBGR3ISdT1WBPNu37b_iH7ppBp9k2TG4YIi76A_mGvo78VsqSUjld3FkXchG0IPs2bEvz0OS86nbHYNx_56Trtv6iuytmzwoKu7guDeoJIUWyT0NtlIulfVLiz1xxl2o8OiVTRzz_14_cJlgdIlPiGc_7uqTMQUueeLp30Fss403Gy_2mRGNJl70EBeukMqQdjCiFA44NDig3gBCjKHojoctDCcxQKZUjedu1toLsHwo_1bcBIW5wbpWg9LxHmm-Do_6Z3IEi0BC6DZjQK_LNmMykGUo-w02OXoR0oV-0_FIeptgaFJwIsdae_I0liJOq3-YXpy4yNaJ0C-3Hobpj3WPjjgO-7BHVZ_b2GukslbWfL3Mtgvy7qN4xpucz_aLvTGOf9KdZGoYhFSHEzQqIfoO8q8XdThQYHnuPgjx_o8gvpoxKzE1hj8lDY-YKnb6wLiZGP6vEz5-pogtqYviRXbNoazmdxP3OY8xoa24eUUIOpLmt11aBbzqfDTT70ljdDc72JeBEJYegqQIyozvYg4yQ5euFCVY1L5-AvmZ9vI87xN04sdagqCPdUIW5PdU6cEIUsToHreitTkJPxD6L0Qvp1W9H2u3Dfxegkj7DMXylY_4HLdduX0A6pkRVag0qdteG_B0n-2Vze5Yw-l_t4BGVLaTFa2ETDYOMGzPdy-2VoBvWNSxwnRTV6A5tzXhjmv4uYlLAVsgY3awdt4talFKMYM0_LpkuOCtuLv2d3lhK_u02AUPRz31SUoEejiyn41iZ0CPU0eRwe3WIHn7GxaEBublV4o-Keh-df5eZOzW1C1yV_3kAiH56HabGCJb81xk8qukdpuv_md1ccWdff_lRBag5eZKp8KoSm8xqt_WJ1VFfGDnrYpENSz5BIKrUZedA5u7pdBiw8k3kl0SLlpjdhbT3Mr-s1Zh0ArhuHoBu9b5lFp0beo1_9TmL1pxOP9p6VaZ6Q2kgsw5jkr7fbM6y27T65NLQ4SfTm3po3n1LWq8zM8D8OqJ-xXQvvRShhrJfWyJwefMB4slr03LEsg6dN6bpzVdCmslqKuLolPlKoUlI0goqQDfAArawgTiTP40kFumC183aTXvoRftcQYht9bs82GA0wdp6SgrSoGPuyzpXooLz2sphFuaIqsz1wmBxPOyw9ModBn4fERQGTm9IZixQBzWYqnISorCkACd9IqxzzJtrMh2AKNmFHbrCJC1STYlLD0QeaFYZKgx8IASs3sdIOiJ3WJo7JmwmFwS93gDkqaDRoHZDoETvdL-zYMj9msacTNY811htdXQxsHHlt0gbF0nb_8j9EsSzfLE1wWTwcfsapbSifjMYSXIugLl16dwO5iqMDFGlVJiIlRenFyaPJJKJzYiHEK__COaHeWzjwkETpozVMcjimcze2qU4ewwG6iIPTk6fNKo5B5b0ItHFUDT290Oxa3AVQHUmw2r-Ot38zQspJfjxQ8izjQ5vRnVJFLyCU5jdnpF-bbrh2xPItxhnJy_f7LQL_VYJD_pHnvX_iP5R_a-XZ98UO5iegNKRrxFMZSzy2VtIF6ka-pBYRuB0nSepsJJ5NvuNElhdcAg8DNNBvCYnuKdEJAu4JtzXwPa0otl63yronZBewLb8Go23k0B_VhVC9G-xaIsjx6WrALScMd0lEPSAf3D--MY2JqmF5UDNjKVZ1xv5XoTgu2yjELf8N2se_4YV9Sw2HqVSlUV2H-KGf-DVKSxAn-KgwsEv1GQUKwmrwf40nAGSObKfcnTV4kS7bXq2af6DF6z8tgElUPgyULCGzfIsAgaaprZrwI0DJSJIb1P_kN1vuPN_oTOtIVGKeTs1BP1K8JxH673HTB3qNwhm4ZsqwhGYBZ7IGrzlRwmk3AeQ0SVf826bu4MmlzDfcEh2MLvvBQgDT0cdlhTircS5tCdvAKqB7JZA8HvG3eU_mYZMPn5k4S12ds_sczlobMbc0lPYT7h9CzKG5yIdQSJy5IzSo2nwFzQdi2Y9v7jUkJwturuXRoZZ1fBBYP7SCoOGAG5BoIw7QBRif2pxzbXwJeasxDmwd-ya7yQqq5_9XlDaeOrLCqvvMuxFW5HvfapC4PXxXm2Sxt7v0abBtnqMZnF6GDtzeLY0u-tWFE33zmjqWtJDUne1SMc0luHRRYbjEOqtehlUj5j9AicJ5lv2DESamoHOzrw-bmA98UL7qlhm4AQRxn8wd5BySf_Ev82WbRdeG1ir6HoMwmo8JF-qNh5k3KMywmWSP6t4cXxsr3IA7RYfzSyNemj8bXXSaoPXp0_gUrLiSBnNPSDDhWSWJZtxpLyq6yBurb4nPIENETE1FN_nxj-etc9fQFvYh5GVoUxX5rl6KFWOgHWkstMsYY6VlaaA37cFNB8bDH1mBnZ5Z9RyieIp5ts1EF_ho_m_IgAFYaLxEIXnODMEesv_uGJQRpboMyapdCcNI3eZDQlNVM7xacrv6uwmeHfj8O1YvlJFnFfY-ViSXOH-DTPWwG9A3R6oYVJuLHkGHSc-GMk79nitgmkxaWjs2yqoJcsGZdFpYuWzaKSFarLFQkq0WqFoWsCygxglBlEYRsBsp1an41NghKp45nAjJzR8jS2U09YYcvosbj7VWRPxVs7HUvmuT1KIJApxTi8mf1Pd1S0SIqjTKDStNVxlKW9YDYmQ-aAstTiShYaRhRAG8_xsJtj0GZtryZxNwbHNXsoKV0NkvmehfE8xFCujWsIf2bWN5fKCvkvM8TjpuIwGD3UJmLj5U6M5f6-7k2umxslI4nbbwfuqQnNDmq9mIzA5HJYkUsrhmjJTLNNsobWP6AYLZjW9w4UCno9v7Skxim0yGAG8zAoQj44vb9TdMK-moHxRlutxczGJfAhpEdKUwqiZ6kIr4GhQgTv-3TaRWteZINGqyFzBargAA-v3sIGssxwsMlSaa0L7TFOLmdk5qPe84ISJKkm98Nrik-2R43WnusabmoAEe5ZWj5p3HI2A8-Ls2AHojh5t5I8rMUksn3aM3TdaUSmSaiIHdlQaR1ePvNxp22OodyfcyvUBqIUPQ6pAppqeSmsG9lZzD4TR4AZnDO3sdAX-HkjerFxAKcnwy6gVK3_LOMbK9nZU1ABKbbOK6a_6x2DmssJGF0pzLCUlpsqKxj08oC78es_Eku9XEPaCTIwaLlpKqg6It5pdJcx7OQH61ZjsZ1nHUv04qO5X98bwfTM7xr4UsmDdVhE5bvCV_CPG4K3A8tr4gLYPqxAwm38-Fxz5m1UtvpCdzpYlomshXcVYHxFJIYdpBddvYwC666wMwMnLX0YQh1k8TdB44AgmBr2Vhs02XQOWeGKSU8v1cyFyU15-mjHqLM-zXP9Vtp7W9eh1TMi2vNQY_auJTxKnPUQK39NbWq-QrBAho4aq9U6cv5_AJVDOwRWVizchRVTfAh0pplCamNGWdPbDq8wMeCgHw6Kxkc084koxbGPAZr5eQCl2f9G6e-VjHRcRwNe0t1QJ099ciNItNEp_gK0ekpgUD9YUb9sYaLFAwkGdLzBYattT5UOF08rnnPlXImZQ-8HtI6BgPFZq8CMENUkEFgp-W1vazPgzZLMQCkXWypGBw2rcp79umSAVeb7Cy27wuI1ZAROpuyOu0nfofYYz5083HBTNj8Gipv_sncJuWawuMS_mYcIL5NwsExRa2ZPKs5k900MOiOTVeKrYRJrBDlH6GEmJeWx6dY67h209qw7gK_hJqlIaAD8WpjU0F4xVM_Oxs9pfHyJyODRfXrdeynDQbbG8TYeqZ2f2iFptvcQFSV0xKTnKWRBi_aUOvmU2PpFRy5LP0U7IoFoIIV8_CYE7ghxt-jFJkgSVC_95imMIxrBbwVQ2vU13DkQm8TYlt-FfWdjkO60RM5xYnwA5y5zKGeWfu6XjiLHltjQqzCnX52HWXYXuXUWHHA_TzoC-5h_fTpxkiONVEV-F_uT9_VjKQF7PEPKcxfPlTyYGd91x6NL4JjrGE3Tmi3d6yA1szSe5_Gf-J7iZGDR40GQ6jSwW5xn8eTwDQ2sPyohZA0sWb8Ima7HM_mPrPL0lYGXz0ibNeaF3Hw0BHKYEsW6vzjxpgYyQlXMAe-ERtcgtrvZKVubllVcU4uNHSMqFdz8udAvzMi-J7uyi80qOyuhgF806S3ig7lDPdgNWw-SxZ9KTDnt2ky7-wclUYMupG0uvNj9UBeQYL8036MLqNStjVN48packnKJqO_wFbkFXGyN-S_BAlktaPEhYgt8VbitMvCBEmO5dbZLrQ7mJ3LUxkeHsrgjR_7PK4tbVpRx8W5evJUBzacN6PD6tyCnAsmuOrl0y-abYEt3RMeZCxc528KC_RJAcjj1CgDfmSScaJahTMT2JZ-IKBQ3guHOHPGCdlYd7hrzM9EVNxOwc3EcilrmlYX7g119m8tKq3HHntg6E01dAu2BInI-wLqkB4mo_Yr2VU1snng8w-wiXP1IqYrLphEOaXaNgSqcZGeb9P-M7U_5AXBmJWjuOPFG7xUEg_4KOK1kmfPuxDeKorU1y6q-qns9O7yuZ8rSNk0BBU6ASiqMHVpbhX7KaIwV6EidoyxXX2eYqA1bSo2Y3SY0ExMJtqNZ-J849yfIZU3OQQnXPITAvni_CUeJmK3BKnxq-_TZen4N5ecpdxc5uaFgLocB5Wm5ocL1Z4P-wpWWY6QHddFMLKpZADi_XPGJgrqN9Mx1zLR793FYC9snIMLnuXlkelnMewhwPb-zHl-brX3AQsyyIcg5goGNBxa3nKTKgyzQi7mbTYt-gnxbD8E4YPQZaLz6MKNglTgs4-4MdbHr4idz37SURKi4LOlJmiiPaG-fF9o5p8nE-5xd5uIGPtqr53SEvOiW6yjl_OqaeQJtVP5861D5IOaRiZWEIH0bjyUFjxo3zv61ia556uL4uG0UxIKMFUDQRUJPfqmiIxSNw2B8BlPajSf1uJUY0p8kZmXgVAtlKBTtNIXyLzbjksgV5KpFOTck0rV7dgxmiWcynrMlqSIk6AJ46aKft1LX-ZI_54idtxNhiRhVc14SHErepdjXmn_Dgm6cZzp4UAlxROoJiCXv1m3k5PkHJtyayCV9ItlOZjp2TmzFSecJHcD2wgaWuiSTY6EfMgtpIccpvdF-u4MzE4kw9-eZ0zGkQ7TdgyNFth0VcJDT-x4Q0sdN770sXeHlSv-P4ELRiG3SYaL8L5GctnxQ3pcASf2p8_UPMd3hACUP-tOWnHE0c3OLUCveBRhjW5D-u9VBgUeJ-kyRdIFOSLxmC7OVKR1J158zeBGIMq4ib9rCVKASgWDBSCiB4NLeVDJXpWo9ukksqNxtHLE8dJyFduRQuroNTfoNBg8kDc6nFaCeWF--X9gTQ0CUZyUYbE9vxJPTVTNHmt7vSEPN9ht6j0jtso29s884NE0Gyeg7yzfakDQauaEtaYZcmnhhrsxMeoDFdF_1FADs_yHUyMTagiTKUZChmoZ56XFykc0ztjPU2N9rQggKBdhKfvVFNECzNk72dNdchVqIIwTQPMyMqQL2tkSjIzPcT745JyKt6PAe8wVU8lKjKbO1757CV7v9ehzgp0hFNmlylbkExWNOclBnPNU4zXvCSFNpKbbhii3OaZQQ43YhYqzTbjCsA4GOq0ubqfV5-doX59BMJUB1df9lrL9SkVCnaHDoU61klLNJEbdB3mesM1uxb5ZF1KSaRC6Z7oLZhcQ4hETkd1tFgoCkqoSQ62IV3S0PIjpyS1az-2t-H8yVeCPioyWSiw8QbzXR1NkQ9HErzlVgWua5aztk3slfbUITanr03UCdR3_idHLz1YGm2COYFoFhkaJnY4Rh0CLkHjWqGA6WHPXkZWd9hzOD6Cc-6W694DbYy3B5140UOnYPt5SCQ_3FsEpo00ohKRl_2pfNrJBPfIzCi6NR13l4489knh-CIlN6t66MateAao3s27sDuLDwRMKAMCwps9Jcbrg6mVlBGuIJUwdeLP2ZKrPOww8KzaUeBWZtC5lwpUAayW8MfL_BMChcCrAhj4oeJwMYxioAZdq7VFppFuKCuATMC6hFGAOjze3CIKQgv9x0v8BOUOEKbtYWIXzUgopF93Fcxz9D-8to6jprAT9v2gXBkSg4g4bIqQsiNwpJaEtwdOoRAU9nDG8RCWCaUuqPaXTxLJ4TtNQ7PZVYXAV9QanbgeezbjZWC4rpSx4C5B3P54CA2zWHXfTInxqx1u4_sL3ea35QJvK6T7lzxY0frz0i-u_CLhZiZZNFl7RH3PXP7FJi0LnehjlKaLni2CSjqpF5VQD9grJCEEqp0IBBY77ChchCneRP0RQJei4twWtEDozLwsCU_3Mf7VnyFQ40rqdwDWBY4U3Ngm5qOOOtLITQu_r1gzjH3L0rUXOa5uqFvKPfR5K1VsTFrjobNQWlc1X24DzhXfsL4stkhWy8rjUEnFUNvYv4P9o3YP_TN85Qv2AXZwHkUxvmCeg-6JxKlIxqR4S8KzSCWVC_qNYbMvD1faaQ1wPNUuWa0XFscdwkaBvDpuxGxkiRMIWGsHZXImuW4UE65AqQdGzRcSOTDJjAmpkmcsx0HghyAnkFZDB44r8vUNj35IYStLWHlJR0yepIyqatGgQJ4pU3rFw6kpMYSM7AkMszTn-jnr2RMrjFIyS6Krrj-AxavLyaKwUlCt4fMLQBufikzkQymlElCpbXd6cqXantptgYJ059l93KeDyPkYD0vIsnwHam0KYidSziGus0PtEtUL8MbzBCYFM_-3V55ciSYgDWnhGYSOklnGrioIOvbUG5QczGjV3271sGMU8Fp59h_g1JwnLPz7uh3txAou1FSgdkvP4L_a7qNcc32S3r0AjTxLtUGK-NgszlH9wKZHCnuM6PBxXwxHXKSytAvHRB_S6nQ8-BrpHBRqUz9GEUElLN0be_8oWq3hWnI9NsEql9XLNu3ILKM_iwav5F_Pef3Vxh1uzFxsID89A3aUxFu4kJXmDXrs-qZxxkGP8Nqhw4WOEOmFm5NTPgzQ3hBWqK5ny4HVuZ_JXNKTW7e92WBE8PyFyTyDfNv5_KcZG8EWCrKwDYjDEmhNj3TFbz0XP5jOTu91fk-B3mAc9-wamSaqht22jNbO02gdvP-V5w2w2jQAokqTPPGyp_-VeKPp3b5voRsfoi5DPg44uDLZ_h3yUJlE8HAnIbHgE51jilfr96cbPnlInY6Uo39zqgPwrJGNy-qAxSyVZcSV-Gi25vliV2n6CKLasPi1fOGhCJceuzbLuzNerQN26pP4zftYbECTgp3Pndd9RTR61VAHCkvuZ9O4h5Q965UlTU34WhdZUsFgbLptCBI94GxrIJ-KhbTBhNqd3o2u3zrglN0UvYEwC7JhVkckb7tg5tg9xIgp2-OUE678KwxQOqM8_5WbXOAb8QnweR-LCGxs4TbaUgvdd9Dqms8ydHdp8yu-rtqgIS_Hj0LNQwWagCMPDAdigDy0aG9Jb64r3I_T57CTFdQT2eXD1F-tdPLD4MPEIysgB_aEAk43K8GFp5VhnGYR_4ezxztuqilZFkgjICHOAlyyF5uYZ3UNuxJGStCXKiICNaCPW-gD78oAqJYi73iCfMa1t_Fpj9RzRMttYUQl_gqpueQzzmsR29V9v8_V2mbICBR8tymqSdZO9RovwUJ9RqXwBDHQpBt4xSh-3DHfiNWMKjogz-nnruOS__hk4Mz9-fSUL34u1_0xwEzRp55oRomy7IRjqlUumXrtimMwQH-WtZuYe2LgJHFOZLxm8uFS-vpE8gz9lKeyKQFXW5b0Qo6hsMQmwJcuN--z2lkpjo0OjVoke_nOWJ-jFewahBWANrUTAh2dkunpwBWKbUddjiS_Qb7jwV4ISqE47RMfvWyJJMMFaavkNBqblKzQbvU4NmihQZYHDpCOsS4B-ru2pKlgxYCMlFx79ZyuKjHvhRr0R522kpdATIvhjiNr5R3pK1cBGjuCSxC7UEuYOykqLgzziFubpJ--XbDTDc_pXZH9Qowklv_8pZW419uJj20boNE8RbJFNt9Cpcxeite1ChJci9SQWL6GEnx8oBeBwzMsM_vi_9ssMv-vw7E1lptk4zWtUEaeiX0Pjhc35yn614RU0BpOpw_9e3y0woGIx2HrOhx3wWLOmYz2woqHJXyBdSS6NUbBmTx9vi7T5wXVM2o0XCB1ZwNj1Vw1bP_abQa2ekRDb8xYNRAYmPPlCljtzd2ybl_ZXlVG4U6tbXVSKoAfAANBCwFSasRlc-T6SvR5iSp7j591Kq_LgJgtfEO05AF9hBESZzKUssF_K0YGbx3XaE_VobJuVfVD-nbuxCRYIHmP9VwsbFUPvyfriPWLNvqV8uuBVMJhVBo9JRxUEadt2ZfduLdp_9z-PZPSblLtjajKch88zh9tgXd5NRmZFZTsJzWG9jvVb6p788hN_va4E6BpsqEhi2NxEIw3PBl9TdyRLH4bYhOihjSE38occG8yvsDEmTiSCo7Wu0JqF1tkFVZMSSzZD_M3yCRGyVEa3d31bh2Q-KRB7UkqC3wcwcAgyLs9-B7ERTk8Q10S0PVaeZFBTrRMBTZWLFRRDRe9LNFpwPqysPed5qyUmja7BEh5WobTejGh8MahJnKgMIHIJ-dgm6jgolzgdz5K12bvtYCJ62C09Ddb2A5rFBZvV5PNp35TwVdbt_y0vVX7DRdlR37PjYzx2gCPpfICR-Yk6gwVtgAIAkL-X1BBvBy-45eU8SkwowszYvD5lW9WP2VINwptQrnwYmFJ-cx8FrjbupeDPSK4C-i5HlGzWP0FXiunLsumOxDZ17pvy_9XFLLvcIGvk_8NUG2GsNk_YodvKZ83w9ulzi5Jm3zlLre_DRKcRcRMw7cjFL0fNYgUWjLddqC2gf7DgOYhwCdYCmWP-MCBC9p3WGVmz2IwBss1crjmNJnlpb4Rycn5waxHJUO1gi_Y1eEn-luuYW6QKT3-bOlMZyMSTS3MJA9gdiL8zrw123UbxfZlj8YtMVuBRjXAHJWFOlHYlwIwKBmM6nayZyf21ufQ0ZlXI_i0EWk9v-tv46VBfx1MxTV1hm1GorPb_EUsqgRcpzA9CL7nP3ZayFO8rw5ZisztA_dw2R9VIwHR1IAeZaAkIXAYR2hoijb1aA4tlrwGutgk5_0BiG7K1S-xCHnBvMxIL8AU6DzV5jyPMbQdfbfHpzZu_387mlPxeeLAUoByT35oSF0oB0YKt8AwM6ysRjljOLPTi8E1HSGsugE6GSfqxkdEaRv09OML7LOwW1Snh-7r3Cr7iW8FsuCtqiXSJxuKIZuJcP8KVGW7Bra9X-32_nPe2KILk9geyCcH6Nd8NLUJLCeAB1ocxPRAKF3r6fBvR0NlPPt-j8EPI06UFm-cNEDt5iIyBPY9VaCQGHneKeUsTD2BV6TLSpX85d2G0M8A3QATbwkFQTWAGUIr1d30R78Ei_oWJC1SxCfnZdthXd1azi9ReYeq6VJwoQOFDz0MiZJlre710aQ9jLPYCrny5wnrUzWWj71KoY2jcZ3gJMeGt-escrIWw_tr92SkmO7J3e7XHxOBCem4KFtwHsaT8CY9SDFa3N051jTyTvpet38eXEIKKCBcus_zEPFtkD599Aeru_quaqhQwfW-M_qowvVUwgccdi-dJvZYcls1I2-SDckegX6QQGl4WfXkEnIngJFmIArULgtinIzWmdq_Yu0sXiSmUAhDra4qpxPNeFZN8an0AEcp-8Qmr0U6gNV5DGtzDyfflOSY8ELsNzxKUNG3HRVQqehrt8qDHlo8HzMz6mATqh_Gno60LcV_z0SycBUxA8A2reESXw5UdySlFnDF9QVH_l6RxiF1Qm465jKWiC9jnHLj4PJOZstcNZQRp1Pk6-_knwrRixHd8lNkeSf8J-ydrYphxI4ANMlThrtv-HJI2eNy9GknfazJdmkjiQMrFaqhMU3bD50fjzYJ6WRODWG8wZotBstImnC9TwYre-QxziIqJqBM7jUbRqmpDhtvcaaVs-S8wjTLH8YuG6mLgByCxPuyiJXN-SPCe0cvLLth9ZL1je6qiFBbLqNWFjkJGyhu_vTDlIKcwzxnJy7lQWzRp03Q2JUoRR_aWk5cKeNZZK9RLlcPya1X4wv-C806EQpeJxzzCZkOks0n43dxNtsIrcczqTkzygCv3xmsE9C2p5fiBxttfR0_ovDkHZD-sQ-ZDqH8QNg8zJmz1Bd1B2w5RTO5bSC0GrOoE49XkCJnBAtLOUhkbmRcWfiUXYLBKmDgwuGsPtQdV0xghMX1xWbNZxG448aH2Yd4le9YPKxylKRVrnM7AsFniFrOA0OflUDa7fUzPjzJjklcsKYaYEJfid_PmAJuUx10rNlHCqQasgvwtH7skanMWo4azqmGSDy-XDia_oUCdcFUkd07gmrrA11hcJ7crK1c9sxp6jLFLWxKIsQ4e2sgw9mnTequNx8-1HtkjTVyiUaZmWMxaTYasMAfj0JaKt96MuGVT255xBQCPDMnmoJAG6zNOoqXxkv6KaWxxKbwUikzyt4QIcvbwAIe7CNAjJ-rCpz4b2ATfm5rlhilqyOFxxKonw86NldUYShQwv4X3HoEOb3l29a19x04Uttn3xoeik2RB8SlYOMwtLD5dmGdUC9lCVr6ByxiHpDQjH2V3irCMlQ5AiBzWampo5DSMv9T7aO_qm2Vw7i1GWY2X05U8NAIjEu6x3gELK4MANJ3AdpfiqLvA6if_ylEoWwIl1ay3pUS613qPl-2V1B0qGr6SPrSQjlcMxufNU1vZepYqbRn42ESw9fDZW1pUY9y60Ad7Vdp7YKGvMqAnizziM9sWc9_y2NrEcdkXRUHterKOAwRm4yZE1hfh5fG9d1FvG6Jy300rylqlCYR9NLtUVXVETEFXXTsjnFgS-Y7gcf5jC1oJSfeLQKlxH3jYd9yV_Y7EjoxXaNNsJPrzeIyl7T5LGhoAh7f4Kzew9ZunwCuPXKA6TOamgxvqTg034MrjbULWx8_HLnIIiGz3A55emTmJVIKnX4ugO3akNxu6mUxdx7vE8-vNg4DZUePk7bDiTP-SHw4486TnfVbXceTJ--H0UfHoM_KcEe7ji3NvSGOGzrNcx0lay4cpuKjzQRvhQ8vFzhGrmIiuT512MK-y_2jm25ao3ZYVEvo4wwWoJFFzDhliqN3H1sm-7BBdqL_VokAECQly1UCx411UiDs1--FIOxFnxloPTQKGV1E-QCq8_6MglqbCEZv7IOJC0BewrUrl58GlGia-uYXsM5AiwUWNhCgPeCmJyD_SrGsaqmqPHIdzs1ZzgnGSrU0AeJWZTMxaB76ext6JzRL22OeyFHYOEeMZ9ZoevKDOtE74MOyOs2RDFU35rzc75-iWy71179i6tKvusDdxYc1TxpJlqXmCKI5swByPCvORo7caGQit6WCFn4145pnJWuKVDf0cHgW8qLy6B8i3qzVMJvFNo-YEX5Ojp7JwxYDS133ZbSLqqgXVzi3juSgeFXHvUkIt2wbxvWCPvFLPOAR0wECSUDNteEJ6q2Ng8ni3x-4L5nRwECHyTfWlzsmL8slmm9TE7XGfrHuSPc80vGyXU3DLd9BiZLQihCE_Lesh7ODnrZg5pk6q-5r5b0ppKvwUtn6dP9qPUqCgK-oZMGAak4EwLMqHyw72ZNga6G1HjDmBeibUBBKl3UPMUw6x-MNWzYddYRMYb7_IqKGCH8a2X78TAWgVuAU41lRW-jljrM5VTvObCssqr-OELqVfaZNa35JqH7X0XnvjvN_dImb2yy117JPR8I8yzP-SpYI0cg0Q5SHfsbY4adNmEijzwpKlXJ8ZFUhoIzyqaH-yVpGvLRbe7lRetfBE90pdo2NzdcNWHnLJPnvarxQU0RNgnEB3ogDGoCOLgHQSItBSbtF8hj_gSgcFcBFj4YOJoRVHuf1ohVOtFr-veMmYVX99x3jkTbP1atviAIadxKvLTlCKUbNPhTjw-IRsZxFxYFm6jNvd2CTzPC8hQAgJb-DIocRxkIaNCM6n6PPMfVkf303t8fKXxEZY6SZVY4fNyxrBoM5NewyW1sMwDawrljIV_LKj6whQS92EmwG6Iu_c5CCgMgfGmBf6AHI7Zw07aeNSDL6D3SKb26IVrQzCTFBKtcqxrtfGds4y411m4-03Hq7gF_o1Y98Em7Iyx37Y2dld_1HaG6zEjcRx00nEkUjKUfO3uIAIVOV8CAGPOkK1mlJ4HOJV41ihoFzv2wONT83-px1EHsr_zZjTzDuWpAw_oNucARdGnhJA8wSk05WfeqxHFeIkxerk1EHlw3Ud5PKIQFAsIJyCmY_vm3lwBzQ1XLBTKPyYD3h_vzhmXxGtmfE8NjloPSPgs4CEBiXEnnq9biwVIs9F4M6uH3aaMjF84yeFxEPSvjZwKTICn7nDu-aVTecwPyzRSJp0TeioG9c44V0FZMQPXA1PwcNNjL30eItqBY23QYk_P8l6K_r8E_z8RXwdaooXfBWoXu1NKBFruIuC4XFYaWOtjptkBFCShEMZ5dDqM5RfuNyFjdwLdQ7VY3hpDtMSESC0yxcnvHKcfb7u5M213r7xaj_nz7OihNDDxmCzXuL-2M441-s0HLOEpQwA5_oeD4EFAg0slZTkKN3n0heaEPfToVhJHykVlK5gmdF1uTPKmmJ88YTAEBpSk7c6zJj0j8PTTkoAmttNBBo2zxuhBNcpSFMgRHHRSRH4R7L_2udJG55xFG5iZxtYWJ8wiUDDEQ3QCXg1gpTbZ6NGI2OK6_5BWuSxQpJYh5tC_sbd7iS8BjxbuxLvWDZoNVYvDW28_qsbVDdygYatxrlPEvGz0yqEvUA2JdJHK3_IeGMYm1nVbxI8eRhwK-ZsU4UIVHSosybatSNrHfXULSJAIm9xRqHdDP2jblRC05xqnOmxl_3qGiWFgW2252-zGrT8ajqE212NeX9JKp9DmTctNyPKkNF_ZEZ1-urP3oC6tGC7PwsXJ0ulP7V17jPOC4wQMNg1OcdFKoVwzD6XYdKpskdqMgr_hz1NgQ2Q5yV5bmNr_FVpJXcGY7iD0vDjLXj20kcewoHcqimBqXcV6Vp9CZKEmaLZxevBWWGMB6PhMXdjxz5HQBjhh9W6t1hchYCb35tLSi5bcXgQhj0moAAgTMmTUHr0KYdoYXlS3_GXewXmBrQI8m8Q3d7p_uDob5qX_-DEzTXzs7nXfgbS5wLvUnHeaVNqTlYRMzrRS_HsqDeYQ3oPZJZvhzVf0XWDF28FzBC8VMVrfK3nVtcufghpSjgLLl5_MSSX0bofBBw82uutJU7ipuuiy4l07m6iyWDW4ecSb5cw7LJ2M3ZNcSkD1mKuUD8gBWNg9mawx808mprnJv0Y-mouqiD9ySIKF4BU-JLc7V7EV1gvbA-XL7hZG97AXonGUAsr_gEGpXF2rrOz3zG33QX0F_TQuiSQzv9o0FG0hxMb5fGBvimq4vJazFHs-wSSMeY-RjI-EKDHjRIGm7xLESO8dgixIiClY6sFkI0qp0IKP6L2S3DfCFVz9xs1MCGDgHDt-QUPqoJQJioICLjq9908f6fGbDjIOgXUfbdoBPp0EPpiGcsSalNYroe6_R8kk-iYOD8lkgAZKgxSk1NtpDaLzYXVW1jFmBPSC-wx1ulreJpK_dQOijJ1ZF4BEQNlQZE-wzvDPhYSQF64wmlp4diL7xnt3IiiUsMUIImrVVOytZ_JTpfaHHTK4IYR1xAr2cGduNtSwCz76VCKncozcGCzBktzSU42en2DZrwEUQBs4djEgD-uU7dnsCI13B4aHC5pGZ8uouWBbISRzzUcJvEVj74ZCpH2YkDjGuk_6LDe0PUU4W4OR7gLudFr9nYc692nHjfm-dZSDWPWJiKJZk3Lv1ghbrDNKKO0fVyVimkcsgBZFt1vAc15uw2ullvJjFB6PaNpGxzBfPNUxNtQ7D48BgmmydEE8CB0obQsy5lBz7Nf-oyA8TdXA-FQklXsTId_qzU2nEiJZV_Lcrf7T9lLgRLKDkkBIDE9KNSAlGLYIONA7GURRIzxP72jb8xXMn017HNj5Pkb2OKViub2liwZENpIWAmRM5Gd3l_G02MtPj-rei-WfJyiS7Z3wIdqTFF6OSEs6eIwJvU5n_G441YtJHFUwTEzIeTuxe3Sey9HDkdBoMR3u3TEbxQHuFEX5rtu4mfnSGYq6Er3hwgiqtQKln9OOANc8C1pQU6RdWX49lirBkaWmtVXvyklLWtvzC9VW2Mr_H1zBbLicUMiOhnOcpKqNwgYwpqFowfN7rvCd9RiXWJGTPgGY4be4c7GmzVIXs-knj_5mqWcQ0m6QZMWvU3Fswm7QxoGxu8ZFZwfJzMsw0XVbP3rQFHrehXAA_fJ-Jj23RsBM4RsT_ntWyCp8HYJcJI0f6WdKMpRsUHOCFGzZpdRGm2KE5L0sncqxGfarTeDth8KK4gDRHkiL46BlAdETKrt1BZ89ouTFuuF-ap2Zg6moXekpNQT0QP79cZHzz9YEH0-eKU4_TXv-XP05z2J3Sq4Z1h9jYcaDvMH-TPjZs9bBelwwbTy4t5CzBve8D5EuZmieJy45jxsdxBUTzdJizz7A6fUyg6BNcSM_Ao_s-hxqqZCBAjTuWwzrLHtPeC4UjqUBmYrdiHZjgQoIronee4vh3j-FvnysVJ2mMBjJS4yM9-YbrDAP0FDlgulIXjiZWEUCZOxr3VNyvonDlWXx0_SuTXTE4V_ykam9jOtA1_jMia4QWP5VZDmpaiV6ROJ0KEFKO4aRMM1wEP2muQUPIdtiPTAVgccaZE8JuDivkbTaW_8FQvdTtp5r-VMcMdwxeaCwFN7OIkV1UY-2XbLy_7mEVfEiRoPn1NG3W5_t9dN9wqf8h4bNAK03qJlPfQq74SRS3qboFmMxCMNTujgP-rVt3DANenL4hPDX1r48j_7Pvoni8KIaEj9016odc-riawgpSrL8Ia0P0S7vhLdiSh6QhLzP2rDkB3WpoFb6Wmq16UJ0K7j-3tF1dC4A50lt_kwZuQCsnUldQdOznMsEYg3-_wRaRHlyd5QH6pvhGQQDRFWVIVhAHwdOrHATTtTtQ0wWeDbpnryWm3J7atJwA9KSoKVlGUi84Qij99I1PCrrhjzCoKmtPma0qjQxGQMHxXp6ep2E846Je1M7-dzB01meA_OTdVI8JtXiJC3aNzVJlkUtLeoO6Rmq3w-oVmCQaoRNWTbf1YM47b-QyiUzVq85CTi8TQydwBP3_1JD4HBMyLUBlu83a87xYr4z2xmO-WkTvHuib_HHKz3x9kNtUwSUQj4Hd2hyBTDQI850ei44po5rUbRUgNRHIO-Wu2e-sGuHKdv7yldbMjHGsvLzwUIRBnBvNcbfhtHGdYbPzbEBU1oPSvKAcSmGoHDaxXAQ5Ec_QWwaKd-NN5-yNS4_hZ89_r1g0tu8E5nXWxxvyhYBXIc700gCFRw5y17Ff_n5H1fDrQEScctlLeV7pFSZO1siQ5zgrR0ZhqXZBQKykr804tUuqoWAISK2ywP9mhixCdblTz8tnfgYsUqfZKxHwwGprkX8EN2iriWNQvDlHpL0VO_TJKZqNADhqCYzAbEYBHRDc347j5uaUMWRAhyif8CxMLse0tqdCUGF_3avqC62OZxIImAV1nGAomkjaBlmSDVzvWtAru61WsgFTqtX2pNjOhP_RcTLU2PzYqj_BYGMZzISq5I9dEGuq81cYxKaJKI1E0skXQlGZ5iuT4TgyeAH_KyV-ZmD4H36ntHA4MDV0BtIX6P7wBr2P3NDcim6NSotUW107Ih5pg2g16GtCs6NUteSMvKOdaPI158EWj6iGWvBagJAynP9NTJUelC5GSjajxPY4OwwCCTLSAMWeMMrviNMvb6uHu9JPmhhuIFAkgVKjW48w3Qj3s_MprwLv6-SfYx17FosB_Dqx9kc_EMGkwvWzfbT994qjVIu5bJd45JWo5V4utBPWSwcZ_LIn51yXhtOVSglLjDR8ElZxX8Ew6y9_cbOVOViOdxNvlzcHtEaufthxHU-q77emCvXGBDQlnbvy-aHcsmBm0EcoUGga14YXNekjtZAq3dRG2Of7xnU6gmiVEK7eOgL45Hxpf7K1NwCAZ6mIrxcIa5m4QQpy2THAwOcA01mQ-T-aIiPJyn9bRN0zzTQP4-26DNd8Hz3HjA5DN8IeqKhwosYOFTkGTtEK2H9IQ7Gt_GEyq5N9DE7kfjmxzURfPu8PTTfYbptpjnKA9CGelnKeEsmJ8UZGztoCaMbKc_mGBYNq_iF5-2LBq9Kw0yto4U8Pcfm5mgnnCMmCtjU2whK6AdKLuz9RsgX4ZEqXAO5nQuPFfFXg0zePv2h16F0GYRXLURnPUbP_IjvE5cesvU7f4Y8A34EZyWLU2hfxacNOlfAt5puYR4Bs4t9tAqk4wZONA2Z8yIzP2ZlGcJV5fuhVAsUpbMAbDqFxIHSKV4gaTAJ4KpCfTv9VaJEhLQXg2x11a91UkpyZv6cqeeLAQegRH6b3-R5S-zR9sp4T3Bf0KjkegRIGqfup98C_m4wrbqsQ5LzpLDm2dYyQYXseOAM6_THpX8HqbDZTx87_IxHjXkzxQNHdggkfIKKg7FKJB8OPRzDJCvNXNG9jI72b5bRCD5b-U2k6PobvTqC8eFibm4cFt8e6k0MtBIyVks1ZtJ7NpiDYLsjADItOQKFAXqi7A0Qlq8MTfchnzh9J9neeiJWSg6XY_0y0UAG-BMq3E_y8iCf_6Egjes5HEXGq2wGJqUGQqii2GwHPH4BExJuI6redGsHO0KaDBw9ICX_kHhBLARqauopXQYJv3nNDEeTZNtSNGtdnI7grVZoH70hhif3LLJelh6GiBmRN2KRy0MXAPoybk7crw8HC2Zt6xYctYVbEkcgWQlXj2fk5T8sqCwNTVG-2EzBntbU_jBIYXjgMM7PZt7pQtZ4D9-Nfz5T4pASYVfXFTHTQQvEGyJLq0g4ffCh_pAamYMkDiMH77QkJe0Ubm_DOJKqJFOOf_9Ovey-k3vTUh2IA1ZwbRJlbfybzjdF7cypN3VVHFbvZGOWRVLW-lD7Kk_Fxh6Z_rI79tN0bm0t4NJruIPL47ZuHpYfZy2jQZ9MdTVqpxqQTFHUvGCvMr9PbLaQfq7Y1JukqmLn9Tj4BXTvcxWcysvyNRW8Z9rqRXCghniCYfTJGEz6keodJKkAu2IwqsHdZ6WFe0hyyp7cBbvLy51iVtY1kPXjJWbsMPfhDAiPKhv0NkP1YCdMfS0hFXqlNe_Uo-cInSsLB_cABFtNbJlq6Oqo_5-IrUolbGWdEtiLUsuG12haj7KB5NQEqCFGlifc98e0xcx2CpUHobdP3oJDtXMFSBjsxTtGDAV-O66vqudemnYT9l1CqnCijp_6QsT3aAQozGjLdaEyvfArtLSRvhX6C4LFAW9obviEmZTPOAgtQl3xXbRskJdJhM44PCtOC8-toT-3TVOWOQMDFT2C6x7gGSL6pN5mKkg_xhC6b_Ii5CRgOwKBDxA2g-sHpPo-uu0wW3VQKSvbpVuChFzef4CGHHysyxSKALCeJ6_L36dklmQbCB39iNJYSJE2Gkf2OnUsAYPHcBVAMyRoHAI1PPPTq38lcrsuFZtkPDEe2i2gPsCyJLZXHze97X4j5vyfagKetiOjz97rCmn65GtlMJ0h_zPGx4KsaEKLUr-FNA6XWCX5hVqCXVWyx1yVRMtzyBUr5tUdnSzDUbuY0fX-umawa-QV3wR5_wbE2gHAj6dyOrDKtkDPW6_TcpKTYxnSm4X39hfCUiKv-lQ7h7TOoEwH5FEbOkvtMV1gB_f9ofYJcy2Dvli1nrYuUXgs1KuupNiZx1Gl-oiqe5AYi1Bv5gSQF0eU5GlN-yQnyobfoSA3RrYtkbx_Xdjl1KfqoNXqMiFiMz23zR0YhNZY_Na7XiVcGaLtgQjmtisS2fJSZDtPPtnbF-kKOQGcy4t4hPTqs4H-Bu5qDJEEROIuxjKu0GvGjd9bJ7nhJTq-15IgiKW1GhA5WXbqgK4ZfJJFzz-VRPsOy78kTQNZ94zzs6azJrSVR5j1mYNFFtEiTN_qYmJ3p1KnqSzrjwDm6YuVnQaXhWoMeLY4s4XQNZs726qOQiKy3NcIRyugEmreYVmKOhVclNrC7xLiiG_i4isIEpgvX1iG6A5RlOzimmribMt2QSVpVOPUvVmG-r12jfF4eEll6CwfHKAtbRuUOlemjWY0dE763_VFDnaiuVcgNRwrSec7a1JFkqimzGoQ8gRURtQaHAt7autbz9VqPJvKOysg9UuZWgdZLjPq0EI3s2BSSEIDQKDF6VT5N2Hl8bE8rgqPgyGatq41SMIxcN4u0SSMnWziR3WI1t7LGzno78JBerWkx_h33nkrL1FgxNAyjAmIKAXyEcHvSorwNw5CiYXJ9FsHktKtrLca_eDwGfXPS53iYO_G_mdaI5anoaPFpTFUWah0Ij6uQcEZa0GgwNPMSIh7YkdkwuMbOpQDZIH2uiIbJ67-rTuq5q_n5bwqliYoUF7k7va8oZYLKiRGS0pTiAoBsOFgpfq-mUeFuFaCkxsMnEU1qEjlO5LZJLKczz4CGbA7FgvOxSXBbHtyU3I5OOhJzk212jKtz53bVU3ULR8gsFeIa_bF3bsaquUpBXPw-hXZ7L7b710LotRH9NziHzWqm8ncASMm3G0UT-HH3Zme3lyryxsoOjVzV0H4pzwViEYxM6yYcevSftfYWc-2TniRCK7W4iifCI3BLi4di9xaA3PpbDazPZ5Bk-lVib45P-dCG1vKvSJs5k4inszdViMSh08ROgeI-oUwwItcXDj5rdgqvWGeQYGuKA5UOKVJza2DmxloG2qp_p99yHSF_QmJRQMe1JXgARxY-0gGInGIctVmPDZS1PAW5t6Dxyhygy5rhyw6WwAzITR4XQNGc32aptHnW46XDhDZbdwGqDbD_6ypBYKLBHig4MtUpttLFEanAcCci-Z3UPSXKHsZhR2J01NlOlslRtdqZEYVW98nMKPYEe_xfQfD9dIVyT5J2qK_0t7D8lxRrV7ReaVnSjo7EX53B_SMRURsl3vKSoR7rhGYRVQlTWSfMGJG3yWa0Iph9jYDeaZ-Vxm17h3oa5o9ZR3I_sYUs8BaeLcGbAjFbATNbTGc5sbg-gGsGL8FntUhvSsaQ73AWMVmRyGgGvl8Zqf6_7h8KvLB1fNhPC6ltpSmWcM72I0iA4HID984gpqBlqJNEWSncLoh2_gHq6wQN5T2wCs3TiBGNoN2_uFSsvIDDem8CilaIZy9r80CImw7L60IqN0yfweqOgm1-Ifp0g6s-br7dJNGi6a4pAw5ezIkTzmmftiqcwjJLpSPDKA0XCXI_Egiyep-nF48IAOdwH2T3HXIXlJkuwiJE30J725kXSAbU5B5S-jt9cBvOrgjc348ZS_jGUJCzevvm4DUwbhAvfsnHl0I2rp4qEfDIRYEdCI9NVB6fd_OIXAjVEv3lLm11aDAwdfPZDfFl8iiyeKlYvSPnMUwa7x3alP_JnpITxSi3DXVHW--GBnaS8MELf0sYiLwuH9kiDJdFpFrqIVBaehbJf6o-jmHpSISfoiQHQIuu6l960fapdVauGGv8tPnvIKvuv_rhMB9XWpBtumeb_lXao2tMGSeC56hvhkztpkMVVYACVhTFpGnJzc9MoyK-LuU7pYSs9FKQ8CwOVSALurjQdb0J9xhWh3j721hii_Aq7rGLHlj96mnhzvGzukpNr6Eus39FqwzJP3qHxcFTDRAHAPY5chaEy0p6lXUge5oE9EuRYi8fYJ75g64TqjMZqUOrL8Oo0isCitf87KfOpt9vUPwAphEATt-PiPAz4lqVk4XMo9ngYmJBvEfSuRQakiJzOQ3pep1ddGPzQ6-eDnKo66xtJNGxBsfGtUAB9QR76UMxI-UNlizpvU74A5CyHyl8R40MS0Sso6tOp_vGzs8CTOFyM1vTT0kTL9IGRGL2g5WyHn9p4RVso2FZmGsGOKz2TyXUsZZ5GmDH1FHeabHq1Yp4NO4ixpGpzY1NTc-8TExlpIDM1CdxNOIzN1fJ1OemrwWQst3SdciEfnYQ6vb753wwHdMwlTzz3ML4D7Vk4_YV2ZG010Rv8lcdn_LMJ4KTREF2L3_mMNbAKPOmh74kO3oEASJ1hDEF72M0RhTIl59PekEo5cPMhigl5axflumMqgaFN-yFSqPAXX5559cXz4r6NP-6o9tfjvsH75K3E05rFY2opZpHTriq6v_SZozPEeCdkGNa_Y-bYsaDhM-U8zKLdnVjt8pqYCwaBKpOlx2DWqtU7nsFqKX7EKjWxgI5lmbBIc4HZzTY0e4so7eiWxQtxeCoi3V0MWc8Va2-rm3IIzS1zLYmuom8xuuwZMmzo15B4W7N2PIRITD9Tlz1gLEGvI1-x7P9D6y9CRBzkhGe-euJLuQkOTrwUcmd55K8PUs6EQ7ILXAyb_tH8xMgQ9wN5uuOcZVvID2l_hqjPj0WBTc9Bm6_RWa-aeLlAIZySS1soF5Z0JBisHOllxZzjo39RDqMT3N5XPv9vG2Csuc7keKozCcg7DaCyT_5rd8JeubtpsntaYtUIFJrmUTHo7WcKmaHJ7TTyfD5X5-QLdiIPHiNVvitY4CXfOv6P_gOBbY-hb6HU0-Gld71g6uVPZMJek0JcT62dnk12JteJ83gDX0QpneRiAWIJReeVoePMWERAEU8ZRZE-LvzWPLubAt63IPBCdPVl2tlXO2PijiXsqA7JAyiN22gitGVJ2Wh71AhHveH9s-YLfZijyJqoRI5tCg-3kCUxdNG3MXsX6GlWgBQF_AjkWvQEo7Ybtquh34SVFKoi-rtHODR1N__TkaZ-Nel9vsMIjHweqlYUC5BhfycLx5B4xms5drpe827-OMB4j5lm6XFX3McBuLe68WBWGUnVpEBRvQrfb1HRTzMI746Apk4aWH06TokzFioKCTN4KA_Bq12nMSc8_hEvHegM6mzvqIoLGNto0NYpR_4dF6rgyk3KvhiNQSZ34gs7FhoTTFpkzKMMPrjWYrR3cgNLHlaioiLaTQQjjVJiA353QIn65OJRXvY18FT6HJf20Bh8IeTskI9qdYbzmEaxEWqWrLqKEUbMAMHSl5Qf9XJ-k-gMCj7jTIElF9OMcfodDm3X2zQhEoWZlVn1G-WgbqNWkN8BzqnxthCM9wqGWYHJt4pcgWWBorfjYGRvWRRipLQ3qXYr02UAv7nuvhuT0Ucjt3CHrozujWT4TojDXxgkSkL5EcCGhLEGiios91h_FsISp0Mrb1WKDA67Mc_3vMCr8JgSo7F0qxy0MKbW6fNcB1czZVbuWLk5hl-_qtITEN5vUUoNjBl3vrTs0CUpOI_H3SM6IZg9zAYYCLSqBm2vEd8j2izo39uLuHoJ30PLgUvpm8C91DF8i3LmONHTNcinoMHP2Jen6-0i7Xl7OPJqBXGy4Py3fe5B3MEOO6BvhX_iKhLDMeJ5zFcI4Mum60vMGLLD-IVRRCedgu-jknRAwhTqutcFeij4ZewO5O5Gtl7Ob_itdOktVjSVQ_QnkU6ApancYGmf2DLgeedLnwnpdznH2RuJsxh8CW5pN2-ZYUruOI0H-4fHO4eW8N-Q9gg8_N4WMKohHnrpuWnxI7RAsXKECoV3Ux5Ox5hPifYCATR6VGMWu86H0Krdv_IhejhzCoQ2mM3a9XrIAiR-4dIr2Ux0gC82WvKBmfiOQ0rYPYLsPWqH2sx2dxoVAxHh3_VPgu0EtOTQDZ5d1M9f-COkAMI_K2ze6QzHf3znw-QxHkBe6Sp73urWmIh_RQjDDjrOx9OYO3qsdZih7lXPU88hniTv4YFWfmGTzErnIrE92kXIlRdHJavZVxAkFjMCt_w3tvjt8JlkwO2i7Stt9dT4KGtEKN9a4IsatUwyUJFwseS2QofWZaXJ2sO-TfU4AAdZKNqS_dFiqPxgWgLN6Z3hXys8gqiUZ8lzoiJJ1_1R0RIgk9YUO97_AiubeO_B_dAghdk7B931a-JPaf1hwkBvUIDD4cBTFsX0iTztsC3017ynx0ZDhoPBzbWFkTmILvjUS5fOF5izmEXQRXc1FsFFXyv58DL4yEwkOyFDsu2r0Ech4_dytTE-00agfXehT4Arh5VywIZnbhda1dZclqh-ua5j4JAOjVZqSSLJe4pcByAd9-JY11byn_yuyUmSte6X77iMphfxsADcFgz6gVeZ1Duv5FjuwTjtJ7-f_9CIl5MvsrJFtC2T8XPqUp2i-Bt6UwLnFfDttBOXmmOmv7mULMH53T-qsSvwpwvSm9LMtTEnPxwYf0YScRZlX_Hk6PWPgmL05WUdZ2xJGUbtavxoNeY7wXXfIvduned_A1RwzevUkjp9GI2lxhGyCkcKGtIyYpVjty0UP8gdU-PK6mZq1IqzuWESfF7mS2YjYG0U0UQrUw8_YY4V_shDOAeu5kQf-nsYZA2bi7Yf3nbiL1ElSBvvHPhdaPJfnGc0nAX6VIWu779rUQre767WGURBLX3rPwmjstN8xpvNi7xOK2O2ds_pEC-EHUYrzxkbqocAmgWG063DXz50cUpIGdqwSGwejHi4OJyBi14NnR7xTpVNdrOKSVoyObxvEuxceZLRjnIaN580tmOXtWdL30bPmKE0hHynCorlO70akY0z6bB7vKaxy0JGctLu-rOy9-l8w0aAqrchojTZPrNwUXPotZqjIc1ex4LVczP6Grat30LOrDE0hD7sapnOS4W3-B8U6Aga2oFtrE9kCSVRmUtNycQqByhOqAcIflP-NA2-BrpRh9ktCuwIk4lC4hl8TCZrMOMNgjKYrjWO7Xd0uRTPN5VykKlpfoThASkDql-91lWkDX4q_4_b77MFxYycdSAGbokKdRhi-Gmwn3NPttr2QVDNnj8bvjmmYtctDQw-ANJAbghXAdmLu4pe_OJBIcd4Izg41fCyUj-_Ynga_WknuWUVUBnAK3hQ-7hKSB7CZ5e3w0-JfCezjIv0ramGDHfUh8r6U-1aEDlhMXKfeFdkIMLRDtDftVOSpuAAtj3er1mjKPplGakAMIr8pWaGDTMWQe6XMWJWe_HhDTnLMVcl1BQwkdRGICYzlu_yy8wmIwJ4xzLjfewJ6jS5_swXd5-MIVCfWJ5c4Hppf6qka3bq1JHLVVjtRKScDZdUkc4HJ3R39w4GeIJDUsVhkNiogzAdWbLX0r8F-jnHt0Dw2nczYO4XFpbzzv7jfjQt-JO0AlJWLNMHiTiyRN4nPNgP47LEDXB6lSZRa7LLkcE22luf-3x4G-kZ2X_2vdTYua3WsS0NiaMqnktENt7xN80ywvJsFzhlIz38qShiRSbFKfmoyd2c73uMpQCjv7Vvo1cKAAg3LMMHO1rKtinKUOoqBdqmFPIIMu-37xo9BqIjWLK-SRs9VKdpOVdYvSsYY5ZBGHmBQhz4oIvPqPgPYVh8P4N41-VhvNiWm9zaJ03GWn2rFTzGHP6SQ03AQfxV3zZHcA34u6BjoxGN0Ge26Uu98-6BIc1usf1904TApL9n7YreEQH3nYQt71GL4PMGI5vXUiwSan1LmcUDloWzu7DZtZFgj21-Kl7WdU1KfKiOJAHKGpfm7Rbn0LpKcmVB2dzxAgSyjRRUtXOJQdojRzy9jAzhgV_1rCx0qQY08COaYYdUEgz2_nDmOHPDKT6KDbMqFtIgqku7jrsxMe4rjNJ-1NcP0u0JixdISI27s6KbHtrTAAGpyRU3By-r9SSIVlkuQ0uMBuO6jzbmTQ-AnXeFtvF6qi1MtqA4DpJ5QLTuatY8JxSIZ7_49sqjEWtOmbctyZ_1iOMSGp1Wmu2axoDHaolTycuTmwmLb_hFj84AGaWOlx3KbAgSDvNWU_h-thfGeuYWhne5Dv26p8eNQqrR1Vi9XYNykFdROYkkS_5Gg5JF81u83Rz3y-zXdjZRreGwCnryJLK9E9zYsBhnfO93V2IXG0K_zJlJfN0u_Hb5zuidJAmMLa1k5MjLB_5cwFeX9AntXVD6rTRu2hTgbYZMy0knPOt-pwp0XocwVegWrjjiU_TCwNRMz2gfUM6zE5fk85gUqAvHdBiam9k_coNf5-J--FebhnuVYrZrQqqQNIOma3hrXDEcSMqZXEQVyQx0oXWRvfcyXZqxZWIIPoKsfLQSREPqQ1c0Fm4ps0Yq0Vz_uaTGr51U-7nrz3KGJuDjHzoXOOR9yUIWGlbxR7OtGZEhv1jLJLX7d0p3V4f2sksgclJ6t7QHyPriMBI3342PiShZ-ND9EyBHzZzHUMDYst1t4Pvvm2swjWVgSzf6j1--XDH_o-mJwgsbgJ1hXUBZ0niQrIiXYicLnT441-ecE1UyOB3uVvzpgCAgMinC6OQ5AU0-4lS8EDm9vMWrUFPB6NcKlj5MTbPwVg6IecIIgFefSrnmsc4aWakkbslxF393GkUaVEjDQuCRjvKjqbbcq-KOLHaCe-CF0m0MMGlPPtIJJZps3W8p6ahAb6JWwAazIp1xLexKKyb1di0lpiEgzc8i2vyH_AfTdAyC94Uxos587QSpjByH2TSFng_JRUeCbMKYD9kv32mPP0L6X6CpY9AqvEqrqCtZaroN3agE2qaeUcWPRuOa7Rvzygfn02fDsS104ktIQPNMo-3yQ_We0jWYCt5261ME5xhfe4UdEpo0WYd2G5YyoNsIktVc5-nNc3hiGEhBSCWHywncb2Y-_-RPiq_dsgZROQViMSfg13f4OqT8Y8x16sWvXOKkusOwVhnUEkkvhZ6Fsns2Pxqf1wtz823AuRAsvwFvdNRzlhml8vkzf1t3_ezF4nE2QA7NHafWzF1H3X0T0FcCBjpbiOvUCCG9mt2Fh96x3LRwXskRsCuLX80b8j5AumyDS0vTeDb88P80GEVGvgXDGmzMh9WQByu7t10QDpuu2KR6xUx4ZRw1vaCHtT5buyk7F8qvEINAF8ucvtmeCkxUCaF0AMW0xFlsLiZgfPDYq6VxnSIf9uRNeZEuK7Iacj-SHrEysqqGS68yIf8TYD2koSezJ6NwTh0rGN4QmbTc9oS5BZPl6rqbUwR4e9r03cWqQiVo504iM1wHQM_hCkd7cTkQ8-f9-1OrPWIN0AbKb0QGQYfuYdoSJtQdzZA-vnjl62sL1kV0yWMRLpEifhNPZq2JA2xGjkVbK7CBSXJ1E25xKeKRDxj26yHhyYS1UWdUf9J9hUb106mtpcsDvVvl3BVp1RGG8-4JMdZw-MOUwK1JOTdslbPuWEKWRv68s_01bIq85Yl8yhEaPSf_RUyT8riUpbmfXzOX-RzI9-tI2YTye90mBPzmVXqO3KsKUBZuDcH3SLHisQQ4Ggl7fIxM19r2NaYI5C-Fixu-fkluEeFIBb1u0anMtXoP0kHcbMGlFHQnuWKmjDVztGS6QsD5oe0R9kc4dq2dEC2q6r43BlE4EIZBX08vOTaadMgt22fHh6ezo5cibl7eJ39MEh7YIhnEGz_1EvMGkTMYL8xeq0PmALZnKYjl4hoT3BXxq-NVG_mVgKx84nwEBJQdSX-6nsv_xrXqjNtW8Z66nYKgMvES8lCAxMksGTZrKtzUJMXVlxZ6rzQaFYNz1wLOBkFjxwiviYi3jDHoFpdE7e_KQeuOlEwikOGq2IglYqPU7EwoIfR9oPLxAF3sDvk9_jtccNGUkmAdX0eYglHMDd8EfRQ9isZYRM8TYQHurFRGHaenZyHyxAF7PgUw0J-Ylx7xiQ8QUeJmAM74FN50o_lxa9EBwErkv1Wh1sD1EgDmRKuRklwfuzWUrquaQ-TfzPKsx1f5Y-JDeDAO3n1DTkXYHDFBCbOZF-GCPDblbvDFuEOSnJCXT5GNG7ONnAlHZfxyG8JUYYpnnGn495Hryr0sYIw9EEvRBoJq3w2YG2GtgNJENET8AbRajLDtrsc-KfX8tBrJUIakz1nxO9nrUnYyOx45b2uccyE9DhFJ2ihqgoCMKa8YmTBYw6j4LkJMcj1z8VFHfuSwb11YKWiOUaV8b-C6YFbTB26XJuv7eRUgY5TUDjtEIjpKDIxlkjdhfjssm9zn2HK27Y17iizdszR5AFcMbEw9YQxlenKfWZhwHuIbAwAS8fBy02SfXKS3Tz7s8yU-0vcchn-MYRpbdglf7sl0VKO3tSMD_hg7EGRNpa9iXwBh2N8rUaA1paPzljIbUOKBqr-cus1DBjGuy-gfF15u5639C41BrNS0OZeciOmKC3m3MhmeoBaaUi4kAq6uRgiNIV1w_xymqiypPAKFQ27jL1TLhc6fEO0Jdj3q7KchQPq1I5shCMpTb3sUIikH7YXFxXPXYBWy1141izcC3fSWRz-Cu7kVetGXR7aJyJf9DeAFkGiJWzc4F1rib-hGBulFalIPBaIG83ld-kyPlFRQhbD7V0-uhjXCKu7k9qd9SLly16jA5IPBa4xfVTbv4iYDnfua0fWjxBVy8j00SJMDU8nS297HtVjaDv55vpl6bX0WToLAsCL6zXtAuAxV2ShQhI6lWHa6Sk1gW2JL-XO9WCyJ6dwzJcHHEoOYQdEBCvlXD_rX00Pctu6D7JLLBUWSEjVD42MXRd56CI2XElQCaXNiWKLsAIfKu0Kh8Do7wYcLk7z4qlEIQBv76M7z3FHGHMFJ17gnqxH3t9y-sfPr1ho_dUF8IMMOYY-HaFsBtNfhPuFunqdrxXI9-Al5i2V2IS8RAuguPY3qN_oAfacJmyOLoscfCQKIh0DCY2k8eu0KojahRdlonq-VoisumtyW76Gkkr5M7eMJeczhLqyshPxPOJ_P498DEUB51dwwtFB4AcDBbjCq0AoBBajtMElM5pkkU1qTfqwLYQdUEY3Z4QTWYh0HWAoqJCGn0p4_lO3PP7hUw7VwGom2QDBUPLIRc_WFB2uT9GyS8UbmPHCpDDtU3Mb7aEZ2UnEuhjm4ybpUsQQ-rs2zaA3qFWOZR46DK5nsIcDurQJWVngEJUwuJkJKl6Eajc6CYdg7rYjts19MPvPwS63iOBvrEXrZyM3_XQMnTO3V1-17baxmRJF70P_871dEfupEEc21rOMESp3v5e5c4vEfB1_Xg5P9vUQCE8pii0UNW-uSIW0nOuKG6mie8GORNq0aYzBPjZDajYFL9eYI8S2Z9C50NveySaoj6AczWZURArt5_C_B2A1TXdvDR6Y6lV74TsLU2TUWtDf2qSvTYWugYvy49Dc8FafTmwcb9Rfj_XJe4NlLByj3lgrWQo_7yjQ8yETMoB0TLCdoW3bghpWEjz19_zZpPuYmUUiFbAiuZIcpBC4kCRMdC1Wn2t1tpxwRf_3oOmYRXQTUnzB2cBd1dqKRehxIjj-KQkOg2Wa5bClbpF3CavnKeZafdfn7mQg7_l4msp-aH6jDy0RBcSMOYW5uavhvi6bjcUni0jICSn-OOCRiQtFrYuYA89Eh-IC0JLS3OGV88GyrDAgffwZ4jtA31mktnCoveWaguwoC5-HUDwX5tpL37VrzWKUQduxD604fjRqnVnQt5nmJgjtyiZG2vW39s1xBiW8N9Uu17s5UZDXAqhiRtxi4TL7uqXmOOQOWSl1ZZEWPiEts5zr4X-uhWb0A-7FjGsGAQuLy9cFDj9gYIitkVWusOOMxpu0Xs1HFjmOBZQDCkuwDMweuPCMLBZXasCAxhuM3O_rnpde4yni5IKky0CALI3aF5iKRdmz4dVjxxLEmf-SFU15g9ijkgrdLl0yAu7iNKnxy2X40qwAgDZY9Qs-btaVoi2aOHWLGCOVrb4OxBanlej3eT0_ShGStvZdJ2wTtrNQ4XTjOcBjkWzpUhMD00dy5H5M3wQBgWirXDobU63X8XshjHSDIGIWOaFc4aXGg9G5NdK6lppxhYfPMKNspjT3136C4hfa-Gdtv7wKH01LML03CNCJPbbWtCOeSCDpm5UF11JHsOFN1x6sl72IjAXiN4M9QVk2yDq0_cIg8dtrKRTTtAMr0Of-UYdsXfylA34RxAb4ffLVhMblU_U8rUGj1PQoJAu9D-6aIQPr7inWR1FgrfrzIGicecR-OmrSJJCG-_GPh3t-I01wDWPTZS7_ZdJae8GcPcjyg8fP86-ONLfVN3SVvExMVgtQf-NTTHlRpcVo5gz51huTqew5dmEZsUBbUTsLh59yUxnZJ-UaV7qWYi3fv-O9kBh9C6f2Ukzxq5GQjRNinMlDasjHhULJxf2foYIQZxDpY-RWgbNbbi1uEIpRnpjuQ5m7I0yfBjvYbYepaHzM85aUprqkz3pS1JI2ATiermZBCDx7nkpiwk7_baiVt6kTrDlUK7-0nYYs5dj1U0i9hSggpQSE9QXpd_9wEFgD4bI_TQ6szhCluUS7mTNAJPFAuaBJ84kUhaBWoHHM2fjuM8G4-dCKXlqLuRtTFUR6kuKlK_724vYi1QFXlSHIm22pHQ3CusSCEbcH1QxyUBrqasbAHotGWZ71zrBvFSspx6tb4rxEQUieoK00rauyg3dQz7sVaE33x2PV2MbQIr3EhBkt50iC70mDNtN-k87hzj_8Sm4HcJP1LxuhJAhgbBomNEc7i4GquAJM_H6MXjJKR7irapTnUMOTUfWwB6i3cwZum8bhpb2JiZYYEKw5Ku11J-PWn2xkNvRxN1UngyxLu__oKxfNVAdf_2pJAddan4Mou7UxPWUY_ylSSqvYGkEP4OzpqAARCmGczSzTuimQ4aX2bhX40vsR5d1E63MTnAqaV6e_Rr2Ktun81EKkuY7BjxRY80OiFRoAlkeOrmEHVa4JKC6fjxDOZG8Z6EgallwNZh-U7GLFUov0rfTc600H1o48QHFe3b8ZtET4asppDt9LjzjgS9GZZUDOOINmoJSGwmgDOSm8geNLWMsWLH5beUxJcUKrLh1tbItkRhTY32rR59h5nU1LzRUBww5rLY2ugp4fbfEEE-0ekFxv7igvpBrhsdN2BH2ENVD5QRPgYGDXv0mIgVS2aw090kTSUjJPWYLfPp0iZO7UHeRZUjAbkn4lIovjZa9Jj5BH_IcNQ-Yh4x4E3BuCCxVBD8HDJkCBZOLzwje2Rg-hXk2IYAF7l6RpjZ8nH7CQT3KO3Sfaefc6_RvA73P7plfMIhBc53vkq9dNGAGgi5x4Amu88pcU6bEHguAAcOGzt3QsxvSsoI18Xk5fpvlXmu4HYP9eXoKCF3KSmZORbFfOfB9nuUH-WMF4ZoN1szsSmyOLctTJ1H2_kogoEZTqCDf1KUBnaQ5Sb3WN9VnDWPJnYOENlRRqTaffwtC8JmRE3CiKrPu3lHXzez1We1LUGOJdFFqMue9_UxEVG92buRe9B4a1ARDCLWoPz0ty-uXNRIEaec-5UxhZGGFoI4rh7w7tvDGbf8T1-A4zJCrpS7vvWjZJkYTDKI3JnSOqDgE0CRQZ8OdflUR91Xodo1BCzBAYf4B4pMAHvI9E7UYHarEAuMbNsMm5NcGgrZ9inbkbnVnbrElyQSPsOLfsTtZN5oeTpkmI3L2PJO8VPqNBeR5TNfArwv3Az8X3lfe_8zR3syt_grY-pJBixmRr8J7ig8ROkJ1OveBlWXUqm1H-ye2m8vAIMwAuOfqptByyBGbP1FF1vKNj2ej05hmt4I5pHQghxxGLdYRuQrFTh0iPoSh0jojmAgYbeXOQv2tcgN6d6x2clpm4jE71O7oAokQgvKG5z5C_4HW_Z1sK8eWFw9o0031wIq1bfx02P-dEIUIyVu8wpqnlO3y2W-M87SYqI0XjCkrTQBMf0Tovb_F-S7uAHRV-MYx2atmncpyiKfQKNYP_mfIt_axNa3TxQciN0D1_0qo57oUvP61AwoHysLzsUCatM3warJV8D_UPBHS4B7eWuuGTrNM0EDWP81G6gsQNawQLkUdp25ThQNuLlUZFgbdT_-s1pldoGrtJ0lfVxMvxq-zBTmYdVS_TumK36Ygtb3CqiMtX8J1J0sDdBKtlvkz4BpWYIwfwfZR2u8LzewcOjGraaCYUZap7Xie908dXppMi-ARrVEsmaZzf0JR8MtABQJ8sNhltc7daI14gd4LzxRnGdFmRrS0wy-xO2CKFp-9hKkuH4z93jFXRzfhVG6MM4KPi6xZYzt1Zzv-LMbBtqbRdRnU-9R0B4IVuZDhiUD6JPDLZMsbdNsVsCx5PWf1z9p5HubG04OYPkASlsnke-hNqkeljaA8BiKQvdDNUyUYYpGxeqAUTSe93aa33W--ghw5OHjegSItykiinrnBEVhjOVr11sqrI55Bdt3vfN7-0z0cZlENsvp2cQPiAZgUsit5tobExgfEry1ME6XwtS-7aBSDHM1S5-BXxyEld-vzREvQ3tXs57vDexpsVbTqIYst2YyhLJbW4Gem8wo8h41wAK8W4FDxTqgKEhxvX0guNpRxtyRW7qPPnhKA_MRmzYYRuFVC8_chYDnO7rWItPQFzcfEK-yrKq2cN3EeRVBwSi-mG003RXAkH1sbYQuiRvMs7puoC004qmwA5IN0SsTxjoTiQto5OOYjqQdBn8Ns3F-it47AJx3T3TUMtr04cGbjb9Rxq9jSBdBrMVSKVyfEvs-X5VEgJFxZQb0rh4-4wLZDjYzxOSBKgN7gPAqwpGALfVUffvlVDVL58PjH05y1uYuUXV2PNZunQ23DQIiLcdYaTI8jQi4ueyRHGqD3eUKdKmb7d3Hb8GCLH3UlhFplioXKX-0q1YuE4RS__xFq41ejt1JyIWND96bYwxoY4LLQ-ViA9NVs8U5UWdKwbRB1IaFLrpa-ScrGRkCzWMQbEChDf5rswLYtLRwKIhFHjoRhiJBMP3mJjv5Px7eRI8m_27LB5L9a93YZTqRoKucc1l1PZnSvpseOKjY3ujMsxkCsW75btIB_TbJOgRsQTpPRKETvzYUxs2Au3-gVaKFMU2AXb8n4eoobwyFi9oF5fD3KBrrRavc31jR_ZVymt9zjGfN3sI-8inHrU_BvcBLLvexx7Hzf-2lsaZD_heR1w2ouqVkldHizQdAZhYUP2eCLwrRv8dx_2yp4fWDAT1d1OfHNO1k7ooL-u8lhMfFjBs0NHvne7JmqGK3t7XS_U88IpjYEtFn23Y4TVPhBBhJrh1o7d8b398Rif9q7ur_d0KXMxP4dWiIkRTVJo6GjcRUBRSDWEsPIGrPrdMdJdFbvRpfGqfMBfB9HDuLUitEQWuMvtFrXMH1sb_mJnVl_VvHqwmKaDBSaukcFu4DEQad1Uaol0fvfKpA1RxqsbW7LTpB7jCl8mTkeKLa1WQnQQgdCOrzsJrI3t1Lt3fxzinw9ca7GXhTtYJtsTYA2lXQMFsOE43x3ffkS9BtR4wsCuPBMcYiL26Z_ZG67xVK2PTEu16TgbCXGZLIKwQd-knoHkHjw4lDwmxI7jgVR3Xcr9ma58-aoUmb_GV_y4C0CDbXZ1IHEkDOP3xqb1mt7AAwiL6dieH2NyL64d-whuKKN8rOZqSPqukBpg888ZHlzqBL64plQgtTeozxqUt7Tc2-2yZrM_P8LBjgZgMlANFuZEM-GfUyTD-METszgGVZLHZ9tQa1t3KlWvosObsG_RUGT0JxzhDi7e7_mu_2X3xaLQccFof-wVWprDla3wKQJmD7kexIVzK8OkPEYDGCjkMqJMpj9zbYc3tMp40GhwrUjz4mkZcbnc_0MLACNOYIhB5WrKix-H7l39ibLO9Z4ECm3oibTmUitgvTOE--pKJ2IV8DsOPGJtCQwc77ptVd7lOOll4sTN0OHhhtr7DylIXh8s_xd5vo2WM5A2GQiID6KJkjfJQEHaosJ-1XWf9O8LE42-OGWd2KeBVDMv4epiuQpZjAxzTmXFi0184gtzX-Svl6tUEFNR4Eq34CwFQbCn5VYpChjwErOiyDRcgKo4ctIFzwsX0zaZ8TwqRzMNf3AWq8Sg9rveJ4SJdzZt3xMUB5AqH2tOv-zwdXG771j6UZMBxMpIids8FQ6lpUDDpewYgZuEeg-GqdwOpZxCnMOfEgnSagYfqYZbP7ITqlEiJ4GjlXFuCUa8spSqw26kyVlSwKt3mm5cL5MScc5Dvq_p1Siq1i4X7RwtS2ZpPDOV4f2lIaNZCtRa7dJfQFehVBzgS48otOXQYy6AuA_HTYZ85E0x34xNS4zEIJd3lnjihDAYCho8-UcWewRQeubPc6SvhzOidtbLeGeoksNuqkfnnYiyXMYQTEXx90HUizT_R3vbUsopTU4UKSkBAOsKGQyLl5hBvE-OEyNi1FsE70RBKG6fl6SV0YIsABMuvA56jCqzsFoGONanl81IADUTrJf4lPLSZUzOfURTj9YtmwJ86B_Tq-7oXm6EsA7Pk9jD7w2aFyrW467HBH3zEPWRTe_l9ZGE2zXK52LzoTP3p7jCOENU88UOYpZThCCsGCc45fHKE__8ln59b4bD9ITy5r-twwHTEdusSPadqsraWrAW4if6vxX0SXhY_o4Xt9L03nVZzY0oRG86soMstsR3wPhsXXtHw6CRvcq7lopaEHxEy1B4M5hUmZrX4gifnvxUxLI_YTx3odoKnDxFFf0_XPTEs6NFuqxUoRR3QAgxjk-Vzs0pAKi3JjY8gibVwdMSZEg7OR8H77gYWnWPTMhEqgrayAD6vO29-rDa8kOQi0g1zLKBDnxaaTu0VQFZn82J9Ho21ZmlPvRwpPPJ9-CSoS14a8M9-vMOiu6sG11kgzJ0LBBBC143zoE2PQ9BRp35worj1gqB08tPkOAPJV5QqRTrazF0gXVbcMrR1MVpDcGgI98dLVs7W482qkiQIw5GgqlkA82OzniyUa0WhxmOMMINxRigpU5a77KXOf2EH2o0u9g5aP5uyT9VdTMTNmm7Icn-ZeHbhYR8Lf4chXLzlPZa1LBSlbZbdp7urKrlJVTy0NayoxkVsh2fEM6iKLMfKpsGVVc6Q7lZdXPRIrQZa4c9CzvcSX6NKT4-FgfUOIADM7vHYPrKaQpZg9BCvxSTSqI6TMB5XxaPdWNIgNaQ9XQKwHK-PVw9AI7G7C0cksYxiK135CrHaTMNwVkzPLECLM0O_Bds9kTrbykMao8N_7-U2_VAyMbzBfEfOeIXaJha0q_iOKjUFgjw1XnlVQ8_KBmKux-U2jzSfGOk9w2EWwojultjxWeUA8R9iZWyc6-z4JkA48gDUpW4qumLjB56_pu4_ibUHSPmi28mfJHe8NlrFBmFt9084Dz1LRmU2Lhz4oERSdc8tPiY2JMkb3sUMu2gadffuRucHfVM7J2wXiWwJR3aR6Ts4WNRCkKkkseArgUNiYQ3KK-pICjnjSAf7dxe0s3bfmAQ1DSk7BhswoZX-JGhSs7rB3nhZII5bLGjLJHebyizzwNb7__xmJNZPz-tYNaswRXYmbIoFyoMjWnnzA4ex9O6f_iFynVgUHO1kDp2hSQTop1H0HoMwdUdpxD6jFin8u2gdBNdiCmVJb2UH_AG_d4jIl5D9NF_IeefTb4W7AxuJUCRkZfPO0dR8RV0fqT1MgRgHMDLD68O9GxPhCwmq_9WzIq25BjsLpLlKOk-LbN_ZmJkNzoFWHdFVFl9ybps7BL2jddc8uk5H9l0NIWnC7ASsfNo9C1ilHhFyaLxMJhidnFgUFghWkNrNvhnFE4VCjz2o5IXbqg2f-yJeHfL7mYT5hG0EHOsP3SZ1YNxjURLcSco2qy001lDigEYfYyUSNkLxxZbIzv1NhE-MNGtzgh_1yBwbKVxZI-bz8252LG_gNWbtatQQzRXt5G5UtuSszfr3hQDmvxMnpssb4N9NwHYjhagzAA0kjDdtaXkyBC-1dFvRXNGe3v3pXnF-gjMKyAYVMVOBIkSdQio0bMjKPMdvPepILNaJ2z1Rv8EWdIs5tS7P7oqWHzB7LqbpBmLumGvLYxylqVEI-EfAkKL5I0Tz37bnfaHkQ2ajy6wYIPHNNiCBvLfynm7s3Ug0IhXxg_lVoW2gt_hM87XwlAVKHl1uBxut2oqlkzEm3U_TXHs5OaCgI4t72fh__Zs4dBK5UAslRcikIL6tOpucGJnwdiTWOXSAphDuhiuUcl3jMrjUIiJsCnnmc08q8RjhreQ6sEkC9ELVEHD_DuuTRhr2YkeB2agtTEAuLZ2PqLh9WVnvqo7wJgPi5LHYd8uQaKhaqgZYoV10E1Bpx0npebW7DEtSt0HQXxWB-HWeP8OVk4Q9eM2NEZShN4UdNFog4CXs_17rNhCyAE0Qwtgw_7Md8USjUdVofsvLUi8zRYravg8CZChQ4GghL3nvwwhh4SFmylBjmxVW7LQAzQIISDvipy3oov1JjH5xGKsYDFtEvyQBQWKbRTyK2Exdge1LoUPgQb19V8OWd1Re9dq9jldGb7KbNj-jvj06RVSYWs4muRud1fToepY9R56NU6jZdeHjq64Oj3o90JVStzF6L4NfzQHr_UU-cUgdOgyP278nb74xLqWmLBzLzpR5jfpT-BwF7KTaRBokt_KZvRj8EvzP6vlfRGl_6caigb6ifwUDHkEUwIjRPsG_aZVlpPzxu6uwjuPATBG4v-vmrwnK26Vxyos0c4kf_ORIOFbtmzcjBtLw8up7_9qLnZThkEYhoSvarQ6bknnaqm2hlWEjKmy7h2tRImzDDPIxG61DEhTaiizY2uqZ0VnqQQIm_PwPto5w1ioWPqxPaRx-GdGOJjskS8sEu5u9xu_bkhXDQzsCuCZOn22Pyod60q0mdSTU4dKBnBA0RYBVn4WmRyfkZmN4s9a0eA32XipO-EBv3F7U-GFDxqs5qUQ_sulsyeWXuvXu2K-H-6Z0ROuhrHkkBzYchi2wgDeCEm-oOcf_BlBCSkJv5xQ9CwhWpGICICMPjeXVkpei1h5VKDrqa46s3kMt2PTAmfXHD9cN-7HM4Su5gASFAMHOj10_Dv20EQx8MLWqZNXedvFg4zH-BKwVY8laua4Bc4-eR_ce3b9-q2FzLySxX8jYNeIkGmR3kecEZSDVgByzq8BqStEQb-ORBOooJUJVQy6bwGAaS9ivRISSNQRyOykx5C_dCHvo_W3xM13BJ30n-txlXLMXmHsOw_yeh9IfzJsivybf3z4sDePJP3vuMVS6TkHBzg6aTfQrnOHc1uMTYuEH44IYSdtfcWXjFo_z-_RIvLvbrgWCZVJ_eskImCUtAAm9wvNhBvzXmEsbK62fGApkJgPp1hIxEswHP6k3s-0Ojiq9uzxCGtKLkUkingV1_PKHWetyiZK43CYtBosoXRRCXMJo5vrChlWfDB-rw5mY6KOPTEP0qzHI4gzqf85p0x2XbvPqnqHk_RRavCatP9PwandC1Xwo70iMdPRck1yEzkCB0SIRITYlrwohzQc_wjza0Maqnv8WUbXFOu0QtKIgEYJmhkJCZOkiO9LlK8y-dABjzZczJ48bq8SvCEs4hyJ1Xs4ruQ_LBN3n6U6rpe0xrTNX0mhVaQopA7eeW2mychI-ftep3js-9sx7ssv-upka5jmVZlNj2RpmX0Z_k9vW_ybHvSAfUZSyllbhQKZ5tp3T9LnLiiIXIGGIAts-szwve2g1a37OiBTrp-Q9qqVs-fOyZ7Tw6Cb6cJnTpDu_iU-OmjWINmuuUrTl_2bPEzzMWm8owVGS-0IC-XdeBeP-K5A73KkjUrPaDjye5EmwMmudtm53UGbUKVk7GNPyPoPURe8JJlW3VglyADlb9nzfNwmjcCzipInL20HIupNewerK85qmbkrwqvn42uhPubciRNh4_nRyFBmWoi-MVRKK85c4F2GMqf7Fbs9BSfBVw_HyFBY1NjPhjIJ3_gZnh5T0J3ytf_9sz6QULqGewWzQcZagMroGbT9r8pF509234F6yNDllhU63YOoKEFy7qcu2M5LFhPgU--L5FKaePgZRVQ2DYmautqHnXzWHCZBHX2hJVZD1KG22gSyG-bhMP8KUaR1sTngWNZ64UGX-onjCztUzgQ9miJBu9VkrCprHMAXDgUJ_m3zBOt1lMBm0MbAaLnNngi2bty7XX27DpbAHzrYTHbpVPlc4cG10id6Ymx_FKmU0RIEoSKQbNGeCcgryjbyr7k9-3-2UbunQABxLR1l3jUhJN9iE996-OzU8ypEUoJbxcVN6t_UA-EVNxfnkMcf2r_xRsaXXpcJgZz5Hyo5JmybhSgiBW3pERIy0sXkOBlfInvDpXu3OWfGFUu6nplKNLN9qnbiXcvuV21WxofgYSGpvUlUEyAFYCysA2TdZ9Bw2bgUQcBBlgbt8o1SIwZQx3glXDsFxuFgL4PVF_V6zZalDmZX5tOHAF5KdZZ9zMB-hxliST7v7ROkrGsHExA3IsfmncGEtX4zGcD33hAJHJwLynRZoitHkx9m1a2HbwYUgw5OaHw5JxkfidcLcgJC_QUGXD7thdbDiSHCygUcvomKUG87AmhAiH3sb1H0f-0S8L04FxE3Es3kAE2ybuq7MnnjRFllK1fh7bHUoId8pYWmejlhzY-BcK7YjguSQnsDwqOLMC856CJ9fkO8Unjkbp_bfIGQqMcFHPQzz9qqBUU2vgv66vFTYb7XgIVyPKO7FMGnE5AnHEWjU2ceIBM9ZORrZMUUc7hodTgrXaNH8exElBbkbSjqS3eoIn2-6epvJsr3mfLKNd3Zylwd3qQcKpE3M1cOfrAMdLoAYxZj8MiuVxXRvEKXVfishU5TSUtwUUpZIstxX5sZjK5gMfdIXYpKO7qW_Zp6gX7cDEA4sHjwBIbZdX2CoFDJDZbPOKRqVrpdXbpakEiSR6alZGBNaVtD72bZLlBgbOJxZgLwNKr3bvPLeB1eB21wg8ekgeUfBE4pm4zQYJnrzwuyOD6_Rrc8UcqcJia8-cAUbdsUQtCQlLFwesiXZWqWWTKN6f4Tiua14gWH-v__HO9ijUBFu5iD6rACeS2iUxeCO95iSjHh3xKZIn0-KzOwRheC9C1B6ZPhQf8s4njPlsI9eYMp9oTtbgi4ShXL8yjPuqlB3e_TntV05cPQSz-NoTxrQM8nrhSCKEkR4HshzSJs-qmO25EPuFrYk75sXJwHdTWMhHq9euRu8xRo-7Wza7XUeN17FsZZCob-f0ztGC-8wFD7ha5aNFlHT8a6STph3sPKsByJDGIZCR7V_1sRcgTIv-HhtBlDQ_8HOCpZWp69FNarDw45V3jrw7tL2bQ5B5HQ38q7Z1LJOTFkxGyZXlOraMHfSPH0rcihLMAIg0FI1V2eB5UQVxhOQfZ7BM5glvv7GNnBQdDqOw6AitTjQyLVwlfDZP3_7KwPXC_eThHbVhOg9dXOmuYLMjWilyn3SkmeTMvZ7xGuJQpQMJgseSo2RSqZJJxjn-6vT9l0a8HLvg_hSMwDYsRp5nkgmsKwbe943L1FD6Hj3UIIYM11B_W5eOQhavEXacnCpxu-ogKTZmQDeUVAuc3Ry06ApHpCK7K_M5QuKNclEMk__FHZBrirI3JVkR_wi0ATEI9s0NzBFltA_ab_tPyes3pgwnWPNuzJ6VoTmIf6BsVU118s09KkKV_2TsfWR-OaxsBeTkDzTRtFnSArFc9gImF67VHrpKYnP8V4lsX0xFfXJ0n9VFKz65gMpgRtJ_80tMHg6i1wXxM1yKzUaRWR8f9qPDuJWyYDJRhNUTd7AoI2MDtYaJMdLN9SjpNv1U7RKTDjPSdWY5Vogm1OK830sqCTHEGjDIsQtfq_mprbQ1FoYP4fg5xFwwToiHl9ekLHXJ88N93wtK8BEfr_UbRZ8aZPNl6mBYTpyGItPzIvn9IeUlDwMqb0LOf2vGcf_wLXRrn1RSvbfSkDXk_xMkO0Tnuw1V6aMFjZeE7U9YLcKn0cJ2mV9g0YsckVH8uqWlMmoTP9C_gk1KRjoDA_jzzCbpRhTouAXbsBeOKlPYhMhimV1rMkemyOIluIW7UyNijWQsAEnvirg-1Ex5ClpoiZv0FnJ3XZPo-EY-3H8jGIib-I3t5E3oUBuCD4h08lkm22BwqgLYqkaBSKHx9jRQC4W71OQE2aNxcVXzoyHMDD_TLA_fVwaYQGH1AHNjRrZNvre9lMgGOMb5UnE3xuk6qKM5frbODT9K9ohUjn1tK_-YQ1Gl-grMZ3DCUaEfT14U1RX4wIqLlNLBw4vf8HlvK7IWwB_ZTh2FZWAdAxiztTD-Yjyr6RRPsKWIw68XjR-5IgoTjKNq-YxxxlGI83mGTlOdbQUwHn1Te1vudLupIYORbp4C2A2xcEjcyy1tFWGBVtiBKk7yps3gA8AVXgDofNxwEdp_4smsMtLjL0BSFSOMJuehk6ZHTSJWnIktVAd6q-kW_jMXcACs7rbTaoCUvyYmeugbDdQ5i4gShvVoPQkjAPzDuN6YwciVCkHEwRgy9kIJOEtlg81dxmlkuCljYdFxoA3M0uDBTnFuxXIMz-olrk9aDoyOuP8Mcphj2vCmyVnY3seRTwZereHNlvqtSOdhX1JK5sA_b9nPMrhMpsJ_9vl8JsyEx8kxFWgsZHMOtDbYtvXRf4il7OGRjIqUR7FN8fN6vlzS1M0idXdLFK42S73m7CZ29K0fxXiWTHKUP3GOonyZ83skGCtxynQNxLTLFqMShd8yKbkUfJSpKf5pgq0c5BlBshWVXsmy08FZBL-5EU1XXaHPaHp6nNww5OfxguTJYw4nXBZQD3A_M9cyjj4SmMmxpRgimmpMMGsdczLVB13eSORx3vVtQhYhPnfyy4cO3ReWBuMYCG0HM9VhD48NfCs6Swrg83Sr1ujcgRUIGcLCQCy1sNS8YLXXUpSFr0uwyIuDm1NMbPqgpJDicZGOlA6e75DmUNM6E8x-SirRVmj4foNvmo6i1QPbbWT7D2q4w-IXFJ6ZIMZi3cSd7HCyXzqOtPjTIyi4xz3fnVvDNbfpCwg9fRK-LdTsXF0_mHLCegwJYykDOYO75405gISkAxrQGzy6zaWkfgfEcnxKGxuEG6nXCTx_g2SvDVH3IfFfjxFVCnzmVUpChhPf31f8MgfszFjyZTy5_qV5RA3RSspt9V_oKbtZXcUDZvogwhKJidjrc10tKl35QZwV5pZouK27nNWdOnzezWsUBpZCR02PJWL-xoOpLMetIP4RFjqER1o1TsBavV26ZBjHF7TqpIEAufwVeiWnm1t2hG7pxJt37ewaJ_tfR7FNnOIC3BZqTUobJZRUaPmEz2r0NLOy5lj3OhoK5LrkCmkYnB-mCRudavtshT1sIYXg80ut7yWrhZqrOqtHUN8cR9uFKsqFPPAgeKor7TpJm7tHKiYyKY-iUcYmh8fT3FgOuj8rm5Jzd-rJs1Hb8WGByhjCONogJzSBC_1hgK5t-6LfXqhQPxlJLa5-d21G6wNKrA-9O_zbklUaE4KgAjvUHu6OKXifhEkwq2VPBp5NmJ_6zaNdhn7l9I16-KNT_K6s812MvkgazaGGmjDqYYq-P4mEpwnG5OcQdjswDRV8hlmHh7x_ZAJMuZGTtNN8aE9FOwVEaNNyDFFnwq1dLcv_WnpyReOkKmKqyoDb1ZiBMk9kLxIOwCIwodutjejGcFPuYJQsFfmNySQw_9YHjGvB5lbYRfjD3GQSJUac2Tan0-MrZ0ImMoLQJEXTNH65d5nMUqLG6OzwLwVmCBi8K3YOAxwu_ekZckGf7uc1lryjFQgMdSIycNS4teBApAxvquOB5BEuIJXpYbZJwrFG659McNbRDnXeafW5aogR1foPTzwm8an9uPp_nBx0nGpfyu4Xi86scSVVv1uToy3EX5oPwM-8BkEHdVv3CXvnhba5jdUlzX4yKTrUzK-1YnBGGNOOkrAipV8NKFc5j0Trr73AZ7GDasV6ezqYgLJFVZyRvdvfTAUb12EbwvUC1S9q7avmROL4U_yofshWaim-m6LJKnrgN7k7bj9eIn17jurrCAIzYSjSZdpPwnT33icIvG1fv57xX7rAtK-F4fZqfDEpOSelcsr86mQe1wQuB1951Ogmp6KrLvj7u-lifwwWtV57RbLvlg9kjULoco4FbbPXYeqZG9M7Hmj97a_q5rrHBGfGUVPfL1vjUQIinMqPRBKHG-gCXozBCUCBxfyrjlhRCNTPgz3uRednUxSwUjh0JoelGHlihzRSEuie1z4f9eAktS0232_tBsHKFaudylR1RoDYDz4f3ogl9i6rr9OjXSdgcs_KaTKoh829XU7k5I66fwhhRsODPLojW4-OnWvOlLjq2XIdPeQ4_z4Cb6O_PYK3BDucuE9P-qpfAalojCkX3veZqDD79aRE_NzMNVfSEFR8bX0XnZTZjKy6U7DzDil4-DRJ3QPoIrHfJ4haGG2_wSIzhxlZ_vCH8R1G1iX90M6OTuKVbfx4EsURhzZ9J-pIiMelPjnrktw_sOQAawjdQg6TYstDl_wgV_K9Kjaixm-SqZts_fyYiVvLxhMya0welPIejKDKn7uLGOd_Lm1HmrLmkE_xjXWEkTO6wWozuJNwSJNZ_7xvEYDz4PtXfgaEjaarkDz-NF5392Uyhyt6RhIg315cl6KfG2-Nfxj-ylZmWoUmiM6-J4PKK6wzppRlWINzY64BL3cG0EZR-8XZDJOOGJzJhcdfmj9RinUzYvagT6DTtxa98fM4mPvFl_Ttstkd_eOsZgktv62sk-PK0Ae0AW1Z_fPXRMKzaAOYFqmUpByxDqwTNNVYeGoNgQ7nyzlqKZurZc_TThSEGZqAiu-gXb_CdiTR-swEsZ6jVUxrv7SVgewHgaaHgGwwWz_ScWDB1Jp2PX-aV9pqYOy6rmPBbM5VbyE4AY67rzrPCjWKB-fRLM7iwyLBg9OyHymDOxDH1yeFSp_mmC6V-NYJhqP68KVMGaj0xIhoGpZV2y1sF-Wb_tgGeC3bjiNRlXkYgipesoDcDzfCQC7CVherrMXmeltBhegog99Cze1PhY5ZdURdhwqLRyedeF008Ro8f51MIDFfpNW4DBK8tzDC3o3_hioAHPTjaTgsf5ihH_jhGmF0Rtx6lvA8tBzNtxPHl_7mNsKTaQbkRP5bSRqkFQxASD1qwEffB_UyLMTlGgFM8FEFDauyDwx6t6Kd4yOBtZ2KNzR5FT0rnpzaPxrMmfKYp493vWeOwqy2n7JIVWd6M0zl8henUoDTtAP1_BEuYapC0nZhZkq_zMDCN2H3E6x42ne_ov927qKOObop3CG3-5RsGbF0nDQ8qyXSEZfpGJLRIoEpqpj_fyXXKC37BubOveo8UC6tYYH1MGAe7-gAIlBoGNzFPUWQcjUfjPGgnXXGqenGIRz4SfwSJD0SncEaxZG3cA_pFPDi8gkpHqpm2jJbO8tZ3RSzKs73mb1buGL7gVpg3Uh4JmJZUIQXkd4dGU6YlzoIwrUCbLbyx2e9CHdrIK2IYq9w6BD2jBWT1LHILQh8bFxhCFiLAS6090KV8BnCYWjNCNfBRhJmG3c0JQxX5kKbKNSUKh84ox05dkL26q2cDdQC8_OAWnm6c03dDanZj6tMT_feZaH9MJCzzKuZYWlgA8BwCWnhW0jz2Z2z-HcV7aigwmgTdq0wx9ZNIjbdmFVpVVdcKCwF870uwKA83UIsPMcsEmEMJZ0yLl8Nw4aMLLagcHZvR34wC5Ko9mZx7_9qEco_velly-0Toc_FKXdTBdNjkKg1BO07tm1g5QfVvpmgDvTYupiZAtDMYG2NXk3cYlgPyjObXjFp4Hjfo0cyT2w5D39-GVwUARmuthiQnp8smBuNHTfK_mFp-Cv5Pi5k6sMb1mLG3cKCqfK9pog69Xb69b8sXvYMHPrLmQ_TYNISn1cHJQPw1a9rEkVUU6Lgj7DlfPQXvWK3bmtgOmnH57skxCF62CiHMhSKCP9it8GkSv0UNYUo5BRJZizp3wsjFsT2bNdcm4hi5Dyv-QkRqth3t1TsaKF-SQ93BreUEFgsTK62bh1rBBnddlSuUVRYE7nWTye3aApGyLyTWq0dNFjvpF6Ztk60duKgllLPBTaJwLvG_pKTIJad9D13MPiAcuEXuPraihdq8kxu-g47OKxsFh3RnA9AQKB5RDkmM0H-w8ar1S22Zv06l6wOvUUXqMrcBFDmOr_g8jwRJKHk3lO2sNsYHOfigKO0we6u0PZQhaeP8YICwpp_5yuGvbNqv04CgGpdsCl3cE9BI1v10SSwCwMkDIq6HTAgEtLFnCFhZ7xValIjzUfCfMM1p8ffuTeZQqHIwfpaUVFxdqUXZuXtV0n_srUeQTK8kf8JsUw0Yw_Rp9jbOLN0tfCXsrVhjuD53X6U5wv75QUkKW73CyoyqsNxkmeo86NXJbSstwyVZR6srenbDP3xCsdYQN5V-NY0n0sRO02Dd305pvrtnORzaBk4H2jdW5MumT9zJAIDDYkSvAI8HmjAAaK45SdCnmanziuVXlys1uY2UFGo_8EkM5TYbQZMEOaaPJzdOgURvp16hQzr0DHh7NGTjdf4VgLMzD7ds2YZL8tkKOkd5qNn_rn2FTK1kFsCZyDVf0Y2HIs-MD8SvNSbPnfuLOXW8c8swJap7HCaNieFMWuQqrM3N6C3iZHw5gyQWzgDGCLqLtl-GAtA-T25VPia_2Zibvwpxqqo8i5RAWH35VaHHSfeMxz0RzlDt6Hj1VEqICN7PkhQVItjxWktFXDwsKFceigurrpsBz1NmQGZ2F-m5Z1P1WvFqpXXdp1lKKkhvHqcyPsd-jM5Up9tHAznLYNIr7xQHtlpeXpTXhWRNDX_UqN3S8Hv2HAtg2VyqxEZjN8LMW73Qu6-7lunrz7xPGlXnSSB4XgyMOeNURt1fVw_kBAdvSvl9IYwD5cyrRYMqxivAB8JkGg4gvevx3-0cdSjhCkIs157ttBQ6hAmIszFgJlPGmVyp3olL_kc2Xducsrh-Kz18Bo1Wq6R3rP8EpTuEokGwfkLHh6-np0PRixsNNGyOo3XX6n4hbO_kK1LKR5lYbLsWlImt8HYviV1atV4V8T8Z-0o2u-vmXzNWQvoEc58nbYEG02cJG1LlY9Tq7-GGkf2ZNEY_UeeGfXtt1XrJurS2vpBXoNKyOiIhVJKwVH1gYP05aRGtfJ0Ud1_VNjhClLvPkpybJnNLGreMYLG9Qy6cFTxSbQtn_cq26xl7YIXSdbW33S2F2YJbkwSAMIOu7qInnHh8NIjZBZgek4gFr0VP1-CZQc6ZJ7Ay4pUjeORhi4jsnAIPtzbYrZuFVJegOGpWh54kPhgo0b3uoBiuIbfeSGix33AQ98CW9abhcPHZ7KJVpX1FD6Nj3e3fbDdXOv-FI4uwRXl7ltLZGdj-q1sH2XoHI6PkLQdELv-DG0F1lII4manMuK-NWbP1kDne2zEMMFXQcFUUZB8_lQ3U1pUbLY3pfJ_ENawgl-b_Pv2pk01lDizbG-9NNql68UdsPe4xZVlWc-yGnrRzZzRzFiefKrQ8AN9NEa67wnGj__6neaPMGjyGWbDfsBtSIg8tNZQrGw_1x4UqUnN1PT9rGYId4nSwAh7bANZ2KZK9_5b80trDqnYgjo3CWUXvr4AY2vOmqtpAcwlC654SJJ9BJL62AHRQ5TgQvu0xwPBuIfhcxM1GN5je0USTwpyENpeoMVKZ03CRG-x1djgI5nEiQj71poQcMdAJamr2meqgzFk8Fj55JMz97JQOaM0ncnS-Ra9YvCb3MQPHVQhWKiO4eW7Jpc-0H4ViSEMm29rR1s9KGnySOeWs-JKOseSmWeXsWMLZJZfWuNt1xH1da4rlCmMgP3Zr01Tg45yjyRcY-yv_yuypxQmPqkvL4PHF5r-IojcJ9RBAyH3c-ydmTr7ys31gBoWb1u-pXQ0ZQ4yuGnXLbeaX6FAIHq6_uyV9cZlzHj5T1tq7yyAYBgpVKtePeS0b9K0j58yqBm4msFn07hdGVi2c3AzTm_BRjogCPIR-MdR5tSWWB-EaJXsqlCXgO7CRcKGSJOB7ncMZeJIfj7dDVToE1qlmW1qDONzcB_ZVKWQcT1Eta7noTRmlK28dF8ZobmkuXfOqiIFFPI6Nds8gvg59PlbvxyHn-8_K3WeZEi25u5mx1cFxdlKAkLwTeF318fYBAlZVtQZK7tI-dEdTSflQG_XSyImd70OKDBG5EqrA10SFJbhVk3KSGc7OGRrfdqBv43E6TmoFpHdhok203byOoe--ha_TaGjRnnUKDp-kFE4OC9DuGdEOPmxojEJVommNMhBS6NtMqIfnTMZYryKtFvaP4NvSo1YlqzR16dcdtzVW919xw0e9nF47eCWf43p7Jlk6r4EM91YxwMJu2P1Cpk_yRRV0z9lBnoAH9pkuO3Uj33ErO-9Xi7wBWGAFm1Rx9NKK2NXMbxx7ErX00pXO-xeceoU9HCpBc_-TTVRw-zxXH24DhWQRCX4JiR6XbS5eTNdtH_KtkA-J0L1VJ60kTZdabTs0NX-XgGPrApSVX_wQeUKeZo1Sqlz-dTVACqZ6D4POI0-N3Cr3Sm0eMv9yiF_CROHCSN5llokJAtH4OCX2i4zmH7J2z0pGiQS-nmA-OVQH1S6AHFZZm77CFIUqfXXjJXO81H6Kp5Se2Ehu8qRNUjtYcIKzDw1inpcM5v_pL4EntSdx4TWdVLdcFwFlT5QEopvrCIhGCDntgUVVU_JTYbcCUny_Th74WmxYFgK8A0K_CERiQSCKkCNQInYpix5eeDpJk2y3TE8sos6NCftPOUPU1a6EK0zEaw35PeRpKpjstYSy6YhytGK4ezGB0Ula2r1c3BcIabsuI6R-rdfDrK0AywC9KSgB9XX5r-4xTi7cMzNmNjVH84jehGH14YApXGHaMfpJ2ZcYfrXbLlEFrfuYG3-9S2hgn1L7hq7KDxyM7r-q8vkseXgeXkdcy8W1v8n8IeL7H-1TTOZE6JYk-ysSYOW8yVX0RuQIwY632NVk8yPVOsRF4Z2u3qzrfNgbIXhB4BX3nLOpVQkRidagWrtBmYWkaQm64Fhn1N4k2EgzEvk9EkZD8_kx6CF5vpd4NtYYesec1AqdtIuBV1GUZgg3Hwd3RYAke-AD_1Kbr9E9wnnnKOlm5GhVPR-0j6BalHmojNNcsKdBH86l_nzJTZSQFnM0zL45SbZ9Gz2vM1nr4af1bBp3agJzQH4tw6miZvWMiMCkLVrUrnUdGHVxNe2u0bZuVnWBdR0O8FRxA9s8a_xGOCVAmb8aUTaiL_uBFd9Bxin3GuDckqZBTV8NnvavgEzXAxMBohCrgY70qc7ncg_gMKrXQzFutRDJAT4DrZQWFe1iZCCYjbISuMcSLxCmBZhFDSgNgP2LyIUBmWFMHuRLVh9YG6yfQnaGIr6-pqec6Ly76LCLnqsr6CLKL-vdewfTluI17hizwKOJ3hsqFNKUXxfF8Hd30Yhq400sr75Uy-57mol5AQr50ml25PAvJ13sXHR8YS9XeXYwEYRnu40xEsozD4RaQYJg7yS0r9UNliSFeuPOmpA13_XShI5fft4CfqnBNwmg6fDW--3NFiyaiVp_j2WQ8tKVBAS17CrHYxzO-ly0StaM0WbeyIWO6_YuNVrJq-d2DqvMI3ad8O4bpywIOTcrVhNMmbbXrktwrFe8E0BYpKr10Ua7ODoOIeF1YsBgMQQgHOTTdz6dZCF8XBCY2-hkgJTCd6nk4TJsIFVyXEUCRtJcasiZ_RkB4wwAr4NxXhbKnNP1eznWvWhdG6ZXAL5u13ZNar1TrtwAjpHT9-D7dEjKhmYFpS5FGGk7oDgUCuDFC0_wT7c5HL9mQOyUHATQEtS6F_oVHiBPalwrHtDCeU_jfwr7vTeKcloylTnhDxekce1ocOCM84a1x89SVaF6IsStGXG7eqQNEKY37Mxi9YPiKnQJ6GjXEtaR71fm1Kmg6qyH29-mrOUh_JeM3zARPkPmmPejy7hR4c0SkMSgmx8LZlknzt2Xsg-o5xkBa0JcnVXZgg8AS-4fnnErA_R-Bz9U8oWgvo43jcrbDsby8BaNOvdEI-fZKNPRLOxB8KLDRsA4zQ8Fb1LpuXK9IowxUUOsuJhsaaQFovb8F5Wvg7gZRIlo_1uURRvYQxF610qUFiwXTsA2gvwuzIPj7WwVRhILZ04YqqljeytSn810rRNEXZ0G0yd9j1BWdgHJ456mZpl9RQC1YDckjF0rj51RSBQEqe50ZUBKZCoYGG5WeVH8epRwJrVzlGxVfEtilHiWwOI0eFqe6anyIRH1c4eLDeuh-dyQt2ufbwqSXT5y5A4WtzO78QoO0qGkmcUGzW1HYS-f7UKQGGBQLv6y1_acw90Nv9p9kGWLfiSvPG5mN9US7r1HQxWIqzSOunSoRvFm8H1HmcmG7zIlrdD3R2KVutsjVNPUkod95SOulMX2UAynU-caNHc2n-EF0Lj74_c4gKzPiisvfTq1HU_gpa29taFDQMBODyg27V1fWX3NcUsNV7UrcCqa3JU-pdjhhRtj45VXvn6cfxQe55NAaAwODmPEi25hsemXO7rBXVsiXxB6K5DzIO2ZRIdexGh9FLL7kZ7JaGgRavb3BcicKLYiryGn3ZR4KIKbl0QIxKPQ08dQk-9uFiXTikXYMhpdlP3LBriqIxUWn4bz4biaoIavwA98J-8A7NcINIfEA74SMHfeEUBS9izTIepUKyntSe6m0dbMFHLvYS7HtLgui0D-St_weGHkp9w-pnJ2c1YGPY5q9vXKE9wlfNhGnKPtYfRamNTDGCMV2k7ubRRfVflVB9fxOEDY4Sih52bUSZ3CyEDZwjEPjRq8JbUs-D0I762WopvxKTpFh_QT0sE5VKdFOjwAzlPP3T9H6Myj3tI48SiQxIhBHXZdBSFeGd0bHDuz3RboW3KyC3B61dd8uSYhzx70fqQoVJmGjEdOTBCo3YKHDJ2AK7UeS2Fg389pnSbSqOLwGMpPOL5H2zTojPEJqmkfgKBlyVMRRGnoBNbY6SwiHYSyrbRDHEVGqi893GZ4JemQxSqydAE-dJsuR6gwUKraIdOLbr3kHfVgsHkBayBszWCgaZ114_eeniTL9IBg7bYtBeJbKcYKPFrCXfzEfZKFa9H1qcO4z5YMWZjMUsab69kkRzd07VDJQ9XcmN0Cy8OT0kSRyvU0EzVwWYKtNqLW6GhQPLHQ6XX1CUZyQO-SvekMP4eifsat4WchHCgokEicxTS61XgkDSETMmrKuKC8_6roK-eq6LjGk9E6Zir0x8ckmh-PFN00wz_ni0zeY0TA5V29ptjFLzcRRjbKEZdqtoNRgPh0FbuAtRtMDG4g0tflkcuTXTkfHOX-iZCjsEwHaiBxuS7vtbA72UscTEjjXGNymNjDpGKPOq6U8wA0cG0eOnMBeSsT5clQNYpTEfKga82piRpR0VYAloCaitNStggzcnK4wTUNH6NGhQ-cXW6FLV93QuX0dthuMY_QYBPxqRdt6A6djQgrw8rhwHASW059XY9wADjQXXH6qCJqQRcHYD0Ca-zQU4t9JNZCwj3VXIvx9e0O_yWrtIm3DbRWWm-CEWFXU_dCVYO4jS2txK7Vz5tUrFuymivA4e8Rav8ROkQrhp05iacWtEYMccM4G84rt09EacGZ9Df6y1PgbsgPpQBn0qTNodVQ1qmhyTuoX9j6_V1EJMD_pl5NYgzWo6f-9ySSlHmmMDNaFYNKF2OhVcLw4sOM8u7CvvPyPv1_HN_qZpfzg506D0fITWaCjfdDuapUiNh4XpItOJkGt6TNyvMTViiZDWKDHtF-udfh4WWXsT59B1cLivY-knkVWGUa6SeuEuNlhxlQRd9jvu3q5wbjPhLcqXY_ntt76g0pLpu3Kflz_tTXTCzM8SohoDynxv4BLJ8PCQgxuIsDKJOTFk4_ieeLoI1FdHmqb_jjacy08Sd9PvZzfn7dgTIvvapFJzCTIXuD_pRVoEVltzrCrmTF96NjXj4nfzc76M0b5PVGrWnV1Z7jGdfAwg7jrVQiVX1fmKhw9B09JsTLH7PC6RFXAZnVuQeg5L69CjXgE4K41-lICVT9qcB6p_VXFypzPytTmIFy6CtXwlp983GL4L82ZBXD8N2WFFheywGjfF0AaBKR8UN2AK5Pn4nZlENfmperTxmEZreBuL3HA_diNXEZBv7mam9ze_HlxG82Q1RkmLPHjRYA1uLFwUmJuJFOK7vLLs7Uc_ZPRiSoHNx0bDVV9Bmk_8EMKLmn2Yit1c_9CDc3Irbv5uvXwrPm3blikiLqJ0cgXd7a4HmxCy7p4ajzWC34x7UK3GOX7wf0ay4ClRB35mrzgCq-OO_G3pIeXmq52dDVe2ALM3dmbYnoCtvxzb18GUB4wtg_7RCKIBLjMkjmXW-ULa3PO4ghpnBEoqYZQKt5PEzpO1TTqhDZo0AqV1FJQ5elFKZSE9LISapUClvTJTGeIsBZN-XV8HGVXCEjW-c7hd0jStu59j418KWyfX9eCkApOIidDjooAU3AuUn5cPbtECEsiC8A06DdcNWnaHfD0peNlqZVBu8keVjU3-f1mrVup8bpaPW0gJMSsjpqD31s1VLkdefpmV-KiNck8qGr-LA5G5CgMUOI7oRqZNsHbiFy0VZj1H8UGkc2Mgac10murKIOjCa7WYwc7WZuVWTtSQIKxjarrPlg1XvlJ-lf8P5FpjfL8RyucdRN7nn79pJ1rxPmurl70KWhVKAHEXkpR_15SeatD-ExJxPZkZZ7V-54x5ogKeUiOllX55kioUCSrH3q1R_EKMfsT4ziOBrjsKDaBZAHuYLXM-CqDB4ref54z9UwzBWzg8KMszJ0VAPE5RuAtKgQ3pHCVMLYiGowYMXQqKT78lf7zFfVv-LTGsXt6W6bqrHVxfMP08OdnNfvWaKC4LWoXnc6l4c-SFpZigRV4vEbJgfXNLNN92bkoAcKw5DKsPLWzm4LiBePvLARZNsnTfF05ZxGNxqxb1ofXIpUwoLWPmGzIM1gIVHRziPNzq1QRKUdrS9hvnrXxexodAZW4ZDVEwqPlCD7ceoS1xE8M3hCCwnOAKt3VEhnnFZqSL0kQccPp2GVF6O57enVhH9FA__N9_psTMJz_lJzAjZ1cQJB_2EzW1_vxAWP7GDFqOPnU7Z1T7548K-aNAZBbHDNSbwgtcsX0qI7XkmyA4XQMIUY5nMM-wZQ6oXzIg8X8T2XA7naBN8k3kRsQuHV7jYfrBArDvWdEzxhlbXolxbnQXHtiF6S_vPykMQWaDydhYwUwQVhsw-jZg6F6UnzsEFBnmbT5PpLVp44YcyCBClFoqNNUlNj4y3qAz1RscpROMQWs0FoR1Dd4j0jnHYR032Fhn7RisZuVaDzPwMDBFqDwoyluaim4_gtGdMYEKnLDdgFA4v0jDy5ngsuejuceuWENjJEqax1ThShK4rQwb17-xE9PTjqe3qiLIOvHu8xmf6cz5FV3IqgvRJCneROBwO1GqenNlhzu4SIVhvy-Bejx5T7VcRhzbAaZ4NMHO4t7_vNJIvlRnHl57nc2lgjgRh2FEz86bBcVjaoAu52-2qjAdy717u8vC2hEMOb7IkQJw8dXuS3lg8BLYq6QeVdIkflN1mA_I2DBBzasxSLarosSkim9qFiqHWZsK0izdxpYRnbk4dbaKzUHD9qStNKdYRhV4Zgng4TB-GvpN_qu0rAU8BlrySgovjHq6OFPa9PeTU575bN2cJEVtc-RI2TFGQSKXtFgasBs3DjdPQSem9EUM4gHioFc5830vhxMRGl7JcRkOtypz9EMgWV1e10nFk4PEFl3FJhvG_tbUDYZLfiL-6ZaDUu0-qUdfTnUl97Wvb7BbBWPO0cIpf7uMFU95nIRo42bxB0nicqAR3vyg2pyLSlM6Ms_IdsSGY9L85KymnRRAGDezFJoXTGjN5NbT-5XTvxYkBAqZeTHnzR2UogZGzHpNV7GddYo71LJfvhF8pObDe14OQEg7soZluqa1OnZSqtBBFWuW8Q93bG253_nuv-zgL4-aIArWyM2kYMQhYVwgt7MITmGJr3KMeVMzMRXtQM4rfs5KJbEbKIfYDy0PplMjJdoqIiLenBMI60FuR5RrJJna8yfzpWRgCFIGdY73xX_LIrueIS-arKGRi5XWL3HhyVM1LMYxZhuzCNwChsaY07J5NkhVd9RXeTTBsL6Ml1dLG7WZHClL4q816Y5z3lmF5YVeONsDtjAudr_PeH3eJzgTpBHBS1RmQfO1a8eENzjLx1jjxe2nTJaEwJnKdbHo4R-DtxCdKoAbcYIES1KWTd1ykPBjfsgkX-EMGw1sBF6TrPRUlTpERSj_8Jo7QFoEAOrX8nRyJR0vKZz6Lk7NoZ0dLG8yQan-rO40qY263K0kJn7wnyBenQwGWDIaJWNrn3poOKQ4VY_o7JvDSApoZ4E0q5IA-9kqBIjQYyfRgfeBwJm6fGxyZjHhQVb6N6ou_8b3wkuhFKeMYcVwbOIFgvOY74dNTUqRFRmGQde8iqLM3LD4vipPv33Nxioadq0ru5HcqZLj5a9S6vixnLRuU2xMC4ykWJAkix06CtsUf_u_e6igpyYH5jkcTZh63TVF_8ygTVDq3k6BtaKM8igqxFmqbfON2aiqYFxbQPltL2PTMnSQ3mz5pm7rLEcCfdAhDwrmV8gegy7c4H_EanOCz8OdyKFLMK8DO1VDtOUAjq4L0Qyr-bYLAzUiuDXbLXwLrk3DUU4FUg2Emp_xs94dUcPmn_97iTZcvqvRXz9e1_ZqLsOqbaof_h4qJJl5uc1LdqDeAD3xRS68AtTUDClhARYfPJjwQcGx9YWvhHQ6FHsxhcQI81fvIpkvuBcojjmxQKkGiaQK6co1o_wg98t4dlVMdyyg5ZtNMyCEBLk5N4oqId8QVoSLPp1iM0UXyDlolTSHUFsnBNV9ORe-MMX-GlKOzTgWpKmU9os0vG9NQtsTirJBqHCtheJbkuyyuk5OCPpzJ_EPhHo3amZexaQI9gDBPlmQNvpswfuiK5p-q2hqDYMdLpBgN7KkWEM2tzlYidrk1kq3uiVk9DRr4JDQtxL3h3y37EnZQcpI7h6SSpHV8p60USaXrDGfeJLra3rOodQ2eJ64CuReAbp41XdfTv34QFDgHvQev1tpa8aGzyZQSkV9OU9s_vCrv3_dM6d0nhwHuitS3m8QWr0zWMnvSKd-Y7iUpmOMefO_S6WeJ_GPVPg1eIULyynKWUpk1wEQNFF4elOwW0WFPgC1h_3_XwS_osWRq8Yb5Swp4kHaQfFv3MaVgtKHs3_PHIB5OJjKsBojh6Mb4W1ym_n53qFZfHCyKxDVx3wYT9dAAxffKWSAeZ57y9zw62c67tfhvc-nS3BxUIT2dA8dSvXT7Kg6YD_OuUuNrfGf2BTxz6U0Q6ZSlo366dLlKAKD3Bgwc9A5n2xF9Yr9dtn4O0sALtRXHc-nX3vtWZeydN8GHuzD1mSXzUiI4wF9gjdSa5TGJBrDVTfe3lJJwr_PFl_0H2AmeMwpIQJU38H3RCJtsnswun-hQ3KMTDRDdMzonI8_zjMcAA12GBW_Uk3FlAeYwuSXOgv_0dT_Qn6QMhWtusAlAJ7YD9TppkaV1SmT2_ucbvRcPO6NM05Wz7CPTDlMoNRa-Gc86LZ8WUq046m3AnDnvGk2moToBRWUGXTI92Pg750bc1mbUB5VNI2qySxMb8LN_HUQ6Eve9JVk79Elh7rX_JdU78zIsE0RIviQBPgCg3yh2JdwmDevQkkEd_QDw98sYRpyvMzrWz22TWt03sF0qlbF51ijW8P7T1AcYNHc4POAhbcgZFLdiIufxIa9J-smQVlg8dkaLulkyVrXgTsCzDKE1ly7RHmuWtDTv2u1UyJIgwoVA-ggYyKhBPFtaERSwA15xoWjofcP_0ZgGZ-KUF1GbCwZDDnKmL-VauHrjnrLn0l3osRMc9k1ftFBcRWMjRolVCuyp_NcFjIUFGXBRGyPpmEhATMRH9dSt14HSJTUmRV7Wu5VBkrZldhJmIIFtkWv4g3ynQ58UwHvHdZHXfHFcOitIb_xGs5GTpba_dsnwyG8BR9AqcBLZ3ihkPfgZ1uSTwZdJfE7khJZ2WsoDof2Cf9zDKBgHcjJLGhPDC0m-AqSeWCdxTYFT0k7ajIHy72g3D7KF6HFsayfztTmQbvQgE-XPSV348CTvzwV1BwKvjs2AAV8x_BTkgJ_p4SZohoWZDPzMpfW6bGPpq01Bar8IA6QLtCuB6oKmuh2i40ZQ8ohVzz0cbla9m0IMP8FKFd_DvmDecG34t_rBm-sl1VOySKlvgKY_g10NeFajz-8cjjYw7w1CSIEt9qNtHBmoBsjFFCX4vVnjhIolPkfewC7_qeLt4KyLV32aOROYGgCijWKsl8OaSN_kjMYmqZilSeRSppYfNYYm_kdD6ayomTimhFQGH873KyMXdftjgvd1jyAIsVuMjDpouiLVr98VZc7yKST9RADFLL__yhgt4KKnR-wnRCUw-EMfwKQ2iz6OGAwni5_5nFHh_YATImUH9bANvgGzelM6Ejg7tSpdkF91CHBxhjq0MEPuUEzjDak0PYKDHzXQ30vgH1ekijWmZLjooe1YnmNGlrSYGIbXyBNfaouqpuFfAp0_hTZlLmLwoqrGiCGyxwl54lUoJ9mtMmcMwYJ87TrRYkVUOYG0U6gGHJislsNZznnN49zeLdrXeICFR1rORJhJF2P1K7cspttgnOqTMRl7AJqJJLsCPPQFQRNEjYF332Xv4uTRyHp18z4dCUV0suhkos28Qj0rzZL0kgUV24XbyIGQhIsfNekr1rMo8ORwoMGGH5v_c2-NuqoJKHXuoT2zowJmWTgS9q8nokQl7m3FIRs3N5qlg3MecYjH6S01vvYqKEvc9FJB2VDucLGH6Jaw8ZUnz_Ikgfo37TVps45pii3OJcqg2IxKCJDkGGXIdvWsUzfmsGsltDUZDCKpxYPBk_VrPRR14Dd9KhPjtLnD7l3YIuibOW_uqkHz8XJBjmIRnvQErIbrbqFxTSep7RyeOHbdcGYaBDur_fGI-Go_CXVmnZRADbfc_T-nMknHsV3dAfl11cM9NEVSFyFBC-al1pbzyi5rkpVXAvN42k_hy2-czkkACbDcjT6dCxZ2RfmCGQGHoPcTLhfFHioA1s1_ZMbfAyQi-3AfjptAquopr1BsxYP5WAT0HTr0X6Mq-tYs4P7IquduV_5G_XBqjiLCrJR6LeVkMt0POOES787iXvn1MoeWcE9hMQz1FzoSsYytUm8L555xM3qzv4QLpqYR2r02MZ8RDmMhKnAuMzZ4Gwk_qnuMni0-6iNReLIJTecFjObJWaVc6n5ozvD0Mc4EfqNlhOp5U7DfQNtzJ2LM8vTZzCCR2CA80yRjRJVzbTHNWKzSzOvFYnFokFUxmQ_YLyYv8A2LDqhIvCJGIIVWi4dr6l90cLP--X7SO3Ot-ZeUc1pJ1wt8tfBK4tUHBIjcZ23Vwenfuz2jGPS9euFjzg_ACzehq9K0k-x3mEehNeX44pgtt4Ny2TGQkjoUuKCR6O9_3WiOfqA9XVkFuCZaBQVAI_Lh0b-KrfF2H_QBEyOiyb87cEHiPn4gI0PRfFToESBlmfJB3U-ugkmey9hVMcYk6xXFgo4dV3mvhHZDrRZmtb-NXeEUSPtqZohLvEG3yq_mxTmht3eGD57sXzGzE_0s2M-FM0MowauRReBy-NdmEcO6ax_Xfp-9rvQGxg5p2lOahcz0Hwl2iBfoLyIP1sTfkGi6_dfqNu-eC7LR8_l9M54Zcf4jM9L97FqQp5PCNrqorwoqzAfP_6CKTAW4rrBprXhOwhh_TrI1M76QcwvuuCUWcAEafPiGCgat5QczR5dAEvtrp9yxM0YvvFBH7Xb4UMQxb720maLMtzpffecEhsuDLYJ1YQUCbjzmPfxJDwEViwmmvukeHuOnneMZD7S3EH1vl_SXjpdu49jRuZ7HtJZ2e0UDp8jiACwbAq_5Ee1GtpDP6uYE7Vl7eHLtahU-Y_4JO84pvBGHfXl9dJiUSAfa4IdCF81bSquRx0m7CiEj4k_k7uj8_5GD0Lhiygtj3ibJOvtD6uQ5bgKxRJXBgLQtx96kckH9VEt64sD4qdIV36LDj4_56JmsKuDlMg4EP7fJ79J9SEsFOTFSOElQPCfLL_1qfrdIwXTfIFq7BVpSb4WnoQN1Vp9oCCR7HD54i3FjUVXeX1c02tflha3I55vik8PE4zAEuIHoP4jO_hjG3-d2RAlOyZ98Daig3DizsP4CQbOpNb0XnyvUIMApHMLSygZGMHtKh9Ps3RBuwthN_lus7hKBoNFDY7z2qn9f_-iD5rCws0PsfmrmHjp8Lts4X04QjtWqANvznBVIQf9lWl9AAobTeQ9G8nx3889F919bt2ewonGsXcS4HgpvlvOzvpoxoLNE3q1e5_ssUqXUsiGIYcNVDQ77kI66444htqW-nPRG-b1N7ieZmWSdXgIvIXsDQxa6c3sHhA04BbKjiNlVV73brG3pBuhpM-38nzWOSBbTx1rqBqwO49qM21Rtjo9Vxjejl3w_6gJBl46Z7JM4t_isR8VjwJOZprvy2sAdV_AlJrnHYvh5vOL0ad7C166ie_QpRro_jUdg-wBBFg1VsZSfiUq35U8owl8yW6az2C3ALbJEUjo8xgc5zs935_ohGKq2E4CmknFRYZCRvijuA_sbjZzJ6SFN3nxAJ5kl1MifzcMOfDgzZnMwnjwMVn563KzaQbGRau_5D7OxCqbNQ6JXI2n4-ySxvtrwulbixxnPgbGccjLWDQDQxFqrWl4PQwZgUMObTMoqJo3Vs_3oh0pqTQoDFQkkPFgrF6O3lEXqrGlbtotedQzsg_kaKK9ZlRMzkDAhFjLnGxSrAIvYphrPtwj3r02xJS21lR3WwnJaQ1nuZ90Vpt8tme6kA98-QZx6Ml66-BeCGXPZ0TKZhm7LXH8dsgI4eoaV5HrEOlp2cdAnECw-mtMGliu113boVw2dmK7871rQRv5QisV6X-_7I6Dm1iF_xBxj_EqNaQhC797lVgvMdiz0774PiXS4kRqO6_dBr6y8SlvS1_mY1R_a31WJ8cV0S337A6uiHj-jdZr1XyTJp2aFuzY5_4vS3RX9i7-3eQIru9YLOQYg4NG1Y7Oz835gkM0vIh4gD_suf0n9nZPLf75u4xPZh6J8kn7xXVWzHrO87YGqMTaacBL2y185RNiodjppYaYcMKeB6w60sKKNf96tHVBcLAyGhFm_sNzd0fWu_8YzLcG-uAdXmNMs0290UieI0Y7UT_zMsNmQGlrXYCFj5dau7MRKOyC09LTonQg7jt6cqu6SZw4AdClczYVYkC86glh3WQfZExmhrRhDSpjsL-yLhJw8hR2G4HZQuunh_mxOh_p-vpskfZmMHmoiEhzYuccaMS2zaeEXJ6qyAqtIauZWLxEuO_O0-mtLGt9-ecfRY6tGG6wh0O2CkoiJ4R8i9SrFlvgr4bUWhJ8mEoy7riLeJTrSGeAXA367-b6arOZ_sM4JYebEdxmtasGMkbrWOQevbtRJE6Jg3owI9wzB2KU3x0oeHngUmGGVCaAdm03jy4WBo3dLuytfsif4cGhtxq9XSrWnAaARwwEjAUqaqoSYzQ7l483u8S7crVuominafcfcDkMYQB1x-2bKjzO4aXaZQyEbdjG2zhpuiZbdQechnRBalHOeh-q9A8rQegb79bM1toGhWffkSeIBIPzc-6hUsMPobvHbNGgumLFr2OVHUJGv0Qm_81B0kPOhTofFU8wqWnwoMPJEuSn-1WjSe84wNvyUwHI5RNuKKZFKhalXLkEpZ3f301F3xWlRzHW-BBv2vHb1xAr6k1-ekfX2Q6kkvPTCTmMuFTtbMq_eECtp_emOuvBo2EmX_3i7GjzS8wMwC3jqPimeTx_HDxvm_WARmnLkZ2sLAoGfnm_yM6iLwRnB3Pn05_z8dmuC84fRu1kRyhHkcsaVKHbZIaBwA_VsxtdSEFA3vaTI52keaQyNu38t3cgz5r3Bjqoj2-R-8HR_Nm2ldHsj-sXdjKpvTf4ZQqfAR9Q6P3k6rRaVI28uUgOx-2bFxtwT1Gbup-_0vNw8AaHIQjLh2Psu--1HJok2ESNZucy9v5HdOjYYtpz3R-yKCMmKC_G_57ed_a1L691wOzIt0VFwOH6-qd8DW5FGZSnzHBAZUduNf2FRh8GKu7PwT8bFA-SyfzdBuLdPsyrk0EDlI6GFfI1csqdW5tiuxMYVOyvT8ZswDEAlP8w6ieDhkXuV33aIvNJixJYw3LHKJOpjFC4xucJmLFdYSp7sJA84EkRE17tYw8RGpzG7k-Nwp_p1Q3OglXlhV2HA5LIA1nhXM2vmQqr92L39qbatn5DBtLuTbC4XD4wzgPTBwkPD9EEAFQn-v4hx6qPkwZIIzHs_Z8G-cnWNihmVkyByGPeEVF3_nGoaCU6KJnGvMi3G5_vgOOPm6XgOs1WrC-OQvwgw21CYuvoo1FS9dusQEgYj_qzr1dN3ZdSfscNIzQKOFeYwsA6IjZyIK3ppNhoay3TNLQ905Tlr16VcM7fg_HTFl-_VZqZdRM3E40nChImcGpJRMetczX09AbAxCm2eNckS5OSfxUAK3TQLETSPnaLrpOX_OpB7l4rBa7sQ9NuuMY2QKmpjZtPHENmno8BGE0lz9XBo7ZK1cdYk2prpfEYyYxG40wrmUIZ7tXEmO03TvMRRV4A21A9ueb4PVN8RcYAAjFgcQ-g3AZ0e7TBgo7Kf6i8XThLoNu2Tu6SO1D2zUFP6Xx49DCew8LQYBNFec4l8slM_i1PFvGhwxR7CPjIAC-qQvo8kN3yE67-xfxVmDm0sYvRFTgOhMshCtBXbSogpxjL2cVXuo5BFfE7w3PCqd6UegbfHj_IOneHRKIZnnPAVsgTb1dAx-ENwJcBwVSFQbNIi6a_AnZJHvF4Ee-YkonPbGN_VYVP3x-N2NvrA-llVMVeh23cuStrAHzpr4NkL03Qn4x5Bj2cE7GU-TaPJ7LVaZXM5doDiurCOqerbAu4bTzdtZ-MkXl1VBxx9hXOUnVpobTE5imTk5EyTz0uHruIEGrQQvu82XWb3bN3qz6pJFV_IMo9_mzRcE81fgiT25RLFqDfBCqoVDm9CwjFcAbXkdds23LPNxWMUoKtUL5lKiaAOh7dq3mz39Mi0gDXpIC50gYGGJCczbnrQ54_x_QSPPflqrptH9STbGBNAYBMf89dV0DC2H_Xwg_SW9cWux1WeFB7EPQaR8n4DrZKcfT6dmUOXuP37KWBOLTKdkYsn-vufNlNPZhBNQs5BqlE8PlqmP3mbPwncBYlZ2CFxH-Wgde-ZZhY97cMW_ryUsPnDIdz1JYTr3LSRVxYdszulcHFJ0pXC2ZwCSKoTApqDJgs6vngTtGepnFxvQUGNvKXUANtlqRxuuUu24mjB55v-vmd_3DRDTp26S7MJSoF28eilnhwsfnGn8FXjL0fd2AHn1slLdy2pA-LZCXn3-fsnvkuyg54R0BdV4vtLzWyhNYS5OSCDJNWQXYYgD_Qad6pKsZOwjZtOFES0mbqr3UcdNfaVXSNinC51z_Bs8sL68iokL1-nbpnfmj0gx8SVpuRmm09pof3wfH6d7wJod57MsYne1ZfCWBN_Of_ZZoDwfw_OfUwDVDBjkNvzY_zzKhEJigOuqh4mSKxLnzE_5Sw2XgRUryQb_0eA0s-0zhHp9vKWhPI6UNs8REU4gE0IR61c-w2IH1uCxDDpudqMr7yxWGOhOqERNrdjdhTD7ZxZZZhGI0qBltp8E30Y6KDUWTq0hat8VFWnwcOaEs9EsI1ub3lhtF0dKdxbS884PFI04JkAl1e1VoBY0H2Cr2UGFD0VWFzoC9fmMcMiecfFXmbHoCyfeGNyGjTKHGaPpK0mJvl3gcOQTSdhjj9pdM0OO8YbIGKbtdgHM25VI8oKyWNXxKYjo9EZt4Zw3dRP-FAEGd96xXgPtLC_XkGAGgPtZQzMsUgwSV-MQ-G-CCowREN_1CkRyZBdal6YagbsRsl5zrJeJg-AGOBKsKQOrKUmWRxmjUlhDPUE8x0iexqLRynBfJqZJI_BH8-rWKtaXbEmq6P5W_APyaUBjB9tjUtXyrECXaWtSzeEuERak6XqzM1wNG86Zl9odqUQZPyHbGcfMIow6aH6sWfAquVhZaL_MkYZ79Y6UHddq_I_s19OACdQJUonZOSQfc7MtSV40LQVoKVajZbhauT0I6xE8CkFIOkBOXwSwub0J98xGkQJjAfmaeJBaF-Z4V4xj1joJSoiQj9lbicE4VNfV8o_PGJzZP3p-hytPA7dItXVjFMV8VyoZf9uXUUwfhWJPuI9tFpq8NbZWTTvMtJC_6LZSXPmBU6VtN0J8lrwTDWK4HRrzdnen60UAwG1Ge6KII4uPKmL4CB6oSVagOZNBdXdWR63T10qEjxi_c5Pc_qo1LbrByurrEE3KNvzf_URe0_rguHzioLXh2beqmzGhR9EBlyxAvlKI80J0wEd9xEbqvGk7DddrCv4LI78fAexm0HBCk7VWOf02yEJgcGrfw4zGf3-fYEq_-K_uj9iUMvk3TJDXCP3cMU2DhVbS6_NBRQK0HgBPZi6Yf6COxyrShHwD3sMTrPPZ0mXzGCpm24mO6BzwIVZ0MGywpo-gsJm-4lqUkcUN5s6YrhvUmX4E9jalP0V7Rh4787h--qK9Bj7L16mtb2tc4RSL-WeiUxWhSzn2rtQoSDKdgI5lnwyP4vVnaXtyqtaoXmzwDfcy-begZAToDc760SIb79V2jW5dTM4iuNpO8kRRQgFPcSzakZmpaYtnPt8gxwVqKzikSsmGK8SoNGrxZkxiHCRqfkwFlAYEd-SZkfnCKoPszn4heEkBO8FKAAv-_Hh8k6Tq2Ptk4hXdLNaBXd4Nn6soYaORoYiJhMRPDhQ5MdzVInlwdwuhhW2Ad1WDJBu641drTP3u4AyW84qFFDXW-Ef6uNRjmhQl_IDxXC3O3YPnwesjCuGsQbBej48pNgaMVLKA6VEl--1hl5BbiE7_hFW6roTgUxIJ9DTOHW9LBaSL8ICL2cNObgmKtfeMjrcIz3H8E1X7yp6_BQAjANRfSuPi7-fV5GojJvh8KsLNAe__TkuqFgoZATydYqnmeo4By3NF1SAHDj8lWeCX01YeoeQz3soz2-q5_toUiOfzOMBSq1H0OR6jSv90mIRqIoU3gUa_77lVtpFJ3wJJp0ouxDWAVnYjQJs6VHMYzl80YyBBBgDwE52O0gc315yEdBtjx3ayROtMq09_p2DdfdMN9tSMV6lGu288cx2DfXIHrV-kXoSYzvnMnFLGhoqPdwbx5tpGEEr2Bd4yKRfMZQiueYBH-HNixnOumBzkvwiWeMJ4MOSCzyrh0Bwgw8HpLZUDtTxEnUEvNdiJz8Kn6JZ1ciGFLrN7PbZUgmWWmPsxR2hXJWN3rStt8GgYa31pEP3OjIvmqPhEgR5As7PLj0085DFcZfKhdP319O7_3XOfdCHR1EDtdPDIggYlqf0dKtj7dfhLQpGmIteZ_8x_Ydtf1JibxYJvSaZLp_B7Jq9ykC6qsUIvIOuTWSjFRyhyyBokTQK1XygWU8uhasde7FSy9_fr0qxorwKOxloWmipMdIf3GenTXLcb4arKwjA9eey_mmtbIs49Iz9re96yeFskHyy_qkNZo_-TvkLAOi5OTJ4JmmuLj6A5wNqoOvTskNjUrd3guAEtp3rMGJ8hbl-jA6WfTD2vHX3JezAAnqw5rJZZmwbsOQJIYA615DJU_oabt9GArCnAK0Lr5XZdctWivf4LqV8gCn6145JiSzSxcOnnUht1tMkUlVAWncWHu_Z1RSqT1h7DIf73QR4H4YWv7X1UNXFi0c4tzxJiMZMa0zmfM-XUNuOlQLeZu4JYSisSCbqq-lhAjSuY9C90dcK723MbQJEu9lug6OWqzenIO6TCuCe6ixlyOf-ijVPuAOpQbRzMnkbUcRKiWEQUoocmWGj4IB2kfmFZZx1R_vr4FQYN5grmcl38oCXqHgiBJczgZR6l9aWsE5J-_9oHAiOBSLfk74I0DN9eDs-PdV8cCfgHR_nmaje6qf8l3pZz9aFcLeeRPica3JEBONWIa2JS7mYY5xZJt9CJFqBx8a8swryenhGySCqpGFyCSiopzxglT6IbNddpj51BrXkTaq3N4AfVJYVlky-Av_EOa57btCzRyMhOFu0qo_i_atLfTr29aM1Vjf9RJUQ27sI5LIJDvc3vKzft2CKHesQsOJKSAh8WL-U6PzP-fQJ756OBBFf9q9mvsUOptVoCNQfE1U7qmJogbLTVq4Q6surP6bkaKbU3vbcJ_qA_kfNkoKg7e4I-9UyaA2LlGloqxnZLEL7XqOqzV21s-s3Vhw5xKoY-GlbQZdbMZCztfjm2owjMofYiNvZ-qHyvMbkXdcBU5JkunC_umR8oCfiDP2MaRWVTIDuNEwic0qKAP1zN6o8204wF0h4o6dFzbFRJ2hQzriAoRO60JXT9uq-TzcfKPdBI4DMTBBFepPKOOBaTvqmJ1zVbuDVybH-cPDs19RDpC5Ts-MIUFrzyov0ZzW8lREFgi4xfEFrpRL1H8AwhvL1AxqLz1iFNaxsEprFHseNLa2sIEYXfHmmO7FWFIqpYfHpSSbcL7X5s8AUWXdbsF6T88g-T_ZYr9qbseIgYk-gZ-Tux9PklxbzJ8pHWmC4csth3qQxxNyvOp5u7Jn6MmOlyKcS2mruc2nz15Wy0odWXUz4WtFqCEmKMBbAZC0ts3N5QmlZQIEXfUwKT_zpyTq90YnJiLIT0nw6G26v1n3ZUak_YbqbTxGGYqb04gEiUrmmnnXSjvsZtFf9IGAbzwB4cgpzbBhttC6i1toGKghpWsoS27IvVY1L8nkmfzXRnihOdwYz5Rry7wiPwt-aeLip7dkkdvnXp89Rnx_AVXLeAXFlQQ0i3abEV5uszLmqwZkop-CiQBjv_5mi04mF97NeYHDDPUtNtMQA6idNGNPvaFZ8_NLnShS5_Vbnv5yVymOUQrv5iLtiC7J-J5zTHOVLodM2kAqmQdkoqmCOrEpsw-sKqX8N3vOqCgQZUT_KP4Lla9qkAD5TMx7cOBcn-8iSeRX6aBbiKyKbP-wQz9DUfHfDA1TIYzjxT4ryMzas3bbKYNQhZWt3IAYmzHpfmD2D7nDkptVm4ubHCNn9e-Zi_NGeS-WOB7nkOBCHeIJ6HWsOm6wUE_7iYjyrb_El_-8Z3ECcfE-IFJl6yiQ3RHoYOxgfUq7EHNvdnw2JX4prZNC56Tpjun5KO5c5H2K9TiH9vdu7kwOcSifukEPwTRMH2qAt5Gdcf_GJdOL19rv6f-bzw6PiveRxHm3HY-cMvJDjGRl2719ZoaSlfyPQ6IaX1yiZ5YhHBXvy9_hKtDRj89moc49XPYCOpEI5fIZyeJLGJ1mzvdVudNIHf33tXFqgTh7Td6iTAdvuHRnYIvpUfQMyvef2x8avxjURCOdBLQk9L-hB3uRPPXq_GwctTB4ZCGtYuM52T-TpJKtRpreTHk2PGCECsFz4ry5s05BefVre5IcUYCwzOUl1m9kwd94smpK9BBCTYlpHdw18eSiDxzLwOHvNUY0aehmcHeotPdTA7rvCe-6o8S6rtgnFgfaSbowkJjr9WrbgCntlYfSn1OdQhmHN03IlRWrcJFk9hLlij9AhRvsBVGb4VoOu6Ti9o_irUxqHzyV9Xc3PmmmSVje0HDR808RgHoyYMzZlEjRWK2ZUWYR8VfrTq_PL3ElyGDAwfaf01un39__I0uvElaxZCHDmSKBKpABGckn3tPha73jWNdgbw0eDZN31Z0DkgzwBI1YlM4k9v-L3Ib2ksMeTNoVXFwxs9uaZU7lmzgkLQ4eFKADJtP8i4TjwTAodFkgj6AVE5w0Zyvo8gWJlppPnbNpmKA-Npu6M4b-9fOkwmEc9biJKC7IFTOqftrFJ_r3tACwaTRc3hpDXZ8mJOZlFT3kL00n2wvmrNBBTDuZ91Fl8iR6kSCBPjyAO2ffyFt7-nWdCF1wpZDrHgxZT_bKWOMM8thQD-ARDX8PboTxLb8dZsst_E9YK0tbUUbYDRI3xMEPZZ7OS1q2k8rLVROAUcRQHW926KmbUfFabzPcuwj6gSOKPfH_uDDtXghloJ3XQQS3DtFILMZsulcVa3Pcbm-HQ3Km6uIHDDN1GsYvgBGkRIr1Fo1OTHNJsb6zbCzMDZttur_ouxYtecmhFpXgtlF39wyTqdvmT5QELR_3nDLkGgce-UXePjHSU-HW5EqBR17zEqLZgMormswJlZKOJk-kVg8PuvkqXronv4IM6Ba8fgIYEUj8LA9M5ac08TrPqBzkfawxnYg4cRnQdIwXeVmbrNTzfuaqAB_H2M3XnFuQLRNlhWBZuhFV_t0F0KMcKPYSg_RRYjyoPJRHtZ1BYnDO-X3LpUibwrwPiezSpcqbfcyqZ7Deu0cBi9lJmM-EyyYWH_AkutT1cnNnDpRpt7r0IzOnAvh4k4AoDhteBmNyCsSrTbry-DhIcncbznD-n0YRHgM03t_yu7jkdFXZSibJ9jLe81-FRGQQRn3wgWLqy_iufuZwRJh6ZW3vBw3Rlk_vTUvmnCGgL6Q84AyemXMAgeKwWHHEbXVOEPjZoLD7x8EpYgKe58WI9fjcNP65t1p10mPK2MlafbI4ulxH2L0-7ffbHaYITb4RKmhgn-hEk_Q288_Zuy2xUCEW7w3LC0va-zVp1kUGTMMArI5oFU4RBrGW6KaJLNoeTABGW9lmQYJG-HrtMQkno9RPjuPZxrfHEGKgXGUmSnzHCI19JMvx6J5_U5u2iTXcQF8tIk1TGoov58wgM5FwpFgjSSNK1zPJDUlu9oxGDa8dLQcCqyTXp0hT1aWQMdJSebXp5q-Erm5YVBfRNz-cFrR84Yb_W_0bHr3Nii9HINKkqZg-xPrT3WKfBjzdEqkuxGyKFnqmqjrlgDgrh3d-Mw6J-M34EfM7oLaTEbj9DxAYrwtkomK8hj4cT0QvEXYkj8p9Qwl7gSA2yPbICbTlyXjn-LZIKDn7s5oDdZXUmHwJi23dKj1KUbgS5bt2tmIcjnQzU48HncnhWW_a_46628xE_PfVrEBsFC2i-HgQQDzZFhNr4uKgMZYZhvTbTmhrUuVG3hHr6S3qllHBYIAS9Fwzk6Lbe1erCimsz1vRlGH-sm1-iuhsOJ8P_h4mgsw5Ox-CbGu8cowKGEMA-qwL4MBWZ35vFLwjZ8o0n0UvTVqMEExQs-BLuXRtX54wO19Pt_gJHrjkqtYIdiSL2u6kyBcrTUb0tOdA_6x3CsufvGgaUIkujWKBa2ZgJquLMMThIrtvM6_uOG8rBshOwX7LRCEEf6rvsQhKuznJXSEabD6NZtzB0CQp14riq_eucrPTBxZERoecvutdqfJ_6VmDM5lYfQanT3YL5RUTBUBMZ6tawc3rYGZfeR426qANcJpV-1DVJmcgXfzZITXuKgwadA1WfvRPucZ0rBQVK3cJq9NBBwAzncwz5LEmMcK6TzjHqpSJ4V_ImUBUD4if2QGMEsBMZseMseSnkHglDiF7kkYJOG-fSV6TRLMvyIU7advEJoAChXJDBFXI2oMu0u1QZL1Tmq_6hdwlKuJQmqT9Pd84EJv1H_VqZe7QotQWl_mDQzqiqWoxi96wB8AgTxBt0JHHjVeMHgQIYDHD15csmz6V8VejCZkvOIJbDhVBDw7JTiNTKQbT4jW0iqV5aAa4UEUKHNDL16cV3VVfqL6BeAqzmjPOnfihRYa5qUjfIUeQ5J_v_isjQzQU7qd9vQjljDCUMoBxE7MaPkL4yCNSsVxHt27eCiR8U5vecWQGQk09s74iYSjcyPdC8UNUz7h2c8onhX-il5BMFi8Gg2XlrL3J-scuKkCJrSX-1gtoeNPCXKdr_EKWeH_DJnCv2wgZ5BOIz8Q4vXHimkMFt9vwcZQEwmOtW0MSoEtWi-purERNZ0Cw0h6AewGVOUYA3Do2khQjfDIAwi9-U9bXcWg7HkTFaDkcLC6_rOUu7dYcyILK5XTTBlhsBPanPmcQsWX7fosfko87X7_LFc7Sysaxhs2PNN7UT7OtCNZNeKXTh3jCGpPRzXYr-5xLxMKZJeDqmFn0vMdMWYhDMPhQun8T20LTMBadlphonPu0ESKjYUFs1CmCvOrdMmeGzCH_hP8et1FdDSx7Yp0Rye_OKfhkFGUnQGvpjIYDZTD5AvtZ84Pcn8CISIMqxPVCbLmeSl2vuZylc6alvxhvbIpXloHUsQ9i-mBIo9is-ZMcyFypbkr0a4TSaWeGO0L8cFpJtWSw43V-UkcPfk4TTYdqRiFpVJ4p1QfZ74WxGRBG0yuZOkAo2dRDY9gTn4zFPqNOIUEQ65tElRglU66_RIIqvh8TtR5O5iJnwnZ0IyW1ue9g4gaFjlAw69uAmgKjhuy2GL8wWKv5OK5_g0K4rOgGI5u-NBjOraANFChoV9H9_iTgnwYz38lRBufZbt85D-ImGAalDM9k9FO0sJs6DQJMNSHFdCoKYoY6tXBY8Stulek4vSj00mXCwm9lq2HF3WoU57usk3-dOjPlgFyjKLxcVSQp1BJtcGHmp8alOoAsRz4XMlg13nJajFhVXLrZxF_qmfRV6sSYnGIY8QqoxQ1Gev_4CX6zSsybtozd1bwB1jzXBxAMe7Cl9oB90Nq-l1tA8aeERhCYi5nT8xgvZ7v9W5ZO1eSG9BAAKWiM3720Xz2lM6IPlNYl0Fu_HrK935640mBJGK2KsVJAhIfHseeEIO4Grj6ehzM1uSgT12sWTDW-JnQIoVYq13ESPxQ0gtMvvAIovtD1gWR9TL3bQxkwzWp7wgF3AGyNgaAEVC0YD5vjKiBNoZ8d9HfTZsD5Zs6K62jVCI4bdToarmd2rTGiwSD63z3tFjcHukttCULB8abOWjSRd4ulZWHXTIz0cKSdeeTUnj0VMzqHAle8QPOEX9F6fbRFasppZqU_hzh2rMVEgw5Gse1hJjuREt9rvcceApIglJHkDf7HsJ9qYIGaAd9BnJeaf8RXi4Sf8u2c-m9T9BWE9YU1DUGRAgUmw-wVLWsyR59l6E1pSNS28ek3yVAt0FjHCX2XKL_Mmecy9Uts-KDBe8n_YX3HOhJhdODICuteD1Rtfoa1z7qtoRnZXbmx1aBYudVcifmOEJwyx2HVM3pTLq765u-4z2K0rqC_23KaSgmzh1vbDT07Ni4aviU1nySz-hyutiSRLxr8hxKY78IXSoedO8YuWkvxXK7N5KEZVpO_dovVBMPxVaPWmFZ7dvIhO3sZIV74nGhnlfiNnibWH1wmcvoLzhraX_Z9hsrdaRpJHxCi0JOWLe02v5-2iSq-87pwZe8_g4jzpS4c0RxxlOygwwmEIpuMop0BC-4xNUHwhbki_sgoJIMeqKE4V7DxfM9yXWze5Z-5-6giQXaszAjMOXeyJrwYgSfOM4xK3eSQAYaIz4taZAbWhrG-GbU8GtR1HwWtFla9UFZjbHYzJC_Di3y7QbnutLmHC3Ox6DSOvwn1_eOBnmxklpDBcmswaaB9MI9zV2TY7XWzfqMBnsGQvjoLuKHubDKTluOSDm3cHh85F9BUkUpQnNrvkfmSml9SBOVnMkL4YNROTBts3-yIlJVIntZpmox2GGJ0V-ZvoYIhNQxvtRR5Ku5uZz26jEMYHcr4Hr2aDImCbT8Ow8f18xb7DY4ilJN4P38aMFgEh2RMOr4GxBQ57U9idyW_goFok-DAOoqc4uItSzF_WYLdRM1F3mkHFa-8XGTUrKJpKPmcbViX9WZD-6ZAJhrIaFno35EkkGb1wha2tqWLyRFv37G5xf9O4wj-BI7t5SbXPipyKI15RzDyhV8Cuwcnwd1xhQKX3H88t93jJGzPTuxURmUFFCaGhN-KizfX5dLAkfuNvfJ1IJmSYAhwJ6FrBtVycWTx3juDsvq172ygI7XjE7Dg1KeX1yKvZmMjTFXX49vXqSw1YlD0EPaacojLOEW36RbkbUFjMJIW3-Kp2CsgMzrYyQYwBV6VF1GVRFzWgXlvace4Ku3Brg9QhDriAusoEviXZQIzPRtQZ15NkmSp50Sg__ynP_fMb65HZ8eSWBD-cb7msQADkqb0pEpvOEOOeNQuAMhQ1igz5u6wXctniAUjQ3l_4hQHfUJfi78oCm2w8KOgWtKV1spav9lT-uGD0tX8qsX6YUYV3oadnUOrYc6kx-XjIx0GyLKoFdciov78qqEmpSauM0GGKbSQK4UROt3Xl8qYwtA-Ogj66l9UY_5tpyvZJtlld0-fLuafdbnNpsDwydAW7n71bM_yg6oRmk8noD438453Cxdkmw-SKz1-1sux4TUyzz7mZ4-v944Srp02VqqqsG-g2BAcU5B5pIvrCvtZnnqYk0KP7XZsk1sZQ7C9FGfwZ9Z3F6PDhgAlcIZhrTD0hVIPqFa7YWNzBvD1UP-wJRhHEdbk4gmIH6Tet96l_IZYr79x0og1lAYgs4HURCFuHf5K4IUrJQRKvOlgfqI4kYO8C845m78KUavZYSeZkcZQ6KDHqlug67EUwyin3hOCAmPUN39-8PU89vocxBHlYh7suRQFg-yBl-eirUjEhE4jZAumI9L2tFI_L6qZwy4ysQvhBcXYm2lMV85MqjDENbQSkjYrXB2_hcUrE0GDIxdtsjeYNd0dKTfDr_enCTWq0p1HHibBlitUbsCmTlGG0zeOnzB6js6-6m27gujWUgtd6PwWNoncdSmA4zmeINRIK8g5ofbTqumKAaYtNabFCUUTB2ILs0r3JVNmBc-GN8c55diWhZqOaI8i6kI-a9C8DtuF7A-yoqfOO4s_3pt9dlrmFa3-fluLIMvQyn_NDTY6VJOTKpHyXDJH_QUqQx9zOQYRaWW3eTDui1SPsuvIlOw3EJhzWolzl79l_14MYghmZbXLuOjhNQjoZHcyWHs84RCWgXkTKIGuLVLPtEeGbNws1bBte2v4n1t8fRAq6wobKRjsCSj5cqMYb5foYeycGSC9M5F-Os_i_XPLijrVGuX3zojUvdpghaQucvO6c1Yk74nwd-gRgceHb-t8teI2N0OTNgGbHs80Db6wtAANDDIuYfNpGONN8Y2rkCRm2VONx86FAZ1Kvk4Nn0nXhf1WDxxCxBiONTKNyVI4DTwhjf3d5I9ZhxbU8qz3eiTrJ7-u9-9I9qTiuRfZRy5cFCgROAGdXPLfCfy24ptQa3Cb6AiT0e1VfKAskUzg9FZeHiajAozYnMNRjgwH39Azcurg0YGhTK3dYiDsbSAElIQTqdcNRvXNwADN0pTwlv5UDrKXsvOVGnF3latN-ZLYU8B6wXhiMtNIxTU6lL9tVqv1cOrKsW7VFMGjfbdV09lwACkQe2bk8zgqmLeYDmwW7xmwVnER73HnExto-30X0SPp58pFzgRT2b7uHS8zFtIZn1pAjMAkUEKLRt-VnQSpTv59BQ9bG3ONY-XFMbBgYJdeh7ONMBPaI9r7CDL0TmbBa7Jl3HDgE8eldwUMdO1r9vkGAWgUU189C-2rwn5t5NNV-sxty-jH3aIhaVu4FaSy0XSUf5ZvUpL3ZHKcJc0XbEuLTqUHfUhFXD5TIQkY3flzJcZx2ruidJI_eCO6fW6hgKbNcg9nDjks9zEIBYfVQfQqrosmCOIYcHAMZiWwTA5huNDUu-zD-G8TGpIqltIuUGu8e1xPsL_wKdELdmofMGj2iCsHPua6jHLh_nISe0pYULBkan7UXcdc34aiaUQ7Gy0J0ENfC9e6BfabeJMzRCo7b-ekmY8vDWzKDi_mcEPn1mHQeOz2c2I9uuApaUWjh_yrVvxvzNukCupaSyCP660h2g2HT7f1GcVEX8HdC8ZlZhjFkl91Y4b7DUXGSvN32YisJu6R1DZDNA60hU3FOS7mZu-y5vU0HdWqduuTTzdBSDIDQOrBA15_hWvr7F_tRmTALsQXAwFInXWxwra6fRtghR6uer0-65eOuEkD4TlkdiqhA80bSUA8vaZBI8iUWi-N0Ow07c-2_ivY7ICjI09E5WpbdulZ91q00CZ8hO9BgztdzoQ0q4TLo9yjSwTnDF0DqRdpRILQNyxBuOKRG_PX-jB44R5RoL5V_qUQg3KGMjNq9nkwGosEBaWe_sGRnY72WgbJOFmRy4bYXuxncqiimGvPajsOHje2Q365-C6_ODzixwdBBI-kPUWn88rPTyUDjQN22UB8ojhMOU2zPG1cDYEvU1ihApIkurNBW0qqHbp9RAdBAmT9apncc1c1NgFBrLsmiSVvRXI6jaeWF4WDVlCNpctj1fWxMzOmqKOuEke2egxgSa8wlxHKioA8FshKOLVPUqhxRgCGor5ZnT8wKTEAyKyXlUSKP2mt4hWVUtdmHA8hYf70Aw_5AoaZNa1aLEvpB9AYcLcIJmbgGaPyIVjT1De2sjT2-5xtodQ-LJJsJLLYaTtjFA7MbbVoQ_mrcPOHPMtNJpUlRfYfQ0XRfxY6zFeeW5v951AnMO0YBs8KYYfgE62PlxFOsg8xKDeXr0UKP0ecjv9OZH6v2ETnLTBu5P-DJOUJKkn9FKPq3Km5S_RUC5pM57CF2gOPzewkFQkBDdnjzpq8UKi6UV0ESsQf6avv7al4XG1d5eEnCU1oPwnYB8Z-SB0VFS_UirACdiyxuCVm8cn-i0dlJiPnO0YjFISPoabXWRiI08TwV03OwVa8uWHGXI46q5R0RkUIXJYmG_Ua3nRI98WxNs5gUxt1-uEFHvU7qD0dlguAQC9e1La-2pgwYyHPY-f1WN7QS2fmVsmtdEnA3x7PC0rZ0LtbXGgUCMJEw-m9mQRhJAcr2lxeAI4u8M9hkJRu5ZAjij9kigkH7x7AcMFq3XTTpd2jmNa7SPlm3CHqPFqk5_bPFjYWS1v7jsjH9nqGdD0RX3k7bYK_w1rHKOUVHnDKYq2CylPOop9tKwJOr4phfmRh1uMC_Gee2Z2NM2NmSDyO_6ix7AHVi3YuLfrGTf3skxSs7vPEy0sR2_kgbg3lJxEWNWTuPOwjHfyY1aLIpyqCwubm7zWO_X0p1kvL09B2mywsc3uJ8VnBpxHw2wx9eU50-Y1IwuZLrHkT7ngaOZKilJ9M39vWtwdjSIe-u6PQt3sEDzPNU-WcZAxTWjHc8ifY0cyQXe1UqVh7lWrJJIQ3wQQHhZwb4OxE8M9eXyO7-RWfKLAsg6H_o8qgLyo22CXlwaHFT0vCYyu4qPDZT_PvDy3KjGgsxXMXy1etQpXORJ8O8dxPVLZeKfvR6FugQZMQaAShSonGmTWIn5GUS70NWAqYmTDexyFJgrZ_9EVPI8vLer_u_BqnNdM2RfU7T-sgZKe_yMftMJ6WOL9Z1auCyp_8R7NwHD1ksEjBhblppj2juCERsZ1RF234X5ifbSx8NPlhe3iUi1UQ_We66UNVFJsQKNQAeazIWmg4sKvK-StH85SOnj-wA_x6w-MUxn9KC8-u47tpvE2w4EVXJW-6ARK7LqijqBxnnfl6qTWkzICHGiqHmQFiRiZ_lPQIZx9XCMc3NLU7ZM_mB2_vh-ej63BTC9fARIY2Q-8vqvu3MVbkh13UHWIzkHwCIn93g0vHzAqCDdlYjhXs6zxQ58G3lbBKmnftEzvKilFdMZ4coNqe3fBaW7Aj0F1suNkIU7Oqb6DdyRisotG8nTK56dogVn7bknC-lUsFNPgZNQ6IESuVMRco-cyiTprztrqQlU4kQRV1sCbTd_PUwsCLJ13d60z0hd2GtbkZ09EVmRZgtUnfjEmVC8EX7VdPPmBSNtI_fqWjSOY_ULjNUL666Vx1YnJ0X3JXgh4z4c-y0RpszK3ogPc-3DRxTGp79k4YYVZsdITuMykqJo-3v9Dv9zICkGRpy5MLrxG4JaylTqlaDjD5JiYLAfWST81Sk4PHJl_CyJoOW9tqtVxsk9FZ_jcyq-_B2X06m1rJjTMGsqWMbFvxLVlvjB0J2gyP_EL5h9pUFhr4z5-aBQ0_x1jNH-mVtVWTaSgVKsrMzeAXhImDe8Dj75Oi1o7x7mm5sywkIQuR7gaWgeIqJPrS5kS3q_ZbEcSJOxycxyHi9OnMDcj4FMf5X55V4XcoWnvWm5Qg1c9aX-iz77I8Qb4jRDv5iBO2MGuCIUGomi3nmHrOFHklFvPPfVv-YQ4pb2TH0yd6-oyqzBigS7zbPLOsUnAnZsEWc1qOYxkUdOxoEBl-D2iPF8RRf1sD56kUA2tFij7Oab8G5MwS_bLtcYvJQ6_zxGEotJMeAzBdbDMfukQAQLOAtzqr_zLYzLsvKK7QuKb1tJZUO8xuxUd5hF13GKLkon0_9bisPoMDyBRECtTrBkZZ31w0G2rjogtEFAZeSWPkoJ1bWqYtO99iRXApnWW7Xbyh-95WSgjQiDMIDAFRVXU9TTGqr-hqB-Y4hukedUykClmjsPPdVMiOP4pGOFhRm4og12iDjVUtidZfK4rfrOCA0eTb7P9vKxYfAfv9xknCOtd1AtdDIpahqgCu7MpdTcNxXZujrq3N-39_gkwpnYmBcx5-gbh2h_gd4ZkCCBXGqWCF6USfIPCz_v5xl6nrh-u9TVw4n3zeTLdMBaJRXGrlEpGo0Yk9IPBOdPMg9uVSNRq54wVWMxZt1oERvzl7o_QDVhehIHRDfwn0W5KQY1VXQj6uzTUuzOZNc86mK5F89OwABdAgfcGb8EAGNvH_AarhURA6tFtW_lQljGtg2VWLQPfur9mgO9O7f-RiByB_2FF7pQrom-XkYUuPYiAn7XbpwGHs-GX9mN2gXqHTALactfGpQxXttQThF1QLSEQtpimXhWW5GKGlEo582x7To9NXj70r9zjfecapeY_a7s5vB6pGfUeSepOg07kihqZ9HKmvAy9J8th562lgxZoTebDdp7tOsY86Otlnae-UpdNL-ANYdNcFPOhOdyi-97TqrYopPrBsd0xhXSSkdWruI_OOocCuMTuaaHd1nF1-wPWlYtzi3Ezlu34I4jBcFz2IWnJQffpaOHDVEMTbS4reVwSEqWxvmyI22J3mb_fgTVCwdHRw3YRVuU5j--NNq-m7-zddX-AOnhCPo-MkFrDfsP8JBwccZVxZSN9XseE7QvMtTWz_3mLzoycGO1hEanzzRZwcwT1jycGyzE1nFgVtTad8cj-YNzPU_sv5TVF-UNWYLAkqpwxvkp8l6GLn3zHJKx5dAck-HZ8f86Z_N0NI8_MNZBrJINNb7DDW9F6vEe5NkEg5NOtNqcq_Jlws-DfFSdA5d6hZkThGTh4v8qulsouqEZlZ4LA-ln0xnBbMPRdTiiyni9o5OHjWpHMBYwuR0h0RrAEZPNZ-7iZwdQmyzzA-V9rKzqJ48CKai8rVWporBkvGUkcnZQI9YzRry_k_FLujlk2fwGbCGuTeWGcEvsx-ExVu1-lyiWtd3uSj5Y_gMIRXvg0GRwFzScUmHPYExvVHGaFFGcII_qi0bY0dCrStvEMSCkAdSq8e4bnEhHy8Bj4GxAGukakeddLkAcJZ1Mo-59JCy4rofRvXn4hxO8mTrBtdAYtoKxyLnkdUhY6cwX5arUa90t99NS62TLL42g0m1gInj79WrunQHRjyxHjZNoRxUdHPORZEfpc5buTqRwliQRzqG5LC9aFagZ3EsQ9jvP2q8SqJeIVmgqkWEdb0RGYJQ5H-6d0WXMtDafUJU3GEKcgfy5SrzjzvFMZtTgWzGY3TBC22HT7F5Lie8Kc_HtqLAUJfIhXILJaE4z5CjPkX9UCrashvH4pb2ChEYwfhhT6dC8z-3JA5YwiHdWs_0rNcsfrl67h3vO7nLgvrK9O_iJQ7dVw9MGcRxAbs_UO13gFZFrXHZNuXCHVSiOO0IzCDGP0dmV1KOBtfEcFGd0_uXOJn_Md2VhgcEfqZ6tKAnGrjR7j2BU7SvesQ2r4Q5fMVQyHvfM3sJck0nRpe14eIaPZB3FRf3DJ4IizXTQA56PoGo35AfBn7HSGszur657JhcZ3Lj9PfIlQs0228wX18C-ewR8WXMvAWMegE8X5KTYOl_qRJt8gkvv6CvcMSvyGRiR9_PlvyBowi0bODcZrHNSXoz0q_uIMxzGTBmvLbObiphnVmXvKFcxkTgX0W90cPRzLe7ZAwjrGNCAASUmmOuLe18KXgMqPAkEsKDEUroYuL2C4qzyiJ_NBsw33rK9i_nvQm_-RnRmEvJ5Ostuk_Xi9nX0hhehvumON46i0qmbrI0FtssXgCmbWNk6YPkBojlaqZPufUlfpsYsLVWpUpqMg7CRUBzsV_6HEh2oa1g733qCHd7g5ksS8s-ZfgKlG7h3qZzUtueNeSH-FMtElCVW21MJ3I1Lno9OhjZcs0l9MKIwtF4D8kTnbpT3h2FlUWmErzA_HJ87CmLfwYR9gS1A1780gFZaPjiVbhvN_fEniADoelkyZGpznCb6OuQjhkl0608BbERoJ9kEnmmHIBPjmm933EQe_BTcOq5_MRFM8TWzPNP0TymduSpqYmsXou-yS6ounaVukUyEuHP270qD06AWUMb0H-BGoL2tLnFSIOrjXCkuY462ItRyrK35HLLpqk_cD88rhdq6rsJBjNoAVEzNghCBen-JrO1iE7s1fj3qWjP6drCuWGvlpLmvU7OMKTbKhG4wIVtSSa4zxG41w8t2OoiSUXwoyYMzCp2OYGy1soUGq8W5jJgpYXJZJwxrsXtN2Po4ZAG8X0B9mNOvXJaJPK_H9kALomWz7BYbfuEtOtWHZS8SdHiKuypze-gM_FIFvQ0iJ2fZbonvU90ujARz2njIyTaOUD1vkkXprXw3MzXkxCcRIn4XWFEFJjdOHAJpHgYCX6-FYIZX0EQthunZaa3V4Omc-951AodJGL4sWqLlZ8BkxHQzn0DLJuRrBLI3sum1aKkefTmZJ6FR1SbgUq9y9RN1qrLGOWbpML0Iyl9As2vDexxQiR5NoM8HzHrNqf8zWPFa_ZD7VkAakWLnbIL_bakXNj6hKjMFm1LXhU-rL95lclqJK4m0msM31asVPOqzIdGAOc7LqIFM6wIc5p2-2z0m7T-Yc9S_WP6j1Ar84pCs0M3jqohHEZ2GPgtLUVULL39UN8W9AD6soSitjd6BZXhacapV1ArPQfV9IWvIsIQDq_9qVpAFVZyzGBFS7kvlRwb4AZ8dUm1AZsW1j14JMFnGPNWxzHe4y8hee0jiDzX7TLTrXlDqww-EqbPmHbNGBSJ4NTNa-10daLmqbGBuTraVaXvVZ4_5-q8zbRbDplZyvAT6fkPsnZkAbDn0yqyZLpRvMmVh--i-Sm_cmAuY1RD3h5PGJd6d12_kER43Hu_WOPlMJM20lEyodezhGBiRqrdhdaaaDt3uMFliYZfoVs42OFP3z_9_B4d8qrWlZsvMmlQKK819X0czA8j38qHUQQxFuF3SYd8S1_n_A67f4XbrcIeDlSgXY0G9aSCbADviqEd5GwzU0aTWEbDp2pW41SUSEaaluJftMyO1o3BCi-16MCMJc-K6QOY0DVyPBaMfsuP1FME5FsYok8zKD2C-zy_YW2U85OyDqJf6tXVoIxJU4RxU7wgrQq5Oxh6ijMak9a0-NKf8_CknqqYv7ucuZ_brDFWWyH14n489QJFrCP5Sp2cvf4hhgwjGudokkTBEFFJGzV6mjm2GqAaGNtHSwDDanEcH8H9z9lIDAqnbvtbGfGjLytCLDK0-_lxAG6CQgoSClYztTjB34WBsD3XdC32FeFsjRvWwOJ2vv9ILv6B5oHGA7EZmyl8GL4dW-WGET80oJcNDOKK0Hk4vaPiOvZ03BpUimBlHpq4F50aoaSldPAiAJNosgHVRgAiPLaPtFb-L8RrbFJjXJagMgtAStJO0n-zfgBqItOAmDkoH9zhW7Rf46rfMAG5Ns0g53sjMKxtHqVQyeAH9vtmxhs9DiYfFx_cJxK4vneqs2xlUa-HWovlfFWA35BVrwjn7-MZsGDYBJprrYf2Jfl8eR3-j6yrQHXrXNWEPW7CEHl-eGF6OlKhlGoewFzVTY7wYJJi46V46ABVnu90mldpT6DaQ_BGnazF6w2wXiXPhiRaVd9VvtzQcdn1u2co6HX5XY90sN6RR8lElEAXFfOmb7QLNnKrJyOGEvHubUP6d4qosWD77x2xnkj2F_Xi4Qr4L8GYXoesNEDmKPPWfDTU_5zg65t4ojTTgFyXIRr-_73Ukyo0WaTI3tbSNIK7qHfMOd_a1yt0XlcHf1NDBPqQokYd5yoflQUztXULtaeSmo7XSOFNuHNbFftzJcvoaqNb5d3rnd6hvqSMmZCof8FsDfqpvRE4CLxaMf5M-gXi7DU7egn9tIAovgh3-APAgISOIUE0dUZ6xtMqoSnT1GZ5VMAqcsKj66ehwxjZ6hcTag7GffFKIvRE51SmahOxi8DgDdCYY0PgY2PThexrOqfgLRlriFC4mFxbsyVqd7Ga5TMl0YaqZEi4X_irVvtFQO_mGXVhxFMeu-7t6pwSnEb3W2ACi5jMPW-J9QwgkvFkkePzHp__lrhAWsmcXkMmfpMZajmHk3OtdE7cH_24mDmpj8oaVuWsGq6D4vZgaSrmCtuOvMM_WZADoeEZ3rljvaRWkwotyVQuC7E_x9McumYz2S8Ydjwpbb6kcXW2CZXHV_fWjt7DnBp9qEB84L9cXJzqU-06j6DZHf3JMpOtRgKr7vIV-B5rDqhG55JuwE48CP6Z6yYT0ygMvDKfiIkU8pI7h_6dQ78_v--T2WRueQR4dZSCguD1F3KfUIUih73C3misXrzPspYpquBMa6KyEo-y9SOH5FkfACJjAZiQL2byPDPoOrW4bi0Au_Ic8QEbxSvZ7mL_pmcL8pFWYH3IpPMqQzIEo7Z-5MLm6KnIFVPqBWdBJq2Fi9ohglDjwDUtXBsDju2qm4-V5C2ofKDHb8iVPH5hAp3dzeUGjDEU4dHy7bJnonse0IMlrZu4wJTY53kzkrIaZzDrDE11QFMutkqC3R3aOxh7Xsd0gfWNlVOVvASSAVF9zV4SUp6fL9RfEeOWkkm9pVjjG0fyLQUWtwoQgdZAleKJWfPy0JkiG6U0NVHfzWlI8qAJXPkFiaACqz481hlTXVYBE4-vH5QyqD6se8OREL_zoWOM6pDznmBD8xlmOT7wff3oPQMzvxbBJwjnK9wf3upoPzpKkHqlO7ODK6yzXc-4wLsyjBUqDcf1lLWFoz0HHdENl59M3MfpUJjebpTRwsQzRx-UI--mTjGe-Los6_7OgM0b_SJtXww55IenPGH-gSySQyvFYlCQOV-J_MImZ_JcVE-QHhhGnrlfafA4SbvRBQgzCxgDUMpslBRvB58dvoWi4gvJAhK0h2UjIhNd1ArpY2Rrr3hf2eRJebSnE0Prza6e0K3HBDJ34t8BXcs9NZsJ9Sh5n1Bhsz2ZC8k8lT1WxVEW9IaCWBKghOhNvWKU6-itQVZn5sh6Mp-BO1ScNqeIFdlz04neaVW5hW7_-kPhR27BxG4F-Q1biy9NKq3b-XXg8Lr4wuYtuo-6LzCavZXUDT86--kz49htAYeRkMm8jjyOIat6NC22_trF2cVpWoFdlFWtnDbQvRow9FeDqzV_whu8_Y39gZ0qASxOZItlgk1fIcqHcDwOO7YbXoKfNLaSIxs1MwZZ-31pV_no30rHzp43AMPZ4Ryu1ngnZiT0Mjll6Y7KNjZTncyezlevPlNwDxsr1sqC2buue_nl3R918_WBrAjjCSIs5QODTpz__ogV_G-DCUjxqQ5HBsD07L8FDAc09CeVWMawuHWRL8GBWqiHKMPPOqYX90uhFaPsvaF0tHUIz1B-r3KibatJavhVE0Zh12BMBfUveuTvnfq25Dhy4qx1NtRyULjb7W3dF-0fTUTgJM_YtZcId399eXSvOAhgqPRTxnCLp_aVstYMcX2IR-gC7wGRr6Tsolbo_JsbvgOAgK28DVRB92mWOJMcDcyPeqOTHSpOwsidY1_CtnM_JA4iGYGt0daLhJHy5SFv5CUH0-Tm9hm6GsixcynhVk6pg60qePFCghICA-XWmKkHgKxmPvBhSDPH90LC5YO_D9c4uobrScMA23tE0dItZ8Sz7TwBaYeuPzrH2DfH_5f5mvqlSBFebgn8eZBYMpLi-zXcsYXP7IpV05h52x6T89Hg9mcLUOe_vr0A3GKkhMHBsMuzxVrBS0BGW0LfQ63B2ablGHAYUGSQ5-B9V3dpy1Igx3gGZf1pSfn5wTUNtzoaAk96sPmO5QkUKR5FUCOeUV26PJt_GVG7PkJ0MiCCNGxeczeQw1q182fzO_O63ul1EYOlKDoy_1OPKvEs5tPRxKvYKh0vipjynnxywgMjnrcSz4lxRCLT3RkwrJBxgGekTBuZm2O3gzMtifcPDVsAGkjPBnuGhZwsYcZEPjxlMurR55mXgAh13qxGjOEekXXlLTQ-LSOMU_01T8F-GD1M_cs-b_SltPzPxc9V1ouW17o_Wm5CnXCZKWeGNdr9JXOST4cGiRMhmb8flXBf-ZZugEXskZsPsaCi36DGIswJC_lp8DHYV7W-gT3HxVjBo2HGGGlFvZ_RXBCt-EsF1TeS7MYBXsjIvaVVKY1LHujMnzW5rK4q4BbC9W_bg9acEGE4zqHSbeiE_WNxN02pvqu9krFi4_vMFK_WCfpyX9InfwAhyt-rarrzNgQ1R--_TyHyNMfaSN6dOII2CRfKBAV_CdPmDjtLAjCHYy_Xmu_LkqOzQNncXsvym9hBTVjXk_OlEgOTxtRRZrEZwLub1_VcJpfhhivpMW9ANiUiLbGwfRXiIYrtiBEclsHvu_sQiAIhzZmObBWv3cuNxqriypsmkz4djjjX_khG7e7a23-Qsu4CGoswzUCEslPMb9pUOPTfDYlpHPoWMY3c16W9UST45O105av05MDLZ5Orhzym01Qf-jk7uZjrwp6ELNDDVVt05EbmpmH9ULZIa-GyCwqQlP_7LWJUpRgRQVZeebyrMHKNo_C0LkWN7PFGPaimTsVPYUzaWwb067D6iN-OXtfnOOFFifVAmedHNjFc-ySLmRzpgYdZUtYoctyF23mACBlj5sFXtOJqH6pKEWqFPmXwdnrej-BBwHmkVg3sDLzXZjkJbwenknQWoz23OqI3-E8jqU3sPfQn-CA1vrx2X_tNUUbiz5E7R4Q7vUnJ90J633ElimxdcsEHgR1QesYMrEU-Awrnx94VMXm7K1vx9JD6H2pAhOBKAiZodssx34rGAienZmHoNesRs7wBnZASLcoL3ern9BL9HkHWtcEIX-4cDMP8S7b5n9N19CTho9Hr44CL7z_TYTe1D1iP1ibhXzwvq4_S1otjw_slK95hfntJvqRKgI7pTECDFsSHIFlYljds9lJ6lBbWPD5BifKPlgd5s3Jmo3zneZk0j1DNA9TAnsM1cesOPZpl1cAyNQqVWbXK26wvNcljWp7j6NQ_etdBHLPAmKP7tCJ4nF0JqRzkZbl1Qi80HNGpya6pQryk8ShbpczvwpaQFfFpO3nGk1xpe-eOBBMIH07JZ2MJO6Jz0hgAFcEtMfsB9cENlfFBrWJzRBy8Jfpc8KNIqXhSc7NtRgyd2133H6p7dkAYaPNzk8NWTZ5r46gvDFvNUIiT8QoH8XYhLE3SuM_xw7nIfSUWHSI8JCSHewLhTNJdxK17mI-C3je9qfczdDui2UNx3JFFFX8cFvSLJqgitMq3U-0NGT0kQLLEbPYEc8mE1Nt-m1casVFefeXaTgtqEHwvznOZc5keoU57eCj-YoQHjzYtdOYWCzrQQTv5lT9G1Zx6qpMlv_ZII9tlHjI3fS6i6mbKWAnwYO6VEu87H4M_V_2H-GmG6tiuUAAHW7bsf4172Ob8YiOHJgK8EFQLt6aYrI2k927T0biIJv8tIUQqRzHxkAgyxwPK8aNBUU-F52zQQvyOW_1LDcRWPIvSkW5AVWZh2eehB44GZUDR11nxRfGxBs2qaEU7P0Op-M-SORdUNMAltZrC9BvK0o7aL6A57wAb4eYFqutfimGz6h5kRZKhMik2bD1OWMEVEn104K7N0hHfTstwfjoEQCjV3zoMuaPspYTfy448kOPHN36pp_BCk_H4FmPXtCAOcKoSyyvt7OEQHLDe2oL5XNq5284-jXSudm4juy5OPgxU3xnoNaEUXTh-GUTWi7zBn4EISdl3-F_ximapuyY_x4BP7seh9K4BYvxo0cT92wBTgIlfuwuw_VtNQzopeTvEW-oR3rcZScuXj1zm8JgQwfj2eEp0kNUgS0JlBDp84CiMoqNsT8goWJn6YbgM9Z4rt1sntbO9CD_V3R3gtnREj4P4oxEqm_dcnd6nsEbs2gwary9YQEOyb6LWZ3ICeLGJtqPz4poVLlqs1RKNmLy6bFAM_lXxTpNDqOVt1fC8W72WBAuLVJ0HVQbflMRYtquXtsj3hmUgKnNQ--CHqwKeKVEi_CbPVapwj8iZ1BCwaW6_SkAFvKcTiXetf0Nj_NYNnGfa8nkvmghs502y7IbKICNGG9kLJ6Zz03WOFIN51Y12RWXLPQ-Qfw6AldYAMugQ5emBHWa2xSkEYl_q0s_MmPlNNsBt0ThxYBJjzpUJvNEFs1QEa615w_YMzXoCZ_EiGb1gsyoKUEU4OoAmWt_kFX0DrA4QLrFp-WiFybgHILZRjLTwV_EYEEWnDdUTWLF60wNZ1K-le4JBkX-RDqqihN1cRGbiitxxmuZIqO0lvRiaa-eevDg2lxlCJr5wEz1qaWIURrYEEP3vxPUrkvBtcBU_EBHVP8rbuUy8LMsbFJT7mWf1gBw5xWpzlYHjG9m8Rq0hWO_98jF12tHmO477PmIs3Nbq0szKs2TBMJYlyJWtw386-aR60ecFVUNslsNDhfLsOtD1Q5dFOrMiGDVwwVRsDECZQaW8a0mF3dBafoSOu4xsHrNdyTjHspqgOlpFs6jAJ3k_IS6_AJQ5PxjZHRs7IyxtovvAGGLSob5Q0ZqryvsLyoG8ZyDQgRpsexPo7IpbhzNtNzJsI8ACoiCz8FmGgWtS4-EoSAPNqCjnXPA-zgqTzHrxMa0bBdIcXKDS7DyQ15yKunJ4pm2FUHhs_27tq99HvjDWg-k2bgdOWzdmbZ4PNoBBlg9VyE2O845Yz2tihdP6RxSxdkpH6dJ2f-vW8VxdIouTqhyAV0nw9ZTW84wWzjFjWzFMFPF7lFz1ABHoQ8fv0_-Md0aspimmSxmJQMA9J_hUpEubO-nRyTeDCrqa84HowgFGynWTDygW9CnCXG3TZovMeSMlT9spL_8v2N-9cntF8rbwlt_i4iAbgHxDBB3VffR1DW6WoUkF-zOmrjpuwEVlKt8-TEaP1We_ZFgA9MOSu4wlyl1sorejYEa9qBOCPzeBWmyswmglMVIWdprhkjstm0EYe8JZDJvZdpHs6EX5xlkkzdAvjmLQDtHbzTiM7t88Cyjxa6gYGtyoNZLutUq75LtplZK69S3xLc1VoKX4E69R1HtLV-C-rIkOG9ZAvG9VJvBSeFfg_ZJFYnTD3Q1HZD8ZglvjC9ao6K71ibppZH8VyLBdTcG68opiCcijmOU6YhRDoUVSinWYgyrm6dV-0Msejce0Wq6mKF1uq9xgtKkFufgKEj9zYTpsI3seU-HHxrQqS6N_HnFO4RTxvXe41QOsmfMEiy1lFmjUDCCXeZSNiclsIp5vukz4ibQIVRPhLqTuIgFI2ne9IUeh8qMKJCf35wYE3-I0RAOp3Qz5Gppjh5924yHaZObS9iJsBI65Yn7uyTm8rycTaJv8clw4otL4xyU8LZJjgLgDQ0lKSL-E8FN5Nx6Ji92JK2DmeV9GBYp_f-nJlGifHipXXmHlhK7l_0tQWqDsEgWZOPOWDYLq_2riH9l23HwVhj5av0j1NJpGUrHdCGHA9baQRV03iVsmCW338eshZlgj3OifyOJK2TzqVK7xyF9lgdZSbg0YgpordOih9a7yojeOBzo2Xs4uBpHp_0qRasPOzVYA9_BCMLGCMu1cOrKlzZ3GxdWFYYEKYeTa6gMN0KH3R87GcJsFEQ0WUbN6R-LqH7dt1rjyqPCVcOMcL2TZS7_nelthhwMx4B_sza0DaJREVUMk6gi40hWQwLJ39CV0GQuNimKRz442pubVo3VW_u_PlgwkrHO6Af78mO5SDpCKvu__eA8H2YhLosBghEHEIfnLZvqlnS0HOnTHK1LVtnxRGRgPCj-4Q1PyBX8PTf9jFltsle70FroO9CEo6rhEvmAGONQZTlEtrz3QRgzVcprLloLuGLQGC_h4i6DZZbsx-LJ9Z1Zc510Qnk8wzW3pWo6uE1ueEL24QDbXMYPZpEUEl6JBu-c-1xaKp66fOkJiUd6IWK5sLV_dFUTnC0HAFHwIY1RjbjyhGo0v9D8xsFqocaWmOq0ubBwoWOOZF0ZGDHRT5XV9Ztl6yv9ETlHmcM1IlYXnZZSk0FyGxO-N6YcoOD7OujoSmf77tkLl5kt9mxg_qorzd7BkkDKjUYDQXFsitOevL262Jm8COMnHKkPzSicLA6HwCHG0z7YSL4DAINbXDBYzReL7Tq7iEbbcB0U6T1y5jiB28DrWBlwEhYn_sGt7sHl1KQe-Sau-eGGj-Owq6YaZhmvh7UPwjZ188mNjJa7qHjWWO1Y0caYjH27uP8Q26uiMC0yJfUFzLRfU1puFovgTDNzMxfGOGDobkHcsyZNKNmOuA7Ig3Fc1jiwz7oefUjowsSeHf-aVpH_JBpT_PVh6TrMzBjvIKhP-BoOJfMdBeWdt7rHHRA5gdneTn9hZGVgKcGb-kecDXetLQKjUgzcDUiloL37sWJDnq8x6C5URvxYL7n9lhpQFFzCmH-xYv5AP0kQNHH8W7e9XwwTKbbWBa018LCBr5ivZ7kgTXUTnKY-l8hHiC0clN6ie1_8wN5Yo1StTqcOew-AAb261ye1E0Q9KtH37ZzqXGOi33K9ABeRUb0cyzAe_s3pJT6_71z34j8goFbR8wLs9Nr7AbHwsk_-dSKc7oun1Lk4DGOKLeAq13gKlYBLtuXVNhHhaScde-VLdT5_ktRKKvkYRNifq3DOJVoaDt6KR2kOMrjT6c0sSqVyZkL7oKsmA5a-bJi5ierY60a6rRWiMM1kGgK3Y5qguASNA63kxiVj1HNjBgSzciCrAAW_Z6-JbRZZHcV6c4rWMWEe8l-BC6dNEfQlAiULGuG7WlvLpOx-oQIngDPPCormTG14WHsJ3rj6RMQsYUIytCy8YZVi9KhKp0080o-lCrAwU6TGMYRGNz_10kO09SKWZvrQqTO-kKV_ENudhYvudGCDHuFsBVMg9499u5VZL4ajM5zsc0m_rf5laduGthO-FWiqPzpONMBe4hEDreDiB6hsSpvNlsTFYzLJR78ZMiez-dzonVyMpHZypghEmXGODW_UscWMFKORtfAyoHjfKyN-FD_DeQt-vDKqSRKxfYBMekK_rEcePf4cEtWpgisufV2-40urqZgqofdcaGkybtwh-SDKHJKmwbZhlUR_qON8bS8slj43uLcGuwWCQPyeeUx-g7sAvdcDi6cLkjXKGupTGI8NhjvGqr-V_dHop7LH4XxyD61jtw-gFnHykEGLquauyzTLcpPQyYnFuvWdSBGj0wVbTkwmdoJP7tIxgigk5VuFcw3HdgBbP-ZKmjasNX2M7wDNoB5eZ-trsvjx7ZW5OgeGGb909EHVoGK5BDCWbYY_Ek45o4emgH2PMZ1y0Tci-Hq8aQyvRY5b4FUtQ_ZyDqPlUcpNNK17hXpyMLwSIXirU_CwoiFiG-x5tMHjP3snnEZOZ9vITP1iFKNBF_0dWR9bBm_TGZ-ix3LKRU91TZG5dCjNkOipRK9BYfc4oe5IOuSAQIV3gP-kIvVpuAcgrqcMdpBIccgqEhdDw2eHmlf-gJjv-MHmLkCACxs22BYj-rzvJKQyvz2hgvNzgw2GMaokovjs-7HVwWSmLaffq7FwKZGYXxclOkAROU-PZDTuXNz4t02NIjEzNigkfoiSb1QVvGLnTILRgJIQHkICknr9KZTxfsfe3AMF0nUH42nHBTTTjjItwawtCTGcVvV4Q6NcHNhGvcovGG5IgVtiGwgE_6j2FHFR5MUNZ5MipZWnhj43AVlj65Vo2f29JjBOzWOrK1_UWSj-wxMwO05sjPjTCw12iqBU9ah_HQn-zra4m7XA6_LjZaR5qSl1tRHqlQM5WdHNkr05yEc38oJWbwoagOG7GmpDxvrGzYNurLBeHqKU72IUwh7pFnCRX9szxwB7JpxPxgC7BpT5HGeS5l-aShx5jRkVne1SnxR_ZGuOFbj-KDh_UqwsNpxSdJUf3FiGcbnYjD-ok-XU7OJ_uETeKTPx3OjwRMuT74OpN87B8UwdNU3R2aqIGo1xmdlZqxuxsX3AbjgIH7Zpg7FGhpxdpUQtQFgByqbBUMwlFHMwNcXaq7usPe8w--M_RU-5KcXxwywUm-jAKiFooKHwoT4qyHh8c-97URJIN4wsTInrmAWmi7KEGLfaMJDsOw0KtfFgaHBod5_kUNTcw8vcl-diuiHzppcPAI5oKyQjiUaPfa-DEcZbNcBq_emu5Vv7gVeI8AGj7sHsJ8Iv4mjiGhyUyA-A9RuPamK4B1E6eQ6y9_rA3dj3cZRrb4IAv8oRDkB7hN_nARmjLsYdQv7duYuC0Jz7O4JacZdLoqiCWkDZVs1L8jI2JxRHsf0l0fjcAmD-BstQd9Mt7miD1jnFsWRx4KggoOurvZbwNRjLe8JVEQWGmZihGt4weCMjTyj3Y63bOjmgVUEYpX4WazxScDhYtk98YqcVYcJAKEf8Pjt12Pwar-4aTwoQKs0oFTz1gV3uPPqMtW-PoyckTl6ynSJm_mre2ZP3KFoswf6kF5hdmMc9eDfpcLVolYGw6n8mGhnEIcLdLVClXSN7YmqO6fkodQWECebivU1TewvG3kPLSfsORzIPnc18XhQIaUVqxG0eiR5H64tMp3sLjqqWQmzZUVJ9LY8hIRr3zSXkXGVgQFOKLzo_wkEJl4vMBuE8ksFMCdaCzdyNJ6NgqC165MzRtYHEO-nc67ZFOIe3ZHVAk6lola2A2JK0BepQQMgVEpgunjXktB4v1ljq6TC89LQyXv6OrvAkHsmP8Fkrs825n5FO0XtfyX-QIttyXsMFPqR7et5VlPn16hnwREEc20biXOWwMXVEgkM6L2iGd6GPDtwhUqaH2kReXCTZV1z88TE4JhHh8HrrVGrelLZEwgLfpv2HlGrijc2di5ok6Z2KUkD8Or2iBPFJ6jJxosJjfwvWaK_cmMUks3Y55dRZsJAn80aJuB5IIIdeJUOLFEalH8MKWjkPuS2gWbkYjsWzE1O4s3U2uajIqh8I-m4qgaXS-IAe6NoLEsU19-gdponzQXYXcZKBnZxZjUkHQ2dc8A-2t3PLx4RTTZg5okm5HbbE-2ZA7e_0zJ6hfijPAQUqRidQrktOB3c2QcsoPZ1pjLxH8RiFOhypf9O2lhAWwdAqRBSKL0sYhzV_yTdopQwou7fq0qyWDMNPu1Lna5ITLkv1lePTqp_fy0tlj_AIjVLyQ2V-tx7T5n_OwPBrXnM625hLI8ukjqiHjmWg8UoSt4YENa7nhTmi5EDuYgEoV4LxznlCVberlliDvJEf-32cPcRjQH6Rn0u6y9LPEcCI_ekpJm31f13sf_Adq9kb1B7yWegJh0kZMFuijiDEYwEIdgJNOGs_jfTgUX_MThsI2IFT18_OyvVZU25ZZ9oHdpqvZDz9z_SngGHDDA2lFb-z4Bcvw5AyaHxyUs4vBQQO0fseVgJeXhfkfBb5CXztPyVuYwBvStxNOM8YKMxj-JT1qOlStPFCUG5X17DafjArYtL1_mHDmXtnZsL9c14lP96G_PlfvL3szMDvBlCIo3ggbxh5O6vd3yOhauwqsSojJLk-DNhqxvoGo73hdgfYUbCEuvAuQqadq-F5DnHU5UbvQ15_hSwp1aimfbUGOdvVfpVa7RFr2wNNF7y4KtAtVqq_O7aE40iSYYBBiHbksQOnROYpB4wWjpeSqPmctmcPuWIhZzHiM71ngQMrxlHvmcTabiTB5gAM4CYTGdvBPa-Ph-Jz594QZMAuisMns6fYcl_3NREfNQX1lAarZQxVt_DLoEODcUUVSI9KR4onknM_0DXB0wTgilPJDYCGI4EhLdhaxS6fpvETAoXynYRzcbyzBzreDKD6Q2uhmyAgEFWY197l3QM292Owqr6F4TZqJCgKe084LlK3yNY489_z3iL9heTbiFPq_U4mi_kiWOGsrgxJaW-hsPUgvQjTdE4353BXBeQO0ldUJydjujmM9Nx_Ifg8Uk3vjrPnm2jTF44RdfHrgot2SEiF2eSJ5r-2sXZzfcnfSTkJwNbeS6ynsVSPDFWBT73W26bl2Wx8e4C-MuNLHWs_9Fsqf5gBiPFROXQ2aXNk6t8FQnGVWpqdzD-X68qenx2r45-LFK_lja-Sv6m3FwjKz8yAOUApF38UkygX3PetknguKG6kSm_eAcnJr5UTDEXFxHnCnviQYj3Dp9ItaAOb-dMGj7NL1D2f5dpj6DhTc3M_Rg4R7Yln51SwjWAaGGeVmG4EHVCB02queDoCqyFGsN0TIB9vzpAdpecoob9WvZsSw9IbIGbnNj6Gkd1xdxmblN4ZZBj-x7LqOfEF8Ja99wg8mzXGh1DzZPcIg4AhRnKN2gUrMPHMlfN1GXbNnn1iuy0L48oLHPUBp1BVW3w6VqKh68czUUV2LXeUy_J7wBhhuxiWiU-gpqiznADYeU_6KBewqZWpXQKEjWPU-fsr_2EEE4Z6LVq0Rf4mPxK5szXcfq0Wti3WQ0wNy0GKnovKkVzo3FBr339dm8hwqkF7o15Tg8PX8FVmthicsEkowE4XrAikIJNMp7Zsk87e5L8dMsVCu3KzQ_rhz7MiSyZuYur9NcmGaQDgFcAaMDwuYzJhomaTCSPb7umfERb_oRdcR8S5m6nOrBALFw_pS4l5FABXRyplNDedP3KpSc_qZz867TR_F8tFnf-IcoTsf-OryGXfS8-4VxWfurx8ehoa9RvHHxyjTXI9ugyxyqi9x1H3yRSgfK6kNexJe-6WPWhRmAmgfvFkij6XmLl6aJ8IMvP0Yj_Ak0hhaJPvhCHGHyd8_ubf9iUWt6sIReF6aNqMoKnjV_5St295JvFNaZTl87R_-qhEFn_RF696nITWOjXez3cSRA7DfuWckPCCnV74PV0K6QtvOCuU8yZIktJU3yEuDXarduIePJrxFCrxXd8hZKLeLtD28geFBQrKvAUfNk3bONwuiq5FUivNqFmg7QEirdW9tLzIVhEEdwZlXkNxsNnifkurPt5DEIS9ee2A2T56bf4Kny_We0TWgLtTTJCUnGnha9cKgIYJjkSG7rBS6mM4EcxSbVoqJhCFmomPkZTBG4nk7ra_thtCAx82CZraxUYe3wqmHnUapKO6Hc9uReQIKaCnupPKGRBXMEH1y-6J0a2RWHZnnMYwxFvXugsdCLkq1f-FBmiGElNPsUQPo73ReMhnXBa8D59h_0Pk4N2SS9F1uq1ZMag5d01HMMO6lRtSjKXVyI78RakFRY73_IaLub8B-5XdkzOADjPoUzg4KXZVXOpCJzALXddkGWQJrq4wSeN0KmVDD9o6k3rC5O5qvOS27rnwVLgomr5BTGqXNztBQEwLpJXnWHB96_zISY_Swis1JMsS2ht22mPTtWq3aYVWoqY8p3a6y8xT6vTe2S4JpSHyX2cuJVtibl_pDZqpMVUvt4xlBMg9_Nzs-OaMJVLr4Rcv7wOVf488BaA8XdEBIaE_mxd9XX-3K9z1FbFBLaEvmUH7cUsNpp7mBkvU8BGV-rXwep3TK3pheCo1jk-vXVu0RrYNgFUJTXo31NDsWj-lwEi17NYmWB1k1sp-sKLGTjHtsRLbuzZ8wgop1pv8XlBzK9LMiJfAPrhjLTDK9fdt3RrSTp7iXMcugcy_tgRaEvvN2H4dCHdD4aFVUQwmwcCe2a2ZfTOlUroKN4va4APME97wWU4grttA2V-EHu32TVTk5bLv_7-LJAKJchIc_Z92IQMQv6RmEhAlqZnteaA4q9ktYEzVkmO8vPkU3JRb7Fn4pdqqYr2VgoXfpeQW7zXJbmM6vaYHKHAwE5r6P5rqrJvLj0oAcN-ODp7iIgyLwGMlmDsE72yC3auKWrF1gfKNrB58ZNxeaveDnIXPcgyqImYApQtQYIZuzczkdHHCr1q8ARADkmte4KTdel_ZKSqRtm1Ctp7xHC3AyNUNtLyTNLTuPLovjPc5DLwyWi7vWhcz2eSqCFGlkq3ZVJrgtEexDaV467cGwrFXiA1GP3WRIEYhfnJjh1CuW5XNXOLQJtAK6fweY--lU0_YFydSOH3A3Uf0Rb-JOY_M0SKX9Z6Fzsfnv7x5w5QFBezoTx3aoYMWaJyjyJ7KO1IEvVeZnq6vf1gVfIREYY9cKKRw7x4F6M8nOR3_ug4ohm0Qi6EsSyPzmdSUXEtxxMlPZAxEC4phWHS4Vko1yCUiRtd8QojPhccrJceyBE4hZ3X83rGgjiq3U1lPijhQWz9NMpdR4gSF-FGih7iLMlEF0Y-lqjbogQuNxBEpAOeUPw80vIeUlJ5T6Y97Frk58xfc5s6Qtt_hRfHyCveXYXz21j49ggSROJ2l_hoRdxrkmJ3gnO7CcEt-S6kSSdRaRrmcHZbDP2FA8DREgKnW9rd0gc3sO3zSqqXKO4mFGgV0KBWCOwWwAd2lS9V0GGAtG7bahWl4cYDx6bVpVaNhcxFph8eTXw1MNzEseEccek7rL34gWQhinZVOEtQJXGUxGFDzNeZFuo8j6j2RUQxMStIzowxhT0kpd13jLmfFtOQQZAMfN4prGbVT2gg81bcuE0Bw8FDv2WbGbSj_r6J0gbiGuOMXXoy-CkKmeMw8G4VUG5wtu5rcqdvtkxC-mNeXA87tVrSsts3YBowwlqXeheshh-eTn8LqGdo514acAS2kc42DrhblrZPQrWEkLDTuvxf-EVsJiANhnWbArt7jMKUeEpSe78Ev2xMsx5HRs2kuaucWf61gDbW7eUMd3ft0_Gjjyz9PogNy9Y4ZgfNoYOcn28P6Jf3yt6nhRLXGGG-XlxxEeeDxJJGSwWVoaLb5wBmr3Pu5STCartPaDx5OrJaiPY86pjSxudHZwMoRwatO_kdsb72PyZlIIwsZYeFkCFL5X8xami2PZ-n5lpAFqMDKCtCxqYefhu9K3zZrT_mbCKn0p_WMEVjs8IgxRiy3LowTgSrkNO5Q5czpEATN1bjrRgYesSDavM-sVqy7bz9DYbeouBVyP5YuYdE8ifJEbp34i_uoGC5vEi6F6Nj1M6ZQw55t0gluCBy3-E9-WlFQoKTpP3eFF1jWBBKVer2hYAZXRmAsO-83CoflRJkcLMbbo-HLbys7xFQUPo-P8mlih1BkFpVf7hMVe5NWT2oiuHXy6Z-sALmp1T6jcoeoTrpYbVh6l1o5ho_ipbfyEJFgRx32Ne-X0VPt0n9w-aZ0vXjOhbWAbI1SYDL-0AH7XcJO5FB-OD7NSa9zk1AzOebR8ui-Pd61ZjM2uF2mW_1yUP5jteOA-AXyEJoRnas5L7u9lyKl_BXtvQJR8xYxSD9XaIlKqYe2PkZWSWRWMoaNWknyYSQr9p-B7RrKq_tr7pATBYDtxKHqlYRF7zAa-7z0-PJnzpL3VaWvaeJcCBWJLpmICITNtz5mu3VdwAxSw9lLSLz-qIdyiTzHX5mSSx1_yyU8ULpD2BON_l2A91ZeALt8ypSxVIdf_2eglaFOmv9FcqUhYRCC_u-5Nsd81Ryoo7djSoXsIgy44he-BNi_Hmz00dSj1Kx3TDqLmRxL6p15jAwswLV19lDFm3YOvQYQv9L3w_PmmmHam_kOXAgVRo3DRFtzyoVZVXMG2Raepg62Em-G5Qc_Bn6cq_br7JBrP888V84Akp5i3NixAnGiINa-uwcMz4m4pUqQw-Z11fss_xI3HP0XhHKkIg2LuhVcfLJFnOj7RLKOgsd4HvTY9JWoykr_z5NAF5zgVBsvCEUYzFT8MGBPfcPCeI9zut_3-fAklrPCwZR-05MhUPACy5JbCEgsdIUTAmMEBv1GD6F8xeG1JieDlLbkCiFGa-xuc-e_BCO-RZ9cOnvPnJ-71R86PdNNVP0NEIBUKtGL3ydK0fZhDhrkXfut6QSr9kUfp_EmwgrPA1Qam99ImIoGNohZFB-2F2vQqGmsduW1wiMZiTs8F5-SJBt6Jrp33SFPisbsDQ6wBe_MSNmflsdeE-qiunFPAO91DQipqHLLuwhO2Ab7DKmXyybp7nswGm9zUGmQuSzcF-Raptagoh2nGY1ENx0cFczdGnvqb4Co3iIpRwiia9tkBG0wgaWiBPUGNkQqHja-ljE42Gy9DuaM0q0ULA__jkPo2wT9f1ItCzsiqArYG2YxDzBU8aWD0knQZXElg9TnhBsmmRVXyi7dim252eoRMlgO7uvlSDLM1Kur1OmYWh99_0DRDNRSi6ktmi6XpdEfMlncSgY91oxLgvwmSA0zyaAa12GLQZsGZLHFz4_kJqbdT2Y2DrhuUak2zBFQti58Ll6a3fEBRGksiBriCRNWQ_h4agCaD2ewv0GByCste_IdKmAGCX5Jz7UkQ6cnxnww7-PMJVm8SdudonY_fsQpuFpQzfrNw3oiS1btSKYdPHyPD8s-HN5zacLIVJReiuFCPRnX-ShNBQMuF0jmWXmuKSwC8ocP0D_LfhqOOaw1ddpQHtkWqcvb-EluO6i1W7igKoiTZzZZvexMsx4Uo-CB7alfhw9VB-8WSubXrsBbVcLGr4Ny6fJdRBDcW140cSRuMiqWi_maso_lLg0x0b1LGW5-_2FMloFz04PhtDdeApDwZsp0R5V9sbOchIz1TcZrREW9CwK267J9OBT0AEUKvY_nOlPzObaJ5eFYySpZQ4L8yLHYku25iNLTuvIPJuayVvVuZ6W1SMjcMw6rKtLbfJW0EhNxSJVZK3XmXNFSbUACZdbR2EJ_gVFOb8sPPVRunPRUXzrmbl99wth8p4oTt96LSXLGMXB16bCsJzaJnQwyfGXBZSwYlVgRiKWGurvRTx-M4sC2WIXidYRSoD-X1ikoihwrpGlRUyihVFUhJYzxyC3jeFeuqUCSupuG3WX5LEwilwppfwCL1H4yiGLM3OuqkHCzndCjIxNnVdIdKuP0kOwhL1rqmBIKzXvCPSGCEC8cu2Vp4EJjlULHBeczf7eDl9nefGiehLhKmwqv7N5-0DuDIaEmUpwL8FNRfVw7jtpteVx0M2JfeZL0WZ5l7IW_JwMFDanBwkaRijzHIxQx2WGzPlTOnRTv0H4RGxG89BB-ZhNqk3RBwfpjYLgQo0Lvb8phS2kxwT39CeVh_JPdQd4199QS5eiuMPwMxsDWLI9FehmQn5TrE5FTLCudIBTTrLt8EAz-NjhDz12E7NV1arog1ywFxedv8wL8IoSHzDKzTuXsJcgQBqa2XkDgtTVCXYAI2i4Ox1LE3rEmbHTGJIR6QSHAkD0rYbg9obFBe--nh6tE-OFCQfhX16EkkGFN4arEimbeI_-s8KkUAMCCSvvH9BdEFLaIZQzdZO1FQCp0OFX8bBA73VWJLQlI8Ux7ss-axUji2SoIZRLc00LDUgeFfOOzs_ZOGNDImRqiThPrjSB8ZivoVtTzmpxmCsp53ISqNtAK8DxClp-NqnnLdEPuK25L_G5rFvBASmnZHmfzzVFJZNuCnYacE8cyWRZl4RuEupC8Ku8awX8JRE9tvN2q4lpz_T1gqnEM8LtSg4qjrM3iaMng7HM336wl7npEombFU5SAqmCHpgpjsPYaEhjPLliGDOL_ogqgZbx46ofFMUyLrAcM-E2W4LNQxz2M4ckpUKks8nMzCd6dvGutUBahsY6G4b-puAFuue5iTt3ztJSWS6RmjmVx05pNBjLHASt-BBrOCFcvDBRdSDYyaG5pubbiiwvvY6b6ea00VSrR999wuo2jU2IcVciax5V0rJPabSYwo9p9wrS7SFKII4d8ro606Ky7gbT6e3hXgL6syKO6onJcl49aCBtcnFxrUmtTWX9uC-2Wn_ojqPerdPbjTLJ6LcyL6YEca-qfU63HMqRwHqhV4AKYur5ZDKEREY2__XIo-KWH33IFpSSpHREb3LIzhl2AwOFVeA-FwxUxUw5Gf5Hj5-slOjQskoG6XzLKByD7PlzmKOX1wZiTYMl0aH2rmhKJgMpyaq-2DUYx7IA-NQLkI0dR5tcyCiNst6m_Y0z0BerfReLABDfDXNUImeiZAhNXmd243tc4G4lsgAZTl_2j-Kk3ufyQPJK1UhPO5CEulZxNlNm-8msHOFYSkh-RWfnA7K6vNqxbwt_SGjNopuxzI4GHMXPsOHEub6LoHCxkk_3ahCS0LHmoBw2pMVUBk7rQPXsfuXkVfQCkhfCF5UtWjAFVbypLSuztydrWsUsp4v09linxWp4NQ3RLyymw_AMTsntM2-SBoryyKlgh3hXD6om7RvNBOP9xOd4LEGLyvQTYYw9yCiuJ6UEX6KBlchygUCrLrHPgo2T5e5edKh-ii-gy0hmyOsF3ldv-bzSqvhW_2u96FklJ_A-mukDqfYmetyzdSjGcC9AvRz16Eir3lkL9UtVrd8XW3BkeqBZeLRxo1B6HhyXZ9gXqotfXt0lKpoA2exINr1LGQaawL5wuO34WXm392dpoxpMBf85p_PFNbMRwQeWyXE70sIUx8NY_cDel6q_Dp_fGgAXfT3t7I_ui5FAvGuzwqHW1lT-M5MBTB_wH0tuMw2ozezoFTbSPcngt7U5ICdOoQ7EznOQ2OOPZOhwDueiYaNI6gJVmipTQyPFQ33wEJyUf7yODm1IDMbCujAjROjxDHH20MI0Y5SJkVs4vhgq5J9K2PiWVjJ5ZwExYMLa9L3WQOOz-EKa5HN7VfJ3JWm4w4X6AScEl4InO9IMuB88ukkLObo1ZHgyclV5u03yTUgiGSbLnqe5EHad7oVnBfAqMbX3757s_KNJQ8GCJglbQWz-JRcQI3SwIl5sSxuIgK9rGSBmq7vFOhw6aZaqTads1t1nVPsVrQMlCWNJl-ruLMPlJREGB5_i1aPvCLk7-am1VUesL9zSJkOC6oVPX6V7Gl8Rzl_Ot-HRliAKXqeHipk5iNaRViVz98zZ8SG7MeNRt0DZm5dVEqT32kxp2VekHgA_UcrC-oGOVGenJOgHFa8ErQEg8XD8CwVAK_ekylqyOR-hcDTwFeyn-NiBorUjjD2r326zPHzQXpmHiMb6ntmxcTBypKM05DYH7AwMHvA0PzlnbhOkpxNhlIH75ba59AqGMnW98XEAu9f7QZlSloRx3bREVp_C3rzAF7_-H_VBsRUxyV5e_XYyyopNmMpuPNfQhcgSfjzFTtkMNT1s09DylS5qIstX4352-L3CkkUIfj0MgY55JR5QHvz6eqWIU5mQybtf8rviLiziqTc6xwqbSudjp3QBiE-R616cZAvLnscLETC0qjjG_Gy_aUrRQktBJexlScaipaBwCqe1uFCPEcu8SqZhCCOPOg4qsz71nAnGSIAUzrkW_Oh5Y2F1AYwAT6ipQH3QQ6NoC66kNsP-tIe6ag0yP0lchDlRPzovrqyQf1DnAPsV3DEoO4Pd3i2brhuIRRfBuf7rd7WS3f9Qq1j7-DqYmzF2jRujdk-u3Ugge07JXd0oZ1vxexMjYAdFHyNsXl6-87tqgEpeEV-4b0LACI2yuKesMZN8rlS1zoaKDg56chDlfMsOYzoTUIvMsvzkSd10oXoiTKOpBuugbX2VRgccVAC_wak680QR2Dxtj85fi39TkdO9lg3WR6Ca5oIjdA2c_rwRIJaZnoKwdORp9VipIVRa4BV3IBiwxHoI2476RJ2pmKs1AKjWLTPCbYNKYdfXneaTkUyTWExSwsD7tqdMi3mHxvZScr2UqjFl-I9UF6ew7mNPJaEdtJ0T9kOQPhjvwyMkGgfxM8B1Q5JprdDVMw1WZYZA8vefgOpxTYjSnQxi-R7qp8-Qcb_g-x3tbHEBq8QTJM3oxh-qHo3p3B1Tsdp90ZSX0hT0MQVbhUwb8hCtqKtfueDM2TQknleIXysCwjgnAXu_wdOS_OYxbL--8QDFWnLuZOTi8Gz2VqvlMs69qIv_2CrWgvLbg2r0oFGIW8S7T82225teY_Sf5xC97zfzfnyPdfZ75hFNB1Wu1m5HKSuaeTGjWche8-FqJC4psCNUfK99TiBDoAL0tPx3JFVc0AIlU8r4D171OXX-gQZNuux_MCh1iCV1KnVUzYkXWcspZ-8Tfbhv-jcPOBJWC3xqGVf0Vqt9iww4YN5dOSUqUJidJR2eqMQtD02XDBZFnX96neFMo6dK4D1_w4aSCQz53cAON6TH3LmrR4ephP-qsWG1FlQ7rmT2I1qEwhYA2ettLNVH-JTOqsvKSKKQs31ZwrA_eQuZIkRb_Gpf1S7J1MlNR7OyokD3ubo8qbItVJ3KnxbijN3W4FqAt2MhXMmDTPpcyRwOqpJizY21kiWC6noEjqq4VOzTO3aCxibU_fV1ty-uQsNyvFwoZOPAThsj4uHEUbh5sdMuXATJr3XCLkfO7L4ZIXgzUIoX1iMJjXNLYAhT_OM8WRQCv5sGhN-jL5nZ39zK-Sc7d6W_h6mzEMHgNnnYVQZ3zGYsd0Fm2kHdNzcqa9ybP8OWRz-JTLWLo4b3xwTlGCyd5w1QADnI1zfripnzCWUBLx36oJlEo49h3q0JDDJBF5WYsCamNjmK8jHLInXCSmGhguwq7mv4jx09tttSel3XSF4VBeXJSEdMk7AwzjYP_ZwAvKCWS40iPNfg9ZGkGH831SzmiX70bDgD9wlTf_esbjIqTt4W5MCp7prDonpDu--zkbucfqOvpImkLkM7egonOxO_QxBS5mwbO-mX7b2AZpuPfr5IaRa5DnNTBhTM6-gWYCWbGOqI7mn75wMgOgEZDbxa5evUWYrLRhOvry5TTnfYR2ihzSNkY4LLL4jB4Aeze-dF0OuuFJFX-s8YnbPPNo6NbAo6pUviSE6kXHKXIeiSH25Uff_QUBH9tWqbnenM0wfK3QGOuz7xl8xukmxeJ7M3MzxjlwrHBSVFBWD8Law1VI_UDggfgJr_KQGXvcfi3RsWQsP4yS-fkKpBeaGQLZ4a7I4VpjqTv1Ooo-NvfWGNDdBX3E4Ad7i9F_G-bFJzbJyf26QR8AanpoEgwc5lGyY0goq2CHhVq8AJf4KoHBPk3GJZjjtxJAPT5OhRQkkUYINxio_h7ebFXZIOUe8BvfbmdvGn_HxuL0j2DBFjXwkYVccuGkpsOwx6sdvvznyMCnuzyftx_jAsSUCKdh4gFLeVhnUfdMcLz5t8r0laSbJRz0i362Pb2LakMaRfDgld9GuISFeNPPbZVHU7wtpR3mXrrM2whC0h7Y5iaQC15ATDBrXq-g3EglRKSdckxVubF4rtyEzBQMb2H7sPfue-0y92FFjPgnxbjfvBuRQuEgewMd9ThM5nvfWoucEHDZC0s1Lr6EQ2aV21TH0jRmCAfktGD8v6scs4UatoLHY-QXYLxTtsN0CO7y_r_KP6OiDRV2QtLwycBhnPTnzGuQKdRqZoUzc9jKcwWy5hH-Hr7zmp_cOFHznyRBKj0FMS860WspSIc-fK1eERJIySqzPl--0rUUvvPN2u0zibJ4sy1Kav9aXtZeVDh8ZsKUzt0PJL7iQu73NfLggAR1rCIApR-EwhND_2VJ1hfEOwigVfemZ5jvqO0QbujayICdCrkUFupD0JiESk45xjIK-TbJ6Aa1ftKAjHbk1qP4I6I6w_7AHJk0YHN0ZOhzzHbuYaY8CvMfIEZZxg2ukO8vm2ACEUKaOa7XTqLVnUOZVqWOElobZOP5hbYrZHg3kg56bVNQUxmk4b6FCe6-eGoDk4fQ3niBms24p8U0jdxWiw9aczm70eMzTVFwncyPWW6Y1AvYwuouSUkoyJA0i6Qz7Qj1zgv4sJodWdfXcPOM-b6JwLJaiAS97dumPWjyYiEqImPfMaqmpe1kWps6QJ1KejBpNdYHQYShXVqEDu-Hb4YHJg8weoFIGo6HZXvqw7_WwvxMjrNGhilZAe_0h9KJ1hkrldh_O4EaaAC5YWgwGVVmZhSlnEqVXbd54s-SJamdmtN02ROGyatxqa2jZ0GbJcG_ItuQyhM-we_V7b3WZZ8-HYNcOc7jQo4fpW-otVJyCWd7K5tjpj3uQiGH_Ef8W11IEeLcwYk3yfGmyIjOTBOy0w4ztdm7rhNljn4dIqgnWLKs0Nb0TeL1KYYAXs3t838pP3tMmyQql6RKcYXWJUgBYuIYdJVMfIXVygc-9c7H6BCIswmhKyH3UykjRqh8wB8FZWuqee00oAt-eqhGpXNjGdOXfzbbnr0H8royszfryf9LwslcVaMYGcjjSiwypjl3TRzhGzJhRt1aXe008mO-FCoSgosEXwKX3q6bMblzK84QxItI7Rtba83_1ujsMvZYq8porFh3IfMQNm7DTeNOX52SNtxTTWsb005S76qN4MBA13ZyhbY04dJL0JC6fomNJlfgL-nFOTWJKl4FhD8kTWV56lJWMHZkgNlcRE6aCPfI-TIQEpBR71qcbOhbtj1-wtanP3fqSEkOYfLfxB0pn3YyfL46YUqYZx8Bc6_uP230r84q56gskvlnMbJnbaeZz93A82dOubWl1c0GvBXuqhPe9go8TkRckMhsnQ6x-NCAfC03nghNT4CwYhiaZRTMOtGMYJwBJaZVWctoZirZePz7wZmlJGYaiW3OS5iZhNTn2jRDfWKPPEDmVYYSlzBjBXUN8KB_y2Vt0uGErqyPL9nlrOoSuL5jiDN3BohHxE7VvmhWENIKJYF1sdxMNtf1lEcJNebSpDTgqkjemrKVhNU1rOLuOwKIvnJ5ewCqH5M3HUmGGXxXxtgQmRnhiLZWBkdtwMBa0orQMnhRwzWJrloBymAOSZ0_-wxYrLDsRRycRCQW1ZAO0huReRYIyu4DbP3GPhTeiJGnIqnAocByfdL-B5SgRhubTemVua9pd32Qo5RF_EVppPlvfj9emJe8SRrlU0sRlYEY8qLlET3Nb53UUNm2Q2TezkpqZbJ2ZxyOIQHSUNBvR_d_jZtBCmSzvSTUIrxOwz3ISTdgTrB4xdW5eNerni26rWrNzn-kH9aLbbphdUrDhV82mDpVSiY6-qGAGxfcmudKa5XaHPLoFbXR6cjcPJcu77mRWM0KLgkKFDl0MJe6GSEHBLKPD6XWA-W-doq3NJ22Ah7f8REHMI6X5UHyve42LVPRAvTgSZzn0f3iNlK7u2XnSzAry7ItZeEWkFOSex3R8DJ1POxxPJPVziKC6EWUTmbpPoSbu9JTq55tOLYtXvVYUTdW3yWwRokSOl1gbyqyOCYmrtsPSANCxnxIZZDGhAvRoqSoVPM4-mAuZSJYkkTjVZ54y2OOx2EPvtL7PZDa8fkZLec_9vTqrPBFsqVCDxCNsMQNgWU4Pn7RyBXhpFHWHdjAGHyqtzs3TA_qrxert8CIeBX2Wx1J0h6TUPvaFkYOvilQ_oUA5S7-QRj1-MgAv44iBNJGduHtdXma7J7EIIJYttq4-Lx1nbubmRzx8uGyU_NIu3P8QbMej_jBvfPmG_MgnTmRomryBDCW0S_GGP7jIZrz8XlUv2TWzL51W5CIo4l3-0o809tSYuP8Ofamms4nKt-sjDm_0IeIhviyDm55D2mVG1a2nNFpNkeaQqCZ6JPdeuIRVVWUefHVFMItje72pBNbHiVF-CI8M_ZjqdkXJe1dWFM2TQP8HiYOkv_Lssnk25yUs4YPwkCcRp5xfwODN0MeMHbR4FQuop2cGd2kR5s9LAg1dN64jBIZisc2REa4auxEsjOuZM2GBDRIfwvdppP6QRWdZ7C_2GwTcI0t4Z8VM-u8xtEgoFGmI2Y3LukVtTS_gGOwee2YYDkC_ukTRVe-tZvF7qPDq2Y4N7HTy1X9S-J6QsswQdt8e78B1zIpozByu20IFeF0yEkrd882Gkl8HUQGgH3MRyfr83rhsaWxHoWoKCnjX2GajXa1Xf6b85sKsiXIMOzcls8kQTqoDHk12Pg2ioTwREpTMvn2aQHybURLT1MAhhC6gP_YjZi641XskfFC66csDacM7r-geenoWJa_AKYKDRrrCWN8w9KAkGvbDXyBM43Q5WMTF22T1S9C9nzxebMKDpmx3y3cfWDMwtRpPLReNlX6_dvrKPvSGVF6TdU4TpfGZcZFKkLptrj644SLkJgXD_7ikwncrKDNYKPO8CDHb7E4sDAsY8MEwq3Z_Qsxfz_pu5MFYZD3NcymM-lhEY83WPkvBrlWYGYHboQFDgfIlWlK5sOeKpk4by40du17iKqtC492qGOUzGW8kQy5vDHFYYWdRbZtPqNp2wZ7gKPp9_fOMwMer3zrNNovOCFGygo4m75MGm6kJPMkxAXld_C_Z4rRhofIayHzy39OQJ4YHOU7FCnmr2TE1URnmD8IPlpoojbKQE1qYlHk_xtpe0MhglI5ocO8Su4maeD7_O-yFxRoeTvSgd34og0QroiRVL5T-sDzM5B_aKbfAWXWTs8q3xfyUlw-XV2WQfq4OBRIg16MiGAUixIxznJn5olQzwU-ejSuruxbPjdmKQkefz9ex7gNTL2JxFqivTYR-6WFaPjA1qplaJthLL_KiEPWJpkhpdykYsRHMw1VsiZa73gMU8DJk6rXRicppTobF5RZ0X4tlVlDo7z6vGoTnjFu5nuTHJQwYhe7EA1blq5SazQN92HGCSBaXI4ILJraFuKDdAUJqO65yegNDbBnOE55mmQFZ48qwsN70xxlFvb6FytJXm4fiCP3jV5czQWA6menFVC7fTW9mP1SoVYr_taEJnDy6RlISd3dZMfx8K6j3WNUk_oKS7DucYzbUcDqOzlQfZnKHqc0bsbl3eyCdSD40fjeoU3F-dwBN8ixIM1hp2_cH9puZqMFMIAo4mv6Gh6g7u53WTzqybg79M_PSqwnIaeDGwUtfeX2xB_7U126T9Dr1xe-5M-jrZzQf9Io0ayGvdC2AfbLqFh3Yr8pcnbQY8sj5iqtJbipq_GlmDeEcwmibda7mIPpC6xcTdSBAVzGOHQXtbHHZau8ZVAyq5uJVAD9OW8VyHwdcBiqa6dYWOgX2ThZa-AkuyFvP3-huRhY03XqZJV27n4kEdQwCkXEz1AnZQLoda9sIaGriwasHRi2gFHV0ioqzIxejTh441FWFVCUeunUKc-LW4GjfPStSPNdBstQHisxMEFus-qvMvxVjgWsAf7Z6iAJ2kPG2SrQVdHG8jzfmoVGwbeppeCYDW8q0CxD0PDZUhgmLCrxsrw65J-YGCl4Q1Azj2xiaUuqU1zGIWTz205fcybaCf-nLYWcGph2S4uYCULmi96FBtqsQ03xpsBvdVfM-OyH0S2uRGQ__WXfSEFE1DU4GhZbJZfl9glrweQ8n8Bt147MYJco8-un0kFOs1z1e-2IaF4wKgBX8GntsPrFJDcjPkovu9j_p5M5tqI3ofhSFMfeA5kgrQxWdnhfkOtqI98IWnCqYp5sTas4zmaPRk817z87ng2Lstzur3TCjLhIcdMVRSvFCl86HvFSerAUdXkTfMOwMLHKgNc-PtrQAFERKGVGNGFTQVRkO2upZlh0Xxh9FdZ-8HeFqcsY1i8CoQiWgtiHzzmPSiKexzk6eaPYfrVk8QtOQK52zCvzn4CFOR7nQ0zZtNCd2GHdzXPnrRT9mFTkIG-t-cOw2byitEopV2eZD29YO6fvMp13QtLW-qymtTAsprRipY81YQU7jDQXMK2ynxLbxW3635FqdmkvQFDbTpXQPhe4bkuV5iLNUBhlGc0rILpQaw27gd4D_DN0PHlj02vuLTMpYdL4uBG6j3W0gBsL816vJlOOyiefGyxjm3hLvvhffWvYVqPuOgLXXrIw619WrIXm7BkEj0d8x01uivbryd3YtaUOcrsyttxode8hlnJf-VL4up7B8PYofFzZpIEyndv6l7upCCA0IQhs2QvAR_2UykvRLZAeBBtA7ae-Q4lIwtpEo8_JRitfIAjCby61w9IAKDcG4tPZRBXbiTJuZiIIu1WFst80iP9xc7m-KMEPJ1iflaPhYf6B8C7YEcUvXEP6Q8Nik_Dpg9Mcr_cdgMQ7Ok6GJjjs2D2bX5K3kZ1iJ-bBvpLcJp0RnFZXc_4fFQf4saaQn9WO-6s2sFezIaXc2zOA3X4Edgz9S3DuElPRHAmbcULr9bcLRvfJjA3NFYMI8b8CPhWSttcy8U01khblEjrsp7Z6oq7biginakkhZc0OHYArhz_TkssLGsbL2FwmPU9L8a0FAnLRVLU5GlMnoEhjY-sbSuu7GUHWfdMT_OFCwN0m-USCv2YLam8cqlxL2BXHhw1pMi9s35xbGw75TuuOezhJgcqfvL1FAZNm66j3SGUmatQEaNRGpt_-IIISjk3xP6nX6frAirElB-_4GUnkNJG2ww6HE8ZUUXg24jiM_wtzfsUspEWfoLN4-UIMYBS1QYa8DF9x76pfAFcjbW2mRqS5b7XCq77-6FgmZjHotRmHyh15lgu8TqK71EDRdinQnOmnJtT3_oLx0iyY7Zw9H_Bb7MxVABQcC8mrVql37H7LGCrIOWlt44j2qo-2iKoAby78H4m2Aj1x6HieW4LHdKna6OCPasQZj2chuzTv5qyYWO-_nJhpQYbXhf5NCo6NcWAGTZDI7HPWuxDeQ_xgURH1L1PuVKx1phUpK4bGMmO7yJsmVOo_pNlT3euu-74H9QTl9t6qkg03CeVcp-jFFmRXFdM9nFOBPQxx_9hOErm6tog0hJjspS7J1lyUunUn8lmyBqT6ypcUoG8q1hdTw-Zuxkm9rwlGJspGRNA-pNYZxlCyPye1FMr8P0R4o28eXLTBzmwzbUP0HHO1TXQtDQHOu6p8qFxUjzwylBtl9_-N8yXfemCufbNwaTE76S-LYvLRDd7cG_iCfDozJ18QMKqzxbUrER-TT8FFPH_P18g7M2P_QdPEPwttGLTwiAVTyTPQ96UDJKtxPmQ4VH1xLG3xzGlo5d0HBnGtjqJYyYz-2CtsTLuGLgU1X154mYP3CUNIDkRH60f5zEefE4TwpJPEVAJaFqHkbUINxVQB0GSTeFDXXAiW70UW6uPOoSffST0c3aykkq_opXuwEBEcs1JYTBnd4j3Om2HnVq8VlMy31GEILUXvSC6cwyPy5MAZKw3igHpTGSL9i-Etrn1L_ArWEcuDx1uN6VDD-Xz8WVXk6MqjA6RjuQ0QYKZYhom-tY8_DQ5x7Y2xY_6x4pjRVKW9vy1lQd5axTN05xsi7X1y89gpayBQyUaCHmyKTFNesif0BipunGXP-vh7uEGWNmyAM6UwZ4pkmVU0XnbF4iO-cL-nRTe6HhGn43ug7-kV9MXZBB60msRVJeYNauyBD7gBxxFiNlRfTOKAlqlQ7GexV1Pt7JijJ6-yYBhzsU6KeJJvp6YmMnJqZ6Jns2FsZvBrgHv53EfxawddPfufUe13Ylnu7CcqBtseMGmT6RaDYNPeJk7G1wofJkdLxgVxusKJtT293PYS8jB1KcUavhgLRa99wy-E7g0tKGygY5-7IZin5sKEQZIONgI3McYKbCGeDal2kX4l3pX5yEjfX2LGsuBr6msdb_h0aAU8GkR27uVTl87a2Nty-HIB5WprXIgMSunWMC4fdMBIPHICiBDi6ebo-9rQ_lM0s2ahkGkwvlhjsZiC5Xis4lewYMIGopZWRzPjKIhN_onQOiHjuXXJZY42jZW5FLIo0qMVNYFIqh31qHu8n4AX7wuXdzEhDQ1yn_MpVjUui8-8SgW5bMGGN5S8MEI4JZ5FYkDepxlWNMv8I_Ps9eBKP5Qk-TpJB_PVddoVzoMAj-W72WTK8NDIUKGPvRzhyWdco7p0-IEpLEoQbvWUfE9lzmtNOJACTnXVLFML6tky8DRepGhPfOJE9iFsnmvl_LRL8C7wfbvkGjai6t3x7dNae3F1YWQNoUsY7OX6ERjGv_gdbWy5_fjbb95bA4nTXndE1KmCH7PQ4dkybYivzoIe1dRK9BsW_NN-9sfkcEpolXZvm2XdA00vTLMxpxDj1_0yvm8sp0zJruQTt0Whsfgo5scSbHa_XkrDr-SKngvQhOvd9bpo4kT_0JwNWCfQLnzkZbuAiVRhN5vChvYaRpX-HHZ_oYck_TXCoq9zGlZ67r1y0d9uGQkfgsVcHogLqfJH5bgmjISk1LZwMvmSXoUP3KyHMpjRhuJiZ6Whe-HjpSoce4-VGJKvRYit27NT43ozEGW1LTi2_nyiUvRSdXWS4J9Y28VibstxaKDf1b6bFnQaxgT2TkjT_cAx0YNEAHN3Q4AZlclVvTbX1Ze1QEN2a6m3Km0ke1GhKNZPcB5_NbLF4kqF0t6xuNfBnXj_qKILkoYSRTvs-5jc2rlglMOBVcQTQFiu2wpHTrzLQXrGmZZP2g07-k4fWloJE6q4QOJQD9Nv0Sxtmue0BT8kEfw0eLzPTaVOuXI_r4VVXjUJjy_Bvv7x9Arq3q-Ih8h0TSLQ-7yTvDAbSVNBBJXDTgK1ZhGQko6S1dq-x8yh-G7vQkbylL9M_6Uu68zD3R79XlSWRGzgtultXqpJ0KNH7whV5oMl6cyzzv9kRmWxKkPm1CLsHJOMQzGXjVPv8_Ekq8BriXzL9RraiJPKlkeMypGpXx3JV47nfYvEUwOKu9J8gWxOVtE-6h87JDBvavMzUQajq36TcHc-9veG4A_xOwCGHo79feKs7mAs4Xzl8b7WzqTetWamRXtrb2ioVTylppffKujSBZsXj-90VZkoZOZHKGfXn74uFTErvAf2ctI43w3l6PvE2DSu3nia0IO4rBUu68Z9EN_inS1xmvh1yjuKXlOl0_K5ywMfhrjSLCgxDLiBwgnO78IQMgjMxF4e7sDKzHwHbBXEzh4WKkEUZytpA3O0-uTh1xgQykRN5zcFFQK_bXeCm5pk8dSoIBOl7bAe5nXgoxs6Q6spZ82A21Y0_3s0iS221OfjU69pC_1R4TF0l7Ac-Pd8Zefy-XHfLBt3wuaGJcqn5nNkISUMRTcK-bQXLhc_zP9m8q9XJ3HMFjFmsFQoNJRmwaUyFQr-i4TIP-CJ-Uq3oYHA11kuZovlAfEcIe7yx_OdDZXuqenYkFnnru3UDsJ8eWuMO7s3v_vFwOqeTqWdn2zzmPA3DxPkzu8tqiptlh0pIV7nNqJipAxtPG3SZ4U2JXKAS409-TnFY69Gjt2sqGsYtOdAD5k-44ZJaJxbxnodMdLUa5APrnM3RlghJNEqBA9ri-rp10YzU-OWYX88F0gKsMowDvZUSV6PgKpAL6L771TkiFkevPMdEmog3de7k3yCqONLK_0x5QdsmRvT4o-HlvSpSEATXJYKcjICrGFUZbI0HqeMmk8at479u5Cj0YrD90sA529BSJY0IKuIMUSq7Ukw0y8RlOwmf3onA-tjZmJpHRwiqzf32CEvWQ9dlM46LYEjT8R9sBw5k9ywlqiYTRfxMFXyCVb06kk-I66lSrca3fLjBsG0YrmGJQmgHfHtuTSj4KaMzklWY927l1CbiuV95CFdSVo2GyDAXtUOoMdnKhOa8mYGTDuvfm1zlfUaPE3cWVYItPc35sxUZn1xu7eKAjR21qoJ1Smacqnkcvj4yIJRz0z_P65B7AK6tB_bWbnCFbtr8hTE9_BDn5H4Dq7N6BaD1x9WTE2KnEdVYh1VN42p6MVn5AOV8mJTglZpUTM8RzH1SG7ezAOxInYZv2lKnKgnuppz_4jzflW86L9tm9JHwHQxP9aXdaOY1wjon-ZUf9a9qZgYuBLq63Bo9PpGHWbZ1j3oNWqO2qLWxV0Pz-dzWHn5U0do_zVX9N1_3wdZHqDg5BozIgJtyltr2FI5AEKCJi3aVOQLp3u5sCBHEGPGvUNsjwub0vXQflasRu3qGep7a6EjxJnAvxnD4mRSOkuTie_1uJQ5wz-eZ6p3fbAof22OI7BUAvYthlQT2AmgIRi7anBmDCkn3oiUe5EHXAavjDgg4YBybvgUQXPa9-j0iF7YoN5-4OCb0-zWw74h0a4Xa7at8Zee2EgcPLopBpiAJHdm6sLKlZ6NrzwY8f_SmHy_l1ptp-0SbxLiEFb7DzRNf6wWmssab7hyaeCJxxA8frBCjDgfn4dQsfcvjJQu0qVv_OCFMsXGLhzF3ZcAGdPusb-U-qhqpl7iIew9-qrfuKYvOklrSpNYFGe9Sm5O0x5Pcb9m_G4uccnUZPNKAJ_ppELyf7cImyLEF6ftn5KKDR8mp-b9Uaasq7k8ZjWAslYx3zWRJSmIL7zhBarSFpnB6C_FRe_68Bs-diUcFCsJ9PQe6__HEfiStUSnLLCQ2UUHyhyYCEW-XtAPkyW1HQ9j-cVTsoUwgDYMjlJ7YLwo-pR0kSENYbbNImYsAYRGxPaekLTwjOd6sSSyNWG20cY2pBGICsz39HbtAKQhjEgl_iahZOLBpvYqHK1wjJOl8eH8XdtFi4hmi1WzSZGUK8FoEAJhxtlOSE9e_apDqWmQBLTK0RgM2ewKnDNUSQL8i3gtelfADca1Oo-80LCxOntEOL8DTXodkkqwPkYWRW975y25e3ZY0FMORpwo2wqQnerNYdTSywM5iNDuwwK6wQ0__eIMDaQKIQYQlUwWjahgLcPgNPIhPPQ1w_KHcCfoFmLETGYFwdsfxtdikXdREYKn9sRWT0NkOvp3pOpzcOiqgI_d9ZyfPBEckaux5gO1UXSIBBu8VMVMe-VV8Dw67lV6stiBMzsp2Ko1UU-0_wjmQZl84xi1_6rIXGkCoK4Zqnj_dSQRdoFqyh4vZ-2vN_Q8RRYbfLmpQ4YjlRSgal7TTCxU-CCt7g51fRHuxckb5eL8mRD9gebhpezeM4icObZAGLrDYqIrzJbk9lFkT532P_1CDOAMQFZ8iIGWNXz0EUq-VBUbFtgPpCH-gA2YiuMQxJl49MRdp9KUGum9cmGuWh79NpC0J8rgfOb3RssLRSTn511h9P8dkSRc1gCLxvDoLdylD7sQfylX1Y0Q52SEvDcxeIZCpqxHM8TzYUKBp9gTEhDd4Fq9yvB6yppxDpZAz6d8ulbL0SGU6AiRw9Kxyu7aQMgQHgh8aVsBxsD1XhFltaIbSXFX7yvylOd_Q5AfMNgEFjYlEDCV1EYpa1OMARYnmuLtd4A-12oo8pJeYPJwWXcLtzM52edS3-6J2jeabUjQ-8KklDPGguXDeJbXMGeZvvEPxrCgCqo0MdbIlpv4Vppf09MW4mzgAWWF3b-61EZ2_dgp64lNXTxYCZwgtRIMHfP6VEmJKV0Itj8X7eBH6boHmfodqQGSi2kWGgK0EMLJjP1nTnDvYbhcD5MzC9UWQ8gsF0jA2Pj3lJDM-qBfp0uVop4b5MMMq97eaoxJ6kUTVOEwv0xZXIqRyYC4fkTYSIMjbmiePkpg4kQktjML6Vvj57MlNVexUN6h2DgkePADd90UAx3dOdPFZCksI5MbPu_fIge56rZWCbw3WPH9rMDfaF8-MgK4B_Z1BnZ_6_cgttDO9nTOUGpeJT-X1OY8TjCe_b4aGI6FTqELLUGUc213IJyix3WSnOFd2GxT5JZSh9qIed0NIeSciPYcr7pMujAHDnsYETJjg8X1sk3D0lz9ft6yMH8Ca_iuVKcrSD-SJ91bZ0rwGNp9LFRGt8K2rQBmsduRNayiOXEmP04mfZ2TtQt-lYhxKx-t5_t3dXB3V6lDNdjPCMQ4lQfF91cFmwyPt1S4qH6YZa2ncyfYYkYQJIQfhtj7ky4FarkeSUCF8dnqdZJy8iU8rVr4NbNDi5bMfHTUa0jqxvblSINPCm6BKXsSFNafq2c3g00UBHzDGRXAKFyZKYqR0UajYDW4ONxHuLcgitW6WwAPzVkXnbYF5IMbBZypNvcP2zweI34lyNsMhKM2KGTjB1iS5vm2eMlCNVfgVZQ8J2j4WTCfRbP6xGdcjOm9MfDaSlgemKLXUneiTA4BxxKsBkIkKda45Ce00BSbG_sMDn48wamGXzVZhuNgnSe2_CbpMH0-Dt8HdjJYv7eyyVb_7Xw_88J5_bhpjh1bh3yRb5tD1Cem4gJCte9_J7LceRMXke-xmrCGpZ0ecPZcbxjGeFh6siqnCLFkY2G9phdRXCtiGsgF0EgsqhT7LqBp5ND-xgyxM2x5zNXphUqMtVG82DsOUf5cB3b3s8RZIJOgI_fi5k-EWsCWW4DnYYk9P6hwLQehFBvZSxH90BsQPG9aUljqyrXJrYrf9zo5YMCYWT-5hcPKfC6IuubXDqj96JVd5dmNFOSBZLa3KcBvGoYtd13C8lPTAYObzeTMTGoMsvrXUqAU5gp7qnVA6k_ilNn6AeQFX3ccmitABYwkNkFpkO1qSW8D-Axc_VdrjhRHQ3otc-R1TLwpXG1SUH1Gr1nCw84IMyW3qmQlUyr6Rogyu1riUVDt0VQzFmeD19KEBd9Np3cOmUu1zQwJPWo8aEeHGtpueXEr0Kn6OeRY6CjhQLSRBsf8L4GZDVgrcl2IihrmAVlB87D-UB6GyYU22f-76_FUFrEXkthg3N-NmOhm-vK7C2mFYVEqUxKeQ0-NiI1DdZkwO1uS-l11uQMS5HTpG0kOhTcLylvo5OH7vdDzdmvb-_o7QcdRp5xF_s1z_WqsFriSSUOVs6PhqlgmJIDPvb1oSwJCoftPoQPxPZOZOVvtSJiRCQ2xGk85aTPcd9F9bE_77B4eX99JwiMqP1fsMlsITGHfOeW8QsRZlicycMgfruA7uVQfoAwnuJK7pMuWG5grxNlMrv-it1ibJVlPRPv5wxNadZVNXvvBOk1YwDDahWUJQEx-6EbKwqK_ETXY7_yzyQlgwh47Dwjse4B4kBT2NANLGe3TwLjkN1gIiDHIiOb_40OPZXHlf9XVbbeP2QHNViBTsc3zJ6EC4ZzaWqLDc8STr1hxUlvvfclFcambPb74_5GRzJWgKxnES17rQZvxZ0-OMyAiBjnjmpORKXmExOkmiKH3707a4_-eWyaIS2yyV_oCFv91cX1H5L4LojLY6ZOg_sNj6K-gKfWE4EGpPBSaaShdWLr_zGZ9RasNvg9QmbbZokqf35OjkDvVibLkEh6p0dO2MbLjyOZJdWBWZNyN42miRe2VrH3PQGvX2Uw7Lo6MnXMI0a1aXYJtqtaCMKS4aDkY48_cxQp2zlfXwJAaTS5ojj1xwBy-NzVVmH308Z9PB9OVh9xr4nfBkpaj-Q389N06DvSfZ1hSINNEtYqE0wuGyFNKPkoxv1RR_aUaQe-ZcrxY8BLoTu9JOPBCThMFyaRxNh-ScTGfLMzXGYrXXPvo-YOceL6m2U-R7ZKHsqgVJ_Q_VkxkGw4iIHHT-oefi5hLhfaAWiWgevgXAjX75Fbd_WGsf07hUN9gzf8escOUKVDXyIWLz-9MNKuwgSi7AytO87goPQzZbYPLOOxoO-ZDS6UuPspWiUZZ5ziwamQhSynxTdP6tg1JcGY6EgVzzDAdglZHQT_VGqiVzCRp9gF-q4inCxQAs2kTvc5ekBw4cewAjXbQsnxUY-MyT4ElmylsiJjB3ucIauPWrh2PmEein08n61Lz2Nqv5-4JNA6_8Dp1rUkGKgZ7bDsg0s0xVhy3FfWUQykJ_-OJNCxLTpyMTurr2qCuCmltBUaluoF83qiy8FhAPQvnwyE-vMkftijLUWMRl6Dk5mFRF5a0wvzK9AeF5LWEmmt3qwCiTi5pxfNqfWrmTkyLcwshWC6PtTojz-6WZa4hSMbwLaM-ImryThQwduwQqWAUFjO7CQRhBBNghz5086wfPvM-t9vAVo6dz_mS0vXvzEzvSaMN5-e9_KzSDIAH8jmWAtGkMgIS4Aape__ykaTTqo9U_SSmZVphA_67LkjviNheyNHklgJBTSFv4XQXIe5UyiRfd_qYSQMufVaY-e70egp5dhqpapyQqS9roNAiTjhVQdhzsNcQb-tSv4vdVGwR36V8FgeF37zHZhRLpWcY_CUg46yNWUjb93gflaiFk8-yV3VLS9Aq-JzHP20Uea8nvGPjr5z_2HY8hm3SPG-JdNnk1qM65Ki5isMTs_N-ctDfKE2Z5p1S_5VM9YFaUPgLunxELK6Bc7eDzj9rc99hv-x9FA0XazZNDwwdA8UMxRjLHwmZB-APsrQiqedENAR51dK060bsoz5jaVSgsFj0LwUbS8v9wjgj25036xS2SVvPgDIIzZJhDp-avAwlkvPUlkVoIqBq4ubeIiL8QdymZ2iANdta_e4jao2b36EPCp9pBdXBnZS1uRpx2DWZmdbfX_UlUfmHSVa1cW7B4I-WBwa0aHLBn6GeR4v70pdSacX-J_8YqekMwS4pmJlQbYYicufgb5vWOjZ5ThW0gmBnXVv4nmthNzjU8lGjF0R1Ot9F9IA7UKjqrizMt2Wib24LRWh0528aGh1yJxvAvfbU_7e1XL9o0ZPHvTiN1SNs3B-UfTyM__fmZgEoW7_oM_xc7n_J1Rj3bRwlX6Ha-qzQ9V2woL5r1QLBs_uLVCaoaq9_uYgTWsn6yoMQ6167E8hTYbsy__aWE7R2UulQnVJOh9wgtW_SwRuaEn0r1KlfDcpaFquDVRzi49a_xEMdoW6kE2G7uSVN0TILxSN_-zRcVRuR4aGBdIn0Sv9Wov5T2ZSm4aLoAnZ_wfKvL6fx44s4NzvjHEB-tVzrFQ1dPJKdNuVHoQA1nkVTEfQBdD-OyQ9YtGtPoPJYG6qdJM-gcb8Zfj15WHQ86pM8ajw86_Bd6M5MenNZHxOAkT-RvBBLRotT6FV49iO5LqNh0_dkIxfVyweE8lzHsJIoz6ldGvx5P0XUIiYB1myzWKqkLoyGuX3btkyFzHwbMxYFsKUvygkjYncGMP9yYGXXDXTaky4WDRWuJTuUFXUe7gYgbJwC9aVVDkYrJ44Tb9IhLheAvb3oJbrNpczue7XUkc8aWr7kpI8rOXwieklB6pE3yuRLCqRv_eOyYvcuR7jaoP5vLuibLw5gBl315lKL9_bccNUKf4h7E5E5kZFMDAoPv_DnLGTUnOHx4NDfJxTxedsCjNuu_bFJlkFQHsBkFXV3GI1VGD1pH2MG4W7wCLjI6S9GJSAQswL34k2h_S0oQhfjYcXECfP7x1HZICgVFRbgsOFCfidGOzDcHlwJU4E7tUlfNHCzT7BmGhmHwK2GwygSpcluycA2FknyJeDTBogtnm2YYW2-3sCGvFq4OGKEJ6uCQ-UHFQjvInNSoYdCtUsll1_gKuPlHbEDLAyEQ2d-DLFakz0ofHzV6d93TdO32SxubfiiWByE65lUTm4jdHoxXkiVcFbJZNbuRG1fvSMGkPe8jebKglrDLHGxcPrQaqu7b-ZqTKWuKhMYYUT-MJOaM9vT90-NhDhdtxdsAFTxz3rXBat1akiQHGMt-Fz5VIw52oIAW4r61O5WFzVG4PM4m0hmaM9ULt-dqkkwNUgaMMnGB2357XrGaJjx2cJz95F73yynsBYpIrNDZtdGlBsJF8mp9Qs8ZR6Q6XtFIhm55itjAZujsYS-BcUHIuIDCVldig4SDB9eDh-8RuW5s3Rj-7mZn4IazV_84YmPdV8DEnmnHdb_8M_666KokVRsb0XeMW86n7TNTcILZmEoteywJXcVtbLzdOu7ZbN7evOEfFS5PY2KVcHSbkF_O7F7Qq-e6T4hjeeRT-C2sVYBXPnYM-bJKD62QEKVXeRkWYeRU9g4ARS44pr-B1P28R1K2hSza9C_3ovsu0uHZTOz7uO9xZ3c7bkiCuYSz6p80caVBBtxROO2SN4d3nLvwKuOkg08eRn7KdlMcvA6NHuu0UI89C1D8yRUYdaQRQpUp2CEY-Nufyf3L6SR7ekxbaWn9JaCn9C06NpbVdWq4DnGFAl-Q5YoYO9ZA0dXvagU0Iatw7-Iwgk-Cky_MzA8mZpdIYj81GTpPQo0WaolwPE9I9jq07qhiLtGuRH3dooj0DnTwmX9-B3_T-4DRnNuPUT4Xz1ZnZg_rDf7pjD5SHE9rCWdwjhF6u5WpEavbuA-Uewzfh3A0IWdeAuL34Y9LxifqRQBTSNgRskABUAh7gD1-QzeiPDDujsEAydXE3oFxvpSQQeb3HbUMymep78aW-grMWxbydeGKltgSSt_fcIwkFM8f_o-dmLbfjfcG_VdmhKhqTWiz0iOF31s-PsD4yUSREZo_DvP7xuYBwbWvls3t7bXAXOPowTsXG-VYppDnIX-dhK5KR1BL6SYAplloBOrG1kfEk9Ddi_kb3m7FsBeBOZsicmbnFvWlGOTPp8pW1eUuHANLl6BScLiM6nTGlDieeJBb4D4eWRlKwxDBxckle9SFfDBLUFbjHwOwT7S-QS3nuyFbEe7k2bxh1oUvy1IoJSvY8DLOYYyzZni1Y9kwL944xQId3h3UdGU5kgB7plB_dgP1CgWLSAkOxojPgS-j56J2JM0lXbvmW3EeuquGZtjElPqd3m8B_HqLy0kaGXdUDL4bSpMSVKUbYsHuEoUSJTJ1sWx0ViL5vOoT6v_B3WlIrOdrTeCb9GbAnckbXSmAw_ca18BXD-eo0YQdsM9mq3dulJwat3e4V3xy4k_C6tZXHFB-zlXPPiZgseSH4nJUMcc-hpILsrxXF3Aun8wIRUR_bG-7rS50syUnZS3yS2E90UIYAnoMrNAi1NzH5lo_v2adEIiwC33zE2_-mH99qGVgaLLkavJCkDxz_aziY-6oyZ4ARDdduaoJSzF0QDr2P1dMRkofR078iH9dNqky9BrP4Rm2e6WXCM_DPyWi5Hbj-cav0qFDTH78KyRCtZ0ERTRL-3GXGEGR5ca5O5PM2AgcmlY9PmiZtzCIPqS8jS46XayP9LI-OLJROENP9C0deq4rCfUmtjhYWBy32L4tOaLKGqqHb4VE4mAbJ0PKBnRxtlRvRPywaois6MnHYGQSp3IIahG8HUBxPEsu--cxKWZXi8TjGrKfCu3-KtlYhkAp0WvGKOB561NhzatXpF8G-DCanSkLMUrVpWRjdoYr_Q74n0ncxklk87RNL2rVHVuLUPU3kJzyiCX1hST-PRIuY4jqywiajliv1TZDQTMN1rDm1oVHjn2NG20B9TC2zxxo4kGx8_sVJlQQQBQvleJQfhJdKXc7_d7hCHNjLhWZeJv2GU3Gvlfqnr8EiJn2I6Wj15jrUCAFI5wNKNRCEaKNO1aHKeE514Z4qlNd4UjDP7vRzcUrN0eV8KEHy6rnd-xQEkSnHczf3YmOrgOsiuRJmKtgzLYT9yVdC6R0wl-wrCfao8hhkIvQMHqxMbs0cNVIHPNO0sjxIfzetvCcLs2XtBTOrhVpWwuEhJn5zWTsY9B8JMFgyyilVOch9jhv3ACRVw6jfy7dc3LQZ5_xl1gdAmoMYSDa3o4VkzJr9jB_ao9ihTkScdvDu-2NzMtQO_Rv9h3pB_i92wuiqhcE2ehOXBtXR-yu4fDUm8NroLfrIzrUdt1s8kcMGp98QAYoG5rER_lwgXsRPmh59XLVLRVNQEK1ZXqKpOm20VEuIqz96UyFlfaN00bsBweOGhIo4EIVxdlBNNOgjZWIPa0HXUwwOEZHQzXi95Xqc6C67ZkrCPZm0CgtkqNtk3vq2YrkxQAn0trZpdLi5OaSryaAuDUufKDFrwbpNpr1CCriT-1kyvbCdhFy7XHHtnxAaqmvczGbLB-g2zKhbXflH7gDouLGTnUDFWqTfFhYtnyTEjlBRolhusZ0EjOoGjRafq-4dbI3UZRU30htCWL4Zlb2u1PnySvx_TeEVrp8UemOz-eW1D6-H5VT4aoc_34H6p5Li05lAq9me3cHjFSmieNbxAWsHaDrsvFaj2Cn_zwaK_7ngMV1hQrRxwCA_ml0-q4mlG1W3u-qFMfKdy5JGKNqZSwWgAvlvAWAYxELMh63DS17KIGQu600DaxAOToXn4kydwW-WXXd6AjD1nGUtYB27gFCt4dn79EeRONIgCLJGsZBRovGJ0IHGKwErTCve_69uP1GjNrDYsOMLBTgxU8aEoWizdxOKip-xWdF-uRf8rXTyHETblP6d2M2NeZ0OPiuwXtQ6qOp7SbbFgDEM320T-oh2xL2KPLpv-nJqmqSRyl68fdaSgpUs0wV0l1mWHXxAcsGXYH_lm1St7cqZ1jh0jghr8bDAR2n2rQI2xKSsFYa54IOSBWTcAw0Wo9ya_OZsk4eE4LdHnDtaxPgpJ0BKViWeomsSgEM5EMyJTGz_zidIxR76EdR8d7TzNzjrvxpOHFXAC-2Z1nI-1MXjPFb16_MbZ5zML3R-2G3A6IZHVtsEkzJau9HFqLdxze3sRnEk55ns9ypjwXSJHQsvvlrNtDyvR-PD0CGetDmhM-KXySi_vkNCGBiZgegZDUcq_vfX7_ja6toM-LLkw1lokw7zhuBjc0-A0Ik12ywjoFsMVHt219zea_IzvBSPYpFGkDDMn5oSLnXFA1uA.Rh2fiPeSNHMBodbvGdDbew", + "decodedPayload": { + "vp_token": "o2d2ZXJzaW9uYzEuMGlkb2N1bWVudHOBo2dkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiam5hbWVTcGFjZXOhcW9yZy5pc28uMTgwMTMuNS4xi9gYWF-kaGRpZ2VzdElEGjNQIe5mcmFuZG9tUHAPWblOZ4H3r_ZcrKAsPKBxZWxlbWVudElkZW50aWZpZXJqYmlydGhfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMTk4MC0wMS0wMdgYWGGkaGRpZ2VzdElEGgaN6vlmcmFuZG9tUKZhxAfWNoGChZmO--yaGa9xZWxlbWVudElkZW50aWZpZXJvZG9jdW1lbnRfbnVtYmVybGVsZW1lbnRWYWx1ZWpETDEyMzQ1Njc42BhY8qRoZGlnZXN0SUQaLiV0x2ZyYW5kb21QCBjZBQHXDutlcTI3ntAQK3FlbGVtZW50SWRlbnRpZmllcnJkcml2aW5nX3ByaXZpbGVnZXNsZWxlbWVudFZhbHVlgqNqaXNzdWVfZGF0ZdkD7GoyMDIwLTAxLTAxa2V4cGlyeV9kYXRl2QPsajIwMzAtMDEtMDF1dmVoaWNsZV9jYXRlZ29yeV9jb2RlYUGjamlzc3VlX2RhdGXZA-xqMjAyMC0wMS0wMWtleHBpcnlfZGF0ZdkD7GoyMDMwLTAxLTAxdXZlaGljbGVfY2F0ZWdvcnlfY29kZWFC2BhYYKRoZGlnZXN0SUQaM5ib12ZyYW5kb21QnRLYAxgOlAcfISZbbS61bXFlbGVtZW50SWRlbnRpZmllcmtleHBpcnlfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMjAzMC0wMS0wMdgYWFikaGRpZ2VzdElEGjzBiKNmcmFuZG9tUPwVv9JyPe87iSLRUy44sY5xZWxlbWVudElkZW50aWZpZXJrZmFtaWx5X25hbWVsZWxlbWVudFZhbHVlZVNtaXRo2BhYV6RoZGlnZXN0SUQaOxSZ1WZyYW5kb21QPPE_vwgGHgNBK9U_tf52enFlbGVtZW50SWRlbnRpZmllcmpnaXZlbl9uYW1lbGVsZW1lbnRWYWx1ZWVBbGljZdgYWF-kaGRpZ2VzdElEGkGI5t5mcmFuZG9tUKsISziBlR3xrqISx4AjcnlxZWxlbWVudElkZW50aWZpZXJqaXNzdWVfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMjAyMC0wMS0wMdgYWF-kaGRpZ2VzdElEGn_7d5JmcmFuZG9tUJfCGDhHLWI7TLuXW4Jmo1FxZWxlbWVudElkZW50aWZpZXJxaXNzdWluZ19hdXRob3JpdHlsZWxlbWVudFZhbHVlZk5ZIERNVtgYWFmkaGRpZ2VzdElEGimCIQxmcmFuZG9tUJhcJSW7tjfoAgk5SwyApZ9xZWxlbWVudElkZW50aWZpZXJvaXNzdWluZ19jb3VudHJ5bGVsZW1lbnRWYWx1ZWJVU9gYWgABm3CkaGRpZ2VzdElEGnKIt5xmcmFuZG9tUNGWPOMe5I2LPMMQlyBXGHhxZWxlbWVudElkZW50aWZpZXJocG9ydHJhaXRsZWxlbWVudFZhbHVlWgABmxz72P7gABBKRklGAAEBAAABAAEAAP7hAGJFeGlmAABNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAQAAAhMAAwAAAAEAAQAAAAAAAAAAAAEAAAABAAAAAQAAAAH78g_QSUNDX1BST0ZJTEUAAQEAAA_AYXBwbAIQAABtbnRyUkdCIFhZWiAH9gAIABYADQAfADZhY3NwQVBQTAAAAABBUFBMAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFkZXNjAAABUAAAAGJkc2NtAAABtAAABJxjcHJ0AAAGUAAAACN3dHB0AAAGdAAAABRyWFlaAAAGiAAAABRnWFlaAAAGnAAAABRiWFlaAAAGsAAAABRyVFJDAAAGxAAACAxhYXJnAAAO0AAAACB2Y2d0AAAO8AAAADBuZGluAAAPIAAAAD9tbW9kAAAPYAAAACh2Y2dwAAAPiAAAADhiVFJDAAAGxAAACAxnVFJDAAAGxAAACAxhYWJnAAAO0AAAACBhYWdnAAAO0AAAACBkZXNjAAAAAAAAAAhEaXNwbGF5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAAAAAmAAAADGhySFIAAAAUAAAB2GtvS1IAAAAMAAAB7G5iTk8AAAASAAAB-GlkAAAAAAASAAACCmh1SFUAAAAUAAACHGNzQ1oAAAAWAAACMGRhREsAAAAcAAACRm5sTkwAAAAWAAACYmZpRkkAAAAQAAACeGl0SVQAAAAYAAACiGVzRVMAAAAWAAACoHJvUk8AAAASAAACtmZyQ0EAAAAWAAACyGFyAAAAAAAUAAAC3nVrVUEAAAAcAAAC8mhlSUwAAAAWAAADDnpoVFcAAAAKAAADJHZpVk4AAAAOAAADLnNrU0sAAAAWAAADPHpoQ04AAAAKAAADJHJ1UlUAAAAkAAADUmVuR0IAAAAUAAADdmZyRlIAAAAWAAADim1zAAAAAAASAAADoGhpSU4AAAASAAADsnRoVEgAAAAMAAADxGNhRVMAAAAYAAAD0GVuQVUAAAAUAAADdmVzWEwAAAASAAACtmRlREUAAAAQAAAD-GVuVVMAAAASAAAD6HB0QlIAAAAYAAAECnBsUEwAAAASAAAEImVsR1IAAAAiAAAENHN2U0UAAAAQAAAEVnRyVFIAAAAUAAAEZnB0UFQAAAAWAAAEemphSlAAAAAMAAAEkABMAEMARAAgAHUAIABiAG8AagBpzuy3_AAgAEwAQwBEAEYAYQByAGcAZQAtAEwAQwBEAEwAQwBEACAAVwBhAHIAbgBhAFMAegDtAG4AZQBzACAATABDAEQAQgBhAHIAZQB2AG4A_QAgAEwAQwBEAEwAQwBEAC0AZgBhAHIAdgBlAHMAawDmAHIAbQBLAGwAZQB1AHIAZQBuAC0ATABDAEQAVgDkAHIAaQAtAEwAQwBEAEwAQwBEACAAYQAgAGMAbwBsAG8AcgBpAEwAQwBEACAAYQAgAGMAbwBsAG8AcgBMAEMARAAgAGMAbwBsAG8AcgBBAEMATAAgAGMAbwB1AGwAZQB1AHIgDwBMAEMARAAgBkUGRAZIBkYGKQQaBD8EOwRMBD8EQAQ-BDIEOAQ5ACAATABDAEQgDwBMAEMARAAgBeYF0QXiBdUF4AXZX2mCcgBMAEMARABMAEMARAAgAE0A4AB1AEYAYQByAGUAYgBuAP0AIABMAEMARAQmBDIENQRCBD0EPgQ5ACAEFgQaAC0ENAQ4BEEEPwQ7BDUEOQBDAG8AbABvAHUAcgAgAEwAQwBEAEwAQwBEACAAYwBvAHUAbABlAHUAcgBXAGEAcgBuAGEAIABMAEMARAkwCQIJFwlACSgAIABMAEMARABMAEMARAAgDioONQBMAEMARAAgAGUAbgAgAGMAbwBsAG8AcgBGAGEAcgBiAC0ATABDAEQAQwBvAGwAbwByACAATABDAEQATABDAEQAIABDAG8AbABvAHIAaQBkAG8ASwBvAGwAbwByACAATABDAEQDiAOzA8cDwQPJA7wDtwAgA74DuAPMA70DtwAgAEwAQwBEAEYA5AByAGcALQBMAEMARABSAGUAbgBrAGwAaQAgAEwAQwBEAEwAQwBEACAAYQAgAGMAbwByAGUAczCrMOkw_ABMAEMARHRleHQAAAAAQ29weXJpZ2h0IEFwcGxlIEluYy4sIDIwMjIAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAIPfAAA9v7777_tYWVogAAAAAAAASr4AALE3AAAKuVhZWiAAAAAAAAAoOAAAEQsAAMi5Y3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA2ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKMAqACtALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD_AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH-AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA-AD_AP9BAYEEwQgBC0EOwRIBFUEYwRxBH8EjASaBKgEtgTEBNME4QTwBP8FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB74H0gflB_gICwgfCDIIRghaCG4IggiWCKoIvgjSCOcI-wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ-woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL8Qv9DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N-A4TDi4OSQ5kDn4Omw62DtIO7g8JDyUPQQ9eD3oPlg-zD48P_BAJECYQQxBhEH8QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT9RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX5xgbGEAYZRiKGK8Y1Rj-GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr8e6R8THz8faR-UH_4f-iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h-yInIlUigiKvIt0jCiM4I2YjlCPCI_AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg_KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi-RL4cv7jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN_M7gz4TQrNGU0njTYNRM1TTWHNcI1_TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D8gPmA-oD_gPyE_YT6iP6JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS-JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0_dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT5lRCVI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW-VcNVyGXNZdJ114XcleGl5sXr1fD19hX_NgBWBXYKpg_GFPYaJh9WJJYpxi8GNDY5dj-2RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn-Wg_aJZo7GlDaZpp8WpIap9q92tPa6dr72xXbK9tCG1gbbluEm5rbsRvHm94b5FwK3CGcOBxOnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj92m3b8d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF-Yn_CfyN_hH7lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn8gEiGmIzokziZmJ_opkisqLMIuWi_yMY4zKjTGNmI37jmaOzo82j96QBpBukNaRP9GokhGSepLjk02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ_JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ-Ln7qgaaDYoUehtqImopajBqN2o-akVqTHpTilqaYapoum_adup-CoUqjEqTepqaocqo-rAqt1q-msXKzQrUStuK4trqGvFq-LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq-hL_7v3q_5cBwwOzBZ8Hjwl_C28NYw9TEUcTOxUvFyMZGxsPHQce_yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z_jQOdC60TzRvtI_0sHTRNPG1EnUy9VO1dHWVdbY11zX8Nhk2OjZbNnx2nba-9uA3AXcit0Q3ZbeHN6i3ynfr6A24L3hROHM4lPi2-Nj8-vkc-T45YTmDeaW5x_nqegy6LzpRunQ6lvq5etw6_vshu0R7ZzuKO6070DvzPBY8OXxcvH74ozzGfOn5DT0wvVQ9d72bfb_54r8Gfio-Tj9x_pX6uf_d_wH7Jj5Kf26_kv63P5t_75wYXJhAAAAAAADAAAAAmZmAADypwAADVkAABPQAAAKW3ZjZ3QAAAAAAAAAAQABAAAAAAAAAAEAAAABAAAAAAAAAAEAAAABAAAAAAAAAAEAAG5kaW4AAAAAAAAANgAArhQAAFHsAABD1wAAsKQAACZmAAAPXAAAUA0AAFQ5AAIzMwACMzMAAjMzAAAAAAAAAABtbW9kAAAAAAAABhAAAKBO_WJtYgAAAAAAAAAAAAAAAAAAAAAAAAAAdmNncAAAAAAAAwAAAAJmZgADAAAAAmZmAAMAAAACZmYAAAACMzM0AAAAAAIzMzQAAAAAAjMzNAD72wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD72wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD7wAARCAIAAgADAREAAhEBAxEB_4QAHQAAAAcBAQEAAAAAAAAAAAAAAAECAwQFBgcICf7EAE8QAAIBAwIEAwQHBgQDBgUBCQECAwAEEQUhBhIxQRNRYQcicYEUIzJCkaGxCBVSYsHRM3Lh8BYkgkNTkqKy8RclNHPCNURjgzZkdMPS4v7EABsBAAIDAQEBAAAAAAAAAAAAAAABAgMEBQYH74QARREAAQQABAMFBQcDBAEDBAEFAQACAxEEEiExBUFREyJhcYEykaGxwQYUI0LR4fAzUvEVJGJyNIKSohYlQ8JTsjXS4vL72gAMAwEAAhEDEQA_AKG5lNtJrkhhgZZrprVnZx4kYLkkovfoMnttXtyztpMKzM4FrcwAHdJAAAceVXtz1VEDskLnV0Hj-Jen2Gk6lm58Wb-Pay4NtIRzeEVzsRuS0m2BnqOtZMbjMbgahDRnePbA0zA1qDoAGa6rTFFFN3r_oOx6V8yVDh_eGmXljcuZvHL6JDAwYkAnoe-WB7b6Y6Vvk-7cQgmhbWSqLhWpA0IrTQjTr1VI7SFzXm79D6eCt5XsL22k0m2tTNb-exUqk7mV158jwwF97JypDDY4PauExuJwsoxsj4skw07rcoNUQ63aEVbaOq2EslaYQLDfE3V8tPfakw2dzO3_yhneKSN0MdpI7LHLJGCrgtheZyFyPTBPUissmKhj7wBo5gc0g28AFwDyCMotwDRevjasbG5_8gNHTQ6Cxob21ShHLqNzLdtql5bCFgsgjiwA5IxGhIyeVsDGM75waiXx4GFkLYWSZry2bJAu3EA7kXzRRmc55cRW-nlQHl5Jyewv_iymQW8FnHOMyTxkTc_vYZmG2GAXPMPPHfaqDGYaDEMOd0hbs022jWjWmzzdRHRTfE97CKDb3O99SdvNVtxqzPrYV9QijjkiiRuTw2jkTfOdttiW5TncY7CutDw1reHnLEXOBcdc4c12lcxetNu6y6rM_EEzUXUCB0oj-aa-aXaw2VnPDcaG9tdyIrwq81yqO0jbL_g-4Fyc9e-arxM2IxMbo-I5o2kh1NYXANHePe1txOlWnG1kbg6CjuNTWu23RQrv51W8csdmqPLlI4JLZmT-sgMzMSOUkE8pOfLpXQw5xs7muxBIb3i4PynvatAAHeA0saKmTsmAhm-lEWNOZPLwUHUXgkVPo7RyoJXHjsMXErbEmTfzOB8D-10sAyRpPbNLTlb3R7DRqAG7HYWd6tUTOa72dRe_M7brBTODcyPFsOY4OPU71eRqbWhtgClP1UrPa2t5k8zryn1_3vXk_s-04XF4rA_labHvr9Uu5xV3bwQYk7kUfd-tqNe8yXDDcBsHB6HavbY77wAh3p8lxCo3NvjH6lZEkCzdidjQUKRdcriK4_jXDY_iFbMX32sm6ij9jRNJtXWOQpJvHJ7rf3qGGkDHZJPZdof19EJEivCxikBIU42P9iqZIzG8sduE0RBGcLj9_wBarSR7MThcADahCne68EdkAAWi8Qf9s5rrBgfE3C8yLHnv4QpKPg_Qyu2Wmxv7AJaybYTzd9EuSjjyPTNZkkCR90YztSKFLhZXEbn_UZ8J_VDsPwrowuEoa87t7p_-nQe5CjNG0b6G2FKnzrA5pYS08kIkwSA-Sud996iUKTce8lupkxiP3c_E1rxPsRDwQUiADErA9Ij4-lQw-zz7AMT5EJB9zlVSGGBnIziqEI-ZAGIAydgnb13oQiR2jKHmIA97brTBykOHJCduwRcty5Od1xvjO9X8sZZ3Vsdfeg6q2ZRqNnJbMx8VArr6G35RXi-Of7ZONt4i36nLq7zI730d5hejwtcSwLsMfbZt9PqPJUWdwGDAL79f5mvVjw2XnCtdbymeCK4PV1DH84rUNgVgcKcQlhiu6n9VJJFSQh1pJoxTSQO52AGaEIDc70IRkZ-VK6QiYKMYB3p2hA46jvQhFjvvQhGpwwOM0Ui0oyyeGsJkbkViwTm2BIGT6VREbc-etdr7AJ5lMuNZeSlLazNbrNAI5SI3kdUYNyINiXHY7jFZH8qMSdnJbACACbALjqKPMUDasERIzN1PhyHj8oXFzH8K21kZBHy5kdhytJ0OHAJBCnof5hw4eTP2s_tXoNw3lbSRYzDcJPeMuVm3Px8_LknJxKmnW7SXMnNIxmhhAHKFPulg2dj_uMVVDI2XFydmzRoDXOvWxqBW1Ud-tqT25Ym2d9QPrfokXECsoje9W5laFPCESs-cn_DFsYwB5VZDO74seVocbJoeNtAuyT8hJzBsXWaG319Eu8uYJo3hkaKWaM5S5iiwZNlCoenugZ3xnIqjCQSxObJGCGnQtJsDUku563pVkKUj2kFrtSOdb_CvKvBQHkklbLOzE_xHPp_SukxjWCmilSdTqrK2hs5rYXup6ovuHwki95pORRg48iMry52-Qrl4iaeKU4fCQmz3i7QDM7bXndHN0V7GMc3PK7wrnQ37AGVcHKFljLIrjlIzjmHXf4Aa6jmB9ZgDXwNcr4ys9kbFP21xZ4aK9sxIHcFpEbEiKOyfdGfUGs-IhxDjngfRA9k-yT1P9r-UaVkbmNFOF674wowTnJCkdSQD9VqH7JV6ckjzoSRgAtknA-FNCABY434zihCGcdM-YBoQlDn6yWIDHPWhCsuIm0qPUb2C1R5pRdSk3BJVc85yFTy9Sc_lXK4YMbJFHLMQ1uUdwanbQl3U8wFa_smtys1PXb8JzSYrVLJbm7WzjiIm5pXnBd225VUDJjYHJDY_KsfEpJpJzBDnc7u0A2gBrmN6A2KB1WmBrQzO-gNdSfd5eCd0uAX2jvHeWwji8RGa5lLFnZpAvMjEhQVGQe3c4qriEv3THB8L4zqcMjaAADbyuaAT3jry1ClC3tYqeKGmpvruCU5FPb-ZDbgadMHkYrcvz8nZeU87AD3sAFSG2GRVUsMuOkkzSigAWiu5uMo17tmiCDrR0KbXthaKafHr87a18E5qdxdz-WLqLTnZLlGhclGaRHViFkY9AzKcYwDj9VVw_DwRYwwyS0WEOGoDS1wstA_ta4WKJ1U5pHOiD2t308QRz4yELiy1C00ma7aMkWgEKu6lmUOoDpknBCHGDjOSfKnDi8Li8c2EO_qakAgA5CcrtACHPFgi6oDqh0ckcRf7AG_XceisLD-LARz-r5KhkSJLdnlZVk5FOUdVGy5Iz3ORnOa5mOM8l1D2bwXF1AEtsinNceddL01V8ORv9swNVvrV6EBQtDQJNc2Ftp0YV2M6PdRYR05iqLg-8ozsCCcnrmuhxd2eNmIllNgBpDHWWmgXE13XdTYBVGGFF0bW-IsaHp4hOcRx_ToIs6bcQaibjkbIBDMUJ5QwyWyAMD81TwN_3SR38rXQZbGutZh-U0BqddyFPGDtWjukPv-Kssbmawtbe6tnuvosbGO7ZSrBS-CV8NsgdB73RjjpiuzjYI8ZNJDKG9oQCwaiw2wDmFHmb1KyxPMTQ9t5dj--BVXHaz3Zma0jaRkBYIF94ruc7bDAG9dWbEx4NgM5AHXlemmve59FnjjdK6mbrEAd8YPkajdrYrC720myHYlj6teZ4d3uM4tw5UPkuxi9OHYcHx-qjX37ANS5Iwdv0Fexx35d3p8guOd1Hxv3zWUpUj5emajaakIPFs5I8bofEX5GrbH6Lh3s5t7w-RQo-2Kx7oUlz5IthKSRJDsfVexrY_4A3EIf6Zuh8RyPomo5ByvkehrHSSPky3J3J5aYbm0HNCkXknLecynBiKgfKteMkLMSXM_LVeiZTl4I1gR4-kjlx8xWjG5RC0s2cb6CFC8-lcxRQbGdh0GKXmmnrPJm5eRmVxytgZwO391rwYcZMoBIdofD6Gkwnrm2naYukec4OQQN-_XrvVuIwsr9C9o0PkkUw9tNCoZ4iF7kDoayvw00ftNKKTt6DmJQAVSJRzAbHOatxW7B0aPqgpERPhzksSeQAfiKjBoyQ-H1QmNwPSsyEeR9nY_qKaEFK4weYMPLpikhP3B5kgmAz5XhvXBrTiNWxv-ivdohTNNcia2nVGIKmJuXcDfbP9Vx_tNhPvnCc1atBPq034QSujwmUxYtnQ6e_wDelH1SMW9_KFyoYc4x677rWT_P8g4nh0ZcdRbfdp8qS4rEIcW8DY6-_X92tFaRmO1hiIAKRqD4cV6QaABcB5txKcqSSHrSQh60IQoQhtihCMZ7UFCPO29RQjAx07UISdhUkIHIGCe9CEYBI2A28zQUIskN-VHJCeCQPbosLTCYlvHyMoqAjlYY36PwrNmlZKe0rLpl62bsG9_DzKsppaMt3z4kbS2aXcckML6CpQsrPknGOYA9gT6tJscz8XMkcMxuiBtd1p-iC5oeC0aaJ60uoo7-S4_d6XUbCTMUxLYB7kjuPOqsTA-TDNiMvZkZe8BW3KjyKlG8NkLstg3oUr-LZLpYvpJUaSS45FgDZHh497mP2l_ComfEPxhwrWkNyauIolx2y7A-KYjZ2YkvW9B4eKfvJXvNUfE1gyrGxR2IMaKRkAtgFiNlBIznFZsMwYfBg5X3YsCw4m6JqzQJ1IGmqsee0loEVXp7-FlVcTReLH5ILCLnHPy45uXO_wCVdeUSCNwi1dRrpfK_VZRVjNsp9za6bBaC6hunMkhC_R5YiHA5iQQ2MfZx-Nc7D8jFyYjsZGDKBedpFbcxd-1deS0SMjbHmB1PIjXfr9KCkqxziVYkKq3MEkHMpGeh6ZFdBzDJHkJIJ5jQ315qgENN170ueWWaKPmEISJcKI8AgMxOD3JHT0GKqhZHE5wBNuPO-QG3IDnpztSeS4Dw6eKYxitSqQ36fnQhEM56YNCaP3kYHYZ8vKhJEJCMeQ_OhCHvKp26gY2zQhaC4s7Qa1rOkX0MzTm6eSE28atKeUs3KDkgcyn5PIivNHFTnCYbGwOAZlAcHEhtuoXsDYOx005LZFGwF0Mm_Khrp-vNQJLDVrrUG8HTPFeUrIG8BVUqV2-yeUZAz13rZHjMFBhRnmposGnO5HU6jMddjSRilfJ3W6nwH6FfaxDaz3TNd2njRQCHIjVwvjNt4YIPKFxgkqDnl3O4rznC5p4YgIH9XOz-ktvK3XNVWXbgFzhvsVunax768LAra9zy6V5A-aXNLBEb291GxSZ9OYWzpBM4KR5yxUnGUIb_Jz0xmqomSvEOHwkhYJQXguaCC7lmGveBbyoBScWjNJK2y3TQm68NtNVVzPz3rWIuZUR5kaPkhLzT_jkcORueVh1ODgfGu5CwNw_3gsBIaQbcA1mneblB01GhrnayPcS_Je5HKyehtMX5ndyDUbpJ7mf-LOVmlk-rGMDA5OnMWOceQBrRgsVAzsIS1rc7AWtHe665j6UNG56qEsb3Z3Ak5TqTp8Otq5a-ltNMkvL2Ai5UNCWDNzcyoCq8wYElixYrn6lcBuEZicY2DDO7hIIFCiC4gkNykd0Cga2WwylkRc8a6jn0Fa3z3VdIstxawPpt415PZTJHbz8KyAMhJQc2zYbO5OeldON0cMzxi4-zZI1znN0LbBrNoLFtqwBSzuDnNHZOzFpAB57XXQ6pm41y9huIZra7MkpjjLq2GMci5Uh15QM7HHcZ671og4Rh5onxysptuoixbXEO7rruuo0FhQfiXtcC11nT3jTUVuq2I3cltJa2_jsgzPKik4IGBkjzGTvXYeIGzNmlyg-y0ka63oD8-G6ygvc0tb9lMcQXtlHpjLp1qYGVeQymQl3LAAg46AHmx6HHpWeGHEND3Yl-a_y0KFE0Rzsir4lcHsc4CMVXPmdFjubByuF7jFW7q46KdqRKW1lBnpFzEfHFea4H6JisZP1fXuv5l2eJdyHDxdG3_-Ue-ybpid9l_QV6_G_1z-fILkFMkknLHJrIkioQn_Rwk6liCre43wNacG8MmF7HQ-RTCbkQxO0bHBUkVU9hjcWHkklW8qwSqzLzKQVYeYNTgl7F-Y6jmPDmhCeMRNyglg26HzWieLsX0NQdR5IRwDxbpFUbswOBRhm5pmjxCSTOea4kYHbmODSndmme7xPzTUiRObTY33JRj6BNbHtvAtceRPxJCfJRQCduXO-APWufRJoJUpDRxWwzcKZJf8Adh8fWthZHhRcmrunJCbkurhsqH9F7Bdh-VVPxczxV0Og0HwUgpF-njLDOvR1PX4f_1q4h-I2OUcwgqKs0sfvLIw26LsDWBsz8vYNJKbdolzLyLlZVjXG45WH5K6OJa3EPDBo-h5FFKNGhSC45lKkcoIPXrWSMFsUgO-nzQmicbY_GsySJQCfvHA7dfSkEJUcck74qKWPYCpsjdK7KwaoU1o4ba3H0hRI0bdAcAEjOPyrqOZFhoR2veIO3if4ACkpOmR3uoTRx2tuzcsw5hEhI5MenbauN9oMe5vBp3Du_lof4tFt4a3tMXGByN-7VTb_RZzqnPfKEWJEzHn3ieuD9da4v2Uw7o-Hgv9kn9D-J8fxDTiy1nIAfz3qY2Ca9QvPoHFCEM52oQhj1poQpFCMCi0I9wSO43oQjI3JHQ9PhSKECSe9JCS3XamhAk96aKQx1yB_ajdCde0u4oEupLaVYZPsyMhCt8D0qhmKgkkMLHguG4BFj0Vhje1uYjQ80mKaWFXELyL8imJ-U_aU9V-eBUpIo5KdI0HKcwvkRz5FFrnA007-J1baZpxYM6pITjlYcuHPZicY8jVTsQxsf3loto5jXTw3tSDCXdmdD5U4sk3j21oGt7eW2kKLNlVAbm6sw6gEHfyqstjbFJOczmvF1qeXIcr-ClIF2ZrdAQfqpGpQq9tbXLzxyXEoKDwAvIqoNwcbl-hz3B67VmwMpbPJC1paxuvfzWS7XS9AALsXyVkzLaHE6npWwSbK3srjUbdpBHHbuhlaNnJGUBypJx9or-9e9GKmnhwsgbbng0Dz_xGoAB9kHoNkRtY6RpNAb15fqpthHaLcTz-Jpp1AqQFSdxsjp9kId3IOd-wxtvWHGGd0ccPEZeyB3LRuWu0ObZoIokcyrosgc50Dc3meRHTnqqeQ5tUEKuqJjxl5mK8_TnO2FyNgPQ13WCpiZCCT_JoA5d63si9b4VjPsitufn5Ez5kbcvvHIGxO1XAhzjW4UNQNU44nuEkumDuVKiRhH_oHQEkbDJGPWqmFkLhCKFjQXr_tz9qRDnjOUz4sVoUEKSEkDOTTQiyR3wKEI8AjJI27UIRHJzQhWmu3H0m7kgJc3MF5PEgCqFCF_dxgZYls9a5eAw5gBfp2bmtJ1JNga7mgAK23U8-eNjfzDQbIr2Y2kMemWt3fxm3l5545FHLHKoALDl3ODnGfIVXhY_vMjsZMyM5200gm3NcSaN0BYIsbbq57uzAiaTpuOhHl0Tt7dS3eofRvpkkItB4iTyCaRuYDIfHVeY-Y8qqwsDIMN2vZh3aaFoyNFE1V3RrzUnudJJlzbc9T6_wAFNP0HV5pBdrPdTQwRk3FvME8WFRly_P3ycdCTy7Yrmj_zw1jewLY2uc7uvaTlc402soOlc7rVX5ycnPZIA1B3A3u_05KrmN9mGW0kkmt7Nj8BLBvDBYkfZO46ZI2zt2rtRDDd6OYBr9Pa0IzGqOhGh_870spz-OabDdvAWpqak9_BBaO9u01rIZJI5H5y7Zt3aR8gDAGN-_Sue7Aswkj92g5HigQNYw3QNa2iTZ8gOauEpmaGGrHL67mSeSXa2Nq2q3cqWc0tra-9HbCJmVpWUlVKkluQ78b8ZxmoYjFzDBxRueBJJoXWAcgIsgihm_8316KTI2mVxy21vKufzrxUKwjmtLy1u7mzubdJYiIZYogWzy48RR3xufOt-MczE4eWGF7XkHvAk1v_JPK9uioiuN7XPBFjTT8qU1_aywRrqEdugFsotmhQTOSO7Hm90-4uQdxk-ZrMMFLHIThXE985w4ljaPQZe8BmNEaWFaZWkDtKGgqtfrpsoOm273X00-JOoS1klbwiPexj3SCRkZro46YYfsdAbe0DNenjoDss8LTJm32O3yVHxJcxrplvaGyUSyzNL5IJbLqBjlA6EZPX0qcjHduXiTugVl00PUne_wBVZDWX2deqzkaGRliHVyB-Jx_WoyyiBhkP9QT_ha0MYZHBg5mlO1lgb1owMiONU-G2a4P2ajLeHdod3lzvp9F1OMuBxeQbNAHw_dMXufpGT3RT6Vetx35a_AfJcopg71jSRUIQ2G_zoQpdwJJZY5olJMy5IA7jY5rfiWOneyRg9ofEaFG6S0cFtgP5a_4ACNlHxNRLIcN_U7zug0HvQnILl5R4HOkPNtGQMAHyq-DFvkuIHKT_NcvBCVb3EzXKwzYZlYksy-8MCjDYiV07Y5dfQXp47pphoorjma3518EmNv1BrOY48QC6HQ9D5CghSCCNL57IOO4_mra4ZcBTv9qE60TNvywRG6kGXbIjyO_nWSGsNH27tzoB9UbJjDEZJzzb_nv91jJJOZ25USjVOcfaAwNh5__xUdkKa3LPppIAzGRjG2P55rp_1cB_1_nyKlyUJlXAHOuTuT9ema5h0Cinb_Hj8wMhQM-e1a8cfxiPAfJMpxS1zay7ZkHKGP4AEPP81cHnE4d9DvCr4QPqi1HRMglQDy7HG9c67OiVJUcEkkoiAAbqT2AqyGF07sjEUrnTNDvtSLQaXA3hA4knAwM-masxvEcNwxnZ58vU8z9DmrI4pZzlhaSfh6nZaiw4LstOhdb64t4y_Ked_eYEfHb4K84PtO17XRYKB0l8_wBgCfktw4Q9ozYqUMH46kKwju9G0eFhYyG5lbYnsfn0A-FYpMFxbjTgMVUcYN1t8NyfEqxmL8fwxp-75955_vsB5LPTSSXMzzStl5CWY-teuhiZBG2GMd1ooLzksjpnmR51OqQct13NWhQRY9KaNECcdqEIdqEIDHegoQ69-1KkWjXJ3ximUJXaooQoQi2A3FNBSd_LrTQCjOwA86AmnHuriSFLeS4leKM5RGckKfQfM1SyCKOQytYA47kDUqRkc5oaToELeWGEuZrfxSYyFPiFeRuzbdcGlPHJIG5H9aPQGxzbr1Q0tF2L6nikTSyTSNLPK0juSWdzkknqSTVscbYmhkYoDQAfRRcS7vE2VIt4E8Jrq551hBMa-GV5zJy5AwT5nzNUTTv_QQw1nNHUGst0dQN-gKsa0UXP228b7RK083aLPLZpE7LGBIhUOSuc5Ckb8I3Paq8YcO4sZOSBehByi62LhVXeg5pxB4ss1-PwTaQJNdRQTTMisoJYJz4uQTgKPU-nX9VN8r8oXSMbZHLbY1ZJHQKIaC4Bx0_nRO_SUtYPDtEm5nVDNJIoVklHNsjDcDcjB64NUnDuxEmbEEVqGgGw5hrVwIonS7GykHhgyx3yvwPgVIuFjt9Lg_4AmLTBlJMUTKVjkJzhj1IKnI64OfWs8LnT8yT4LKQfaN2W1Vi-jhVdKKscAyId6_Dof4fFM3Ed94UqvYpFGvJFIwiwA0Y_iP3iDvg5PlWiF-GD2FstuOYgZuTzfsjkNr9KDmyUQW0NOXTxTEk6ysY4iLeIqoZVJAdlHUjccx_Cr2Q5Bnf33WaOlgHkDpoPNQLs2g0H-JgEdD06VqIpVIidhjr3oQk53zQhBsbfn-0IQ28qE0AKEK41nThaXOqy3cU0dwLorHGSEKszsQSDuy8o6jufx42Cx3bmGOEgsy2TqRQAG40GvIkkgWrRFkjLng3pptv4VAuIrZmuXgc23hIqNDK5Z5T0YggY3O-K2QyTM7MSDPmJIcAAGjUiwTZoabIe1pzZdPA8-qt4Ge_kgj0fV7szxKQvioEZgUPPzSdCBhQAe3TcVw5WtwTXux8DcjjfdJcLsZQGb263HoCtTfxiBC82Ounnr8aKNH8tndx2OmY_eYaSKaUlXRznIZWfpt6b1teG4iB0-Lv_uQ1zW6hw0qiG7--KraC1-SL29QTpXxTsTaze6jO9rbRx3FrEI5oTyKjRKdkbs-SDtneqHtwGFwrGzPJY82HCy7MfzD60AKwGaWUloojccq6eKVMum35wqS3BuJms-aRo0LfXDHuooAwyqCMdM9ahE7F4KLOxuVokoAkezqLcSTYc6rOhrYIcIpXUTZy6-fgOWikalb37ANGe-nR5Lm4SO0QtE0dwzE5yCrYbYYJ8xjsazYCXC9qMLGQI2l7zTg6OtiDmFt1NgcwbVkzZMvaHUkAbEH8aHRRbybUNMghS4vTJellnUrIzNAuChQjHLgjGRnPpW7Cw4XiMr3MjqKi06AB5sODhrm0N1ofNUyPkgaAXW7ffblXRV13qF1dryz3DuHcyuvKFUOdsgD0xXVw2DhwpzRtAIGUGyTl31vxvqs0kr9Nz8-qVY2i3K3MslvPLHDCzlomC8p7E5GD4BvjOOlLG4kwOija9oc5wFGzY51Wu3PYaWpRR5g4kEgBZ3iyRpLq1kNmLeM2yheQHlfBIZhnqSQc-tQjAYXRl5cQTd7i9QDXQEV4LQzVoOWr7AJaiWlhPb-pBBdQtG-Fmwe6leZSPkRXE4zjYzwuWWF13bfW6P1XU4bCXY2NruWvu1TF44unmmUDmRiGA7rnANdnh2HEfD8o2iixovyOt-hKz8yTtsQ9_Un9pN7nnjJ7xr6ldDGe209WhZiitYUnm5HYgBSTj0qOFhE0mV21WgJbW0bqZLRi-OqN9oVN2HZI0vwxJHQ7hBUdULlVUZYnArIAXkBu6VKecw2rwRSHxI8F--M9RXWNw4d0cZ7zd_C96UlXnGMDG3fPWuR5KKG4PShCnWkyzSr8qYkRThx3GO9dXCTNmkGf2gDr1HipXaZWwnLBhJFgHIfn7ADqhuCk0c1wrraSmTxtcQxwJKGJwzH6IZxn4a6E7DiI2xNdZNEnqOqah3TiSbEf2E91R6DrXJxUokkpvst0CRSH3PNsM9gen5qzJIDHKQeYDPbpQhTLDleGVOXqQN-m4xXTwPfjfGef1sKQUEkZIPr2xXMIrQpc09qMsNvPcXNzPFDBCoeWaRgqRqAMszHYCtPEHtZM5zjVV8kwCTouWcR-3CCAmx4LhgdC3v-veI_gLy9fBi2aT7M2B2weteUxf2qOGuLBCzd39LbDhC8Z3nK3qfoOa47xJxlx_xFqqiLiXU7lVP1Iil8FQT3WOIKq_Df81wH4RxEnfnfr_h-irlgJfUOoV3ZTe2XR4BNecf3WlRsM8t7ftzMP4jZY_hTi47iYrbFK7XoSr7wDTMRVvIb96K90z26e3mwxa6fxyuowRDlEcmnQlSP4AN4YYfjWf_w10vbSDM7x1HuVw-9Rs7Jj5PAV9FfwftBe1C1XxtT8c0a9Y_aIJRvxD8P8V2oftLiIxkytryoe4H5lhk4aXHMbvzB-atdN_algdhFrHAV9C2cFrW5DgeuGX6tdBn2nb7wDkj5xH-fVZHYFwOh-H-LU6X60P_M9QCrd3t9pjHtd2pKj7AKk5v0rdF9ocDJo4lp8R-lqp2ElbtRW00fjHhLiDl_cnEumXhbosdynP7wCEkN-VdOLF4fEf0ng-qpdG5ntClcupTqGAPQkYzWmqUKScb09UkYXelaEQCnpRqhHygHPypIQGGOOlNCFJFIUJpJGTTGiRR4HTtQEJySYSW8MH0eFGhLZkQYZwTtzeeMfnVMcJjkfIXkh1aE2BXTpfNTL_aG1Vc-qaIIGcHHnV160op2Ge5WN7SGZlS5Kq6ZwGOdvzqmSGFzhO9urLo8x1r0U2udRY07pybTprZo_GMbc8hiKxSK7BgRkEDvvt2PrVcWOina4sBFC7IIFHUa-mvRN0LmEZq3rTVS9RsWS7vFvppI7p3DwLNbMrzKc78XIXO23pWHA4vNDF92aDGBTsrwWtPmdSBrr8q6WKnO7QnNysan3dUwHjjs-W4I8WMyQpCqmORCcHxCw679GD-jateV7p7j5k5XFxOZpAsZQDsNLsc1XYDO9vqK2Pnf-pqdHspg0STwhlV4nkXkbGPtDG2OtWRPbioyJC01o4A5h638dVFwMZ7t-HVOWk96onuorlAI2SeRZXB8Vg22x-0ck_jVeIggOSF7DqHNFA90VrqNhQ-CbHvFvB2onx10T_x_SbI311IJGiX-NyIhQ23vHlZgBup94eedqzNf4Ad5_u8IoE57JsP0p2t73VcuqmR2jM7zfLpXTl5p7wbe5klsLnUZIphMZFmuS4iuGGAvMPu4XO_XeqDJNh2NxUMILS2i1obmYNScpG-tDQ8tlZla8mNzqN7m6J5WFD1EzXMseo_QUtkuMhPBXljcqcEqO3b9-Vb4CGQsdhu1zlm-bUgHWj1ryVMxLyJMtX0206KPeW89tcPBdZE6k-Ip-0rZOQfX69aMLNHiIw-H2aFHkRXL7AVUjXRnK_fmmG7etaFBDGOlCaLpnrmhCB3-7bUIQGwJYGhJWvEqyPevPLFCkn0i4jc-JmViJCcyKfs4BwPSuRwgsDDHG4ltNI07oBaB3T6YEjU9VdJmLGucNfifMclXWsiR-MjwQyGSJlUyZ9wnuMdGPQE7b10MQxzsrg5wAIJArUcwb9cyBrooMIFggbc-SvPASwgfTRayaZPeRcwuLt1ZWQJuBj_PMc7jfYedec7Z2LkGLzidkZotjBBBLtDqdcorpuVuyiNpjrISNzXT-qBzjTeeCTTre-trtQ8Ms0TKXGMcyHORg5GPSupkOOqVkro5GEhzWkEA9HDbbms99jbS0OadiR8uajF18KRrqaY3UcoaNCnu82ffLHqDt0Hl2rXkcXNELR2ZBBN6-AG4rXdV2KJeTmB_lqTp11L5JluQ89skQafms4wfCJGNgTspyAd_KseOwzDE2IhryabTyRmG-tDUjcabq2J5Li4WK10GyWt_q2nSIkk8sePCnYxvzknchgxyATzHI6ZO9BwWCxrC5rQfaaARWgoVQqwKFE2aR2ssTgCeh6-vPqo0N7Otu9k9xKIJX95ORtydsk_wAWcDqdutapcHE6VuIa1pe0U2x02A6KtsrgwsJ0O6RBaTTBT8UnLIxjjbGFMnYEnA_OrZcTHFYzDu6kc8vMgCz4FFsZdsP4p2BxaTS2d-ZxAW5Z4oZACWXPKT1BwaplacTG3EYbLnrulwJ0NX0IsKTT2ZLH3XMD6FZjiORmuYopJXfkiHKCxwinqoB2G4JyPP0pFmSZ7wALrkNa52NdtNei0xG4wLv-JrR2b-RNO7MxjhJ3OfhXnPtOM2Fjgb6eQfW_mu3wXSZ8p_K0qBE7RESAc3YjHUHt869Sx_YvseXp0XGBvVSLyMM0ZjOV8IFSe4FaccG2wt2IH1UikWbiOdO-chvgarwkojmDjtsfIpDRExktbhuUkMhx8RUHZsLKQNx_PikpJ5Fha_jUqxGAvZW6EiuhbGRnFsGp5dPFNMWT7X4jE4lBQk-vesuDd-Nlds6wfVCaKlGIP2gcH81kLS0lp3CSTv2NRQpFj74AUZ_kb5K14H6sfIpjdR8DHSsnJKlZcwt4EueXJWIIgPmTk_lXZB7CJs53DQB6_spbKLPGscgePPhyDmT0Fc3ERiNwLPZOo_T0SKQTy8yqc8wxnG5FUJI1VBynOevMBvgUk0_ZDHisGJAQnp0wQa24JxBeP6N-7VMKBxHqel8O2s2r-pOyweIqRRxKXmnkfdIokH2pG6AfM4FY-JTswb3Oftdgeev1TDSdeS4v_QeJb7iCb-RxKqiDnLWuiwzc0KMBs0rD7Fcd36yCOVe5r93xni-I4niHNBoXrW3ovQ4bAsw7BNiBZOzfqVk9L8Yu-IruMXMxijl2yFy7AdQi7YUfxHAHqdq4xlEIyxjVaBhzP39TQ-PoOnitBBDIrPpvA1qlnZW-VudVkbLyHviQjZfRcA4-BqQbzebPwCC_szkgFdTuff4ApSb0_hywvna4jklvQGIe8mQhGI68o6t-NWZyNNlTlDjrqUu6h8KPnigeG2X3RLKhy36SMdvU0wb3UqrZU7WtxeP7AMrp93cH6KQbf0UfKnmA3USAm59A18IDJbxwodgpkUE_KgSNSLHHZN_4J65IuTAvKexdafbAKBizDVQLjgXWieaFJQ3b_Eg_OpCdvNUuwd-yaT5hfe2XhLlbQtZ1aONOiJIxQj7I2VrfBxOWHSOQj10WKXh8u9Aq7sv2lPaxoJ8DXba0u9sZu7Pw37FcfpXUi4_jG_mDvMD-UsL4OGGpGkfz1W00P5rrSpCqcRcJXEJJ96SymDgD7K-_9104ftJynj5Qfof1VDoW_lPvXSeHPbf_L6KJVtrHiiG1uHwFhvka3YkjOAze6fxrqwcYwc-gdR8dP2-KqMTxytblJY5UWeJ0eNhlWRgyn1BGxrpA3qFA6bpfQetMaoRZ7UIQGO-woQipJIHGd6YSCMEb_dfyoTOqWtzPHbyWqSEQysruuBuVzg_maqdBG-Zs5HfaCAegO6lncAWDYpuSJ4naKVGR1OGVhgirI5GyMD2GwdlFwINFTNMt9MmWf54XxtiqqI-WHnJ33x6j69YOITYyIxjCxh93fey8tOWvP3BXQticCZHV6WoiSyrKskcjCRWBRgdwQdsfOtr2McwscNDdjwO9qoEg2DqE9MQ1sniTTNcGSRnV16KQDnmO5J37AAqqIESEsAEYAog9L0rYACqPipOotFnvWf95lISV0KTpI5lhYcmcFVUbjr-9qsdGHBzCBldd9STv4OqQdVG9QpMcunfR7aaWyLyQyETqkmPFQg7nuGycZG2AO9YpIsV2kkcclBw7tj2TpoOooXrvZVodHTXObqN_EJ62tbm6mlteRrWG-TxIUmDOZDzALyE45iScZ7Ak9qoxGIjw7Gzk9o-I04tptaWbAutNa5kBTYxziW-yHDS9b-eevNMTLbmxBupJhdhscnOGEgP3j7DgAD19MVphMgxFQhpiI3qqOug62bJJCrcG5O8Tm-f-KNc3VzeSCa6naVwoUMx6AdK04fDxYVuSFtA8lW97pDbjZTbM7sXkcsx3JJyTVzWhrQ1ooBQJJNlFtimhDBJ5R3ppJGMD8-tCaPruRQhAZO2fh8aEKdqV3cw8QXt5HM3ji6mPiHc9SO_ptWHD8WGTBR4ctGTK3TYbA_PVNsjge0vXqmo5IoIlhtbuUJcxIl4PDAwObJVT1Ix-dN0T9nGSVguMkx6nXSgT0N6eSszBoDWE6jvfoFYXniTXa3enJFqlvbQ-ASbfGAeYKHXOS2BnIGNh0rmYfLHCYMUTC97s3tXdZScpqg29CLN2tD_c7PGMzQK289_FVZlFxHa2yQe9GDGWUsxky2R7ueo9K7DWfd3ySudo43rQDdK9RzPNZbDg1tbfFWF5pljp9641JruGEqHhQBHkkXYMGYHCkfCuVhuIYjHYdpwga51kOJzADpQIt19bWh8DIX1KSBy2s-eqY07xzJLbafGZpLyFoQueVlGckZzgnC_ma24xrA1s2KOURuDuoJqh4jU8-gVUV6tj1LgQogL8OC3K3u9Tg47fKttMvy19_P1VeoTtuIvDnZrswyLH_ihSfF3wVyOmxqqZzg5gDMwvU2O7pvRTYAQTdGvejuIL20H0K6WSIZEnhsdiSNjjoTio4eXD8n7cQkOPs3z0O3XdN7Xx9x2nNFbWstwXZEYpEvPKwH2FyAT6YqU07IKDzq40B1KTWF-3LdQNS02G_APicsibK4GxHlirHMzAXomyQsSdF4avJWu4UngDNAeXPNlvlivLfaKFzThpXey2QE_D5F3eETh_bRjcsNfz1UJOHcbTXeNjsqYIPzNen_Eri9vXJWi8PWE1tBCWlkcoeQ83cHpsO9b9GtdCxj5NNCdPmn2znaAIQ8KRyABNKnfPTIauPLjMFD_cjR_-h-qsbDi5PZY73FTZuDDcCKQaXMrOAknvkfPr9VGfjnCnlr3TtvY6_t0WkcPx5FiM_wA9Um44QlE2Y7C8EKL8eBhgV7nFWf4A1DwwyZWTtLQKq9x7kzgse0_0iqe_8SvrKYmF2KrhkMkZX4T0piXDvfeGla7mKIPyVDxLF_UYR6FV-p6dd283jPA3JIocMu49dxWrGsPa52jRwB_VAe12xUEDPTesVqSfsB_zIwOoYflWzA_1wOoPyQN0wVxkEbjY1j9IU27ZRb28P4gb94_566WMfUUUfhfwpMpERM8EkJILLl09B3FURfjROi5jUfUfVCYwWOSeYnfrWO0qSgTgd89B5nNCalaavj-gkIPI058PIXYBtq1YR-SQk9D4im0W6lxfU-KZdXbUePNXUJFbzy6Nw5a84xDDGeWe5yOsjkhAw6ZfH2RXzzjHEJpu9fed3R4AaE_IDzPRdnhsDHE4iUd1m3if9usq9lJBZw8R6zCJbrUv7wBOssY5kBx4hHZAdlX_3wG_mmEf049huV13bfeJ9Sdh1_QDl1Ws07R1s9NlgupQby6QfS2JxhevhgjoADvj7WoUXO7qgHk6n2ique6g1YC2iUxaJaHkVF903bjsfJdt_StLYnMGh1VRew90bLSaIJ5rUTSgKjgCGJUwqIOmB2qt5LTqFABjj3VJmjsYiWuAhIGScZIH5KiHl2wUsh6qIQb12hsbTkAOHkc_Z-W4zTORvtlGrfZSJtO0bSUNxf3pDdS8jAsT-Dt8qQ77ALAUu-72iq5b9b5yNIsGeMHea4blWpGMD2nJ21vipMWlahKOZ70HP3YIQoH7AFN_aoljeZTD6gCautD1FNxIxPYvelR_9aYEY2UrLuar_vS9VdeSa60hlxjlnuWk_JtqmC0HRBa6t_n6izWpez2z1D-xr_QreQjpGwUfiMVeJXt22WWTBQzauLQfL5KWT1H2carbOwhntbkKMgQ3Ks2PML3FMY1o0csb6DPOsZDvI6-751ZcD4b60D2Z3yXOk3Ms9iG-usZyzQSjuCufdPqpB2HXpXUwPFnYN-aM6dDssE2BmDe8F6u9nHtM4e9pGltdaUXtr63UfS9OnI8a3J2z7Ohzs49MgHavfYDiEPEGZojrzHTyXIkjdEacFrMYOMg4rdaggfLA2oQi69NqEIYzQikAMUEpoHPagiwkpNzfm9a4uLyFZbqd1bx8leXGxHKNtxWSDBjC9nHA6o2AjLV3fPMddFa6XtLLxbjz7ZNiAG2e48eEEOEEfN9Y2e4Hl61d2xEwhDTqLzV3R4E9VDJ3S-x5c0iF445o5HTxEVwzKDy8wB3APb81KRrnsIYaJBo9EmkBwJS0unguhd2v1ZRy0fNhuUb869cDzqt2HbND2M2tgA8r25DxUg8sdmalJYXUqwGFVdrqRo4o1bLllON1-e1J2Mijc_tLAYASSNKPQ89tQn2TjVc9B1SbuaGSXNrbG2XlCFPELZI6k9NyR06U8PE9jKmdnNk3QFA7AUToOSi97XHuik0Wb3QXJC_Z3O3oKvygWKq_io5ikkb5qe6SPoDQgoAbZH8UISTtQhA5JAHemhEevvZFCEB6DPxFCEYAzhjjz26UIUnVsrq98VOD5JlPunp79qjCa4eP7qPkEhsog5WkGWwG2JParnaAkDVA8VJ08370pINMndJ5WAj9X9OZhnl67Z_vWXGjDdiZMY0FjRrYvTS9rVsOfPUR1KXIbPxpZYGmsngRTCpBctKvXLZ93cE96rYJzGxkgEjXk5jsA09BWulDkVIlgcS22kbc9R8k5dWYEH_8tZcRPKoXxmUyvLgF25RsQGz19POqsNivxPuEze8Gn2Qcobs0WeZbW3ipPj_vbMOl89yefxTs7jV5LdbNZhezsfHQv_jyAHEgJOxK5yOgAqEUZ4Y2Qzkdi0DKaOYAnVlAa0aopud25AZ7XPpfVNxSzDT_iwMH0iJAJw6s2LdjhS2B8lOdgaulZH56Zic-RxttEDvgWas8huotJEZjqxv9KPdOksiqtmltyIEZF5tyOrHPc1pgY6JpzPL_NgmtAeQrkFXI7MdBSfhgt7pbeCAsl07iNjLIOQg7KRt7oGN8n6wplllw7nyyaxgX3RrfPnqT8BSa1sgDW-0Tz2_wox5kLqr9zlCUbZh6HuK1VnDXEeNHcfuq9iQhjsR-NSCSXBNNBIs8TlJE-yymqp4I8TG6GYW07gqyKV8LxJGaIVj7AMRSuuZ7C0lcfeKb1wf7AKbiZpFO9regK636tSO1kiY49SEluI9RxiIQQjyjj7vVjfs1gjrKXP4AN3-AKDuOYoiowG-QTEmr-pLs9_KB_KcfpWyLg3DofYhb-i_nazP8ljJPakPy-VJk3dywPNdTHyzI3562NwuHb_MbfcP0Wd08x3efef1RC7ugfduph8JG_vUXYTDu0dG0-g_RDcRM3Z595_VPx6vqsP2L6UjyY8w_OskvBeHTe1CL-jT9UtTOKYyPaQ-uvzT91hZ1C6hp1vOB95RyOPmKz7-RJD78eIcyuR7w-Kt_1Jk2mJia7xHdPwUafQ-GdUOYp3s5m6eKNj71D6tVum4nhdcRCJG9WaH7ANp-ik1mCnNQyFh6O1HvCrpOCdWsrmOaBVuISftoQQR8Rt-lbOFcXwWIxLWtfldeztDz9FSlweIw4zPbY6jUfD5FmriCa2kMc0TI2SMMMZrS9hYSFnBDtintQHLOq7ECNV6-VbeItyyhvQAfNMpiJzC6SgfZPTz481jikMUgeORQlTosMx5M4J5l_wApqzExiOQhu248igpIYjdVwF97BOcms6FO0OXOsWYxlmnQcw6nfvVsQJdQ6H9FTiPfHmvKt7dSR6Np2g3Jx-7ZLxWjYZ5ZDdSZBHcZzXyiUOMzyT8DwH6SSvQQvAwscY8b47P6FuNMvBxlxdp_EV00KgWjRSWy-6sE8K4CIv4ACVw6_Bh92udl-7x9kOR99810pj56kZLyIquhHIeH_qFruseLa_Q4iQ1z__nHRCcnHxOF_wCg1pZoLWdwIHif9-yi2rx3d4tqPq7S0j54_wAoGWPqT0q4O0oKhzOQV8_Epuee0sZooFgXNzN923Udh2J7Z86r_LWygf2jdQrfWRextP8v0ax5vckkOZJcdWI8zUyCNGqOl6Jq54wupQLLQYuRB7viMvT65qHZNGr51MOrZOWWkSl_3hrUxlc77WEL6uy_rUHu5AKYJPNXseozeGEtGgRFGAIojJgfkKoLArAaSXu1J_9y7uvUG4WIfgu9Ry1spAlHGNFmx9Q7lu6tI_8monOOakDamRaTop94WAOcj3lb6pqGd45pm-aYuOH5FmBVtPiIPbJH5a0xYtzdHKiSInVpVXe8F6LLCfo9tyMNyueZSPgf-Gtglzi2lZDYNPCxN_wnp-mSSztPe2cLbmdGaVYiPMbnHmrBh6g0sweKIBS7MRnO0kDwN_wenqqpdQ4q4b1C14h0a8jE9gpkgvrT3uaE7FiBs8Rzh1OcdDWjBzPwMvaQuIP40WfFsfimd8B1cxvXUeHXovVHsw9oVl7RuGI9XRI4L63Ig1G2Vs-DNjII_kYe8p-I7GvpvDsc3iEAkbvzC81LGYnFpWtrcoIDbai0gjotNCgoQ270BCG2dyP-UWo0kk7e92GBvT30QhgA-8eo2pFNAjYb97mmknBK6ymUkmQk-8c56bHY9arMTHMEdd3p4dPIqWYg5ualX08s1hYIWgEMSuiLHJk83Nli4P2Sc1iwsDIsTO_vF7iCSRQqqAaeYFKyR5dGxulC-d-d9FCZ3ZQjOSqZKjOwJ61vDWhxcBvV-myqJ0S5vohjh8ESiQJ9dzEYL9P2fTGOtVxCYOf2tVfdq7qhd-N3sm7LQy-v_JrPbtV6ihSQhTQkkH4aEIDqM9MUISs7k9NjihCG_Ly9e_xoQpGsbatfqG6XUwyOh981ThP7AB4_IfIKA2CiAACrlIIwxDgqcEbjHnRoRRQnbaeOK6S5ubcXKqxdo2YgSHfqfjVE8T9IjHE7IToCADW2w8lNjw1wLhacd7aG5huLdvHxiWRZIgq8-clAM7r03qtjJpYnxS927aCDZrk4mtDqTSk4sa4FuvM8vTySBMTcNLnwxKW5hGAMKx3AB26bVY6EdkGbkAVmvUjYk7_-qId3sx0v9JbypBPJ-79plhY4Xn2ZlByOYDbr26bUmsdLE04loL7DUA7aXrtomXBriIyaQnubi7k8W6naWQkku27Enrk9T46IcPFhW5IG5R0G3oOXok97pDbjZTeBj87Yq66Ud0oHA2Xcd6SdJPbehJHsOm4p7p0geg3J9PKlsirRUJoU0IfA0kI9yAaEkWT0NCEY6fHrQhKAxt-NJLdP215dWTc1rO8foDsfiOlZMVgsNjhlxLA79-h3WnD8qfCm4HFvy92ymPqGn-gvLrGmRuc7yxDlOfUdDXLHCsVgqOAm0_sf3h5XuF0BxCDE_wDlx6_3N0PqNioV_wAIafq7m40a9QNj7CJ5WHyNSxPG5Y39uJQOj9W3vN9_L8q0YMT-4SQP4D3XKkuODNZtieaI7dyjf0yK0w43CYkXDM0-uvxWWWGeDSSNw9L6SS_Dmoy2sbnw18E8jbt0xkHpXTkcySBri4W3TfluFVnvZp9y5Lx17T_DSFurDh6_tpTEDHLqjgtCj5CsCAEzOP8j_g7Z614rif2hyuOHwQt3XouvhsAAztsSco6Lk93_QuOppBeWnE2pWsIHKsz3EkbP-hQ2_wAhXEbjcaNXzuvwNKx7YnkGOMADax-6qUh1DUHN0GnnyzSPI68o5mPMxyT3JJ-dZSFcCXakqdY3V3a3cM1sWMwZZYzF3YHoQO_bHrVMjMworTG7or_Xbq2v8_3_YAhJQY54Ds1tOd2XHkd2HxbuCKpZex5LY85hnA8x0P4APmqEahKB4auUHKFfHfBz6uKsoLPmUiS-SWwg0i1i5VmdXmZBl5X_D9Z2Hz-mgW0k2g0WhoG6nyWcjzfQnR7aKEDljmJBC9mf8_n2p9pSZgrQJ8agunqIdLhUyf4AfuuT7wBC9h8d6jo7UqOTLonbKyvbuYXF2k8ztuPE94n1JPuqPSoucAO6pBh3KtLi602yAW_1VTyjAhgBbHpnYVUGvdsFYQ0c1EfjDRrTL29so9XIz6Q_rUhA9ygXsbq4qBL_VrKF-USwL26E1YMG4hZ3Y3DtO6lWftMNx_gPp0w8vEwf1zUXYOlNmLif_JU0-0AMuXsoQ34s2R_eqnYQ9VeJWnZTbDilb1DL5BnVAcGSLEqj88u4-YqrI6A2Ck4CQU4KTdxLcJ9JtyG5hkjsRWlrw_UbrPlMZorn3ENnccPyDXNEhb-EX97uGPYxP08VB9042bsR1BBrXG4TDI_dZZQ6A9tF6-fUfykr2e8dLwPxanEmnB_oEi-FqdlFsssDHIeNc42PvKDsGBXOGFdThePfwvEBzvZO_l-qxYuFmMaZI9CN_wBR4eHI6dF6-tbq2vrWG9s5klguI0likU7OjAFSPiCK-ktcHtDm7HZefNjQp3bcY3popF8aE0ZGDg9qEkM4ppoA4zgDfrSSKI7dPn4KYSQI36PnTQgQegG3pvStCBJG-d_jTQnYXtkSVZoml54ikeG5eR8jDfr6NUyslcWdm6gHAnS7FGx_OikwtF5gmTv3q5RQ7U0IHbqaSEW9CED0poREDrQhJ70IRgZ_5ulCEYIHvHt0GetCFK1dCNWv4774zLj1981RhP4Ax4_6o-QURsomMj6lXoQOMbgilonRQ5Tt0OdxjemikeB97NCEB5Y-dJNGKEJXQZA-VCEYZh8fUVEo2Rr5nAOP-0JobgfGmhKyFAAO5G-2xoQkkY2oTCG3lkedJJFudhvihCM-WKE0RzmhCMb5RihJHjtt_WhNKx8x1pFRR0UjmiwcbdqEyhtnJyMd6PBClxaxqdqpEd_IiKMnnYFVHmeboK5eJ4Rw6cF88TfE7fKlug4jjY6ZFIfLf92uD62r28Xep6e2gadfyfuqclSYiEm1THUL3W3BGC5-3222r99jHYSSZ0fDmZWjd1n8XyXqWyzYeEPxrszjs3QfwdT-Dw4RHBqWqSreTxoMe6hICxRDsEB_XcmqAWQtytGiziOXEntH7oB5KalvbWR8Zn6lTDqSPdX9nv4AhUTnf8K0NiZv3j_h-p-C13s94A1H2g6_BZC3nvBIw-qjBK75sEgY7kkgY71TiJhh2b-rRhoDiH2dl670j5mT2e2GnJaz7SixjCy_RTHCrHzyEJOD03rhv8jKToF6FuCiaKqlUar6yLwLcw3UejcS69YNd7SiSSO4jdc5ClWUZIO4bORmmOJSXZaFW7h0euUkX-rFap-xXOsfLpPF1vIy45fFheMt8d2H9VobxUfmCp_0ttUHKJoP_MfGXBVtda4I7HVtXTMenW8U3JHCxGTK7uACQBsMdcZp_wCoRvPQKeHwHYkvOpGw-q5tqnsc9rNlcRW2ocOXiz-jKcTkrP8zYJ2KE42rUzEwVmtY5MLiSbI39rO6lwvxJwlqQ07V4gt1kDlUCUs23urjqwyMgdKsEjZhmGyokjkwxDCNVF1261uHMN1I9pjZvGUq4PTHL538HFTZl_KFXI1zRbzXms0-l8QXb9srGXwn-Ty7F_VQev91pbkbq4rDI2eQ_hjTqfoiuPZlxjPAbu40zVGi_wC9kgkWL4cAfnUhiYhoCFnfw3EP_zrVJccGavA3Ksac3qj7ANM1YJ2lZ34NlbtXxVdcaJrVu3M0AyOhUgH48GpiRjuazuwc7Na9ydtuINf00ckq-LH05Z4gw-TYyPxpOjY9TjxeJw-h1HiFfaVxdbNILiJptPuV6urEofiRuPn91lkwxqtwulDxGKT2u6fgt9pPGUk0KfvGJctss6_Zf0ONgfLtXPMJiNtXSzCRveUvX8rlbb54aY6kMoZgUDJIvkR_bf4AStETw89Cs8rHNHd_yuXazYQ28L-xoyH-Fz4s8A3azk7geaH7AH2reO-Mjt1yJG5PxYtuY6eXgu6fs1-0ZL2BvZ9qNxzPAjXGlOx-1F1eH8ruw_-vKvX7AGcx5ePucp1Hs_Ufp5lcvGRtvtG7Fd5xXqRqseyFCEWM0JI6Eyhg46H4KEkNuU5O9NBQPMDk4OfnRaSIgjBBIHQHvTQjyGAz9bnGMUISCNyDTQgKSEZ65oQiyeooQht2oQhQhExpoSe4A76dCEYGzZ7eXehCMrykgkbdRQhDiTV7e01fUYmYu4uZfcXt79wD9Vlw0gGHjroPkFKOIvAPJZmfV76cFQ_IGBwqZBUfHvTMjitLYmhQmdnA5yTjqxNRJJU6AT5tZ3UwzbwuR_EDgfjWHF8UweB0nkDT05-7daoMFPif-TCfHl71YwRa1CQou4_4jyc3-1gb5pcM41G17vJhWg8DmPtlo_4AUriwea6KxSCBZGOAVlBUn6lWR_ajAE1Jmb9tNKmTgWKGseV3k4WpVxa3Nq2Lq2eLP4S4H89K7WHxkGLGaB4d5HX3brmTYaXDmpmkfz3JsAAkgnpt61pO1qikZBBwRjFJCNT2ziknSBxnJoQEecry8oGO9CEnoe1CaMDt0oSR4wvXf070IRE-VCChjzPShCMA493ehNEp3-0JFLwQfjSKSVjGc9qSEWBtTTQ3bCj9UroEnkkASaC4N7XPa7DfrLw3wyovbZSVkKsRHduDvzNtiBSO27kY6V8-47xh2PkOEw7qjHtHmfDyXq-HYNuAj_eRuaQ-yOQ8T8fPZcfht1e8l1TUpP3pqcx5pJZByxJ5ALthQMALsMDAAGK4gb3Q1ujRyWjTOZXnO87k7eg6Dl8k7873E5HMZ2AOWJwAPgOg9B1qQAbsm5xebcbKl2FpLd6hBp1nALm_unEcK8vux574vQVF7gwFztkNaXuDRuV7r5hPszteBuF4biWJTfXaBpZSuHcnqW76gHTHSvK4qYzvsr1GDgEEY6rp_Lk4rOtYKAiJ2Aoq0WgbeTypUlmRG3k8qMpTzJP0WXfGd-uKMpUs4VdccI6Jd3-6pdaFYS3qDC3L2yGZR6PjmH81MPkAoFRLmk2RZVXf6y3hHVLhrq90OJ5HABPQbdNht_U96tZPK0UCq3tY82Uu19m2k2QCabbwWIAxzW0CpIf6vGR-NPtn3rqkGsbsoM3sY4du5PFvoJ75gc5uZjIc-eXzj9YqRxElUNFL4M7pbeyrh2FeX5wsygdPHlI_JsVWZ5eqmCzkqy89mXBjjkn8diXHT332_EmgYmVvNSLWO5LEcX7s4-z_iS1mW1tm0u7YZS4iVWCt5lcDI8-9aIuIyMPe2VEmDgmFOavNvG_sd1f2c38XWLBYY5CUttXsUPhzA9VcdDkdVbfH4QrsRYsyDM038Li4jhbYO9X7AKh9f3WJutEl0qUGzeKJLg5AXe1n5F_gb02wT2qztQ_UrH53_E_hmr53_K-4U1xFZtGv18MFj8Yc_ZPdT6dZpWFh7RiuYReR4RcQ8NyNz32jhEvY0IKOMx3cXeOQd_j1zjzGNMGIDhTlmmwpJuP2uXiOhXNdOv9-H5dg1fRppLG5s51nhVz_1tKDsCfvIeh9D4q6sUzoXtmj3BtcB8YJLCK8Oh_Re2uBuMbDjvhi04j08eH84KXFvne3uF2kjPwO481KnvX0jB4pmMhbM3nv8Fcl7TGcpV_g1qKigMDBHXNJCMDz-0KNpfbANCEkEg4yR2OKEIEbZHKPLfpQhIbfJ_2akEJIwOo_CmhH0-YoQizjGO3SkUI92AXyyaEIiO1CEKEIU0IEemaEknBO9CaHwOfWkhKBOQSOo_KmhZXVUxql6Dg_4xKMjoTzmubhv-DPIfILemER5pfCRWZz0xuR_pTnxEWFjMszqaOf45qcUT9niOMWSrCO3trVXMgW5uY15zH51fT1rhxniH2hDjhT2UIF5vzuH7EdF1uzw3DtZe_J0_KPPqkXOoXRnR_FPhnDqoGBjyrsYXg-D8TIH8dl7G3ak-p-gWPEY_EYk991DoNAiuYlci5tzhmHOAOrDz6PpXexI7Sp4DR37nj8LJQOqYmWMr5IiQIG2dQdlNYpgJW9oNuY6H5EbbK94f8qv_KI2NxJ48EYyFlHOAvcb_6tc53CMHjAe7lkGoc3Q1z23-67rSzHYiIUDmH5p1B960gtbLVU8bST8c_KC9tzZDD6KM_0rIMbiOGOEePOaM7SD9OHU9VA4WHHguwgyvG7D74Aqfoq4ggkEEEHBDDcGu6CCMwNg9FyCCDRFID6Y0ymCjBGem2KSNkQAI60IRd9qEI-poQh8KEIdGwaEI-xGNupoQiB64GKE0YwTQkUtfIgk0UopQIDYHTvkdKimk7AYwdvIdaBqhck9qnHE2oX03s_8embEa41ieFsNuNrVD2JG8jdht1JrxH2n852Y-6Yc6ncr0vBeGh57aXYLjeq6eDN-7_KGMvE310i5IQ42RPPHTPcg4rykEeVo6rqYh_aOoBVc6EyDTLBOYlsEj_x-PlWkqgikoJHap4FsPEJOMr1kbzHp5edLxKdVovQ_wCzJ7JF1LU24g1a3LJBhppD0B7RL2P438dQa4nEcTfcaV2uH8Su-7devYkyQiLgAAADsB0FccLsKytbAP4AbNTDVW59KeumwAD3ankVJlKcGnQDrH6Zp5Al2hShYQAf8IoyBLtCh9EhGwhWnkRmKH0WH7uB-FGVPMeqI2sX7cD4KMoSzFF9FTtCKWVPMUPo4HSIUZUZkBEAP4MUUlaTJBDMvLLCrA-YBpZUw4jYqi1Xhy1cF4Y_CbzTYfMVBzAtEcp5rF6_w7Z6hZT-RrNjDd2dwpSWKVOZXHw7H161Fj3ROzNK0B17_LyR7YPZRdeze7N_piSX7Dt85Ajk94xnvGx8wNw3UjruM13MPOJhexXJxmD_IdrD_PRcq1GyEfgX1mzT2zY8GbPvL74Au35R2PwzWsEOFFcpxBojb6aLQadqL3FpCGPNIhPKx-96H8gH4F86zZTG9Xe2zxWO4-4cV5RqFkmGlQvEf68UfbjP4y55gO6nHaujDLk0O3yXIx0Amb2rd69_UenLqrr2Ee0xeC-IFttUujHo-qsttfh3IW3l6RXI8h9xvTr0Fek4Lj7uM_ZP5h3zXDxEfbR9oNxv5P3XrpgQele7tc60YAz0pJWhjPShCVy42OxHUUWhGV6EHei0Jth3_nOfSmmERO2KEUiwQcj8fKpJJJ7elCEoYyST-9KSEWO_900IsAnJoQhjf8UIQpoQ7UIRHOw_WhCI9cgfPzpIRrsCc0IWau4XudVvYY1yzXEmNsAe-cmuK_Gw4DBtnnNNAHmdNh4rq4bDvxMgijGvy8SnY5YLYPY2R5pChLTd2b09Kx8O4ZJxqX_zxIUKJjj9DoXdSulLiY8Ew4fCGz6Z3XwHgoVvKRcJJnctvnYYNeggl7GQPHLfyXIT1zCFjwu_hNlT7I3T461YqINZQ_KdP6rtvcdE0LWQnNqWKknnjP4AC36tRwj41wE0d2nof3TCBYLzTLEMDaaPHQ-npQTRMzR4Ob7OXNJIcPbuksbllPvIx_Q1Q5pgeJIzpuD6qStNO1P-FNH__C3ZuaNx1hbuPhmpzMY4XVsfy6dR-iqewgiSM04arXjw9biJHIl_GufITgd_jivPd_gL6Zw5_wDgeQHh1K30zizdNJh7neJ8fBVhVgShUhgdweor0IIc0OBsFcYgtNEaowd8b7pQgJOSDnJzQgob56EBHudqE0RyNs0JIbYoRSLI8z-UJpQwRsDtQkjA-Y9KEkoDffoT1zQkjxnfYZ6Uk1jfazx0_s_8Nn1ezTn1G7lWx09eUkCdwffPmFXLY7kAd65HGMf5yw5y-0dFswkIkdmPL9rh3Cl9bWWj3S2yyT-nc3skTvKSzYUKWYnqSzsST3OB518qcx8s3aSefvXtmzMii7OPe_lX1VfxBcfui0SwtmBurvmklcdVB6kHzY7egGB1rfHq2ysbtNVWpaDTbJWI_wCaulwP9Iz1-Z6H0qV3qoAULWj5n3B11xJxFZadFFJJLcSKvuLuAegHkxHyA3-Vkxc3ZMK2YPDmV697cK8O2XC2iWujadCkaQqA3h9C2N_j4a8u95kda9QxgY2gtbZ2pUB26n4qkAqnuVrAoBDVaFmcVZRRB1BUVYAqi5PiDfdBUqUM6ULdevJRSM9pX0ZTvyUUlmKH0Yfw_lTpGYovoo8qWVPOiNmvrRlRnQ-iJRlTzImtUA6UZUBxTb20aDOKRCkHEqFPCrZ93Y1AhTBpZrWrBVOeXKtVDwtkTrCwPFvDNhr2k3WianF4lpdpyk90P3WHkQdx8KcMphdmC0NI2OxXiLjPhfVPZfxbcaDq6c9ncN4sT89yVCdmX7e2K9AxwkYHNXncdhjhZLHslR2tI4ZfqNoLrBjYfcfqP0B-Ip-0LO6oa6jpsUSW0eu2lxoNy6pLIRLbSMM-HMMlT4M5UjyNTvIMx25-Src0OcYzz28Dy_Rcl1a0awu2uGiaOKR2hnTvDIDh0Pp3HnXRYc4yncfwLgYhnZvzbA6HwPMfovY_sP1a71n2W6Fc3srS3ECS2byFslxDIyK2ep9wL6FfRODzOnwTHO3Fj3H5KXFxDOzeQt18SRXTtVIwAckdKjqhGDQhD9UkJJUE7jAqQKEnYDpTTQOSOmw26UIKJunNtvTSRHBwBn1zQhBsDpTQk7dfyoQgfShCGPh86EIHckkb00kRz9k0IRdaE0Yxjf90kKu1WKKw1K9tMlmkuZVnfO4PMcD0ryn2ewv6uOi4ni2_gVljb80O-R16L1GJcOHRfc4_bPtH-KhUm1uAGGDE_XzH7tXqQXYXEW7dp1_nkuPslXUYjuJEXZc5HwNLFRiKZzRty9dUFSbYrPB4ZO4BT7pPT4DW3DEYiLs3bjT0O3uKAoao_MQAQ69lHQiuUbaddwjZS3kMgF8ijI9yZexroSSF7fvTN9nDr7lSRckae42Tby7qRuUbz7p8KgQxvdJ_Ddseh_mnklSTGPCd7a4wFY4yOgbswqtn8TjBNsfgeRHyTVvo99JBKtjI7K6e9C-ehHb61QLKLoZhfI-IWeRrmESM0I-a1UyrrEDXUKhbyFczxjrKv4Y9fSuFC93BJhhpDcDvYcfyn60-HQrfIBxOMzsH8rfaH5w_uHj1Cq8bZBz-16E6HVce7R4I2pJoZHl-NCSBGFB7d6EIjk5x0oQjx_rQmiG59KEIxnO1CilAn_tFoSlGc464yaihG2cAhdqEBcH7AGndWNve8J6a4zFFJNfSLj_RHuqP7Kfxrxv2qkzOZF4E_ED5V2uGgMbnPW_cCsRwQI1sEgIxPPNjmO_IhXmkPxPSvETuIeSvQwM_DA5n-6lKvY49W4hmvpADbWWIUX6JgM4-HUn8CrRKMoapPhJdQ2CjW0LalPNq1xtAuShIwOQf32HwqTpRsFFkRkd4L1N-zN7O_wBzaV_xvrMRXUNURvoqOMGKFsZbB7sMfBcedcDiGIMr4o2XosDhxG3MfReg7CESSjPQViaFreaCvoosqNqvCxPOqfiUodxtTCrJtWNlLytyE7HcVY0qlwU8jIBqxUoYOdmIoTRN4i9TkUJiii5ie-aE6CX_34RoSQ34zQhFg-dFWhFjahFqNOcjlHQVEqbSozqSOlRpWAqk1tfq-npVMgWmE6rKXkYcFGHWs90tgF6rgX_TvB1trfBP_9dAJ9IfJcDJ8N9iP7Fyn4a6vDpi0liqxkIlw7geWq8zcMXhvbF9Hun5_HPA57MOmPnXTcMrtF5uMZhlKK7keO4F_GvI-QzAdj0Yf6IfnVw1Cg8Zx4rPe0izErpr4QH0XWE8O4A2CXiDZj-sP-1dAeXMfJYeINzt7Xk7Q_4AYbH1Xff2Xpmf2TQoxOI9SulX8YjP-k19E4Df3P1P0XlZvaXWc5FdlVlA46DajdIIxjvsPhSTKA3-UJIUIREZ7VK0JABB3I3_UJlE2D0NNJEFyTkjPqadoREZAHbrRqhF500IAb8pIQ3_GhCM_Z6dNqEkX8inRRaAA7UkIwO-cUtU1mb66kXVr7xiXWS5lZ_jzn3qy8LezDYeOGqjygV0AGleS6JeXuLnGyd0V_AHjW5RubYK2O_ka63EYs7BOPX-I8UidS8MEpJ5sGNtu4rLifxIo5fQ-iRTdrN4MwkOCp91vgarw03YSBx22PkgJy-jMdwWXo45hv4AjVmPi7ObMNnaplItJRDJhxlHHK49Kqw8ojdTtWu0KE5gW0jW0jExPuSO3kwq0gYd5hk9h3P9HzQErBY_RJeUSIMRv2I_h-HrS7Mu_wBvJ7Q9k9R08vqmjQmaIRBiLiHoSuDgf27UNBxTezPtt28R0PlyRV7rR6Lqsj6HdQkpPCcMP59iP1rLNBHjYXQzDQ6H5fRZmyPwkokjOo_leqttSgjeJdSs1CwTn31H7ZSdx8PKufwzESRudw_FG3s2P5zeR8xz-LVj8WPaMXB7Dtx_a7mPLooJQjHQgeRrsaFc1F1O-9CEnBbcGhNHt0G1CEOmPKhKks8gBKnO-48__UJFDlU_ZPXrQjdGB2FRQjyTjl_KhCHcA7AUIXnn5puGSbXbC6UArYadFPJ8GuCgP7icV4r_RNz8oeDfquzhHZcP-rIcFSScty8hyYIY0HoWXLH84BryLmB516n9rvte5pA6AfJS4oHuoTYx-41w5R2B-yCMyN8lAHzqiVoi79WqN5cCOv4ACfcruxsbG81C1026VodLtuW4vigyfCXZIgO5bpWZthpcdyuhFG06HRu58ui9f4A22ovF--9VQwyToIobbottCN1iUegxzHqW-Fced4zZWrtMaazH7C6TphBk-IqDVTJoFoLcZUVpasDzqpiwEjOM1KlWXI0QoceXShIm1ZQSeIg8-9WA2qinMYNNCIkDYjrTQk8mNx0pIR0IQoSJQphA1SXIUE-dBTpRSCSRUFNJZMDNJAKpdbCiD8mqX_LTDushenD9rKSulEFlOMdJi1rQNS0uWMOt1bSR8p7nl2_PFX8V-SUFW5QbaeYpeCmtZrPULqCNArWbGVMDB5CR7vyr0vtBeTLOzkLFJuQZpMP0nTmzjAJI3P8gGpxUQqpdHX1UCSJNa0W-4flUFrmPMXMcclym6EH1xyn7ADVYLY4OVDmCZjoT6YfHcH-LqP_J2sfSeB9S0CQKs2m6i0vKT_3JKq9R6NG1fQPs_KHYdzOh_nyXjZ2kO1XcOo26131SgBzHGO9K0I8bk56UIQHXbbIoQgTsdv4ASnuikFyxwBk0HuiynuaG6kxaNqdwfctJAMZy_u_rXNxHGsBhf-kgvw1-VrfDwvGT6ywjz0-akDh9osNeahawA-bZ_XFYhx_tzWEw736NUPfqtJ4MY_-8rW-tn-KHPccJ2rGOXXZJnGxWCItv4cEfnUTxHi7zTMM0eZ1-YUxgMA325ifIf9UM61w54oht7TUJGPeRwg_ACnG7_Q4h4ZGyMef4KsGG4UNy8-n6FBm4i00nEVpIvrzFv-1SXfaPkYvcf0Uuy4RzD00NWt5mVUupoyxwB4QP5KBL5pQcojjd5afUJ_duEP4AzPb9_wCE2Zp5G5LfXIyw25XRVNQfxziGEdWNwrhXNtH9X40_5Jwkv5CcE9Hafz3JmaTX_fd5Cy_xKqsP0zWzCfaLA4w5WSZXdHaH5Pis0_Bp8OMz2WOo1CYXXNQXdjE_xTf4q7IkcucYWFSouIWx9faqR5o2_wCBqQlrcKBg6FWFvq1lce4s_I3dZPd_0_OpCRpVZicCs3qYzqd5uD7zEm_7AFGsOG_os8h8gtidsJuY_RZD_jjCnyPlXYwMwd_t5PZPwUgUcassVxbSIGaP31z-dTUWxubFLA7caj00-SFExjYVz0gpbMbix2-3AQD22O1by7_xhb7Mz9fz9KSir3OcH0rnqClR_wDNQeEf4WHdPVfKtrT56hyfmbt5KQQT7mYxExHiIPcbO58warZ-OwRH2ht-n-eKaBLSqJUJEybkDqw_i-PpUrM47Ruj276Pj9-CFLsr2FJxcF-SVvdfP2HXzPkasDo8SbLsr7gf3UHtDxRW20yOaJvBni57O7ADlWBA8nHqK5vFeF4mZgxGGH8serTYojm0-BClgJhE8wzew_Q-HQ-igXdrJZzvbS45lOAc7Edj6lWYPFx46Fs8ex-B5j0KzYnDuwspify-PimG67dO2a0qlDHcUIRjzoKRSgBgnFCVoDAx7uaEIwcdBUUI9x1oQi3znpmhCNsnJJznqaE1yf2z-L69RrakZf4A4NuJ4h_Nb3sUp_IV5jjUQdM8n74Aj6Tha6uENxhvn4lx_haQRfvUsdgyfgF37T468MwWAvQvH8rvD5Fb-Y0Vrpq3t1IqNcZ3Y9FJ5j6Ow-QrNIO1fXILRFoM3Vb_2N8MvxLxPa_SI-a3iZdQuRy5DOT5Uh9Byg48hjvWLGSdjHouxhm9o8N5bn-L1vCqRIsUYwqYA3rgXZ1XbpaHSTlx8KvZqssq1FjhiFNaWrmyK2jjwMH9VdSzlJkh3zilSdo4TyN6GgaJHUKYPeG9TUUCoIxRSaSI8DYmnSEPD5aSSHJjfNFIQxvmgpph_eNJMIhGKVItJdMikQgFUHEKctuT4KpkWuA2Vjb89qxuXTjVNqVzHawvcSI7LGhZgi8xwOuw3O3lvTiPeC0eIXhL2rvHovG1xNpawrBOWdVTIwpJ90qdwfn2r1eH_zbK8pxAhkttVRbXkVxbNykl7XEq57xk4YfLrU8uQrLmEgIPJRbj-m9lZTheYHmHYHofxrQqHLQ_s_ayuk-1690thyJxBazR8o7TxnxMfPlbH6avS_Zyfs8R2R5j9a_qvN8SZ3y8c_nzXqMbbV7ZctGCNubaik0Nu1OiUtt1NtNHv_0KY4PDjP39PdB-Hc1ycZxvBYHuvfmd0bqf95ro4bheKxItraHU6fupJstEscm9vmuZF6xw7KPif5awffeLY_4A8WIRt_udv_q-h81r67cPwn7kSF7ujdvf66gXPG1jYKY9LsYk5dsovMfm3T5aj7oQnObHzukPTYfX-KwcSMYy4SJrB13P496pbjinWdSZ2kuzDAo5mwevkPLPyrtcP8Tg4nZo4g0DnufLW1lkxGIm1lkJ-A9wVWLm4vL_xrku_hqWVXP2QK6kEpxGJznYAkDy5KkNA2CglVYEtkEjI67msF59ShOo3hW5lLktKeRTjcDvWuP4GAy8zoPLmVJRiFIwM5A86x0FEqQP6WgZi4E0g28wp_rW1o-6R2Pbd8AhEqx2q-JKA0rfZXryjsfjQwDCND3avOw_nNFBLsZr3xCYZ3UdXLHKgfCuZNwbD4ZcTiGA9XbH3hacPjMRhj6E4jw5e4qVLcaZeSFJ4zGx2E0Y2J9a4U3DMdwd98MkMsY_I7f7ANJ_x5FdL_3hMfpim5Hf3D-_w-aj3Gnz2J5yiSxk7OO3xrbw_jMOOeYXgxyjdrtD-afOlkxfDpcIM47zDzCiEcuzfka6x8VgCe1RQuq3vhsTi4kGR_nNVYb6izyHyCiFHGQQVJBzkH1q4Et1Cat4DFciO56MAVb8dxXo4i3EBs_PY_IhSCqZE8ORk6FSRXnns7N5YeSiU7ZuEnXmHuSDkbJ7Gr4HII5e9s7Q-RTCKWFoZmiY432J7iqp4TBIWFIo4nMThwcMnl3qMchjeHN_gQE7cxqsizw5VH55fRvKr4UzK4TRbO1HgVIpTSMOW8hHKThZB2Df-03uJrFRaG9fA_uhIkRXTx4sBSffUD_DVCZjXN7WMacx0P-eKStdAvmKmwdmBALRjPbuP5-dRikLdAVmmZ-YLUgnU9O5Qea6sl285Ic_niuMf7tWOzf7AIpj-Nf6juZ6rcP5_haOskXxb6oVbu3rXdOi5QQJ7b8oTRnJx5evahK0ogAnG9IlJKI2677pStCGwPUUkIs4OSM-XlTQlHscnNCEQPfIBBxQhcy9uklxpulafrNuuedbvSJ9sgxXUQGD41B-VeZ-0ZMTWyjoQfIj5l2OFAPOU9QuC8ONJdW1-kf27jnA-HiMP0Arww7jB5LuB2Z7ndSfmVb3zC61SDSIcFYVEIB6Z2yf0qloysJ6rcBTmsPJepv2duHhZ8KXvERT7wCtujFC3conu5-eK4HEpLeGDku9wxlgvPP-Lrq1zV1SrzSXIZfhV8axzLV2DfWp8a1N3XNeFf1esqMg4oQkcgPbeikJyMnpTSToGaRS3KPAp6J0iwMbUJUktnFIISSCdhTQkcmNzSUrQwPKhK0h6E1nuJmxa-uRVEmy14Yd5Ye-besRXVjVVeDIG2RUWnKbV68V_tD4OPo_E93MiHwjKJEG_uqQBy-g2OPn0r1eDkzMC8zxiLK_MFzPR7kwyyodyqcwHmhHvD4K2uFilxmOLXKa0gkleDlDGW3ZAfh7wP9UxyUydVT-Zfahw5xTw_xwQ6xJq45XIwH4ExCRR_0OoPxrpYN7sPLHPyB-S4WMbmvz6i9xOAHIXoGIGO4ztX0ojouIrCy0O8ul8WXFvD155OuPQVxMZx3D8d_YwgySdG_rXy966uG4TNO3tJCGM6n5P1UszaTpTRi1hWeRhnx5jhQPMf-VSOF8V4o5oxz6yY7XK3evHT9lahicFgTWFZnf7cdvT5lSajxhLNqBSFnlSPmIBJWMYHXHU76ddrh3C8Fw_E5cMyi2-8dToOv-Uss-IxOLd-M_ToNv95rJ3N5dXjYuJic7gdF8-gqDnGTVxVTWNb_ISIY3uAIlI3bJPkBU4onTODGqSXcyqEW2gOYl3J_jPnV-IkaAIIvZG_iUFFbhhbzyBSzPiJd-5qUP8cEknXu-_dIJkhjIIowTzNhc9RWVrS9wa3cpBKu3Bk8NDlYvdHx7mtOMcO0DG7NFD-plFbxqqNcynKp9kfxGjDsa0GaTZvxKKSshAbqcAvI3uL-edWg9n7uJtXHYfX05JFFBbvcsZZXKoTl3I3PwqMOHdiHGWU0OZSQnukI8GBcRL1HQt8ac-KD29lDo39p2haRAyeJJtGo8Rt_wqGEja5-d3st1PomnLbUJ4J3YtzRyE86Hofh5VwuLcLh4wTJJ3ZNw7mDyC24PiEuDNN1adwdk5c2cLxfTdPOY85ePO6H8VzeHcSnhn707iWj7AMruTvXqtWLwccsf3vB-zzHMKLqW2p3ZA5f6ZkwM9PePnXfw35Fl9B8lxkwNhnb8VempdhceHIYnwFkP8GtvD4R2UmQ7H9qQKkahaqxM8eOYLzOvn-1p4hhrJlZvuR9Uiq5QhZQW2J3OOlcfySU24BntY7pT_yryt8O_6_Wupim_ecO2cbjf6eadXqovhspw2B7vNua5SSlW7RSobMk4YZBP4f4AStuFIla7DuOh28CpBMRSC3crICVPuuvp5_KqYn5g8h40OhHh-26SMH-LNykAqdj9OvnUtcJJR1B-IQjkR7OZJoXY-9zRvnr46jLF2RDmm2nY_r8oIBC1mj-kMxajB1Xqvr3X8GqsVh48fh3QSbH8HkfRZ4ZX8OYSM3Hx6-9TtUtIo5I7q1_6muRzx-h7r4qxcKxckrHYfEf1Y9HePR3qPirsfAyNwmh_pv1Hh1HooYXrnz2rqWueSjwe9CErGw33pJoYbpuT1oQgR0yfjQhDmIGCBihNEQeo34qaKR5yAcKDj4aElzL2_XkUPCWn2UjqDc6pG45s55YkZtu33hXl_tVJkwwHX-fwLt8EZct-IXAuCbmO3s472ZsA2kkpyep5814qRuuVdfCkFrXHbf8oaA5vZWvmJMt0X4Mntk9fwzVbhlFLQ1_aW_qvfPsvtoLT2T4OpbAiOS3Ey7YwDgf0ryWMJMziV7Dh4Aw7K5_stInYVnWs7K4004KCrmLHLqtXp7EyR_EVqZusEmy0Y6VoWJGKaRSuXJpJWlAAUItKXFCAgSaEyUBntRSSHLTpJEcLvQUJtmBotO0R2IFBTSWG2KSazXFOfAI9RWeVbMNusNeHJrE4rqRqBOvOuKgtC4b609wQ2q8K_wDElnCGuLNDFLt9qPqPwIrucNm_IVzOJw9rASNwvIEM4ia2vBuqnkf1H7tmu-vG3s5Sbi4WC6iVJSotxhnPcef8U6tDn0rH2npLZ8O8B8J20DtfCzm1e4RV94y30oMSY8-SOPH6autKA2GKIamifV34C5GIcSb4_wCfBe3re707RrSEiFZ71YY_GkkYcsb4g5gD3wc9K7n3TiPF9cW7sov_RufP568lb54wfDtMO3tJOp2Hl-3vVNc8XS3Es0iyfSGSNsE7RqfQd69HwnBYXh7XjDMqmnXmfVY5ZZ8a7PiHX8cvcqa9vJ5YUup5_EfwhEg7DHXauk97YoWzjctA_X9JABo0TDLyT3km3KIyRnbIOKg8dnLM4dNP7UmocUbzSBI_eJ6_CufFE6Z2RgQpErrAptbbfm2d_wCL0HpWqaRsDTDCfMoTC86YIIAfA2I_OsNaaJJ-YCG3t488mTzvjr-VtxAyQxxc9XH12-CNkVsORzdZDCFefy97ttRg-6XTH4gv12CFHiRpJUTHX3ySegzuapijOIflHr6qSkHwZF8Y8zQw7Io--fP91tJY_vkfht0A_uP_40ygbccxur5sK2OVVO5HbamIr73OK0HIfJCYnnM5CKoRV6Lnb0-dZsRinT50aNGwSTJXBHTfpv0rMUKS48G3VAPfm99u2w6Vsk_Aw4jO7tT9ck9kwoycFjj0rFVpKTYTTQXBMCFs7OnY_GsOP8SzjEXYkd4eyf_T9rXg8XJg5M7djuOoR6x72pXkzgsy3UqyDOxbmO9dJgMmHbJzAAPu35eayKAATjFQKErGD1OR1GKSFaQs9xbpMqgzRnG_RvMH813I3vxEAlb_bb5eoPmFJRLiFSPpUP4AhucEfwHyrnYiJpaJovZPwPRIhPWUgWN0bJQbkHuDs1XYGXKwsdsNfTY_RMJm4T-PKynDA9O23Y1jnhMDyz3JFNZYAOBgjow86osg6ICfueWWNbpV3bCyehHp61uxQEzBiG89D9_umeqSoM0XgMuJIxlPUdxUGHt4-zPtN1HlzH1HilaELIA0Mqnwm6n6FvMVCKRoHZSeyfgev0R4KXp94-l3JimI8KQ-9y_kwqLmuhfkcqpY848VtNMnhuYjpdzJ9VOQ0Mmdkk7EehrlcTikgeOI4YW5gpw_uZ_7AK7hXYKVkrTgpjQdsejv39qJNbTW0rwToEkQ4Za6UGIjxMbZoTbXbFYJYnwPMcgojdIAzV1qCIn4KVoR7Dv4PWmhDpQhDfG3X0oQo13fWdofrrhckZAHvH4BQTSmGudrSjnU57gn-BYMyEZEkrcq_h1rkYzjuAwJyyyAnoNT_v1pdLDcHxWK1Y0112HxXEP2itfRPoenzXMMk2nWs00kaN7sc0wxGD-hF5v6oV5DjfEf5TfExgIG9Hca6X86Lt4bCN4dDJI4gkChXWvouMBzb-ItvG7KVhaIb58D6tc06uJUQezgodK-C0XDbLDcW1tDGH9FCKvYkDYfoKzymrK1xCgGjkvoZw1p50rgDhywJGY9NiJGN856n1OM146Z2eV58V7fDs7OCMeCmRjNVjVWOU631OwtXHj3caBTgsTtzfw57t6DetDGlZJNlstOYFomBO-CMgg_gd60MFFYJNQtLzAYya06LCEpZBnYClaacDA9SKaiiLqO4_GlshJ8eMdWFFhOkk3MfY70WEUgLlf8fzotOkf0n6UUWhEZw22MfOiwlSGTjYE0IpNtMF-2CD2JpWmG9EoMNjnrQELOcUjMbH7fWqJVsw26wl59sisLl1o1Fx0-FRVyhcT-JHrfC-o6bJEJPHtpAqnuwGR-YFa8K_JI0ql-oIXzZ1C1FpqF9pKoVEMrBAewGSPyr2DddV4F4DXOZ0Q0mK0v_uxTU5jFaNdJBdSgZKQlgWPyXm_KpNovDXaWqzqwHoaXV_Zvbf4AHPtI1T2s6hYctjbXmdItyMKGjXw4Bv4AdijAIx1bHka9VwfDiac4kjut28_2XGxBMpPiuv3N7c3gzcSbDogOFHy716YuLt1U1gZsl24YwTkIuZOSMAeZNbMM0mCTLuaHxUwnZUSeM20JBkgOFGftef91pmjZOzsI92fHr4UJxoee2y7lCVVWLDfA71eYS-C5DRIAP7pTCiSToqGC1OFP2m7tWCTENjb2WH25nmUrTOCgUnILbjFYUIQxh5UjxnLAEdsd6thjMsgYOZS5pd3Izzs3KVX_K-oHl-FW4uQSTOcNth5DRBS2xHZr_ufGOT94H6tWEdnhQObj4B-6EqKFgotlyJJBzSsOw8qvjhcG9g32nauPQdEAJy4mtrY_ZBdBiNB0X81fO-HDEE6kbDkP4pqvkmmmkLyNzY36FcqaZ87s7zqoosAAE75x61UhOwRePKqtnl6sewFaMLF20oadufkgJM0vjyPLkbnAHQgVGeXtpDJ128uSCjhhedgQ-Au7N0x_rRDA6c0NB1QAlzXAAMVucKTkt0LfHyq2XEADssPoOvM-aZPRPawxj1u8mC-5LNIwBJwVLHI_Gq2v_IMkaNHNaa6gjUe9JQ3jVAJFbmRt1Pf90poxH32m2nb50JBYndtz9-dUoU3T9XSCblOSpDgE7ev-V08FMY4ZK5a_z3JhOSFYX6lIM204AcHsTUpXNhd2rdY3_jxTSPDFpOkg96KRtiOhU1nyDCTgnVh-R0RshNH8kTIx9-2yvqV7VZNGZIy0-1Hp5jkgi1DIz0zk9fhXNSpP2jr8jQyHMcw5Tnz_GteDka1xif_LvnyTCSyvbyMC3K8ZyMdzWdwdh5K5gpJdwnNy3Ea4SQfge4qzENBqVvsu-B5hHijh5bhBbyEBwPqyf7SfSnGe3aIXe0PZP08uaam6bqM1mxtZk5owcFGOGX8VQ1xYaKpkiD1sdP1TTOIbcW1xeJHdQgCOV9mK_wALA9fjXJOEn8fI_EYBudh1fHzH7Jn1C3tczGsEOLOV7dGv-jo7-FIu9K1C196S2ZkO4kjHMhHoRWvC8VwmNFRv_3Q6OHoVhxHD4ThtXt06jUe8KGdhliMYOcnGK6WR25WKxsos2p6fDhXu4y3QKp5j6VRJoWVIMc7YKO2p3M21lpsjZ6PJ7q1y8VxvA4P6rIPmfcLXQw_CcViPZafkPjSi3BnJ_wDmOqCMdfCg3OPlXJPHcVje7w-AuH5zu6P96rojhMGF1xUgB6DU_wA9FFN9ZWwAsrRSxyfElOTn8dqh_pGOx-vEJ6H5rNB7-Vgx2Gw37iRa9Xan3Ku4q4vXhnhHUuI9XIkjsUykZAAllYYjTbqObGfQGuyzC8P8JgHSwxNzXVmidR1Ou_RZ38yfFn4Z5IHLYLypxdf32oxRtqU7S3d5i5uGbq8krjc_LNfOYnOmmfM82Suljvw4GQDoD_yo922IrSJgCJpds-QOf-VY1VyH2W9SFtvZpYSanxLY2iqW5p4IuUDJJeRQMfLmrDinZYz9Lo4dpc73fNfRXVrdbRYdPj6zZ20MHX6FBn4zXjo-8C_qV7uYdm8MH9QB8FgOMfaLp_DVpOttyTSQgLJKcmONj0UBfekc_wAI_Gt8GFLtXaLLI-tVzDUOPuPZ2E1vBJorS5EMkyhbyWP6FFKkxqdsBVA3HxrpxxRbDVY3uc4XSyk3t29rOjzr67uN9UxEeTklnWX3h1yGUn4fWtTcNH0XMmlo6FTLj5r324WSKX1u2mBG5NqhPz6rH9VYcJGQsgmcDsnNO_bZ9qtlIv_yk0-6ifIcLCFYD6JSu4PxGPSqDhGDW1f54aRRatxpP_Y3E2oR8t89jATvHKkYAIOcBg3u59Pdz2YUfdWHYqQe3mum-zb293fFjJpmqPam-LcsTRn-u5IH2QfusRkhTncY3BBGeaDJqFawA7rr4GpQ3FvHcxy5jkHMpO36zWIurdPIdkr55QA4EmaM4R2TksahGRsc08yOzKTNq8FrE9xcyJFFGvM7uwAUeZJptJeaCBFa5pxh7fdL0NXGm25ZE2M8y-6T2CrkEnyHXzxWtmHJ3Ks7GhZWCl_a_v_S5EJ4UzEmFMlzcrbu2_2uXlOPhirThB_cqizoEzf7ALcGj2hEU_CRlJGcpfjA-fh0hgSfzKl0jWHZJtP25-F3YSXfAurrGu3PFOJF_NBR9wcDoVHt21srGX5r7wBnXEkZjj03U7TK455WXY_ADeqpMC-low8zAUqz5rHDOrtHJbXYlilPJ4ij_DeTY2H9VzpsI9mpXci7zczdlrbV0uEEsZypFY6IVhKtYIgI1LDYDJq1mhCqJ7wXzH5sNodA9qPE2jbKttfMYsdkJyv8V7WHvxNd4LwPEPwMZJH0PzWXi1SAOUu5eSCcqk2BnlBxlx6jGcVYWE7bhZGzsaaee6dD6vovRvsi161vNATht1ghvdHXw2ji2WeBjlLhP8g2fePZgc4yBXtOCYpk-FbG3Qt_n-rDPGYpC1y3XKwALYrsKlT_VlhsmnPXm5l-PQV18KRDhTIepI-QUuShK7DcnODnHfNcoOIOYbqNqbEoktEinZiZieVic4I6V1Yy2TDtilPtk0fI6JqJIsisYX2KncYrlvY6Mlr5Cikgghm6YG5xUUKRa_VNJO2CI1wM9MmtuD7Dzzf2jTzOiAo3Rs8pbsM77CsdWNEqVgYBG8TzMqxxRhQCd2Yjf6ld44YNcx0pprQAPEppm7ufAkMUACs27sRk5qjF4rsiY4dCdykVB3PvHJPeuUTeqSBOFAwPjSQjZ0KjlABBPMB-VNCfUm3tS3SSc4_-RW0fgYYnm_T0H-lCRCs08nIGPmWPYedZ4YTO7KPVCFxOHXwIto1752PmatnnBHZRez4_EpnomlGe2e9ZElM1U-Jczsoxy3E0ZHpzEirIvxMDC8chXwsI5KMk-DyugKH_S9s-Y9acUnZ2Dq07j-p2hLByDxEJeM9GHb8-VEkOQZ2m2nn6vRJSbEkJPLnCiPBI6E71qwOjJXu2qk9kLVo41MM2XWXGQD5k-dU4SYN_Ck9k76aAVJNtKIJLeQq6KvNE4xsMdDXR-6uEL8X-tGrT7P9qpbpjncPDeBch18N_Uisfb5mWYnqKd6fslaZurf-O5j9fcPvKcbkGs2Jh7GQhu248kJrlG2Bv13rP9JKVcYngjuV6r_j965HQ1uxH88TcQN9j9pnZN27Kea1diFk6Hyfsaqw5Drhfs798j5Eh0SeQxnDZBBIOO1ZnNc00dwglPkfSULkZkTHOB94fxfGtT74Acs7Qe2N_EdfNASIpDFIJMbbc465qiCYwSCQck7Uv-VeaZODa3cyQvhkCuQCM7jFUcS4RgsRKDPGHNdqDzo-Io6LTDjJ8L7ReQOnL3bJ99RuZZ5oriOKZlBZOdMk437SucfsnghNJHC-RlXVPNaeYK2Di8x9trXebQkPqMkNz8SwwRxuByssYyM9DUX7ZTAifs5pJHtO1vPPa6AS_1iYew1rfJoUOXUL1pvrriQ8jbrnA6_2rTg-EYHhsgdHCAWnerPxsrNNjsTPpI8102-VJidPBmZFUYHTHkf4A3rp4hnZyFnLl5HVY6SPMAZ6E-VVBFUuS_tCvqWo2Oj4KaZDJMpgudcu1UbCGEMvMfQAN-Nec42-R7hCzYAk_z6bq9oqMDmfkP3XG9XuHvNQhmlm8USyRyRnAH1YUkDbyJx8q8nC0RtIC6WKeZZGuJu6I8q-ie1Nmi1DSLdx0VnP8H69Sb_BKcliSNvn4l3j5lTh2PiD2mWYlDEWd4LkAdPqxtn0yc1xOLydnDY8l6Pg0InxAb8heyeIFvNUuLqO0nECPIQ0pG-M4JUd8Dz_7AA34_hy2NgtemmcZJXOHMqjsOBNCl5Lqe7lQQgqJRLyy77aYN1QnzXB9avGIcdbpUuJZoBZWj0Pg7giwilm0jhuaWSYFZLxY2d2z3Mr9z6NTbiHXepWWRr53kD1VRqHAXsSjmabV9C062mbdnudcWJs9c4Mw_StjZsS7Zp9xXNkkwzPblb_1Xt7PPYVdQumn3WinmyDjiKBjvjuZsjp-dSc_GDdjvcUmYnBf7wArD-hZHWv2c-ArsNd8Oi5ZCeZhY6ok5B9CpY1llxWKZqW6eK6OHOCxHdDh6ELn6ufs-xM0jaXxPex3HKfqtQhDh8DADEYP8g9ahFxgtNPbS1ScLa72HH11tOcO-yPi3SJIJNDvrSzuSUEi-MRFzAggjIwQCMg7MD33xW4cUglaQVz34Oli1b-r0npN3qAsY11Bh9IbLSAYA5j1O22_U-tcx8gc7RamxZRqrS3lZmHvZpA6pObSsllKJVlqmrWV4_Gq6ho30PSmiMzSr7iyciKvd-hyQM4HmR5VdDiGRE5lY2Mn2QuNT6zPiN0M0-qWEVwWzzc0k2Ac53wvocVN3EoxsFfHh3nU7qmn7Z91PUiA_Hqw8_WNNNAGfTDZPfrQzioGzPiq5cE5-7q9FJtP2ONVvU5o-MZyT0b51HGO_V81obxMu2j6P_LE_BMbu_8KXcfsTMY1N1xwVOdxJAYxjzHXerBjz6ZtLO7CsPMqruP2OtWgGLLiuxl2wS1wpU4PkwUj9H91L7UIym3C11RWP_PvHXD3iXFndRQDHKRb38lSQeWBvj0b4TWeTFxONFdHDMMZ7pXS_Z_d6jbldJ1dPDuQpVxvgso2Iz2I_MH8DnTNF23ZbXg1a6NGFMBU-WKgFnvVfN_5rOzi0z5oDXLVFUiVbWYsCc_WQI248wSa9ngDmwzSvEfaH4PiJHUNPvaFyMQxXFpdKzhJIkWVAT7iFXwfh7rZ-VaiacCuOGiRjwdwL6OvwK6FovDPGfDNppXFWn3bWwMKXFjecpktWRxloncZCg7grIFGe_lrEeJ4e4YqMaHp9fqrY2txDBE51Eeze3ku2cG-0rR-I2j0-8lTTdYwA9nKxXxD9xFvtg9cZJHm3WvU4DisOOaKNOPLl6LNJE6F2VwW7u0aK2ggYFSQXYEEfCvR4m4sPHCfElQKikZIIByTsNhtXP3STt0vJHDFnOI8nHqa2Yr2Imf4fmU06tzDMFjvFYrjCyKfeX0PnU2YmOYBmJ5bO5oSfoXMMw3MLr2zttR9xzaxPBHmikt4IIbZYZ5wOY8x5ep8sVqdBFDCIpX1Zs1z-eiEmCeHxVhtIPeJ3ZhviowzxdoGYdu_MoR3E_vNORlYyUiB-83c_Kpz8jvGbk3Rvi7mfRBVeSd2bJJPXzrkWSbKijUEg4XPuk75qLpCWlupHiTuI0B-Z-FaYoAW9pKab4UJxYI7gAwM597lIbHMo_3mrW4ZmIowE70b9ITc5a4nMcH2QAqD0FRnJxM-WPYaDyCEc0iwp9FhOcEeI34R8vhTnkbC3sI_U-Keyj8yQCcZ86xJJarnGdy32cb0AEmghWF6sU95qCQHlaSZ34M9iGPQ1rwbY5sLkiNEtBo9R0KAq0DBxvkdRisexpCcimkhbmiYqT19flU45nwnMw0hPyXM06iM55OpCoAD6FWS4uSVuU7eApCbXwiMgMQrZ5cbmsyakoTcw-EhIeMZA_iXuPlW9jziouzvvN-I6enJFpMAEsMseSCPrF8xjrVMZzxPj9jvD00PwSS4Y_pFqYFb38xzJkdR3FWR_wC6h7I-03UeXMJhRnIPL9Ltv1rBaLT1mAzPasdpVPyI6VuwRDy6B2zh8Uwo_I6kAqQd8H8ViIINHdRUiRRMqzsQSp5ZN-p7H91pnPbsE3MaO-h9U90hJGSQuuFIOcZ6Dy_pWdj3RvDm7hCVMisBcRjCP5r6Q-VXTMBAlj5k_A8x5dEFOxA3Ns1sftx-9FnqfMVdD7uYXQ_mbqPqE90JJDHcxXGRuqsfPpgipSSdniGTdQD5ClaF_Hyoh68hMfy6j4jTxsdNaelj03HwQUzLmWMXCglkwsm_Udj7AL4qok_Hj_XmND6v46JoOeeOKbkDcuYmBz4j6H-US_iQsk6aH02-CE0VUluVeVSR3yfhWZG6rOJNKuZYbXiHT_Br640b-RBPZqR4moabcJyXVsn7AO8GA8fmVIG7YrNi8K8xHFR8u6R1B36itidpXT6FeUL-2tbS9aztL9b23sZCLa5ClfHtmPuvyndWBxzKd1JI7V8_laI5CBsVsiOZoB3b4ilXtyLrWbFicBIzlvkBUAKYQrpH9pm-RXq35hiyaTWdf8ilUctpatyH6ZiK83x93daxew-y7Le6U8gSvSOoXT8W1iDPJMcBVGSxJ6DzNcV5rQL0TIr1Kztvqd7q8slrwzNAkULmOfV5YVniV1OGjtYz_s7qdmkbMSkEKJCDjUyKPDU6cW47Nuv7AHdPJciXES48luEOWMaF-91uGDbTm46cgClr_MdA1i58fiW81riCYnmL-tqs0qL94iQpGq-gUAVpbxLEezFTB_xAH-n8rCeCYO88wMh_9OJ-Gg-CePB_so0V1gsOD5CurknlWG00uKeTPqeU4-JNVuxWJOjpT_z5CtMfDcJGLELGj7qP0V0PZGdStTff7BvSBAuGDXllbRc36XKb7I1QMbOx1B7vef1V_wDp-DkaSWM_5rf0VLc-ySIQvdwex7RDAP4At7CYQOp9HiA5T461fesY3Uvd67fX8rP7AKfwmXTK0eTa-VLJaldzcO3SaXDc6rpkscY_9PXZDe28hycMJR7-jtkZHz2ql8na2cQwEdW0CP1W1vCnQRiTh8pArYnM0-_UJ_T5d_ecr-bLYtp-twwm5SzeUSR3MQ6yW8o2lj4x9pOp86qkwjWt7aF2aM6XzaehSwXEHSS_c8W3JLuOjh_xPNbbhXWl1m0GMiaPZlY5YfGqWgg5TutEzcuq2-laXPcuuAQO571pjYTqufLIGhT5UtGsYTI4PKB1PX91N7cotUxuzmgsHqupEyqOV3klYJFEgyzMTgADzzWV1ldWNlCysjecWG6uJrXh7TF1D-K7RT353OYNPjkU4ZVKgy3BU7HkCrnbnNXnDRwAHEvq9Q0b6pOg9dVgZjMRjHFuBYKGmd3s2OgGrvMaeKi2HF72kudX5o8lsT5qDRNLS2VfQSyB5D4earmzRs_pQjzJs_ClN3BsbPriJyfBoDR9SrBOMOBLxuSfjXil2PUy6vdL7wChxipOxso6D0H-Kv4A-nYx7QJ_5RVtpsOh6kS3D7tF4nSU9rfie6Lf6CVmU_4AhNROMk_MAfNo_QfNUv8LFHye0-DnD-kJ660nji2tp3sfaLNezrymGHXNFtLuM-YaSFYZht39j46RxMTq7SEV1aSD4bCiMBPGCYcS7NyDgHD10BWbn8x4k0Jy3GnA7JbKMvqvDcr3lugH3pbWXE8Y8yrPgdjU_uuFxP4A40mV3R-nuI0UDjsfgv4AzIc7P_o9a82nVXVrfabrUVnrui31pfx_at7m3bmjlA6gHAII7qQDvWV7JIHdjKKIXWw-JhxsPaQOzNK2iEGMMvQjNRCqXzn7AGw3SP4AaC1a48QkLHZhjjpiIAj4AK9lw2_uzQvGfaim8Sv7AIs-S5EDEiG4jYHw5Mcw6NExKnP8itbv_SuM0tFvHL9HRdt9i-m67qvDksGke0XVtLk06c28toII54lR_fjdQ52VgW2x1B869JwlsuIgtkxFcqsfH1VD29kSytlsZ_Y9bcRXMY4n15b9E95mh0e0tZMdT5YgyPwzXSg4HHiJxnd4mmhvyTMrngNdqAtlZcPadw7GtjprXzxIowbu-luSM-XiMeX8DFdrEwx4d4jiugBuSfmflSrOilMpXOQuWG2B0rNugFP3vKJgoOeRVXHwFbMaamydAB8E1HDYwTknqDmsloQiiMzhQCSTjr-1OOLtpBGOaQSrxhJO3J0TCj8CrsXKJJTl2Gg9EHdP2KBGRiMSTZx6KO9b6HtETmk-06_QJqLcSB5Aq_YT3R_U1gxMoe-m-yNB9T-oKa9c_CqLUU5FGGDTyjEaHB_mPYfCtEEbTcsnsj99Ewm5Z5JGLP4AIY6DyFQklMzrPoOiRUqyXlhY8-PGHIvxwa6GBbTHO2LraPddoTbN9EhBUBZ5V6D_g_1qkH_pFp7bvgP3RsotYtkIZz2JoRSnwRfRyoIBnk-yvZR5n1rpQw_dy07vdsOnipUhO6xatM_NkLcSZxt1Y1y8BJ2XZO5UPdSiNEJArzPBPzBkbCygb87Z8_jW2UtfIYptwdHfK-vmmmXhaEYfGexG4YVkkY6A08eXT0SRtGFbCthM4G3Wq0I1BwHHusp2I_v4qRQnIY5mIeNTzq2Q65zk1ZF2gcHRg2OidKcYWV1vPBlBPuyR4OQe5Hpg10XRPa9uJDSP_h86TUWZRa3LtEWVkbKeXL9Vz3h2EmOXkdPL7CjshdopxcRbJNv7AJW7irMUxukzPZd8D0TTCMUcSADKkNjH81RG8xvDxyKSfvl5Z8q3uMA6emTWjHNqbMNna-9MpEEipJh8-G45G-Hn4qqw8gY4h3su0P--iAkTQvE5R13G2R3quRhicWHkgo4ZRExDjMb_OP-_KpwyiMkO1ad_19EJRSS1lEiMTykMrY2Ydqm4Pwsgcw7ag9f9zRsnr8I0UM0f2d-nbPatePDXRMkZtr4dUJTkT2BfByFBPxXb5Km74fCl3h8v2-iNwoUTvE5YDOxyD0I7iubHIYXZhr5fBCkRwgh442-rnXmjJ7MNwD6lbWxiixurXix5jXX9JhRBkjIzt1rn_bqKnKETT0YM2ZJMb5cDsPLpsa3f08ET7cU70XKPaT_IjxhJr7GXCOnLDNwzafT6IrhDiGbxCFWHHT-Tyc0hxjKglt9z4249icLFi2wM9p2_mu3gsBLPE_EN0yiz8jp58_IeS4C1nL6-l09cSTFDCgJwCS2M5rCAXCgscrhE-3cgvan_JjaDwrwDr91ninh3Tru4vI7dIrnWLeKRkC8xOHYHGTjPoa81xjB4maduRpIrkF7P_PcW4fDhZC-UN1A108VsOM-IbXWZ7HhDh_iGyuLziGdreWfTb6Oc2tii89w_NEx5SyDw13B98n_tZMNhJMOH8rEM0YNAebjoPdv-LZxDH8fiXZ8NwcgJlNOLTswC3eRI09VrIJ7LRbOOKOJLe1gjWKKFAAERRhUUDyAAFc4F8rySbJ3K7_oBlDIxQFAAbADl6LCcYce6PbWjavxnxTDw7w0j4ithnlu3H3Io096Vvh7o7kDet-Hw0uId2WGGvM9FmxmLw3C4e1lP-nyHNXkM3tmf2WcR-0bgfgC59m3B2iaPcaj6-dctBc8Q6kkag81vY8ypCmMtzSNjlUsGOwPbwvDMPA6nHM5eKxnG8Ri7Mbcrep3536V5H5oHFPDpgkuuMIPa1xHqd2CsWpahxJbWVqJgeZXNtHBMcdcL8wOOh2rqRlh0YAuHOJw0SSvcR15WNuengslwWnHHDetDj7R21nTtMt7hrwW66tPb3U1r8gzGJYwrMzBuUOFALDOMbUOfBL6HofkoQwYuK5tQRruQT5V9DfbdwJxD_JtIiuPaXd3HF_AEsqQx8StaeFrHD_yD3P3gkShJouimeMKQR7yHIJ4s2BbJbsPo4cuS9Zw7jMuDNym28-o8T1HjoR0q1h4OFPoyHQr2Z5IoCt5pupQOC8Dkc0c8Mg2zgggj3WBHUEiuAMQ_DydozfmOo6Hz569LisLBj2U4b-g82nk4Hkfn8hDgjiaWDilILyBI9Thufomp2kEZCNL1WaNR0jmT3wPusHHbNa8Zhw3JiIdWu2-oPksmExrsXHJh8Samj0d0PRw8xv8r0necRcH4M22dX8n0fTFRBI6z3i-IFONyi8z5xn3dq1xYaSQAxtXnp8fh43ESPGii6nrPDWv-QZdF4n0TU-ZOeJbPUoJWm_yKG5n7AOkGoTYaSiC3ZaMHjIHyNp419PmvOet63d69xNecOwTSwR2i41WeNuV4Ufb-NGeqO4yrt1VOcDBbNVPy4CATPFud7I-p8uS6UhPE8QcBEajZXaEbm_yjz9nerHNM63a6jJaq8UttofDunRc11f3DLb2lrCuAFDE9eyqu5Pl1rDhYXYp5ce84-8-K7EuJh4dFQFAejR4fsstfcVabpXCKcX4AcGadqGiNqCaUvGvG9ybLSmupA5H0aA_WXAUI5bkDYC7ivRYbhF_1nV4BeTx_2ncQXYfUddh6Dcqr1b5pD2ncAWMM2l_tA_s_-000kMP_n03hu6kVVYkMWkawRAige8TJncYBrW_g2FkHP37ra4X7ANRY2J15h8R8ik8AftdcOe1PX5I4T8z7AGf5Ke71icWsV3w0SLhrhugS1J5mJOwVZOYkgKCcKck3BwGns3WR1XTw32nlzgGw0-N_A8l2mXX24asbHifh_W38h4OvvdSV2Mklsc4K8xAfYggq4DKQVYAiuBPA-E23cbj5F7DDPixzMzKB8Ofori71rTb-CHUNKnEizLzAqcYPr-1le8O1Csiie00Vgb82_A-spxJZBbbR9UuEg1iBfdjildsRXijohDHlfGAVfPUV0cPIeIxHCv1e0Ww-W7fquNjYBwecY-IVG4gSDz2f9g76BXYbF2FlGjrysq4I8j9VlYRQK6EkdONL9tftTym89tnGU3OD8OrfRwQcn3YUBHyxXt8EMsDB4LwH2k73EJvA17gFyzTpA0jWEj8SZWjBOwydx-YFaZBpY5LjQEZuzOx0_nquo-w_iq30DWP6dnMMM6iw1DnbCxjP1E59FbKMeyvmt_CcYMHiaee4_wCauLO1hzD2m6Hy5fzovUFtGRG7N9qVhEvmPM4r-Jh25Ii7m4ho8tyswTd5l7mVlHRsEDqMVVjXZsQ_zSKbiUvKitg8zDHrvVELc0jR1IQEu7wbqRlbqx6Hp_vFW4lxfO9w6ovVR-jA4UYPc7VnSsKTaAIJbg4PhDY_zHat-EaY801eyNPM6JghMwRCeTMh90e87elUYeLtn07Ro1PkhS7bErvcvsWVlRfJQN_-V0sK7tnOmO5BA8AAmFWkbDGw64rjA6JI1id2VE3LHGasYwyODG7lJHcsCRHHnw4vdHqe5q7ESNJEbPZb4TzKE2qeJKscYILMAuaqa0yODG7lCnpEGuF5SPCtvcX1bG9dqJgdOAD3WaDxPNCrpHaR2kc5ZjnNcZ8jpHF7tyhJG5GM7nAqOyFLCJZKJJgGmb_C-XqfWt7WNwYzv1fyHTxUtktWkiR7q45vGlPIudm9Sc0mPLInYgm3P0H1KPNDVl8PVb2M7BbiRcHr5o1zsnZjJ0H0UUqctMkBIXMyhS3ckbda1YrvtZL1FHzGiZSlcRxmOeFuTmAA_g8yD7SoRzBrezkFt-XkfokEJYZGxgNIz4pRwM5GO_lSfA4EBneB2P-9E6vZL4O3tyVkY3Ep6KDhR8fM1cWw4X6p3n5OQRVJRuLg-4x8Jc8vKBygfhvVT4dM4UDQ6DT9JWkx3U8biTnZyMAqTkYquLFSwvzgk-pRalXEkM0STmPni6FgPfQ_GuliJYpWCUttvMjcFPdNLEADEx5oJfsv7C3bPlVUcQa3sibjfseh5X0QohjaKQxybGPqPOudIwxvLHbhJSLgFrS3lTqoK5_34K2Yj4TDRydLH49yZ2UQnYAAb7iawbpWpDESwiX_4WEbvlexrXIe2iD6bdD9cj-bJ7phj_oywZjvjyrLSSci5ZYvozvg5zGfI-VaoHCZvYPP7U9D08ihO2i-JFNbPlSCGHN1VvOteEZ2kb4PJ_gqSVp4wZbeQDO-R-RqXDzTnwP7AJ1R4KEVKMYyTts1cpwLCWnkkU5b3Bt5OYxh0B5yvqO_xq_D8g4c2Rben1QE_LaQu_jeOio_v8Oxwf1rVLhWOd2ucBrtdd_RBUbVtSjsbC5v8UDJp9rJMobZSQMAnyGSCfhXN4vjWxYdzoh3WNNeOivw8YmlazqVo9N4g0_g_wBnOjez_TpRezakk15qswQEajd3SAy82fPm5R5BFANfB4sQ6eZ-Ml9pztPADQe_UlfX28Ojw-Gjgj2rU9XEa_p6LwjxPb3Gha1BcQtcJ4AeESAcp5o5GTO-eyjIPfIr2cLswB5r9Jj8TE_Ly1HuJC9n6zD2A6fxl7HeE-JNW4l1S1vdbtX1GaKKONolDOyLyg4xlVB-JrgT4blhxT2tYCG6L0uC-y2HxWAie6RzS6yQK8h8FqvZv_ELD2de1fShJqsuppq2mak0Hi2wiKCMRZB5SQxwx3GKMXj3cSwLg5tUR9f0U8BwhnBOKMyvzZmvrSqql2PVvZdZasytcm4FupyYlcqW7YyN8VxGx5Scq9c7Guy5QsHrnsE9kWp6tLqOvcINqWoeEsCTXV_cv5HRRjESc_Kg6nZepJ6mtLcfPAMjTQXOl4dBjX5rMMx8SdPILpWg6_xXo-gHhuPi68vbEwm2EWqILwGAry-ExkJLLykjfscHOKsHEnA5qCofwqFptoXM_wD8IcOxuiDWjHaDCvaCANCVByo5XZgMdj1Haq5MW2Qd5vxIV0GGMDrj2O45H029aU1PZn_PbK5F9cEzzxMHheaZmMJXHKUBJUFSAQcbGoNxpiblaArXYMzPL3C7S9b8VT2mw3XDN1xNxnrdvekfSIW1-48PIOQWwdsHBqUeOxAP8ahiOH8drblFDw0V1wx7FNI9nfCsfDFtdai2n28xls1vLsXD2asBmCJ-UHwubLBWzylmwcHFVYphnkMstAnpz4U8FK3CxCCCy0HS9SB08ufguNftAcOx6FxBod9pTzKvEUH_nukSbkMrJKrxbgE_exkA9cV2-DPDcPIw65TY9V5f_RRufi43g5e0GU-NGxa22m_sX4EXESTcTazfeOygvDpqiFFbqcPKZHP9VYeNTEdxvv4A2VbfsvAD6I8ny0HxsqFx1-x_Nc6UV4D83uDLFEsKWWsxo0bAMCCk0agxtsNypzjGR1pQcSY11ys9R-n_qeL8HLLGGQSXpVOA28wNFO_Z38LGq8K6gvEuny_vSy16e2uwZwxuXRE3MnUk-ZO_XvWPjcTJ5mubtl0-K37ZqeTCQSRyjUPN-4c1V8fex3QfaF7S9N13jHXbptK0GWM2vCFxZhbAlOvikSAy8xzzNsSp5cgVjwnETgo-xYzXmV1-JcMHFJBK53cA0aNvMnmfMLp3tdbVPa__Mj_Nr5dC0u3trm1vNLutPs3ibT9rckxGNOZkAGSpAAyrMMb1pj8sGE2N1hPBIyKzHw8CvPPFH_KB4vmtZdfutHn6gqAjWk8tq8q53U_VMB575AMZ71W3ibof-biL4LHutXv8RDiGNbiADl2rQ-RpbD2V-wrQOBvaJwtxvrekW8dlwdMt7Y6Zo7IWuLqMN4LTXEvK7BHYvy_eONwKuj8mGMLXEuJ5n-Dks2J4MJy3sgGAcgOnU7rSe2jWNXueOl409lXBV_Lp-uxvHxhw_qlzb2tvevtyXtu4ZhHc9eZtg_KOYMSac2Lwk7KmNO5FTw3D6IYGUTYej1bdA-Nnmue8Fj2ow3l3bXHsw1sWBfnhWOSCSRAev2X9H5ShyevKDkVysRhY5adFIL97gft7l6FmOeHHtoi0eYJ-dH09y7BqPsrl4g4M1Kz1iIqdQ06eOS2IyyExnHQ4yDg465FGCifBOyXmCFm4lNHisNLh6trmkfBcS4b5rn_QKaDpkljwLa6rZm1iSKd-GnPiKg5ObxIpV8TPL5ojNeomwXDe0Ic6nX7dz4l4LCcR466Fjo2FzaFHJe2m_NebfaZr6hanxHr2o8TadZwXmpalc3V1BFAwaK4cnmA5iXTGOhbauvE2OONrR0XCx82KxWJkmm0cXGxtr9LjvhSyyeJBbuyIvM22cL9sR07VGwkA460rXRP3pNqyGzuLOC9jPIRdSLGJAdirF_dIxscnpUREJKZ12WlksjJM49ob6IXp_ge99p-kxWelatwTHeabCF8CW31aCRolxkcsnMQ6AdATlcYyR09twuTimGfHh8TGHtbqCC2631N1QRIIpDmj08P0K6FZ2eo37wDzJsJ7Ukk4uOUEb6hIPyNZMb5o-HwPcXPt17N1-WnxWiDhWLxGrW0Op0_f8KallZ203iT34fMpDeHGPKsUXHuIzPD4DhDQ5v0H0-ZWv7T4JB_9M4vo3X5fkmXm0iNuZbKaYtvlzgZPzqDsLx_FnNNM2O__R-31S7fhcPsRl3n7AJ-iIanbp_haVbqOm-M_pQPs_iX7ANXGPPv7AFR_q0LfYw7f96Iv3ujDkbTrYqTkqP4A2qQ-z0zAQzGSD1P-pf-vGdHQM_nonEnsJ0ZH0yeISHJaEE5_CpjC8ewjS3D8gPadw4b146_NL_1wybSSLL8g_wA-SdTTUlkV7OZmCxlBG6FWG3XpWqD_TT8CVo4thixoFZm6t15-p8UDhsOJ1wUwd4HQ_wA9FSzQTW0nhzxMjfwn6ldPC4qHGx9ph3hw8OXn09aXMmgkw7skrSClJ9RbmU_bkJWP0Hc_0rpRnsIjJ-Y6Dy5lUqKdts4rMfBMKVaIQWnAyy-5GP9jXQwTCLmG-zf6x_ROksMqzCGNvq4Uck-bYOT6dXB7Wy9m3ZgJ8zWpSKj2dnLfzrbQvCjsDjxZRGDgdMnauNLL2LMxa53g0En3BNrS80K9VouHLGzh8Scx2dxdJDzGC599WJOyIAQQx5Tv1GO4NZeMYh3DssYLg8uoOYay1uToQascuo5LVhmt1OhIGxUG9uY9VkltdIjSCwtn6kjmIWVskLzknduowM7DHlscOwspmYca8umk7pOuUAAnyrc3W5UZXh9iMU0a-KK5vpNT1NdREYu5uU-Ok1vmMAADm679G5O2DWz_q2T7AGEJyRN0YQ6jVknlprYA1sKBkL3dpuedjRNayA-qzzt9iR2kOB1JJ_tWnEC8jx-ZrT4FQUiFEazmjYZ5QGXzXfenH6Jh3t5t1-h-iN0yhQOOfmIB-7WRCmoRZxpGgLSyHJXOMf_HnXTZJ9wYGbuduOn_p7JM2nToPFSEshGTke8PiKzz8KVneYCW76I80EJgdcY6dj3rDaSIe6wIOCR1z90rQpVlMRHLCIwxI5hnO-Oo_Ct-BlLc7N9LrrW49QmEEXwP6Yt2D27ZyrDOPQ-tSB7AdrBrGdwfkfoUJdzFHdW4uIAWZBvvvirsXG3ExCaLUjfyTq02dtOBwPek2B6GqDpgqPM6I5KIoDHA-AzWC1EpcDGGTMm6sOVh5g__zV8EgjfbvZOh8j7LTvRFLD8LlH-g79GNuxqMsZieWHkkmie2AcmoVaAp1sPpPM2eWQxlW_m8m-Oa62Hd96zf30QfHofOwpboCTxDFeKMFSEmA9ds0Nk7QtxY3Gjv1S8VHvk5bqQDueb4v7esuOZ2eId4oKaG3vAAdAe_nWNIqTEfGtSgUFozzBR3XuD4961t_GgLebTY8ufx1TCouMYvH8I4lt1G82j3AGDuNh-FcXiZ_wBo8joVfhhmkDeqtOE9NXV-PdFhCstvp8EU7jy5VwP-fhXwuMkjLzJX3HEABsdbAX_tl5n5sTQ3F_NHEAw-l6hKMfwmYlT6TV7aE1qvlnFmh1tHV3zX0i9n2hrpnAfBmhIvKlhw7plvjGN_o6sx_Fia8aT2sj3nm4n8r3EVQwxxj4rGj8KXxxNFp3FPAOvRIscNnq82lTN0xHe2zRqT7wDxUj7Gunhe9HJEOl-4_pa4vELjnw-IPJ5B_wDUK-dLoFu7MNwc981kBK6LmgaKPqWi2OqLy3MStjoSoJHwOMip7jVQa4sOizmo-zW1uRz2eoTQtuD5Yw_qapdh436C0sxrmaEWqiT2P3tzvJxXPCpJyETnYD8nb4qj50iG5JV_6pVswK00v2O8KWRWXU5L7VpBjIurgrGT7kTA_HNTbh427BUScQnfoCG-QWtQ6Vw_ZeHa21vZ26DAjhjCL-DAq0uDAsOR0rrOpWP1nWbjVLjnc8sabIgPT69Y5JC80uhFA2MLgvtCvoOMfb1wVwNAVkj0OVbu-I3KzSMG5Ce3LGgJ9Sa9Dw9hw2Bknd-bb6ea8bxaVuO4vDhI9Qw6-d2fcAvVqyh0DgAA9vKuQNl6UjVJml5YjgZztUrrVAGq4B7MuIJeEvbXxv_NtWfkTW7ga3o7k48VkiUSxjtkx4cf7aaupi4xNhGYhn9dD-6_Mrg4eX_txabCv2kpzfOvqF2HWOFuEuN41i1_TFe5Qe5cQyNDMnqHXH95rl5myCni13o5psMbjOnRZef2L3dnn7h_jS9VB9mK9XxMenOuCfwql2GYdtFsZxMH6oz3Ktl4D5pNnIRHc2l0nZo7rB_BwtUnDOGxV4xmFduKSLfg_wBpt1OIpoYLaLO8kl0pOP4AKoqH3d53cUzisKwd3X0Wx0f2fQ27xvqM4ndCGPu8xYj1OwHoBn6atEeGYwa6rFLjHv0ZoFqvoNvEnJb26Ig7KAAauyhuyzBxJsqr1u7XTtJ1C-l2W1s7iZvgkTN_SkwW8N6kImcGROceQJ-BXPPZvYm09m3AE9zGAF0WzeYYwfeAc5x6PmjiozYqQjr6ys-zQy4SBr6QaffV_NfMf2xzGTjviJ2AJudcv_gPv_wa4kA_SvX8fSNvkvA8Zfmxcp6vcfiV0v4AZct7HTf3lxfNHG5tpEsL1JFDxzafMnLPE6HYhgB6gkEVzeJYkxSNi66rv7Znh7cRDJiG-02hXVpGo9V0HQOEuA9X8h4h4a13hnRJYdHnRrCQ26u01mSyLyuAGI-rHc4zjO1dnh2Of2Ighw4mfZonQAab1r4Vhx3D8G4pxndkaAPM7jT3LR6LwfwHwpM0_CnC0NtIGLCR5ZZAp_lR3IH8V0IPs_icRZx0uUH4jLr37wCfNZHY_C4U_wCzi16u3536PJWst9c3ORNK7AnGCSB-A2rv8TheCwI_AjA8dz_51gnxuIxP5V5PwHwTsGmX14w-pdVBxzPsB_euiATqsTpGN3Vhb4ODBN1dntjwx_f09KmIgN1QcQeQU2PSNPhHI0AkHXLnmqeRoVZleeakpDDFkRRIg_lUCpUBsoFx5pXMf8ifShJDLA5DMD2welPcUdk-dqYuoW9zH5E1yzivbc9GkUGRPUHrXAxXAwH7AHjh57OToNj6npS7GH8s4t7HGDOzrzCrNe4PtPCW-0qY_RCAqMPeCejA7jf460YLi_32T_tim5JWiq_Ka6fwp4zDGBongOeM8-Y81krzTbuzb-6McoGzLup_1rqFh2WNj2v2T4nLZWyjI5lBUf9j1PyrrvP3OEVuNv4Asdz-bKy03Z2SyWFxdfTIVkDLEsTZ5mB3LA9MDFc7CCWSR0TWGi3V_wCUajQ89RfuUgBlLr16KTHdQ2liraXDbPcxRzRXDcrO7REbyFSOVQAcBgc9D-1kndHBI6EOdRLCHWGgEH2QQbIdVlWA022DXXxv7CfutNlvYrQagk0N59EEFtAV5prmRTkMRtyx8jAA9dvSuLFjBDJKIaLC-3nYMB08bcXAmttVJ0ecDNvVDxP-UmYl0-5s55EiFpfXDRpbRRgmMx4w7HOSpPXPT0PbvRNxmAYxrDnEmayfaAvSvDltsFDuPaTVHl0Uq6K2PD1jp-nT3bPqQd7uKFwVflOPsgc2-B8ga5ry3_5MyRraiIAcQbs0aBJy03XlzVp7sTWsJ724UbVYgJxzt_hyyo5G2PeNduRhGDiLt290-5ZOSi27Kkwy45JgY3bfBz7v4qrwjg2YNOztD9FMI7SBhckyKSkO5yPLoKnhYbmIfs2_eEgNUqNpJtQV5s5ZhkdMDsKhHL54xXaO5m0wpEt3JBdO642bkIB6-pq2fFSQYp5vTokTqnJIoroLJHyqxGVY9H5D9GrZoo8UMzdCdj18-hTq1XsjoxRvdIOMGuQ5pYcrhRSRxSeFMsvOfcbBHmKnFJ2Ugf0Qls8llOyxkYzkeRU78q5znYSc5P8ChPQXdvGWlFu6k7MqN7p-RrRBjIYjnyEXuAdPcVIFJvxzCJ4SDAV9wDbl9D-0scS5rHM9jlXLzQVEOM4wcjrXP4UrS4kjkljS4l8GN3CtJjmCLnBOBuSPKoyF7WOcxuZwBofL37FDRmIvZSbm0L2MN9E5nRD8Up5CvI2Tyqc98YPz_8q4YgYhuVwp7PEHM3kRXQ20-ik6PK3NdqCRkFh2P6_hUSQoKS0lut4ZrRGiiEg5UduYhT13_08E-TDljpDbhuRp8FJxaXW3ZOMiWtwzqOaGTKuo7fGum9owcxcNWO0I_npSEWpKv1b4xIKYBHcjv6BNHEm05jwbBH4-aCFESMMTzPy7EjPn9Vy7UU7Cxt5RK2QU-0p7rjerYJTDIH5N_LmgaJcmnx3Ukulsw8K9je33-FZEKg5-J_Kq8fhhT8vykaeRV0LssgKX_POILeXhzWuObh8JYaQVZ8YPMEK_jmvg8eHcMb2Z5WvsIxjZcIJgdA0fJeWdb0u71bTbviKQMsDS_Q4VO7MXb33HbC82M-vSvTiQNIC8HNC6SN0x2GnvX1E011TT5NIIYrp9quR6RKv5K8fCbba9tiG1JXgPkoXF_DU3GPCmo6HbNy3ckYmsnzjkuomEkLZ7e-i_jW7By9jM2Tlz4ua53EcN96wz8RuRp5jUfFavhnWoOJOHdN4gt4zGNRtknZD1SQjEiH1VwykdsU5YuxkLOirwuI-9QNm6jXz9_FWoXOKSmTqhgjpQUWjGQaQQUzcziNSc9BQTSbRazGrSyTsWkJ22A8hWaQ2tcQDdlTGB2LBGKHs_KDynscHrjrioDSiVeSapcG9ins_8g0n21cS6nxJfxXs9gtxE08UhKzTTY5XIxgER526gsR6nv4SxzJMLHFFpm1roB-68XwLhMuHx8085vJpfUu5_638leq9LkM1orN1AGfj37SuSzUL0cvdOiXeyckZOdlUsfwpuNKLDZXmT228L-_qntE4M1rhG7Ftra3MaWsrEqqyo_OGZuyhOYEDJIyN66fDMXGyCWOXar6i4f2hwEs2Jw82HNOJyjzGo-Frv0c2WEkZA7jlGAPgD2ri3rovSObpqryx1AygJKfeHfzq9rllcytlYoVOzKDVgUKSyFHRRQmAknekpgIfChCxXtieS19mnEj25xPd2D-fAPOa5IgQfjLV-EYDiGk7A2fTX-LBxKQtwcgG5GUebqH1VjFosFpZWeiQIPCsoEtIgBgAJGEH7AKRWDEHP3zvd_FdvhjWxTMj9bfCl8hfadHPc8d6wUgldILqZiMZ__Rj7AFr2sRGQL9vxJrnYp-mxPzXVvZXZa5whp7cN8S6X679dZ0oX6ns6gLeRli55WBw7hGBK9VEYyK5PE2NnaHs1yr2H2Vc_BuOHmGUvbbfHn-muXgt_wiDPrGnyRBueTQ5i7AZzy3_qB-Z_Cux9inOdiJ28hX0Kxfa9rQ3DyDdwd8HuC6HbcP3UvKZ5fBTuMEt-H56-htYSvCOna3QK2ttMs7P3ooeZx1d9z7YVaGhqodK526lHcAgnPfPap2oIYycdKRKSIrvQi0WKYKEMChCPcHpuKaEl6khStM1FrCYrKPEtpfdljPQjz6NcvivDhxCMOj0lbq0-PTyXQ4fjjg30_VjtCPqnrjSFg1AgKJLZl8SI42YHoPl_QVt-z2MPEos8gpzNHDof33Rj4GMHPTDbTq0-Cpbzhe2vNQjihvEjjbmzE2eVMAknPyrdxfFRYOI4mRpfl2a3nZ37AGRhyJXiNxq-aq7WS7XTUnt0swsdxMmVTmYZTlUMCCGQtjGRgtt1Irm8SnjnaI5LaHhpA2aGtdZO4s66i9uS0szNbY5E_EJmS71bTruHxNNCjTIBAYbiPxEQNuS3nknIyT9DbauXHh8Li4nZJf-rs2Zpokjp5DQ0Ahz9I3Cx7IqiErR_pOp6laWtvEzS-E8MjXDc8cKEkloxt4YVSfPHWr3wQxxvdMQIwQ7u90uobE_mLzSIy6R4a3eq15eQ5UrWAI8svD1jNa3BjZ54p2tieeNQOaPAOwIB36OMbEy4piX8ZreJzhzHvAblDgOzBvK77te426hXRgOJhaQa1ut_BNW7tALrU9PvYrWyYLFm2tnaSMRfZ5lb3l5lYsWyemPSuJL34mGnYXvFu7zmgHPqacAR3S2qoaKQOW3sNDbQG9PDfUFQbmdbqO5fGGWUtJnoSGO_8GvdtkGK4ff9gG3-c_ULBuFWKVUtGzlcnqNxXMJsJKdcMq2xZftXDDJ-X6_xrp4t4bh-0G8lX-DVNRY3VLhZHzgMDsdxXMifkka7xSCkaiqLeyMnRsEbb8rTxFtYhx66plNwTiHnDjKt1Xt8flWeCXsjlOrT7L4wkpTKLoLG7DxCMxSn_--vrW-RjcSAxx739XdR49D8JqC6lMrIoBQ4x3P6lct7XMJa7QhJOSZeCKcDJT-tvl0_KtMp7SFsnTun02-CYCaJ5ffU4YnoBWXZBUizcsrQyxsYG2LBSQrdt-la8JiI2n_vKRld8CgXSdgg-jylJY2EnOGgmjBMgdd15Vzg5PLkn9Vz4fBJBJkJsVqDWUtO5J3BAvwU2V-_NSo4dT1K8jhje2Pjx8zJHjkj4QYkLc2OV25cnpjBIxXLkfhcJAXvDtDQJ0Jy6tAo6gXQ5HmrQJJXUK1-F9fNPy3lvayS2mpeNcC8iZnmDlklYjlimXmwcKBy75evWrcBGZJGyQlrWghpFUWtu3tdVjU06x0UnPDQWv1v1s8j5Ei305Xa94ahSX-aZ_EDtgh1jRiE5Q2AxJ82zkdMb1YmaTDyR46WsgGWtbtxAsEiyABZ20GyGxg5oRv5Bf4AOapp7R4Lo2croro_huS4KqT-jbG_WumydssQmjsgix19yzOaWuylTtQZUliR4BHHLAsbtGxKylSQZQT13HbauhFOHDPI4lsmpBAthrY1zArfVTfoQK_fxSVtGktDA-GeJjyHtjt-VdH_u-XDmN27Sa8gob-KAAFOJMg_ma4psaHdKkQHMOUY7nNIlFKbakMImDnxbZ1kQhTuoOSPlWxv65h7P4zbrxFbJtNEFV_s89nOp8ZcJ8acGw3Kafp0fEkyyzkBpJgr44hC_dTBDFvgB3r8txJ7cHxGWhbrNfFfSOGh2I4f2Q2J_g8k3r3B2gcS_S-HtFto49K0i1ntrOLkILiJCWnLD_TNLj02rG_FtacgOq7DuHtc3sQO7RHuF_Ol6V4Mvf3hwpoN8pz5J0u0l-ZiWuVGKLmjkT4yiY5gx_VrT4FtNMjEMQYH3zvnyrU3QLJIbOiqdHZOG-LLrhx-ZbPXml1XSifsLcAZu7UeRP4AjqO4aXH2NtrgZog8bt0PlyP09y5LXDCYkxH2ZNR4O_MPXcdStinKwDAjes602j9KEZkTLgbUJ5lDuoy4O1RcFJrqVFe25LEFelUEWtDHUs7xbxBYcE8M3vFGp4KWwWO3hz_11dOcQ26Dqzu3YdAGPRSatw-HdO_INOp6BU43HNwcRkdvsB1J2Hv3WY9jWiXdjptxfaxKsuqXkj3V8wOfr9W5mA9AMCoSzMxU5fH_IoDyAVmGwr4DhGxS-263O8z6my63packYGNt6kzZQl1KPVIybOZkOTy4ok9nRKI97Vce9q1lero1nrGlOU1HTbyK5tHPTxkPMit_K2Cp9GqmLEDCyNe_2To7yO604nBu4hA-GP2xTm_5m7e_Zb_Sb-x1vTLTXNKcG01CFZ4h5A9VPkVbKkdipFWyRGJ2Q_9VUM7cRGJBz9dPAq5so25hmhqi9XMYIX8VcFQnQS3TNCmDSMI3lTQXgJQTG5OKKVZfeyxnGkkur4X4KcGQIWgW4fiPVSd1S1tfdt0PrJcyIQP8YWPatUI7OF8p5jKPXf8Lm4lxnxMUDdh3nemw9SfgtHCoF_CDuFYk_gT7AErmzDuEei9BgbOIZ5r9lajw2use0XWi9qCt5qbqqnBAjD_9-XWvRZ8rAFyn8XtcU9xG5-C9ScNcO6LxB7GrOx17To7yPTEaaDnJ5opofsyRt1QjHbscdNq4jZXR4hwB0I1XahiaxgY8aN19RqCsBwFp1vp_E2pW1sOddI0fTrEsRkiWcy3cg-Xip-Ve5-wcLuzxGJd-Zwr00-i8b5u5AJ8NhW_kiBPm8l_1W5AOP03r3-8IjAHcn1oSQZdgRkjpuKE0kjfFCEMYoQhihCLlOc00IcootBRFcnHlTtCJkOMHb1p2laujOw4ftnH245HgQgfMflXH8dKcJxjFRt_OxrvW6_Vdid2fhkTzu1zm-m6gWwhgliVLqWJJIilw3ghihYEFQPvDGPxrXO2aeN-aMOIcCwXV1RBJ5HfRc1haxwpxFjXT8Kp4lS20zUrX53alzW8FuyqtzbhYxyjmEZC55ySxIJGx7_ZrHPh5S10eIhykkew-zmOjnWay6UKB1XQZK0kNY-6HMcvTdVeopf2wil1Fbfw7mKFEML4qPGpBUkJ0xnqwz9biqMJJh5szMPeZpcTYJOZ1g0Xb30BqqvdWSiRtOfsa8q9P4qzuJX0hrnXBaxOt6zW8DGV-acdOfdgxDDq3TIxtWzBxxNwLMO95D209wptN5tboC0ZSLrfVWucWOMlaHQb-_XVUcU99ol3NLLZAXcCqhcjaFtvtAbEkDBB23-VGfsuLMaTJbHknxd5XrQvkOSoaXwuutRp5K1sHk15riO9FrJ9PvYufwpcSR8o91FBDHkYDGV6Y32rEzCtwb2PjLg2GNx1GhF6kmx3rOgOuq0NeZruu8R_jnoqtPDa_uU5cJIXwpbJxzE16jhVH4M7FuqwhQ5IWgnMTDmx9k9cg9KzYiIwvLPd4plT_mNTAIQuWt1U_I9a38xjey7Ju7APjoU-SgYyCoBJ7Dl71yDqkpl2fFggucb8KN8R_s1vxZ7WGOb0PmP8UFRMnbv-Vzkk_DIAhjlOY2Izv_yt_EK0Qyho7OT2T4D1CFKeNpx4bkfSFGVbtItb3xnE_hv4A6g2PUefVPdMQx8_i2iqwLJtnuynNZMOC_PARqR8RqgdFHIQAkSZYjBUr02_vWQFK1KhCy2iWrTyJ4kzMsbyBIAAm7FuvN0-XxrFKckplyjQVYFu1OwHTqrGm25b3523zVpoxM1veiQwrc2tsXgmx7-NzKQwI22wM75DtmtWMf59ZFhzZcXc_Zc2iC1wIJ52NOW6th2ceYGnVQYoZWsrqzL3Ik8RbiQEAwtGpIMjH_RwT2yDn0rHK4Rzslc0aAtAOjw40ctbCwOfgoAW0tvx8K6qwtrznWJxJbgO0SW0MNouVPvN7jyYJAdVycke9XNnw9FwIdfeLiXmjsO81tiy0mttlc19jlWgFD-noULi5vLeGC-1KeUSXNwqTTpGrO6RHZlcHJy6_iAfQ9CSCDFMDsI0EMaWlpLgA5w0zNI07nzSc97QDIdSd-dDofNV-oWtpBePd6fK93aoY5ibkjJL9PK3dum5pYWWaSERYhuSQ2O6NBloWOQ8BzVcjWtdmYbbpujv9JpdMs_pFgsIh5jC-GUyQsxK4z1UEEA4rfhoY-ze-OQup1OFg0aFnTbNzUZHOLQHCunl-ylWc1tcabFGuFuYXKSfaJkQ5IbyGOldThUmIMj2PNs0rbu-FbnqnbSwVuPiq-5iEq-PGi8wPJIMfe6ZpYuPOO2bvs7zVZUYMCCHVW2IA6cvrtXPRaUuA-Y25MYYEnv4AGpNeY3Bzdwktn_GbojX6N9IQqpuPoOrouOpmhEbn7wAcbfjXyz_VsEfFWyVQcLX0X_MSZsK4H4rgfeP1WQu9Xi4F1S3uNUIWztzcWF_yjOA-G58dyG3x3-eVeUhaX8lzTva9eZBHAyY7A6-R3XW_2e9aj1v2ZaGxcM1jE9g37wDCcqv7AJStMjJiZGHwPwXJd3sLE8crb_jp8CutWb5jV4KyOSOIdCh4l0iTTXuZrSaN0ubO8gOJrO6jOY50P4Snt3BYHYmtMMron9xr8eHRYcTh2YiMsea5g9DyPooGkcWzWE0ejceSWVlee7FHq8Mo_d17J0wxbBtZjueR_cJ-yw6VY6JklugPpzCyMxE2HIjxYocncj99D8LY3EdzZIkl3byRxyDKSMPcceauPdYeoJqiwtze_qFHe-RRjAozDkpBh5qPJdod2YDJ2qJPVMUFj7aNxrp3BdrGl25Oo3SlraxSMy3cw80gHvH7ADNyoOrMBS7GSQW0UOp0A9evgkMVDG7I63O5Nbq4-nIdTyXLuEuGeJfazxDDxjx5ctBZaOzjStNjlEsVqz_O5Ye685AAZwMKByqABVkkzDEcLh3W0-06va8B0CjBhZm4gY7GtAePYZuGeJ_uceq7Vp2jWOmwLaWEAiiHUdST9knqaoaxrRQWuSZ0hzOKu4raWGEExkKRkE7ZH6zUwQFUWuOpSHHMpQ79pnUUk00bVFqXDFlqltJZ35ss0En2kb5Qexqh0LXjK4aLRHiHRuDmGiubXVpxD_I9Vl1TTbCfWeD_6QNqNjAQ11ZTnA-lQKcBuYAB0yA5AOzb1rh7N8PZSmi3Z3h0Pl18VhxLJ24g4rCNsO9tnMn65vnzC6rwlquj4TaSNd0HUIdR08HkMsOcxv3SVCA8TjoVcA5_Gk6F0Rp4_T0SZiY8QPwzrzB0I8xyV6HWRfqnAosFSohLAYDcD9UKKMkL5ogdt6LpNVHE_EsfDSC1h02bVNbnUNZ6NA4S4mz0kkJ_wIB1aVwBge6GbAq-OLN3nmm9VknxOQ9nEM0nID--Ci8McOXOmR3OqazdxXmu6u6z-neRoVj50ERwwqd1hjB5VHU-8x3aozS9qQ1ujRt9fenhsP4AdwXPNvdufp5Dkl6hdtY2Op6mmCbKxuLgZO3uof_1kcMxDfFd3hjbnB6An8LxXwjwzFPwVrPtAkQi6a6WztHZjhiXZpX6eQPka6EspzFo5J4FrXyFx8l2fgyEp7LLa3jKh7zNuG7ZkcIx-QJPyrmZ7c55WkwmSXsW_mIHvNfALmfsyKahpms8WLzKvEmuXuoRqVwVgV_AgXft4cII-NfYPszhvuvDIwdzqvkn2jxgx_FZ527F2nkNB8AFsBjy613_1XERkDO2KLSQ5cii00WD3FK0UgAaLRSHKSM7UWhAKOtFo3R8o3OKLQkkedMJEojnAGfjimElaTqE4ctoioZpbl2UjfYDFcKA5-NyuH9WNHvNrrzd3hcYPN7j-VSh6fbCadjzIBAhmPOwXPL2HmfIV32yMiIc8Ei-Qv35B1PJctjS4lVfFMJa0iaaxlgMcqrM5Uk-IVY79wFztkemawdt2js4lDi4ktGlZTV7Xmqjr1K0xDWi0jr9_S1UQXn0zTU064gRhaqGWaMcpWMElkfb3tzt5Emo4DhodjziGkhhvMDRs0KLel1VeC2GTNGGHcc_oVI1G5j1G2vr_UNXhuLgQwxwIY2cjozcrZ91gdjtjc_LE6R5xUTMNCWR5nncAaWBYrXrqb0VjiHtc97rOlc_FRLa1ntUaW6tGngYxsbm3lBaE7vuy5Gd8tkHGKJp2SkNidleM3dcKDtm6A0eXdre1FjS0W4adRy_nNWzXNlovD-W9uqpdOX4d1YgvNghZEkxnkUbgAjJY-WKhiMPPJiuzkPdNGjs1hruObdFzjveoACva9kUQA36vUHwVVMI01FpYo-WKWZwV5feU5O3ptXq8MG9rFNH_LtPI1qFiCahj4c-C36JbvhSe6A9KcYGIIid7TDp4gHb0TSTKP3i-WwJCYz7AEqsyA41zXbO7p-XzSvVRiHjcpzYZDj9-dc5zSxxaeSFJt-aa2ntz5ofWLn4_wDfrW3DfiwSQ8xqEDUKNyqeu2-B5f_-VgSSmVQ3Kp5gNsjufSkUKTAyOot2YoQR4b7wv2HpmtcDw8CJxr609D08imE9KrT8uEUpLAfrFHXbvWqS5R27RT2e0OvigqJdxgTFwDyv_--WDWLFMDZTl2Oo9UiE5YRQPK5n9Agif39GKoGKnkyQDvncDue9c_FPkYwdnd2NgCasXoSNK39gKyMAnVLR4La-abT9LgRwurwySgB1wQcsvTfHQ-gpNEzoW9rWbWwLqz0O5Avw0tHda62XXJXQEmuSDU4bUSLC6-JayjxH4NmwFTC_zkjoCcdSKMbFGzCh0RyvINuboMwFkm3E3oBz0J1WgEzOz1Y008PQeKsisd7p1pc3cF8EiV1jeaPEisSo5kcAAEEc2MYJBwfLwc3EoMBPJHHIw5qsMNggWcpGp1Bq70C6sWAnxLWnKdP_tPX6BRL3TLLT9YLcag6Tq2Gluplyjlgw93omMk7_HmzWvBfaXF4qKURYUvjeNmggaCrutffyU34KhgrtpwD_z4_omV0rRLK9b-fbzTpYxI0wjkUoSxB5yRjmU5OwPkMjY1D7AFLj2LiHYtZHnJAu7FcqJNV4hL_rwqE95znV02-A-Sc1DTdLs7KbxnuH4B4sxlyx8JwSjbnY9AQOlGDxv2hmd2URYA--TfaadQTl8yCU3s4Uxuocarrz5VWwR6TIHjs7l4ZHxjxMjcfH69dVvFvtBwlpdiYWvaast3Fc9D76vqqxheGYo5YZC13j6_-pm8iksrrxZUBSQYmUdK9lgeJwcQibxDDm2P0cOYdtr5FycVhpMHKYpd_gVEurcRvypkxneL8E_wDvUcTB2D-GrTssxTSIXPKu3U5PbzrOPBHJHw5xIeDfa_wveO7nTte0u_0O7IX_4ZFxC2PTLivA_biBoiZif_a934K9h9k5XuxP3dv9w4eoGYfIhX7tY4XXU_pd5aj-RY6qFuIpYxzLzgYYD12DfI14J5LJW4mLUHdfQosssRgk0v4An6FWfsv4Sz-G-p8L-owQxXqOqb66JFxnfGxIWrMY5vbtlHMLmwxPEMmHPIgj3a_Bep7eQfaqwLnu3VhFdJy9eoqwEBUuGqqNa0Hh_W0kTUtNhk8VSkjfZLr3DY2YehzUHMYTm2PVWslkaMu46HUfFYsezKz0GNl4C444n8TDNzG30_UGazc_zWz9jI-AFW_eX13yHeY19-_xWV2AhcbjaWH7AImh7tR8Ag-le1CSVmk9ptjICcgjh9Y_xCSqD4gKqLozu0_64_W1f2c1n4QerB9CEBwvx9dFfpntf1a1jxh00iyjs2f8ylpJR_0stSErYx-G3Xq45vhoPgqzhjKfx3kjo0BgPmRZ-IVBxX_NbDQuGb_UtKv5QmnEkc160kmZLiIMPELvu7nGd2YnrWaSJ-IDnyPLiBf4Gy7HD9IsOTFDG1gPTf1Jsn1W38b1jSdO0C1jgt41jROXlAOAR32I6jFQjeAwDZRlY4yknVXmn4beC4NvaWUy53UJytj89aLzfmS1ZyHuWjPF2jatYNDFZG1umYZDHmHL3w3yqUbTn30VMsgLCK1UC41Oxs1zNMufIGtJe0DVZGtc86BU97xjbIp8KJ2A7n3RVLpwNlpZhidSoI1y21aOW3uY_qpFKuoOCQeozUO0DwQehV0cb8ZGOZuCPmuaaHwPrOoyXGr4O8SXOkXljcyRW15byvFOUDsFBkT7ABFwBlXBHpVWClxMLKjd3eYOo_nkr6LYbAzzESs7wJpzTTtzz9-q2UHEft706PwLyx4H8mUAL5ImjfT_s_zFoeWMn7pArex7HauBb9Gx8aK4r8JmaRPDh_yFH3ix8k_Dx__ZplW3f2W8NwSkn--TiVzCB6hY2f9Crg7Dk-2fd9bCz9MZWjG37wBj4qU1bX2s66Amr4f-Xw_bMMPDwvZNHOw7j-ZcF5V8vqxGfWpCdrP-bPUm_gKHzUTg3y_6RIa6NGUe82fktNwxw9ofDFg9jpkP6O_i3E8h5prh_wCOWRsvI34zEmoOlfIbebP46K2OCOAZYW5R8_MnUq4nnRIWKsPKokhWBpvVYD2raw-geybjHV4xlhp4tU-MrBT6VQjAdM0LrYIFjJJRyHz0XmzgbW5eIOEY-E9Kt-TSdHtpZ7uXHvS3kgblC-SqDt3Y5JxtnRii2AgE6uRw1wc-m7Aa-dfRb32g3V7wH_AC1pD7APNJrSOysk6EXl17sfzHPVOBw5xM7YRzISxOLGEw02MP9Qa8zoFT-Ho0HD2iWHD5ufq9NtorUHzKKFJ-ZBPzr_lDEII2xN2aKXxNzi9xceanqMNg1akg4OcnvQhADOx_GhJHynO3alaEAoxjG9K0rQK9NvjRaLQ5cdqLQgQM56UWmi5ebrttRaRRMoBODkDvjqKldpE1qrTW4_Ai0_T0H6Dbh29Gbc1wOBnt5MTjD6d9DybouvxT4FkOH7tZr9nVQWVksvdHvTEk4G_IO5-fevTF3ZRCzWY_wLlAaaKBq06XcdpaSpLMVle8uEe6KrMEXYDyIBA8zvjBrh4uIwg5KBdTGUwEtzHz9u1OwW3CHO7WzWp137gUPUrCxjE1uXa2SX-tZ4nM6TTDcgnrg9vltgnFhxcuEwkeHaMzz33ggMLWHQaA1p0W58bLPIbXvZUZ5mhtdRtdbd7W8nSDktzB7pVV93YEBTjG5ztnuTXMEbZJoJsGA-Npdbs1HU6gWCa3OmhSzFrXNl0JrStFCsbu6dPo0t5LHa2yuyxq2EDN1B8wd85rvYfBwGc4t7AaouJ6DY14GqVLXurLeilSQQarJZveXssUQgbmHhhhBGN0QdCzNuTtjfbyrlGeeN8zoow57nA9MzidSeQDRQ32Cuytky5jQr3D51AWXw7yVJF-rd-mc432Nd_h8wjysd7Jr0PIrOEtFP_0wTtzE7dMYNagwjiFeP0KNbTd3GDi5hPusxDfytVGMjBPbx7E6-BQUm5Csyz8z8qAn7MNjVeL_xbN_cB7xuhFaSCGdHPRThsb_HrVeGl7GVr7AOUgaFOXMAgnKDODuPLB6U8XF2UxaNtx5JEJoLzbFtvQZ-dZrQgArAgqcE5xmjdCl28rMVcEGaIYPk6eR88V0cNiC8hw9tv7AMh09OSYS72KI2yTRqeQZAx1AO-Px2qzGxNMTZGbD9H5EHqlWuj-tcA2lhbXMzXEayNFF0KEgqTgkHr06j915XGcV4fg6mxT2tykgE73saFXXLp4rTBhMROcsTSb53gtFYcGXEFnzavq8VtHcK0lzac3JLhD_oPMMklu2BXn9OO43iMgHCcISAQGyOHdGYamhyrmSul9ww2DBOOmAO5a3fT6dFO_dcU0UY0-4ZiyJBhslmROhYAnlC7nBGMDIzvUG8DlzOHFpSRq4hndbmdyBNBxIrUDc0onjUbG1gIgDtbtTXkNvUpxbPTJNNuZXuV5sJG1s1weS4CEHIHqT0HXJrqQ8IhweKjiig0NnOW2WF3I6kWK0K503FJ8VG5z9dq0uga6bbpEGl6NEz3EOjhuTFyzyjkwmccq769nJGc522rpSOndlidIBfcAaMxLj6Y6aVQ2FWVia5urwDW-umnTxtKubTSoIx-7bdo7W4YRoG5AG5T1bqRhsEDoRk0Yft5DWMyuewEmgdM2tNqmnu3Z5FKRwAuMkA6b5OvMaqr1fTIb1Wga4aR4fqY5I3PIFU4AUdMbDyrdh8NEB2jI-zzUSKo2Rz4dT1UHYiQHK52atPD0WQljELGJ0YSISHBO2c9qm0FporTdiwriaRptMt7p8MFBjlyeq9M_Hoa8_wDZ8jh_FsXgmj4MgOroDV-7N8F3Mc44nAw4h24tp-P-KGqBgdPlcdOaFv0r2bWgg4WQ-LT8clxAooR1JhfmH3SOw_3tXOc0scWu3CNlQcYQ3EWl2Wv2sBa44d1CHVo1bPvooKSgY_lb4q899pcEcdw97G7j5F1-B43_jjY5h-Ug_wA-S63wVqdhNNNwrqka3OmXuHg5-g5hlCD2yCPwFfF8JiDE6hz3X17HQhw7SP4AyP2VLq-ktoftUltEyUm0qOWCRjlmjWfl949SVL8z1IAzXQ4kQ6GN48v97liwgp7vFv11-a7zo98bi1id_tMgJ-PenG4karmTMyuNKbLMwB5WIqZKpAUR7hj1OagSrg0JAPN3pIOic5wgzsABkknYCiwEiLUOPWobi4FtpsbXkmcHwz_o-dREoJpqk6ItFv0C1lnoMd3aPDqaK_jxlGiB2UEYO_etkILDmK57p8jrZyXm_wBpNz_ZfZxqV3o_DXBema5pati01Wdp5MLgYWSGAg8w3HNlc8ucb9ql8OEj7qPcPAUPiVubicTN_QY0-JJ-Q1-K51pPtK_aA_fkV1q3D7D1xpUcuJ7VIP3dLImP6wkd3fn4vE93IweuRXIeGZcrXuDjsdx66Ae5WQx8YfJ382OZeoHdJH7Ekk30vTqu7abxS97aRX1ndvdWcv2XYcroR1SRfuOOhB-RIIJ57ZnA5Xbj6aLozYMM1Gx_mvQ_zZK1Xi_TNIgFzqt19HjO-Tu7Y7Io3ZumAPMVdmLhooQ4UOd3zlbzP-ePRcs1T2q-3C8vbm_8a4P8d03QkdYrW21eyupb26XvI7Qn-s-SkdugzVvZYIACRzy_7jloeGu6i-bGvefukbGxDQZ7zu8dNvIhanhHV_bBxaZLG38LtrWViqi-jWWCBVIHMfClLPkHOwOCNzjpQ1jCR2ZPr6v_J9p92PaT9bGoq_r87LuWncKDhfRbXSopDN9HjCvKRu7Y3b9mtQi7MUuN947Z2Yqo1DWzpd0qXtuRbSfZlQ7g-TCqHSZTRWtkXat7p1VlFPDPGssMiyI4yrKdj4KtDg4WFSQQaKdV2H2WIoRvunVuJfvMT-g4NMEpEBOrcSyLyuc07KWm64_62FxOeFvYPLEiq82pXoKozBecJ0BJ7ZFWYJva4sN5BbZpPufCZpwNTQH49FTewrgjXoeEtA_e_D80OzhtY5pLW4PiXF1O68zu-wxknuMgYFQxIEk7nXfT5lXh5BBhGRtFOIs-ZUH2scTxcXe1TQvZ_YP8ljwhFJxFqrJjka8f-q0hPqpLyEeajyr2X2Q4cH8kzvHs_wAC8r5rcd2OFjwDDq45nfQfVSOnLgZHkelfSF8-RgN1IotFowvelaLSgMUrtIodvWhKkKRQhvQEI8HtRaEAp8qVoRMpA26UwhKt4jLPFDy_8kiofmRVeJl7GF8nQE-4FWQx9rK1nUgfFS9a5rrXJ1UZ98RL4AMf3rD5nYC3AQs5uF-82tvFX5rjpPA17tFHlumSaRI5pI4GXwW8M_aQddsjO9dXFMZO8HKCWHu3yKwh5bYuhzVfqemR28p1RdRt7kW0SNJBExBWNjuW-LYXA3-GqMBjC-aponR3mDXEAixzAo6ZdjproulBAI4-0Dg7nXh_lZz-at1LPGlkxe4uUltl8R2ERz5nl-8SMDJ32rLiBJLIMRLJ-UhxoCxXXkBqaGikHg21o56a7fqnNStrie51C6Z-SRYRPeRqpTw3LKGQhjk4O-RntWPBzRwxQx13cxa0kg2ACQ7QUNNwVOVri5zudWeVeCuItXtdIWxt10Qm7QGYoz9yz4pVw25DYznORvjA6CyTheIxMRgZKSJKJ01AbmBaWnTKDRBFaK0Ttiru6_zW1VfRTbaksVrazpI04j4SVf4AAJ3wTgAkde3TpXUfiosBhCA5r9Mtkt28Q3Uk3sd1UGfid0Ea-5VNwym8lZVAUSsRy9Bv2zSjNNaR4KhWihGvSrECSLB27qR_SvRHLJiv6TfiCPoVLmoEMqxzSRyjMUhPOvpvvXHhmEUjmv1abv4AX0UU7JAVikgBzyESxHzXvirnwlkb8TrXeB6jmnSjjJHM32TknHn--Vc4i0lNbmu7Fcn3_cnPqDXQefvOGDvzM09E9womVxsucbk4G1c1JGQOQFiObPlSQrDSNB1nWJObTLFpFXczN7saevMdvlWPGcTw_DqdK6ncgNTfktOHwk2JPcGnU7LUJw_pFhGyarefTJTu9vbDEYb7ADHzO9ZnY_jHF2lkDRBE7cnVx8tNL30HqFpc_AYH6oe0f0HshTo9RiWOK209E01I-cuUGQ2N1GR72TjHXG1UQfZ2DBudM9vbONan2tTqTZI0u-ZWWfjM-JAjYcjddBt8Ew9l4rGaGdmtlkSJrmSIryltyWxn1rsNxZiAjlaBJRIYCDoNKbt06LmdkH55p7t1ZHXrun2vZ7h000KsqJKnKLclVcL1CrtzFvXz5TVH3aOMHFm2ktN56JBdqNTdZejdNFLtXGo9wCNttPDnamXSyya5cWdv5FhkHNGqNFzLICuSGIB6KT4MAVzsOY4-GRzzZnNNEm6qjWgJui4fErQ8OOIcxlA9K34_RQ20-8t_D6lMLUGN4czTLyHCk4HUgHAHxPUVubjsPNm7EdobDu603qQLOwJ16-ipMMjaz53QjU6bFCaVoEiuZZlvDyKtu7w-6vXmznBJU4x1HY9KIYhM58TG9mLJeA7U9KqwA4A3sRySc7IA8nNppp_DpyVc558Nk87E82w3Ppius0BorkNtfqVnJs3zWZ4ksxZa9fWgbn4OTHMFxk8oz346xYbEfeomzgVm1XQydn3OifhC_ueKJ-k0rIPnn61cXhpDvtPIw7GOj_m0u5IK4QwHm75VXwhp0-jkHxY8mMk-XUflXrIgZm9g72m6j5Pr9riJbgXsXiq31se0gB6jzqbx98i7QDvt34fFPdMPBEUEFwPEjnQxsBuORgQw9dia5zmh7S3qmw5XAha32SWdtq2g2NnKRHe6PPPo1xhcskkBDRk_GJ4yPhXwfiOCOE4hLh3cjY8j6_zX2HAY3t-GxzN1runzH-ghTuP0aD2m8JSk5N9p-p2OfNhGsq_nHSxeuDv60j5Pqr4IQZWDqHj8Zvoun2MxEcMinZkU_iAak0rnvFkqdLMw27HpUyVQAo5k9ahasq01falZ6ZaNeXkojjXz-k-QpOeGC3KbIjI7KAsdDqt9xrqaafbNJFZF-UrGPtY_U-Z6Csgc6d1clucxuEbmOpXWOHOH_LRoFtrKIBj5pxuzH8104omxCguBiMQ-c5nLUwwiJQO9aAsRdZUbVdHttUhKuOWTGFcfoaT2BwU45TGb9LlfE_A5jkdHtwhbJxy5R_UVzJsOu5hsaCN1grrg2wt7h5HsZLd5Mc728zxh8dM8pwaoBdHoAun54Mg1Nqw0ThaGOfnsLAvMf61kZpH7APExJFT__5FRJIBq4rc6FwBe3jq91JhO4QkAfFv-Cr88OTuufPj2s9ldI0nRbTR7cQ28ahiMMwGPwroMjDBQXHkldK6ypFykciYYDpv-VIqDTR0WG4o0yC5ikhO6N94b8PnWOZocF08NKWkFc70zXLnh3VJNJvOfwicgen4S_wBRWFr3RGjsuvJEJ2h7d1t7fUlcA8wYNuCOhrYHghc8sU-GdGxg1MFVOBCmgBU5j9VPxVd3ouRe1pIuJ_2geBeBbiGK5s9B0mfWLiGRA6eNkLGxU7EhiSNuo9KrhcWwvlG5K6mOAaIMO7UAOeR47D86rQe0fjxuGrYaXo8JvNdv1It4QfsA7eI57DPTPr9VMPjgb2kh22Crw2HfO4ULJ2Xn32NaTcfRuIeK9Quoby717WJs3MRLJLHbkxBlJAJUyeMQcDIwe9fWPs1EWYATOFF-vpyv8r9X5o5c_EZGB2bKctjYkbkeFro2FUnlXavQrhoEHNIoQwT2pWjRDlO5FO07CML3NK0iUrlAO-1FpIBBucbUIQwPKlaEY3_YotCJunQdc00J7TMjUrXPQzp-tYuKa4Gb7q79LVgT7uov6w-acuwYr2-uG2bxnjT8knP9Vp4QQzh0Un7EAe7VPGWMTJf5x-ah2tss8qoxbkU5bA_AD1Na2NBtz5Gt1J8AsoBJWf8ht5Vhku5JLdZJpizRc7eIgyVVCuMbcmTv3HpWeXiQmk-7ta7vAG6GXKKIo3zvTyXVMRjiBNX4VXXMlta3Nu2lyKWtlT-4A-_IN-flYbEE4xj_tc2Fkk8T24oe1fd00G1WDr116pvcGuBj9c_HqrazSG9Q3eo8Q3DTcqhVhjJHuMSquxxjcA58m89qrweElfiWw4XDgRg6ucdraLIq-WlK3M1wLnvN-Hn6qYn1TR4buT-HZeNFK25nmI5Ryj6AdM7g56bV1sTLi3tyMlDHn2nNbd0dA3NsK5Uq-0jY40LHiUWpWuoWckg1DXIHN08QYfSDIXiK-7IMDBAHuk4BwMedcTCS4aSvu8JAZm3bQBB1B5jXUVdqyQPbZe6ya57qjm_6pkjAzzSkZ-ddqMW1o8lmUxmxqy8p2zjbp0rrvdXEBX40T9pm8hxJ4iYKuTjA6HuDWLHQhj47PZd8-YQVI0-UFkt5_sjPhknpkbj8VfgcQ0kRSctj4wmExJAY5TFg5ycdBt2NYJ4zDIY-iiU9aSCGUmT_DjDlj1237WrMLP2MlnY6FMFTLPhvVNSufo9lauUPSQ7KR5_Cs_EXxcNJdO4BvInn9dVKON8z6ziFnw-vRai04X8e0QK-ryDUrxdxAp-rU-tcAYjH4V0wbezjP93bnyH6fNa3NwuA_wDIOd_5o2Hmf4eSfvNYvbxfA5hDbgYEMXuqB8q6WB4PhcCe0AzPO7nan5lz4VxKfFd0nK3oNB-6hBWA6bCuteq5-iNSFDbAgj8kb1EiyNU79JTwvHDFIxASbmKgNnODjcfpmq2yxvkc0btoHTqL34t6Ui0gAnYqTe3AMcET2At54kTEqe6X2J5iANyQRv129ax4OCnSOEudji62nWuVAk2BodPFWyyaNGWiK1TRu7ido47q4keJCowx2UYx6dhj9VoGFihDnwsAcQdQB5_MqsyOfQebCfvGt7ZQ9sUSYmSJ0iJaMR9Mjm3wc7Edqy4Vk0zsk1lndcCaDsx65aGnQqchawW3fUabV69VHkDWyvZShH2U-6wbl-8QD2J77DBrUysQW4hljfcVe4FjnXLwKgbjBY7X6WiuxbvIPoVtNHFyA_WNzMw_i2GAPhRhu2aw9u5pdf9RQHhqSfeiTJfcBA8Vi9UkEmoXDc2VDnt3G35KZ1dp1WuPRgUvUybawsYBkOB4vz75zXkuBPOJ4pi8a3YGh770aF6HiQ7DBYfDnfc_z1US5IS48cLjxgJFIO6k9_jnNe2xfcm7VnPUev_rhHdPM0xaO5tU9GXGDzH6lWOkLXDFRaXoR06-_wCad803dQlJBcwgoCcNv5lu4-dRxMQIE8Xsn8IKoZONON_ZtrcvEvB_CsHElrqyQjVdPkleKRJ7cEJcxsgJyYmKsMHIVdjXgPtF9n75RmbiYrBA1qj4CvQ8J44_h0T8aDmurQ2KI5gjw0Spfb5a-0TXuDdZm4G1fh99H1-AXRvDzRFJiYWCOUXm2fJ2HSvG8Q4RNh8LI06gg1oR5eq9VwjjcWIkjNUWvF68j3T4CvSmnc0dutq59-2-pb8qcf0riwPD2NcOa6mIjMUrmHkSFaBvFtzj_SD4qv9LIe6VXvLykk9FyTULV7QuN8Q8VX3FOtNYwu6QwOyOTkcgHUKPn1_tWSUVq_fou5FE3Dsob412bhm20rg7SYWuZIhdTRrzHtGMZ5R_pWqINhbbtyvP8hz4U8huwW04Zuv3lOLhZhJCBn3TkE9q2RuzarnYlvZijutRt3xtWhYtEfqKEJq4t4LqMxXEauh7NSLbGqkHFpsFUNzwZp0rlo3ZQfuMvMKoOHaVobi3tGql2HDlhZjAgRsefT4OlTZEGqD4Q5-5VuqqigKAAO3YVbtsqbvVGWHTai0kzIucqfvdKRpO1i-I1ks7onGFk3GO9ZJbBXRw9Paud8c6C2t6abnTm8PULI-LA34RHVD-EbVRlEndcurhn9TkOx-ahcFcSx3aRWM4Ks49wEfYbuh_Os0byx3ZvV2JhNZwuhadAzMPLrW1q5UjgrOaVUU5OFQZJqbjQKpaCSuW8L20_EPtm454ylDJ4MVnottJgfdjM0hHn_0qfhVbb_JjB5rr4SI-9Od_aGt9wzH8lVHtq0z7AIc4WdtK1K5ueKeJLgaRpBcgCOWQEy3BxviGIPIT2IXzrRw3hP4AqGMZDdnc-SzYvjLuGYCXFgAUMreuZ3P7ANIs-dKm0DQ7LhrQ9P8e0sEWem20drCW-0VQY5j-k5Y-pNfco2NiYI27BfE3GySVYFM7kk53qdqNoAYHxpIRgb8oQh8e1CERznahCPtihCGKVoRgDy9KRQlHAwADjvtQEIioxzZHw71JCOCQwzRTdkkVvwNVYiPtoXx9QR7wpxP_ORr6hB9xU7iQH57SwIvu83OAPvFt81j8DOZ-GQMG4GU-YJBW_izMmNkA5m_fqm7QQwTiKWMyRwKZJeVuXL893ceRxXbkhc9nYsdQBBcauwNxXiNFhYWtdqPpqufXV_dXwdLq8YqsskwV2PLzMfeOO7HA_CuY5rDM6ZrKLqBrkBsPADqugXueKcepRmN9QczvFFbxABSIk5QcDGQPM1fgsEQy3OJaOZOvWkE5zdUpd3dwmxZrWwFvFLyRJhid1A5n6LY3rWC6DB5WvJBc6tBZBN6_5dgpPcCNBX4-qQItJm0vx5Y54LlSxDoC0cjDGI2GPdJGT16fhXnny4puLyRlpYQO7s4dXDXX3KQbGY7Ng9eXkinuOaFdMsZ5Htbp4pZS8PIVlVfeVT9DP8YrRhMIcRiGySgCQW0Ufyk6EjrQv3pOfTSxnsnr16KujVpb4gAe9KWJx65rp4RpfI1oVbUtTz-kXUHHidvLNXulvGF42JRzT9QGe4spGAV25lPke1Xup08mGfsTY8D66ZUTl5JCHGCp3yNgRXLc1zTR3CNldWOnT-xAjQ2xkkjOGYnlBHlnvt2rovb59gDx7bdD8gc_ioue1o1Wi0zhGysYfpevMio3vJGVHO3oq9vifSvKTcUdLIcPw5vaPG5_K315rUzCZWdtjHZGch-Y_orO71tjCttYobaBfd5QfeKjsxq7D4GDpBiMe_tZPH2R5D7Con8o4tMOFGRnhufM_wCVUsBnC7_nfFdpcsGkWPlimUkY8uppISo3MTrMFRuVgwVhkNg9CPKoys7VhZdWK0TacrgUTHmcvyKCTnC9B8BUmjKALuuup87QTZtORG15FMyTs4cAhGAHh48zk83yxiqZBOCRHWWjvd5j4K5qYLPzXd_BK8dUs5reIcqvMsuCxzyjIGdsHc5qHYOOJjmfqWtIsDmavc2NqTLwI3NHMpu4iEUoQTiT3QSQCMEjpv9dKsgk7VmYty6netdd9NrUXtyuoG9kjOSSTknuat9nQKJtFc3jxwNLLK7LDEVAJOyDflHkKrEUbLLWgWb4z1Pip255AJ8PRY62ha6vI4mG8r_7AI5P5a5vEcV9zwkmI_taT-8vjS6-Eg7edkI5kfv4FJ12QzaiUQjCARr4e_9n4q5X2Ww3YcNa87vJcfkPgPit_HJhLjC0bNofU_NR2TxLRCQeeFiGz9HpXrXHtMMDzaa9D665PJC3kj4QRkEROArHqfQ-mKrglEbiH6ydD6vokE5EzWkrRTtzI7HnGOo7MKtjkOEkMT5Wn-80x0TN3beFIuPeRxs3XNVYmHsHaatOxQQqfjGK6uOC9fiiLtPb2f0yIZJPPCwcfoa5nFITiuHzR-APuOvwW3h83YzZgu9aFxJaa7p-lcVWLhrPX_C31FCOn1qAkfENzV8IhaYM0Lt2kj3L-7inNxWXEs2e0OHqNfitPaS8jjJyG2NbAVzXCwodz_t5NbsBgAMuO6nY_nQ9tEFTYdAuT2-iw6fxtqVq4YNIRcJnoy5HeqcQ3O5pK7RfbQ7qrXj7AFzX5F4Lk4s0_QZtbj0yPnubaG4CSrGr8kccwPNge8QOwNaI8GcXL2ealyJMSMJG54aXEch05rjcH_bntDsLf-HwjwBo1vCAcS3NxLPJk9CQqqPLOM12YuFCFuVzz-AfW15ufi5mdnEQI8XH9ABdy4Ntv26-PeHtO4utLPg2DTNXt1ubZBcTQy8hz1UE_rVEuGa0kAvP7tVkGOboZBHr7wB7HzVzccOftoaaPEm4Xsb7AJTutrqbgn8c2aznDvGxePQFdFuLwTtwz8j9oJxr60Zw_EZde9mmvqEyX9IoLtdv7C2Kpc2Rm0nvaQr2jBS6Bo9HD-pu1_avawHhcRcE3NvKrcjG4sLmEA-vKCBUC-flR8nBT644bfUeYP0Kdvf2q_HRTo_Bwm5ujW9ldXAb8YAFV9tMNCR__gP1Vv3DDEWLPoUmH27e1LVl59H5kPEs6Dbng0SXl_4APJVrDM_XMPeT4gVRJh8Ew04H8BNy-175oGI5t_YLxbcjybSSoPw5eatLI5ncx7j5aWeRmBbzr1auece_t08X6zW9k0Li72N3Gn-xGiyiy1C7NrIyMTytyNEGIODuMjaujDgXP5rQLi4zG4bD32Zs9LH-Jzgb5sC_5ueopw9pnsp1TTbqziae6uor1Li2UDAwcxqRk7Df6tYuKYb_tGHtdfhzWngmM-9TFj2EDry966hp13NcRXl9cqYo0IAVvulVy_9n4qwNAc9o8rXoiypAxqxHs20ifVtauNbaRxbrO06bdck4FZXDtp-0C24uURR5OZXY7G9QvcQxqOaFlQnPUlcn6lbnNyaLgObqmNbuhDpso5-VpcRg5xjPU_IZqp50WjCRGaZrF5j0D2y-2vhh7vWuG_ZxwrxJw_rV9NewtNfS293EXc48U84H2AgXCkcqjfevQw8CE5ZA15Ejmh1dR4XQ056riY_j0omkxAjDosxG5see_KgNOS0umapxjxzrk_H3H6mWGmXRtxY6RpVjcmeHTrQ4aV-f_0sz7aPZURdtxXuuAcEbwphkfq88_BeN45xqXir2xkZWM2aNrO5PUnyCvsb9IwP0r0K4SUqFiSSAO5oQkkYORuKEItwc96EIAb_0IR7ZGxpWhAnPakhAKT060IRgE9T1oQjJJ3O5oQk48utO0IdveGxFANaoIvRXF06z-dbawBmZE-iyejjox-VcXhEjeHY2fAu599nkfaHoV2Mb7ucLHjBuBkd5jY-oVfOGtbdLfB8SUiSTzI7CvSu7kYYN3an-LkVQpY6GKbS9TFwfdnSZhFGEDFiSRgjuN-mDWafB4d0LvvvsnlfzpdOJxbThulRkSiezulihkZc2zhSSkin_O2MBskHPlWPFyz8yWJ0Jtt1lB7uU8-dkbhTaRTmuFHl59FFvVDXRtnmSNYByksSwLZ3Pu9TWzGTNL2sZq0CvhfooEUSOikXltB9BhXT9p54lkdnDP3wuT8QyUxnGSTnb8VxcPLJ94d95aGmhWnKzQz4zzNAK2RrcgDDY_nJJhu7q2sVPiOnhOywpnHJnHOcevL3rrRwwRxvxJaC59NB8BevparD3AVeyiIjQNLOwCtISsY6bdzWzDj_tEZ3DU6AeBSGijLswA2IOaxAlpB5pKZdxvJeL8KFmmQMoA36NbMeLlD289UE0LK0Wl8KT-iYp5kEsueV4gfdH4zGs_EMXh8PAMViHVWh6k8qUY2S4t4jw4s_LxK0cNxZcPIILFRc3YPvvj-qP7KO53rgtjxnGW_jExQH4o9p37bp5LV2sHDP-VSS8ydh5dfNVk01xcyvNdTO8h-82-_l6V24MPFhYxFA0NaOQXJmlfO8ySGz1TYHbtVyqSsdxkChCIgDbG_nQhAg9B1oQiwfx_KhNA9SaLSQViGBwDynO9IixSd0nEkKeJlI28VOTLLnl3ByPI7fr91XJFnLTZGU3od9CKJ9Uw6rFbpvbfbrVqVogATvQhVvEM3hWHg5OZm5fkNz7AEquQ0FdA23qt0GHlnlvJNkgjJB9T7pn4a8Z9rJnPgjwUXtSOHuH_ke5ep4EwCV-JfswfH7AUO7GLqUsFPOxbfuDvn469c_DNwVYeP2WgAeQGi4sjzK9z3bk3_0q15ZJHtiEAlTA69R0_StOE79dCfzCvXcKI6JkMvMWeM5z0BwMeVY0tk9yrcw-Gg9-Fcrk7suNx8u1a2n_xHl_M3bxHT05KSO2lUxm0nb-t9gf8TUsPM0t7CX2Tt4FARw2ypeG0ulDRTxvE-OhRlIP9UMw5Exw8n9gR56KTD2bw4K59huoQn2cvwJIQupcA3sumSrneS0d2eGUenvY-VfAeN4V-B4lIHbP6Y0-Wq-r4DmbiuFtY32ojX7pOoPv0XVNIuzPF4bH38_zFZ2OzClbKzKVOvYw4iu-hTKufQ1o9tnkqAaNLJ8W6Vy3lprsKnmizDLjoyHpmskxOUeC38eTM3syttwRaeG_iQ8phlPiEHfd_tbdxn5TWqJxcQ8Lm4p2Vx67rm_ty_Y54a4pS5409ldrFomtjM97psYP0S4PUyxxj7Dz1ITvvy10m4-TD71e83rz7dYm4XD84EA5JPgfPp6Lpn_KntLg4M4MsPZj_R5P3RNbzTfum8unH0ORGYs1sJ-isjFmVZOUlXAGeWtjJWS96M2Pl5rk4zh80Du82vkvSOia1pfEOkWmu6TN4tlfxiaCTlKllPmD0OQcirDQXOVkuMbMw-dO0io9xp-m3WRdWFrPnr8tuj7qKrdGx_tNB9ArGyPZ7DiPUpqHRdCtCHg0bT8iO6WsY_QVFsUTdmj3BN08z5HPJ9T6qnKyhfq9h5LsKu8lSfFUPEHtA4R4Wa9j1_iK1sn062S7ukllwyROcJt1LMQcKNz4xkJrdXRYaSUgMG68Ce0b2U6l7f4A2033tO13T_jTbe8ENvptvc-_dx2kScsY5DtFzAlzncFzWDFcVLB2UK9JhOEQgiaQWep5Uu88Fezzhn2b4OnRtBsIYjIAbiRRlpCPu8x3O_UnqfKuU1r3u7WU68lrlnY0djB7PM9f2CoONLddP0r53WzETXxZeUdlJyx_PFVzOMe25WjBuLn9jsFK4f0iDhvQooVjHMq8xH4TnoKsw8YaLKrml7aQnkp1hGLS2aSbaSQmRz9k1N74xLiqnd51Bc75tPFU-j4D-jJaSFb--C6Xp4B3NzdHw1I_yp4j7wDRUsFA7GYhsY6q90gwOFlxR5Ch_wBnaD3Cz-LnuiLBpcMekH7-VYUtWXH2eUAKwr-hxfh8j8GT8X6rDq3xHNvqF838fimNkdFP7Tk0Ph0Por7Ri1u8mlSseaI80Z_iU-Xz7U11-GY-PiOHbPFz3HMHmD01XJ4lg38OYsf7AJ6H1Vpg9MfGt9rnoipyP_00IUrQiFFoSgM5zSQhg9BQhKaMhS56ZxQhBcA7frihCIdMdqSEKEImHpQhADG470rKCrHRLmFJnsLzH0W9ARs_df_rfjXF41h5C1mOw4_FhOYf4h-Zvqupwydge7DTexJofA8ij1FjZXkqPGXugd2YbKO2PTFd3C8SgxsLcVh9c3w8PRY8TA_CSuik3CxGvW8keqNdksfExIWU9GBAJHzxVMx7Qkv1tWwPtu-oSbYWc1q-pXFzcPepIzyqye5g9G5juWJpcPbNDiC7I0QsboRvm2AralqOQtzEnMq4KfecnlIXOcEhif02zUHOdd73uqwLUi1m-ixI9lPcx3TuYpAhwhjIHLv33ztis7sO7Ey9lIwOZpV79tQdPKlNrsotpo_RTL21mvI5r66vVYxnw1EkmZZnyOc46nGepq3E4iLD8hmAhYcrBQod0b49BvakWueDI4_qqd2eS4Z2fnYlv4Q9V3rU57n555sqpSdP0u4vyxQeHFnDSHI29BQ1pcqnytZ5rcaVoVt9BhdWWGKHKzTv1I7AevwqHE-JswsUcTBnmOjWjfzPgrMPhn8wdo85YxuTt5Dx6J-fU40jNlpieDbkbtnDynzY1gwvDHGT_3jznl5D4rfABOfHjJ93wgys5nm7xJVeuVXKSHHN8PnXZdZ1K5myDk_Z5s75R3PnSu0IIrEntjyoQjwMAZ7UIQ6KdtyNiaEID_QBJ3HahCHunpnOw3-f_zTQiIGNxv90IQVe4-dK0IBf8flRaEarkbjp1pWhAgZ2G1FoWZ1-48W-MIf3YV5c_wAx3P4AQVS91lboG02-qc5TacP95cPdvv4AA_-D468UP7uP2iJ3bAPj7wD5O-C9N_8fCPGQ_D7A-KgXA8SCGbI6FG88jp-Ve7m74TJfQ-m3wXn-0TMchRg4fdSGxjqR_s1nY8xuDxuEk5dqUmdl3R_eU9QObf4AvV2LYGSnLsdR6ppEUjRyI6nl5enKB1qhkhjcHt3COaXPEmRPGgKS5wufsnuKuxDGipWey78HmEFSbJmm5IJFLmM5jcb4p7g-lasLiA_LHLuNj5Ewb0WS4k1W75mnF9p7R7Txm0-SUWGvW6n3ZrVwAhx5jB5T2cDzxXhPtnwMTufKwd6_jy9-xXpOA8WOCmGc93Y-LTvfluPFd00fVrGeK01jSL6K9sLyMT21zEcpPC2cMPwIPkQR2r9SCWOpwojQjovosjGvaCw206g9QtpayJPHgHKSLkVsY7XRc17S1IOnx31rNZT_5UbP9Gh7A6wotkLHZgpPBiT-a40-8wHiJUb_MvY0Ye2aFRxhEnebzXQ4bgqiurlWXdSOoPnW27Gq5NEGws1xDwtp2rTy6jBDFDczY8dfCVorjBz5ZGfdJzvnFUGHKczDRXQhxr-ySahVNvpmtaRErcN8Qa1w26uzMml3f7LszHLEwyBo9yc55QatZipWDvaqboMPOdWg-i0djxp7TrOJUXjaC9xsTdadbk7dyV5BS_1JzdwkOBQSbH8qYfaP_Shs2scOE-bWIH76al_q46FS_wDppn5w_wDcmv4A4ie1dnHh67w4F7j53I3-zipDiwPIqLvs5GBv7wDIJrUeNOP5ST-PecZfRYxkONLgjtnbboXBdh_0kfGrP5Re_wBkUqf5IghNnX1tZm24P0qO5e9s9FtzeSt4zaheA3E3Nn_XiS8zFvXNUvkkk3KtBjhFBX2m6XY6WrTqTJM5JeZ92YmkyNrNeaqlmfL3dh0TjEzuZZDyxpvv0HrTOupUdtBussNO_f2sNq08eLeE-HACOoHf8d6y5O0fmdyWztOwjyDc7p28dZp-Rd0Q_nVjnclBooKtv_sufo8eSAd8dzVDncgr82ULXBvaBf4A_F_tKOlC8VdJ4JiZ2CKZPpOryruhA6CKLCjPQu3mBXsfstgZC5uLa2xetmqHh1XC-1eMbDXDg7Vos1zceR8hoPMp2OCO1QT3A5nIzGmMkjzNfVooxhAJpfa5D5V4Sq3VpE3ji1Z3C3UY5onPRh1KH85ryvF8K__PYgcVw4uKT6qwcnc3t5ea7MRZxWEYSY1IB3D1_wCJ-iuUkEsfiRBuQnBz1B7gjzruwTx4iJs0RzNcNCOa8tLC-CQxSCnDcI-h33FXFVosDz3pIQ5e-2KEIwMUIRjehCMFcYK5PnnpSQknB-6BSQjGewp2EIwDn81ElFoMNsgUgUIwNublpoSOXy-FMGtUbq6t5I9dtks55AmoQL5TI3SVR90nzry8zH7Z-c4mIE4d_tNH9D1A5-K7sTm8XiEMhqZvsn64dD5Fmtc02Z4XgaNkuLd-ZQdtx1HzFejZJHiYhLEbadQRsuSA_Dylkgo8ws1KSlnhGcmeQuc9eUdM1rf6FhABu83-DZbNgmIzGhDMocsCCGGQPLFYbSKlWCRrM9xM5UR5-yNwT0rXg6a5052aL5eSYUeBDNOArBmcn3sYxvVMTHTSBg3KjurLTtFlZRJfZWIksI8YJz1yew9KsYy91RLMBo1aix06BbYXd6fBtIzyqAMGQ_wqPLzP6xgxmOe1_wB0wYzSH3NHM31HIK3D8RrmfecSaZ8XHw8D1RX1-98wTk8K3jGIoUOyD186twOAjwILyc0jvacdz6iqxWMfiiG1TBs0bD5VH9RkA7jH81u2WW0Am2Oai0JXINkwAQftUIREdRtj80IQCsc_rRaEfIdsdDv1pIQwR23p2hFyg7EdBRaEOXON-tFoRgdgNvWkkjwfLFFpo8DbIO9FoSXbw0eUkYUcx-AouggCzSxTCS5m3wZJnOMHuxrFLM2FjpX_NBJ9NV1Y4y8tibudPernVHUTppqkBFiCgeTH_P-fnXnvsRGZYp55d5nEe7W_7cSu7x6QNljwzNmD9_sAqtFZoJoGXLIA4BG4I6_lXr8rfFJEdxr-jQ_BcEJNoiPKWdcLGhZsHrUcLG2R5L7ZAJQApBltp7bmdTCM-GeXcAdRmtbnQYiIPcMtGhXLonuoc9vLDIFfB5hlWHRh6VgmhdA6jsknLZlfNtK4Cy9CT0bsanhy11xP2d8D1QE03NFLy8hVlwCBt71UPa5hLToQkpeq2mm63Yy6Rq8ayWup27wOWAI5XGGyPTYj1UdwK6uLMWMYIZR7TdD7ADxVrHZXArl3sI9qbez3V9S9jXG9xP4AuW01i5s9H1WZfdtpi5PhSN2WQYfG2DzEdTXw7j7Cz2rsRB7Q9oda0vzXuvs_xfsGfcsXoy-6760nWiehXrHQb145G064ysiH3Qf59PKvPQvvZemxMJadVpI2CyJcAbH3XrYDYXNcFYxwrzq_KOYDAbG4FMDW1USapXsVwTGrE9RvVwNrK5uqTcXChOZevei0Nbqq2XUpYshMHPYgH5ahmoq9rVRXmoasobw4tNvMD3RPAYmz9cyHH7lothPe0WhgI_MVnZtb8wUHPA2ju2f6z1dlB_4AFHtTqLqVbmI_N8FFj1nj6a5EacI6BZxYyZJr6Sbl-SgZ3pkQjYkpF96WtLpt5rkYD31zZ5_gtrYoo_-mYmqyRyUD3t1eWtzPNjnZm-JoBKpc0DZWKBnA5jn0qYVJNJ24tRNbfRyzKr7a5e48qkRYpRa7K61X-tPHY24trdQruMAD_q1W45RSuiaZHZis1d3H0dORD__VncVta29Vz32kcdz4J6YNN4dMU_FerRsukwPulsueV7-f6GGLfH4b4qjO9dLgvCZeLYgRtHdGpKjjuIx8Ig-8O1kPsDqf_vJvz061zzgzQ9G4Zs4dLtbiRpJrhpr3ULzJkuJnGZZmx94kDA6AADfFfZIpIOFYbJgIszmjS-Z09y-XOldiJS-V1lxskqwtrK41G6dEIkQEPJMdlSPmC85zuF34qpnxerXS2XONAbkmrrTrR8FFjC86Kw1CALqMFpBKssfMWDrkCQKdiM7j8V3ZwcS6JsjKsEuaaPSwa09yHDK_um65qbZ3-y3MkJPLIu-O7J2z7MOn6xXgZnf7AEzi3PjF4R7tt8hNbeHVdSVjeORZTpO0aH68ePirBTzAMu4I7V6tj2vaHNNg7dF5VzS0lrhRCPAJyuKkkhy9yAcUWEIb8wRSsItKwp7Y26Ci0Wk8o9aVpWjwBStFoClaLR43wBQhDAAzQEbIulSQUFQM2DnAGTjamEBIIAHUhhupAoNEEHZANahW66lZ6jGltrakugxFeIMOP4w715yXhmJ4bIcRwnY6mM7HxB5FdpuPhxrBDj5xs8bjz-rN8ZcMXdoU1SxQT-eUCmSDcI3mcdAa2M-0GFxzmxOuN7RWV-hvwOxU34PnibmHfZycNR-yyyKGIwmQoyQxxzZ2reQQFlvmpE4khs1gJJaRy7dMgLsOla37AIWGazm42fIaBBtKjDW8HiE_W3JwD9L3PxNTb7tYS4-078BLZbW1sohG1_fORbI2AFO8reQ9PM-v8crG4uQvGDwmsrhvyaOZvkRyChhMOzL54xGjB8T08R1TN5dy30okkwgVSI412VVHRQOwrVhMHHgYyxmpOrnc3HqVTicS_FPzP0A2HIDoFGAx1rUSs5Sxk4pJBKIAH2sHOCMUJouUAdvjQhDYthsAdzQhAH3cbH8ChCM46Z6dKEI9vug9qEIt8nfG380IQ6-6AcDoPKhCVy497t2pFCGPTFJCGMbA0IVfr04gsMIcNK4XIO-Op_T46g80FdA23ql0eEXGoREp_hZcnm9Nv1FeZ-1GJ-7cMkA3fTffv4AV6PgkPbY1v7HX5PiUd9It88s0IHiwscY6smcCu9wjBiLhUUTBT82gnxvvH3ElZcfN94xL9B1-A0HyUcyBLmG7X_E32h69GFdMvDZ2YgbO36Tv1WXxRJH8K3iY-yOUfDNRZH2TZ29BXxQk2i-IssH7AHibfEbiq8L6IHw_3DTzGqAhbyRuDbXJHhndfNW7Yow8rXN7CX2eXgUvBNSRNFKYiACD4vjWeWN0TzG5LZPSt9IjFyuDJGcSdNx2NXykTx9r6YaH-FPdKmybOBjvyFkI-ealN34PG48iR9UeK4Nxdw2dR4w480oKQ109pOrZ9ws4xv25sopVuxzvgmvmXF5DhsdmOxLvofqvX4Pw4xmHlYN8rfLmPpoV0T5nP22X1_cx-yvjq4aLXtLPgaVeTtym7Vfs2793LgD3D54bdhXmuK8P_L7d4Yd07gcj19ea7nBOJunP6m4z6o3RpPPwPU_29V6u0bUI7yIZ2LDDA9jWCJ4cAQt88ZaVobPLJyH_S_mK0hYX-K0tkZ1KDrUwqHGkUtvIBgrQQVJpBVTd27KSQD46rIK0NpVkyS9PCJ-FRtWABRnjn_Wz7hSJ8FMN8UhVmBw0JX8ilZRl8VNtYgxHMaYNqt1BXdnbtssa9asaCqHkc1d21gUTmk3NXBqyudeyF2YrSB7iXAVBn90O0FoZbjQWF1G-5nkvJyPe6A1jkdzXTjZQyhc09pXtFHBtlClpZNqWvasxi0vTwcGZuhdj2jXck7dDuMEjRw7AS8SnEMYslWYvExcOw_3ibbkOp_RYDRNB1C1-maprerxXuvXym61TUJsESOq5S3jGPdiUZCLgDYkbmvsDMPBwCFmBiaXOJbmodT4m8_RfNsZipuIzOnmdr4gNgOngpt5Jpz3gaytpreJo0BHUiTA5uXJ2Gc96WEZOyLLiXW6z_r0v06LHKWOdbBQR6eBFc4ZnOM7xMAGA6g-faung4w-dpI218jyKi0n3qXcyNHI9xkKUhWNG6bmutOezc6Xo0Aev_Jnqq60DfS4GQ4IfmG-SCP4A2riwQsxLxDKMzToQeY6Ijc5jg5pojVaKy1KO4R5Mj-s_XKM7DP2x_WvL8WU_Z3F_-fMScO8ns3H4uvsk9OhXSxsDeLwnFwj4ZvtAfmH5wVmoHyI7b16ok815Y2gF22pckeSBVl2bakkhjrv0HegpoYoSQx2otMI1HlRaaUOUfazn0oSRHfYdOlAQkYycDcjrimkgfeOSRttt1NCLQbPKVAGD1J600BGoZiQuAW6ZO1CNk7a3l1ac4tpOUSLyuu3Kw79HTesmNwGG4gzLiWB3zHkVow2Mnwbs0DyP90VTqllbOWnk03xV2z8J5XQd--4rgHgWMwWvDMSQP_X55v4APRdqPjGHxemNhBd_c3QqrNvo928UpvZLePAULJHhcDsD7r3q1_E-NwvBxeFEjQAO4a0HgbPwWn_tw6f6lMWno4fVOjTbuacyi4imgB5giPnJHTbtUx9r4BJiM2Pa-PwLT_rH-IPBMQ7WFzXjwP4APmtHf3hvps8ojhjHLDGOiL-fHNdnAYMYKOicz3audzJ_bZefxWIOJeKFNGjRyA_dRtyMHqK3LMixk7bUJUl8oHXyotNE3NgkgE5yTStCLzyOvamhDc7Df19KEkAOtCEYGR0oTRjO4Gd9tvOhCBypw3UflQhABsg-dJCV1ABHT4KSEMZO_ekhAdc43_UIWf8km5riKAZxGuSO2T0_SqZCteHFAlI0YGCG9ut8xx4B6b8J_UCvGfakfeJ8Jg__nX4QPla9RwT4KKfEdG_qf0VXC8ts4mTB5CAd-ue1e3imMEvaN_gXnwVJlgXw5I4wSsn1sW_Qj_Q_39VvlhaWEM9k95v1Ckiz8sDTDrJBg_9lPWkT2sbpP_m6-bUKHFL8LK4UZVw2e_wrnskMTw8ckrTl3EqTsq7A-8vlg71ZimCOVwG248jqgp1F-lwCHIaZBlPVR901aw_e4-zPtt28R0_RG6bt5jA-WAKNs48wR0_P4qzwy9k-yNNiPDmEbKR4QS1nRXJ8MrLGSMhlO3561OiyQyM3Apw8jomdlQ8GcOabxR7Y-K-FNRjBGt8GpeQPjdbi2mQqw8vtEfOvlf2uuGdrxzI-La-i9z5lnNcXtfsYz74AFwPwtcs9pvsx1G6vHlgt3TiPSSCgjYo15EhyOVhuJFxlGG-2DnvycBjhH6G_Vp_lLo8X8OcSBLEO-Nq0zDz9OHI9V1z5nL2_txoY-EOMLhI-KoY_q5cco1aJBu4HRbhce-nfHMO9YeI4D_i7tof-R_4Aj9eCs4XxT7VG_dsT7XH7AMwOf7Ycxz3C9P-XfrOElRhk9cd6oY7qpyx1otRZhTyyJurVpasD6hVssaHcqCDVlKFlEYICc-En8UZQpgnqkmKMfZRfwpZQnZSDGn4A_Cik9UXhxk4Man8inSSSbW1bdreMn7KKMoPJQLndU5Fbwx_YiVfhRlAUC4ncp8r3xTUVkeJtREkhtw-Iod2OeprLK_Wluw8dC-a43_UvajoXs90ldW1f7mLm5LRaXpqNiW8kHU-ka_eb9Dc7VwYd-LflbsNyum98WBiE0_PRo5uPQeA5n0328--yfXOIuMeMOMfaLxbdfSNQkuBo1lCMmK2jQ-8sY7LsowMd6-p_ZRmHwETp2jUCh66knyFe9fOuNYyfGYpzp97IA5ADSgPeuoPpl8t1FC8M0SzOgYzKRysSAeYYGSCT0BwDW7_7AByMdNYcRmOlGxV6ancAeq5hicHBp0vqo7JHaXWYZ0kaKVWidASjBTnO4yCNsjHnV0bnTx95paCNQdxfQjTrragQGO0N0rLS42u5dQmuI4A8sPjtI2Iyp8TJ8MdMnp0rTgoxh3xBhdTe7Q712KBcd6FWSrGHtC4mtdenu8Si4kCwyqsVncwQvgkTjcsBggkACtWIxLpGCF72ufucvT4ulk7JTCjoCB4qFo8cUt_GJ5hDHvzyEZCLnBI-VVYd7os8rBZDXEDxrRQjAc6nGgl2rfu5WvEdXfmKRtggOAdzjsD7AGqjE4GDHcOezFt0kAof2n64eIO3krsPO_CSiWM6j8jp6q9tbhYHjUe7bz78e_4AguRkofQ9vX9VxuD8ueGR3C8f7WjAo_3tOx863-KXF8HGWjHYUdx-4_tPRWI5Sp3xXoL-Lz9KBJJBZaSETY2A-7tnzoQiG1CLQI26UkFAZ86EJQwTuQPWhCSVzgg7n5KEkXLggMcZ70whAn3eVW2J_PtUkIYUtnJCk4I64FCdoE5J5myACBSSKN1wQgwcDJPx7UWlSJRhgSSMeVJCh3uj2l7ljzRu25ZNgT9kdD4aiWAq1kzmeIVTd6RqEAURW6MqA8jQ558ddwdzVUkfaDI_UeK0xztBsGir9uZeb3Qc561qB0WQIiDnl5sgVIFNDl3wMn9UIS3yScoBg427UIScNvzfd3I-dCEXKcHmyST9UIRjOOx6gCmChFg5BAHShCMjJGMgYyBmmhHtzZxkf_3pWhFykbE7kd6LQhjsVxSQlEf69JCUq7ZOw9KSECB5d6AhZLVpVl1GduY5DFRt5bY_I1Q424rfEKYE_ZEDRNQAO-2fwFeQ4rrx7B3tX1K9Hgf7AO14iv9sqso2RyjbOM9v59a9dsuDSk2MvMfo7sMg88e_QjqP1_GulgZbPZO8x59PVAT0ESpLNanYbunwIwRWmFmSR8PLceR0KdclW8hYgYySceXSuLVaJEJ-Q-JaxTbHw8xt-orVL6JAyTp3T5EEaJuGQxuhOSqtzAZxvWZrix2YbhJP3CpOq3SLyqx-sUb4jd_xrViQ2RoxDBodx0P_pnXVOWbCRZLVwR7rcmf8euP-_Or4E8StMDt6Nfp9UwVS-z64Nt-03o-Dyc3Cd3DIc5yHYY_5NfKftq6qvkWfMr2_2RYXzED7APjk_wD1_RdH5qvBUetWy6xZgR3UJBLr1Ddj4Dj4hXinOMJzcl7HDPa9vZP5F5X5oHAk7XjcUaGZbDVLOVZpUt25JI5VPuzxEEYbI7fp07uBxzXN7GXVp08x0XD8xwdznHEwaPGummvUHqu-fs-_tCJxyY-E-LWitOLIE93l9yPVkXYywj_soweePv1XbYYMZgTgD2jNYjz7ALT0Ph0KngeIf-kOxmFTjlsHgc2jk764eo029NcO6vHcKIHYZO6-RpRP9FVYiLLqFqYJQ6cp6itIWXKnKakAhsKSaGRTpCLlHUgUJIFVboBQlSUFVRQoFt6qDrGopZ2xCMPEfZfT1qEjqFKUcZLtdlwX2x-1bSfZ3oovLqM3t_dkrpumo_LJeSj_xPVYV6s_oQN-lEEDsW_KNuZXXc5mCiE8ou_ZbzcfoOp92u3kW61jUde1G_8783uv3jqMo5TMAEht403WOJNwIwAQB1z9nmJ6zy2ICGHQfNYImvme7GYo24D0AGwA5Af9s6rf6xjTYk4QtZ7qUwrqd1PeTyFCxHO55jy99wdvKvdcLDoMBnjbbtSBtdaD00XgcWe0xLu0PPX11-q6VqT3ulqtpdwOTc2i4-kyiVomJy7xkfYJIHrjrRghh8YTLC4DK83lFWKNB2moo7jTkoSl8QyvG45nY8yFIGnyXdgJZ7KWK2QtdNDaxgtHGyAI4diSVJXfy96qRimwzZGPDpDTA5xoEh1uBaBQoHu9aUxGXtsihqdPLTXz3TGgLaSQzpdGOFi_MbpyxKhFJ5Qo65J616vCieG54wXVQyaC9fazHatdOaoiyuBa7Tx-iRdvJe6fFDd345kSeWOK2aZeWFAg5fdPTJOAevlXClmaeISzwxiiGkurU26txegGppWE54wHHmRXTT-ont72S5vBe2qW8qxxwSBEChdupxtkgZPrmutwJsMkDhA8lrrok61ZugdaGyhIH9jnFEKOGa7uvChiVo-XA93m5UXqfyrTJI3EYhrNmDQfVV-0dEuzvI2u57e4f7lro8p_lPZhXmftFh5cSf4AUMN_Wisjxbzb-jXzXR4biGNe7Dzf05ND8HkVfafcs5ksLps3FuQM_wDeL7FXTwGOj8hh2YiPZw_yFxMfg3YKZ0TuSmdsbmtaxIiTQhFtk0JI8UIQAxsNiDQmlklskjPYEf2otCS2MnGR6UJJJyCDjYjanaEnlzjI270WhGeoDZwNvhRaEAncHPr0otCMAY3FJJLYcrBS2wHUfChCCHDAsMjuDRaEvBLe_ktsOuRilaN90xJH__DKgg7_1MFSBSTnPK2TjPTpmpWnaNAozliNugpWnaG6ggdCelO0IlRjnY4O1O0IAb96dtutFoQQN9vfIxii0JODnb5KaEfKeoHzNFoQwD0HyoQjIBGc_GhCAAxStK0oD1xSJRaUvbbelaaCn3xknOcUiUlibvP0qVcZYyuD2PU1QSuiz2Qp-hsrG506frMvug9Mgbj7AH9V5P_TskgdBxKMX2R18iQR8QR6r0PBXtlEuDeazjTzVddWlxaOYrlCCTnP3W-Br0OB4hh-JM7TDOsdOY8xuuTicLNhH9JhR-B8imdwcKCpG_zrWCQbG6o2KsvGWaOG9HWM8kmOwOx_vXcEwlazE82mj9FS8VBu0ZLiSM9FYkemd65eKZ2czm-P_qJ3S7XMgltuviJldvvDerMNT2vhP9hp5jVAKYQfePL4GrISkn_aRY-ZZCPDl9xx6efyq_DSiN2V_su0P--iAVUcXcRjg20F6lk17qE84tdKsIT_15dP5hB5IAeZm7KOuSKyY7FDg9yyOrLqPHp8Fow-HfPIGMFk6AdT6nVcr7Z04j8o1v266jrvFlx41_bxeDyrtFCnMRyRj_qA9AK-YfafEHF4ZsjjZJv-r3X2LifHxCVkgrK0j-Fe2J-RleGRQysCrKfvDyrzWbML-r0GXIdOS457QeCjFdG7sh79GYm7Ov4AC35KqZIYHZTsupE8YhlO3C89cZ8HxRXa6nZtNaTwyCZHibkkhlXdWUjcMD0I64r0WDxtjK_UHQ-S83xbgwJ7aPQjXTkeo8V275nv5p-24wvIeBuOrpLLiuJvCgupB4UWpsNgN9kn26dH_e8cHLjOHOwY7aDWP8j5lmwHFmcQd92xPdmHoHfo7w58tdF6x0niOG4UR3JMcy7HbH8-VVMlDgr9MOWkgK8S-jlGVnX4atzhUZKTgmJ3Dg0wUUj4VvOglFIjPjqwz4aLCVJP0kA5Mi_jRaVJuXUkHuq_MT0C7k-lIvA3UmxlxoCyuFe3z5oLQfZhImgWdsvEHF18hNpo8MoCQL73ly_3EHl33-YqUGFfjO-40zrz5FfLNFw4hr255TswH8uPIDmF5OF7q3FGq3XGvGWptqup3fuS3ATlh2xiC2T_sS-6M7ZO5rTLKyBvZxaN-fmrMFhpcTIcViTmeefIeDRyATHFek3z4J3t5Ongw-BIwJ6F-gA89u9ZMPKZJgeS38-EMwUgHQrUfs98UHiXgqDSJlkXVNGlNlNEIyGMeSySeecHDYH3c96-n4MxAlYIGDVo262dPrevRfI5o5LMj5yT7D_12-7t7mbSYL3V_FureJijyxyEmSRjsxBG2B7udhjHWrcLDw_hmLdhyQJiAQyhTWgbWDrZ73mrZM78w9-oHz-_RVtnKqzJc2sZjeMrIASSsoX_uD13rryYfDcUifFG0NcL28t_ArOxxaQ4JyKyvJ9Na4WBy93MY05UCqZH67tsvUYGPPyqbJ4-GYIxuNEMzakk1yrm7Y3rpQUsjpBmrc_H-KPrT2cjsyNdm7SURzCdkce6oUkMo_iGMeQFee4a2eNgaQ3sqtuUEbm6o-BB81LEFpcd83O_28U2qtaaa5JIeZgeQDAAx18th-pr05a_C4bNWrufTw9VTyUa3JhgnmyRlfCG-M56_kKzYcCOJ8vTQeZ_ZIGtUwqkgqcbb8rMDSiVd3TyLb2urRj--MKj4pxseg-HavP8GM8I4nJw_wDJIBIzwv2h6G_cF2Mc0Y3BMxR9pvdd9Cru1nS7gW5hI5XGevQ9xXpgbFheVc0sOUpwjpjv1otRQI6-Z700JSgnAwT4qLQgvKAQVyO3pSJTCMKcjK7d-1JCR1bI2FMKKGDsPiaaECFzjB6bbUIRsFVRurMwznyppFJwDjGR2OTQkgqnOAufSkmlRqGODt60WhGgHXGaiShOxAghiOYA96iUimWBLk8pJzuR51YFNJy3PzNtkZGBTtCIK3vHl26HanaSGDy8x3yfnQCmCjCEkYAYnt_rRaLQyAeYD9nqKLTtJxggbDHXG-aaLRMvXJp2mhyBj9eVFpWgFwdjvRaLSgAMHtRaEagZ90mo2kgcEDGaSSB8sdKSdoBQ1NGyymt2jwX4z892Y-IpPr7rVLt1vhdbAoHMUYOmFIP3T0-dQcA4FrtQdPRXAlpDhuFaQa_MF8K8hS4Q98Af-GvJ4v_JQPf2uCkMTvDUemxHoV3sPx-VrcmIaHj8_UFKMvDVyCZIJICe6ggfln5KoGH602D0jkbIPGif7kAfiVaZuDYjV7Cw-F_Sx8EuC30NecRaj_sg5SsjD6oFaoOO_aHBZhLhA8EUaB_7AFcUhguFSexPXnX1ARXGiC8KyWl9FIwUKcnOcd9s1GT_aEOBxuGcw0AT9c-8B80O4CJBeHmDv94E_JQZNNvtPkWeaEqqMDzL_y_lXb8Zx7A42QOw8neGtHQ-476hXLxPDcVhNZW6dRqE3dRxxysIk2J8TJO3KemK7OKYI5SBsdR5HVYSo1xPb2kM1zeXENvbwIZJZpGwkaDqxPlWVz2tBc7YKTGGQ5QudWGvHVoNS9s-rW8gstPsbiPh61lODFZIDzTMD0lnZdyNwuACQa-Y_aPij6L81uBYe4KH-D034173_O4NmDgdxKUbAlvkBqfNx08lgP2Vbu61HjLibXbkjxntvpMhAwOYzKTgf5VR48wCBkTfJH2LmdLisRO7cgn8r3vcRs0aXKD3ZED7AIjNeZj1jafBenmpszm-KpdTtYdRtXtLgbHdW7q3mKi9oeKKkxxYbC5BxpwgbnxY2jVZ0znbZx51CCZ0Lsrl1GlszdV59489nMd14jxRck6bKwHl0HqK9PguIlmh2XluM8AZiQXx6OWw9lP_XevcC-Dwl7VLW61OC39Uiv2kY3Kp096Q58VQMY5hzYGCx7XYjhLMR-LhTV8uX_Lg4fjsuCP3fiDSSOfP3416v8K9tHs446tI5-GeOdLlkYAm1upha3Kf9o5MH9jI9a40sGJw5qRh8xqF6GDE4TFi4ZB5HQ_FbSK_u5VDWsnjL9wSrIP7ACk1T2tb2PNavu2bYX9a_JOfvLVV2MVz442_tR217FROF6tPuRDVdT--HL44mp9qVH_uOipuIfaRoHCcC3PFnF2j-JFIeVHv_yOHmPkoJ5m-QNWRiWY_hgnyVcrIoBmmcGj7AJGvn5FwD2m_tkLcxT-B7FA9_PJmKbiS7iaK2h7EW0bAPK3X3iF-GN66cPDQz4TGH70_r6y57uK57i4Y3M47vIIaPLmfPRcC0fS2n1C4v_65utT1TVJC9_dzvzS3BJ-yT0C-g9BUsVjczcrdANgr4BwnsnF8hzPd7RPNeg_Z7_JbnWxb-rxBG1rYRKBFCNmZeyr9Dzbqa4L3F5JPNehlnbC0Rx7qs_aUS0stCkt7OBIbWwtJVSNB7q4TP5RXTwvdyM6kLHjf7Bke760_JcY4W4guvZVc8Je0bTLqQ6be2KaPrnKo-tiYY52A6lWB3--6u-5z2eE8RMeLlhkbetgfp4jRw8V4_ieCb5ww-NiOzcrvM7E_Fp8KXqawuNR1GFr_QbuNTLFmSON18MxsByFMk8zMASR1GDjAr1OJGGZI1uMbnsjK6rJPMu2oAmgdqXlmGRwLozXUfp5pekQTX4kMD3hkW2tiwiL9RVbJIDbhMAA8uP1NdjBPbgppMQGZQ4051bmwBpfevXXShrSgwGWm3dDQXt-ifuYtXm4ctruyjkP0i4WWQGQSGUhsI_Lj3cMcEkjJIqjiWLwZxXYzEaNNd2srSLLSb1sa3StyymEObzPW7PI0qm-sdQa5_eesrGpky7hOUYcADDBejHAPr171bwP_o8FkJOSLret2RV7jlfgqZmPvPJz7AJr0S9SvYdSWxFxbQ2UZYmV4YyGEew37AIiBv-5NaJ4poMPnEjpHOsgOOl60B0Bujuk97ZCARQ8FAlW3h-j280jPBzl3aMYJTOAQD0OBVuIMrcIxjAA8guo7WdvT-Kvug67JmQw-M72yyCIklBIQW5fXG2azMztaO0IJ51t6XqkaJ02VzZmRrma1dSwktlIJGxdR_rXI-15-6TYbHDeItB_-uGoXY4QO27XCn47dPMbfzwSOGbsI8lnK5Hie-i9gw6j4K7rDWi85iGWA5aLChhuSvn-VNZUnlOMZ9QKEJanl-1nHcA0IpGU6ZUqAPmRSSQJYqM74o6Y6Ci0JvGBtTQhgqMgEY6mnaSLC4zz_93FFoRuGOGKhRjlxjuPSi0JPLjHUA07QjOQcnr2pEoS1QkYUdOu1Ruktk4Fxvylieu9RJtJOFeUlRHjBJy3XH-UWkmHX32KuAQSc_CpgqdpvqCNvU43p-SEfKoGQw6ZIzjemCmjRVxzHG55QC2D0_SldGkI2t5xCtyYj8bEqHA2yOo_SodrGZOyB71XXh_ApZDlzVoklduu1WWoIgAAcdaVp0iKnrii0kBkDHbyp2mgQD0UfKnaSBGRjH9b0WmlqowCBjzqJKKSsbZ65FK00kjGxI33z3p2hFjyxt6UWlSZurS3vIfAuYQ6ZyPMHzBpbqTXFhsKlu-GHB5rW5Vl7JJnK-mRVZatIxH5yr9ND1SLObRm9UIbNBBVrZmHmo8lndREeLbTLjuUOKjSmHg800hAf3gMYwQe3rT1TsIYIw4HK2cZG2KHd4U7UIFg2FYWWsXdtgNKZk6FXOQB8etef8h9m8Fjhmjb2b6Rbpr8jb15LrYTjGJwxp5zN5g9PPf0UjVbW28OG8hDGB8dBuoI2qfAeJzYyF2Cxn5aA0T1byPjR08iDzUuLYSOPLicP7Tft4eH45rivtcXUuLeKNB9lGmzzQwaqhv4AUWjX7wDZVZiSW-CEAebfCsX2lx7sHHfIC66uJoD0q1PhWEbi3tgG7zqejRqT5FC9vOvadw_wKOHYClut7GIY7dGxiCMBVQAdslB8Aa8FwOB82J7Z2tXr8le1-0WJjweAMI0zCgPAcvLZVH_JljzTa9EIwss-hTzKT1YK0eNh610-OO1Z_wBgPgVi-xjMpkBG8bj4Qve3DPhatolkZCcSW64bG_SvP8cAx14n9rv84lsxd1APwVRrOmTWE5Vk27HsaUjMqcMgkCzGtaYupQ4UAToDyE9_Q1mkbmF81rik7I-C5XxNw_bXMjRTxGKQ5AI-0pHf7fanBM6PQrpWHttcc459nlrfK9pqFqnPjmjkXbOfvIf1HSu9hcc6HvNOi4fFODQcQbleNeRXDeIuGNa4auozPJcvaxHEU8ZOYxnpj_p_KvSYbFx4ltt3XzTiXCMTw1_fst5EKNaa_wAW27mfSuINaUIftxXEgx5Zw21Xu7MaSALC105OaJziOv4ACrH7AOKntNtW-p9ovE0bAbgalMoH8PUfu0Dt2D3BWHiGMj2mcPU_qpNr_Qfa1ryNaf4AHHEtzG594vqErKPiS39Zqt8WFh1LB7gtOGxPE8YcjJHn1PzVrpvB015dDUNfvJ9RuM5-vcsPnk5NYZeI1pGKC76E4AXu7TFOLj8ro_DfCGoa1dRW9lbO7E8q8iYA9AOgFcebEF51K9Vh8LHA3QUF6R9m3sY0jh1ItV4gjSW6XDpFnKg-Z8zWCSS1N0hdozQLps-oxKOVegGAANgKoLuQQyMkrhPt1t5LzgXXtWdByx20zAEZBzIiD4s1ugcfvEbT1WjFtBweI8GH9gLlvCGgWfFfAGr4JXqKCw8VSBuhYbkD0YKwx5CoYud-GxTMQ3kuTgYo8Rh5MLL_LtPK-foaIWo_Z94ivrrh-bhHW-VtR4WvY7GTIJ5oSSIm2xkDDD8Y86-l8NxDcdBkjPeLSG9aOo9xoL9zjMNJg5zHKNWuo-Y5-u67a8OlW99c_R1Y2hlGQo7OcAjqDgb4p8sV6fCMxUPDREP-1an7AJdOXM1YGg1VTuzEhI9lQ9SeO4uHt7O2vUjit1hAjkIQSZyrtuQATk8v5axYrDTuxTISWl13qATlqi3qaboDqk5zSDV1VevX3pPiW83PFFCh03T3FxNZrIw5wfddwx7_jvuT0rPxJxwzW4WN1yOBb2lfmHeAIHLxr1TaQ69O6NcvwtQtVitZri3hsZJQPBBZJSAUbJ90N97bBrpu-8doyDEAEjKBlB1B3JHLW_RUyZCRk-PJFqc2ny3k30KF44yqogdwcFftY_PHxqh3bZiJnBxBqwK05KMhaXWzZQgQ55cnmwBue9Gp0UFbQ_Ua3bkP5s4YBth7vcVzftnGJsFiG9APgR-66nCH9MfGfGveCq12ksr55IusUxYbeTGr4BN2-GilPNrT4AsOLiyzSR9CfmVs42DKsiYKsOYZ7giuja49ZdEtQOvMOnlStCNcEhSeUYPrRaVpblpDgnO2AuelJCQ3OCSdiRv46EkRVmDO5Ge586eqRISDljlvjv3poPgjCcy8ysoOQMdz-00IsMd_l0pEoRomTvjoc5FFhFJaxJze-OUenaolySXkYx2qJNopKPLtjJJ7f-0JI2DKeRhkA7AnOP-UkJ8CDmRpVk5c-_y4DEd8Z6VW_tch7Ks3K9r5E2lod35km8idVijeJFVY-aIqqkspJI5sdTj5KqwrmvL3tcSbo70C0AGgdgrZLaGgjTl4g9aQsBMbpIbV4kklygYnl6g5yT0yNqeNMIgc_ENzNbqRV7FOAPLw2M0TolR21zHBOi2uYwSrSIvNhgf8t9t-3bFRdNDJKx_aU40QCasEbZdNeeoJtSDHtYRl0_Tx1TCWzPHK3vfUYzuBuTjAHX7Sr9JmxyNB_NY2PLXfYD-qtrXOaT0-uiaEchJcIzlgfexnYd6tMjRuR0_bzUACkADuKladpRj3-78zSBQk8oUjYGnaEZ5c5AHl0p2hFGnO4QDrtStCXyYCjH9ClaLR8oBwCB8aVotH8Y3yMf1otFpJQdDRaERTHQ-gp2nYRFBnoKLTtDk2p2kgAR0yKLRSamtYJtp7eOQeTKDS3TD3N2Krr3hi1ki57RjE-MqpOUP5RUS1XMnINFZ2WHw-eOSP31bB8xtuKrulsDrFhW-mR_TtGuLPGWjbKAbkZ3H95ryuIIwP2jgkumzDKfXu_PKfReiwg-98Kli5sNj9_quTaXxfpFz_d5NG0-6hu2l4dW2eSJAVilimaTkEnqGbONsqvXG3G-2gGIa2RhvIaPmb5637AGZd2WJ7Jw1c39Gx75VwD5oziey4h45mt9PeYwad_wAugkjKDO3MVB7Fs4PpS4HhXYbDgu3dqsn2pxjcTiRG0mmafr4V0f4AYtjccTays263Gg3HLnqProl_vWD_RENERH54-RXa-xIIklJ5xn9tXuT2dS8-gWqAnMJKHPoa4eH0zDxXosdrlPhXuWt1CyhvYGhlQEdvStT2hw1XNjeWO0WD1jRZrFiwUtEejf3rE-MtOi6cUwesbxFoEWqwNhQJ176eOh-VZnNo2Fvhlyb_LnGoaaGjm0_Vbd3WLLHA-sjz59PQ9x0_I1dDN2ZW3R2y5zxDw1KQYb3TvEt5gRHKEJRx6H5RW2Kd1541TNho5m5X-grjfFfsmuIpmn0EqoY5aBzgD1H5q9BhOLtIyzLwXFfsg8HtMFt0Kj-N7NYlkU3fPdSjcx8vKoP-mpzcTJHc0Cowf2aYwjtu8emw_dbmw4bWJPCWFVEe3KmAq_PoPhXJkxBebJteqgwTIhlAqui6RwT_J9Q1hobu6geG1f3kyv1kqjuint_MayvlL5AtfciGq9BcLcG6ZwvaLHBbor5cbHB8ye5676tZHvvQKouMm_uV07NI2ASxNVGypbKzttDYWU99djCRxswXudq04eHM6yqjiAHU1cb7AGio1032MayBygPHDAo9PHjB_Mn4K0Ra46MDx-RU5Xf7AG7FuP4Aa0e94XFvZHehNSg98f4AMwCMg9_d2_NaXFWdw0ubw9_fB6hSL_iay9jntzsuJtStubRNfsntLzEZcBse4xA32PJ03xnG9dz_LY0RxmyQW7EHUXzG90RdLjfaiAiaOcD2xX7qbpr9ghemdJWx4lFpfcKXn_ysXRJzPHOrocnHun9nYjIwdtq-iYXi5wuEZPxFzczy5za1vS6I5E6Acl5Q4fM8siB03tQNbiGlJPDZ6rbzSXTgvHBIcBQuRtjG2Mb__1aMcJZS8wua_q4AZRoKuyddCqZWdkCA4G-io57e1MscdrzPG3IA7jB58DmHrgk1Rhu1fQmABvl0vT3jdUODbpqsoTNaXdzqAiwngSIC0YwRspKkggkHY4_Gt87I8XjHW76kQ7Q8xsDRFevuVjS6M5uqqZFjQugKzIxIV1OBkH_QB37HzrDme4Bx0PPn7NVVonbGdIHwbS3meVPD6tjLeEebZl3xnpuaZiM8kdPLacDpz4DopMfluxd_BOwHl11GCDeflORkdd_0rP4AaU5sLivI_Ba-HmsZH72Ca1WPOo3IVQQJCdjnHxrLwJ2bhkBP5oRxMVjZQP_itNoo59LtnJDBcoSPQkV2mnRcGYU8qekacwPTbyzTVSAQMMcgz-Ui6kKdpukXepzEWaAqu7SMcKnxPn-VzuI8Vw_CmB-IdqdgNz9eHituCwE_EHZYRoNzyCs30bh3TSF1PUpZpCM8kK_2yfxNcNvEuM48Z8Hhwxh2Luflt8AV1XYDhuC0xMpc7o36H8kJiS44T9eVdNvGAPXnxv4A-Kr24X_QHUzMHp_7AKqkz4HBoRP5_wC6bZeFrkkJDfQkg7DD8-W9XAcfi5xv54_RQJ4RIfzt9x_VD51aRdMI7TXkUsdo54Spz4qmOI8VjNTYQn7oQfgl9xwMn5LEgH7kCER4W1Ahmt2trkKNjFKCfwNRH2lwbTlxAdGf6TSP1UXcFxRFw5Xj7iR8lEl028tQfpNlNHjclkO3zrpwY7DYv6hI13kdfduudNhp8PpMwt8wf4JgKpwFA3_jvWrUKkHROCMZKkKCBv0A2qNpWhhVKllDDGcE7Yp2khyMDyMiqV-AzRaVqRKJreWO1uLSFpIWOxOS2cYBIPb6tYWFmKYZopHZXAelcwCOf0WhwdEQx7RY_mtFOz2V3AESG2K_TDvGArOjISWCgbqufntg1lgxsEuZz9LMWxsgEOHdJ2BdprW1q98MjaDW-1uNCdDrXQKLe3Edw3JDbiGFQFVe5xndsdWNdDBQvhGeR-Z5JN8hfJt8gqJpA-g0UB_LPin_G2EttdXBJ5YYThDLyZXoWz4cbd6z87E5J442iiSO9lzUeQrx1716K6CPMxzjsBtdac9fdoitdR8J7WaaN82vMokRRnDAjfPUjtk0sTw9j2ysi_4AyUS0k1Y6EHQHnpSUeILXNLvy6XQSYY5rdJLdkmiFxFzc_hkuuei5GwVtsn1pSvZiHiVpa4sdVWKP7KubmiyL1TYCxpabAI3rXy8jsVCkhhRF5J-aTmYMnKQFA6EE-fw7V0GSSPeczabQo3r82PDlqqCGtAo6_wApJCFjv1q61C7RFNulK00TIOwxipWkjCdwFzRaE6A6N9W3brjqKVoRcu3QZxuaVoSuXCjA60rQksuQBjcUWhJKGi00nlx2p2ikOQnpQhHybYzvRaLQ8Mdyc0WhAIACT4KeySx3HN_pXD3NrGsX4NnZlOZ5ZDgFgcHHmehxVGIkZA3tJDQXRwbXzjIwbLzN7RP2g9W1w3Ggez7xINNf-u5umbwzP1GObsuD5kdSd87CvL4RxDcZI1xGXLdH42vy8l28M52GBZD3r3-fv7NFU-xtb638-tNeeMmPwxbx_wATBd3O2NsnA9K8zxN0f3UxDzXf8bFMcUMSRpQA9N1e_tHezVXurjiTTLf32ia8PKNnQf8q_FciQehk8qhwLHWwRPPgrPtHw3tbnjGtX_tx9R5norD5j-Arr55coGZF0K6j9yMYYTQMV_Fx8d6q-0zsrY_64Pwctf2JbbpDy7N3zb6q9tez-Uct3bbYVxIPga5MG5Xcxuleq3uMoD3rYuWd1Cu7WOQMjoGVuoNRcL0U2uI2WJ13h5rdmltlJjz07r7esckdGwujDiM2hWL1rQYtSUMp8K6jyY5VH9H81mLeq6EUxZpyWUbT9LUyW1xaryOcTQH7AA2b6JT0B9atikfE6jstNh2o3WP8w4L0xM3try2xY4KnOCfVeoPw2rY4EjMFNjs3dcqbSvZ5eapJiztJ711GTyqI40H47GotkkfpSplETO8d1v4AhP2UW1nIJJY4NRvI-U5dSLODc7jH6I3r0-NWOposn0XOfKSaauq2GmxabGQjtJK2PEmf_TbDb0G2wG1ZXvL7ACUGitTqVIWF5W5UUkmoValmDd1oNG0AF1klG_XcfpV8cfVZZp9NFYcWzRWGii2QBRM4XHmBufyFb2U0EqiAZnZivOv_XDfQPZFLZscNI9irerPPzt-YqnA9_HtPgVsxJrg-IPUsH7y_ZcD5lKsbrS7tZMEyBMYz37LvVvEjTSCudw6Mua2S1pP2jNBg1bhiylZcSR3KBHC5I2OfxBP8Vj8HiDBOSNiFt4rghj4GYjuHAj8g_BY_2ba7_TPZnpq8YcH3j-lp8bmLU7QLzvHj_zR_eUg5Dr_ynPlXpsNxSOKUw6AHXKfZd4E8ivMTcJnZD2ntVuR7TfGuY5k7rvfs69qvCHtDkt5YHtlubl_r_KUhHi94e8j8w42_Mgr0r0k2OOOZIYyWvFmt7sHQcyB18AvMvwxgcHupzDzG3r0Pgtgkd_pOonS5vDA8dZXjwGViu6EZHQg_Mda6fDW4fiT8sQL2IuyCAd7ratVndnhcWHqnRBLfaXdXJdWlmuD8a-PyrFCuWdlTsNx36AJqUuIbg3u0Ia6gTlsuN923_k6cwfNTDTKwnnemuw3Oip7m3kt55IHdJPDblMkR5lb7ACnv1qUEwmYH0RfI6EeYWd7chIu_LZP-bb28moW8Nxc-AhOXfl5ioGTjA7_D4aua6VjmmJud1ihYHMczyUo2tc4BxoJzTkSXWoPDRgfEZnLDGeucDyrk_aebs-H8h5O9j3upbuEsz8-MdDfuBTGoMralcliQrStkjyz7AKVPg7THw6Bp_sb4lTxBwdi5T7yPzWi4ZTxNM5Rg8kjfhtXWY7RcXEaPVsq82BzAEDrjFStUI4onnlSCIczuwVfIk1VLMyBjpH-AAk-ilGx0rxGwanQeq0etS_umzg0OwlwxXmldTgk9_wAf5K8xwbBHiTzxfGCy4kMB2AHP3_L0XFMQMBE3h2GNV7R8T6vNUV4jCSCKJWLLGuABjc79_OvZSguc1u-gXmyNgEs28Nspa8zLI37Zr9-pqwRRwi5d-idBu6TNqLwjEDxW4AGyqARt3zSOLc32NAjM47J20vBLbtdS3NtJNGp8I865Kgb5K0Ruc5he4a8vJWU6rIUewt_FuRySEE5LMvl33FYGN-8HK_Ub4lUwEOsaKa2varDKzRXb6ECAEfDLjt161ysZwXhuMeXdiB4jT9Ut8XFsZBoyQkdDqPipFvqNlqMhTUdHty3KWMsP1b_eWKyR8ExUTsuBxTm-DxnH_e5aBxGDEH7dQNPi3ulIWw0W7I-haq9tI3SO6TGfTmFVnHcUwf4A5WHzjqw6_wDtOqX3Th-J_wDHmLD0eNPeE1daDqdkplltRJEPvRnnUjPmOlasLxvBYx3Zsfld0dofis2J4VisMMzm23qNR8FD4OWUc6x594-8p2-BrrbLn-J2Bbi_vgrB5TI6tJ4cYztjOBt2-ArBiDFw_CnL3Q0ENsnflvdm1ojD95ddbNmgndTnjF3czwXKyNcj3vq94zzA8oJ6dOo8vWs_DoJH8eOKVhaGab-OGWroa-9W4h7WyOcx1l3w12TVjpkt7DL8dzAhTmYox5W2XOenTGavxvEW4ORvaMcQaFjbU1135FCDDmdpDSNOXNKF-1rE8cNt4XjD3JHAaRMqAw3-Bsd6rdgW4p7ZJX9sp1AsNNE5Sa3I93VTGIMYLWNq-fMaa-9JtIFjiGoczJbxOAWGOfmxsAPL1671bi5y6X_q0AvcNAby1Yu9NxyHNQiZTe1OgB9b7RO3dleyh7m2WZ4rpBLOQ-VB3Yrzd8Y_GseFxmFjAgnLc0ZIbpRI0bda0TZ0tXTQyut7Lpws9OtX8KDZvbQ3EctzCZYwQSgPWuri45ZIXMgdlceaywuY14c8WEiU-LO7rEE8RiwQdFz2qyFhijawm6FWdz8lRc7O4uGiQQScHYGrLSQZfe3HN6Zp2hKWNd88o2yAf_0WhHy-6MjoPxqNoRlTgAntStCMoBzA7Y6UBCLlP2adoSSuOlFoR8ueuBRaaTy-eaLSRhc7YotFo-T6bp1xQmFx_wBpH_R3CfCcsnD7AAjG3FPEb9SO1scyQxtnfnkXYkb9C59cVz4XxSHCtOoJ-A810cLw6Wd4aQbOwG5_Zec72z5rHtx16S84hunco3hm3UmKC2UE5UjoMY6E5O9eO4hxqNp7SQ2eX_DkF67AcBxE7cjhkaOXP16roP4A8MOGvZrwtc6msUeoavMBAtzKoEcBbqYk7EDI5uvwrzQ4lNj9cpNNHT-r1-D8ThsE0vDbdtZ5eQ2UXha3t9JsrTWFX-wyrKxz7wBkSV6fHB-VVSyl8hYdlvjgAiLv9S7DqFieIeH7AA7Yo13bgPAXGQXAIAPmGGQfQmudC8wSXyKg5uZuXny8_wCaeq5t7CbXReDvaFrelWubW31vTJp9OjdjnxFlQzWjA7eJEVOO5TB32rtccc7E4aKTctcL5xo-R5rmfZuOLBYyaNujZGOyjxsZm-beXhqvWnAThb0sDtLDv4q5-GJsLo40W1dJjHuL-iugFxzukyR8ynA6UiLQDSrry2Dodqg4Kxrll9V0BJsywKFfrjsazPj9hbYp60Kyl9pLOTzRASrsQy5DDyIqtundct7JAdlXPptjfp4NxEQUHLgfbj5N9iPj6Wd72nLspdo5qs9M4V0lYozPzXTRjlBPuADOcFR1I2670OmcdBosz9HErQCEIgjjQKi7KqjAFVVaqukqGwlnYKAd_KmGkoLw3VXlhpMVsoZ1BarWsAWV8pdstDp1sAhncdBkf0FaGjRZJHWaCzfFCDUNXtLBlzHCviyf_6X903nKzzWuHus815x_bWnY8DpbjcyajZLjz52Vv-UcOH69B6NKvx-nB3jrIz75iuIewXTbm44nuiZBNaabGWLqSU8Q-6oAPxJ6dqnxhzWxAjcrncILu0dHu0Lfe2Yq2iW9q2dnMpx190VyMBo-16IC4zaxHs54kj0fV5NK1CMxQ3h5fFTI5T504Hkc9PMj0rdi4u0bnbyVI7rtVZcb6ySO7un1_hCddL1pm8QeGeS2vW6-8OkcnquBnpg1PBcWdFTZtWjbq3yK5eP8OyfNLAcrz7_XeDh18RspnAf_Quo8M3MfB3tZ0p7SZHbkvnUmQ5UKDzA4lUfI75TXuOEcYbDN95cTI3LlFVbbs2Rpd8z8LxON4cYXdi9vZvvY-yf6rui7tpt5o_FVppz-HcwfQkti9xPBL8gOCS5w3Rsfd65NdnEYt2GwQkD60sktGmhcaDTXLxXKMWaQRluWhr-c0xOLZ7eHwCy-GoEvM4y0hJyyqeg5QOnpU4u0a93a7nUUNm6aE8zZKpcWloy8t_Pw-CPTo3a8icofdOThdhsevkeldLAH7cM_nIqIVnocdtzyX3ivI0SMzFvu57fka8N9sZ3Pw8eDZ7Urvl-5C772fYBNJiXbMb4_2CqHiMh8SXHNIhkX3sY7_nscdq9axojaGN2Gg9NFwHOL3Fx5rScOJzaezMpHiSuwyfgOtXsOiwYn21cb_9wFPvFTnepbrPSteF4BJqfjuByW8by48j0H-157_TSv65DDx7yOa0ep1-i7fAYwcX2rtmNLvooN7cSXuoPPGSWLe5XomwtiDcPF7LQGjyGn_6q5M0rp5DI7cm0NVv_bSkluWcCQAFnxuu3Qetb95Wwnu-18kVZyt3WFv5b1K8ZvrGt4myQo2LfFu_WuY6Qu1WpkLW7-lRLW1W7lRGGQNnOcNjPXfrtU4IjO8NHr9K4KZHLFNfMId41jeNFZAfdx1z910IphLii1nshpAUr1TIEthY_Vs6ST7eG2F_1xWahhsOB-Z_wH_qJApCHWNUtjhbqQlfuSDm_WsWYjZVGJh5K30rie-jZ3uIIpfqnOwKk_hW3BHM830Kj52byKnWHEtlFCt3dwTRM3uxjHMCR1O2-3wpQkRR9s7nt-qpOGcNQrLTNelWYPpep4kc5wrdfip-Fc3GYHDcRH66YHHqd_fupQYnFYF2aFxb4vdsrl7_Tb4m31uyEc2-bi350gn6Je9cg8Nx_DRm4fLmb7AGP19zt_Rb7v2Ex2mMjyuP9mb6o2UCWS3jQ_Qbjw3iVU5o-YGfmHvNnt2GK6EUcsrv4AcszBxJo0cleyPG97Gy57nMaPwzRGml969z8V0RwT-attCj2ImlRwXUg80nXIDA-6AMbY3xSxEGMkle4S5WkEAiqbVUSCDZJsGiKHJEckLWNGWzz4fI3y8kQgnjF7A8VzG0aiXwgA64_nPbbvU_vUb3QTscwgmsxsHzbpzI5pCJwD2OBHOv19CjktfHU3N5OA7IrEjMjOvNy8wA2AA6eYxUIsSIT2OHZYsjk0NOXNVnck75FJ0ZeM8jqNeZOtelBMlY5J44V8OKGMqp8QnBI3LlRvuOvXyq1pfHE6Q255s6crrQE0KB28NaUDT3Bo0Ar18SPFL4S6RGmHixC4RxAkIyhBOGABJwMVAtge4RGnFhbmLj3gQLaSQBZ6qQMjQXajNdAbeO6PUGjgV7aGGSB3CePEeUquFByD1BJNQwDXzETyOD2jNldrZBdqCNuWhTnLWAsaKJqxpWyYlLXbPcRwwwiKNSUVsbDC5GepJ3rZHWDayJ7i4ucaJ8bdr8DYKpx7Yl7QAAB-iYCluY4B71qVSUEyM52HlTtMo-UFsKD94FRStKx9496VpWgBhshtxjFFoQ5V5stvnrRadogm-ASRRaLQ5dyDsaLTtFy980Wi0mQrErTSsqRxjmd2ICqB1JJ2Ap3SYF6Bct41_aG4H8YWWDRmfiC8TIxbP8dsjdMNOdm3-iMN8q5uJ4vhsOS0HM7oPqV1MNwieYZnjKFwrizjr2p-1eKeG-1VdG0LPLPHCWhtgp6KQD8s78xsTj0rzWN4-5xyk7_Nb5SvT8DgBeLjFAbuO3p1PwV1wF7Lls7P3BNY2dyB487KEvL0Y6bf8Uf4g6g968fjuIuld3tSNh-UfqfFey4fw-LBsLYue7uZ_QeAXUdP06w0q0Sw020jtreP_McYwPn9_OuPI9z3ZnGyum1oaKGywHtavjNc6VoStvMxmcZ6DoD6tdLAsytL05PZDeqjaTZifh2Nxuqxyoe_uEkfkcH9VU81MVbEfw65a_Vb32e6o0-mxRTtlxEFfzJGxqnFMo2FkF81l_bPwNqbWb4b4GtJFqenyJeypCuXZoyD8qD6MAe8PvKMdq6XCsZHmGHxGoOnp0XL8nhZXsM2G0eKOm-lajx6jmNOi7l-zv_Q9O9pnD1jr5kEiuo2a01G1B3trkJkjz9G-0h7g-YNEuDdgZ-zOo5HqFZDj28SwvajRw3HQ_odwu-xD-pfQVcAsZItK6H0NCVpqaIfI0ipBVlxajJwKgQptcqi-06KbaRN-zdxVTmWtLJCNlntR4elY-NbDMi-W3MPI0m6aLS2YHQpnTVmaURujCTm5M47j_rD5D22-NMx3sh7wBa1dto74oaccnp1NMR9VldMNgrGG0jiHLGuP-1YGgKhziVIhtTLIE7dTUg2youdQVy6CFFix9kczfH7AEFW7aLO3U2snNh7u8uz5pn4NfQKN_z7AEqmQ610XRaNgvL7AO2aTJw7Zok3JIur2vJhsMSIZemN85qXDT7uj9fUK_iA_wDtIo1-I376lyo_Ytwm3DvCYurnDXOqS_SHY7koBhc58_eNYOKYgTzUNgquGQdjCXHd2qp_ateC5u7u1BPLb2eM-TMw_vSwYyi116qM-RWK4u0p7Ax3luWWS3ZSpHUp2PyIx-Fa8LIH2080YiPu2Nwur4LaxDxPw5bXkihnZBHOh7OvX8Z61y52GGQgKtjswtV_FnCGkcT2R03W7WOdX2hmcbhuwLdQ3k3fvVuHxMuGdnhNFQxEEOKjMOIbmb4lyNOHuPfZbqhuODtYuRAHx9Gk95XH4DIdif17b16vh_HQTmvK7nXPzXkOIfZp7B-AczeV7j6dF1Lgn5oDQdZVbDjG37cOoKBH8hUm0kbuC3WI-jAj6avXYbicUwp2h-C8jPgpYSRW3vXXtJcSv80BjljaJnDqQwZcbFWHUfCu7gNZS7lR-SyN3Vjymx4bJYFZLw43G-D7AP4AIP818-c77VPtI1o9mAfEa_4A9RHuXpB_seDFx9qU_A_sPiqbD8IIPLkMw_T6te3u15rZbLRIxDpkIODlSSQOXO5xv4MVa3Zc6c28qeDyqyAqebGT1qSrVpo7G20rVrxRhvCSJT7mJ_vXn6KntOJYKH7k53uA_Rdrhp7PBYqXnQb_yVEtohax-Nyr8soxGD51fOvWR1A3OfaNV-q447uqyHGd4ZdWlskOUgff7Of_D5TWXFO_FcFsgZVuWfPNnHKc78GKz3StU6Uta2hyMTXIB2-6vlXRN4OD7k_XyCmdAi0xA0zvgBVj3Gc47f0NHDGgyOdyA-f4KTVGuXNw0lwAyopCADoOuB-tZMRL20hfy5eQ2SKbUPNMDI3MznmLM2M-e9UpKbpaL8knOrhREwJHbJH5K6HDhcjumUqQTElyXPMFzEoIjRicIex-O1ZJpu2dY25eAUSkgos0c0hAOfEbADYbJxgfKq_FCtNMvtUuLxs37huwLFHHufHB2q2GN88gY1Vuia_cLZtBFO8bCWG2juS45Qxbkx0HmCSNsmsHaywMcCC8srWqzXz-EDnQWbK15Gzbvxqv1RNY3PM3KgUwMsUgIXCs22M58yd6BjsOQ0k-2C4VdkN1-Q208kGCTUdDR20v504IpH02VHgINrKSXClixOBylh0GAd_Squ1ZHjGOY7SRugJqudgVqb323Knkc6Ehw9k7_3yq-ikS3MmoW7C1kdMAxvbIVOYccwI88YI37vWRmFZgJg7EAH4web5vbxrNd6eKtMpnZ-HpyLRXs76tVSjTWiQWKXEMiut2FUI6fWZB97l2xjmHXPTbetkWJfNinRSCjGTZae7RHdza3dGyKVTo2tiDmH2q0O_jXr8pu5s5YLdxLaLF4bg8znEh5hsuPl5VfBi4ppQWSZswOg9kUdT6m9quSJzGm21RHmbSriK1tZ7iIRGVwBy87ggAgZPundgTtVUMmIxUccjnZRrdA2SDoNRo0ga2FN7Y43OaBZ_nTnabltMpbyQwTFpgzcvIeXY_d7sPM1oZisrpGSObTSBd9f_tBRvkq3R2GloOv406pjfsp33rWVSlD6Y0rQgnOu65HYmhCMg42qKdIYHmMDehFIY9aEAIE52wBjyHWmikxe3tlp1u97qF3Da28f2pZnCqPmf0qLnNYMzjQ6lSYx0jsrBZPRcf80_aW4a0mWTTeD_J9cvFyDKcpBGfXHvN-QriYrj0EX5EZvHYfqfRd_B_Z6aY_jGvAan16LmlxN7TvatKlxr57I9jzs4ikXw7BV7YjH6J8-b815DiPH9ZbbI70Gn49V7Ph3AIoKLGa-Ov6fRCLhvRNMni0_S4ZdZ1JlCh8e6Mdox0RR_Edh2HeuO7EyytzOOVv4-K7seCihIvvO_m3gtrw_wdBZGK91cxXN1CPqIkX-i2Hki9z7MfX81zpcSX22PQfErcyMis3LYcgtMQS3QnNZFZvunUtyBzPQUrXLOJrK317jvVUkDEWFnHBGVYjlkYE_1NdVrzDCwdVY1naS5TyHzVrwFbrNpVrazdJkkRvTOazYl1SkhRiNRpzhS5OlaoLab3RHM1u_N59Af5-dSk7_VF7aHkV0uOTOMncd_KsJ1UaorH-JbXHsX5oD60bhvTjNot-UTXbCBSWEYYkyoo2PKWLDy3_Eiu5hMb56YIJjqNiuPisJ2L3YqAb1mHXxXsLhzXNH8n0W213QNRhvtPu15oZ4myrDAJHowzuDuDsa0EFu6yB4dqFPZMUbJpLLzDBqJUgVFlj4xSpNRZrQSLgD3qiWqYdSgNblG5SKhlVma0pbCCSRZWQCQYHMNiR5Hz6dTaSFFzrFKyYcwA6486e6r2Ski9KdItWdhbBAZGFTaFQ916IrtwVdz9UypxBZUjEKlgQSCx-JOf-1ndva3t12Xlr21Twe0v2nLwBYlXsdAuorvV7lRnwpFQgQA_xe9uO2RnvRrgGunfu4UB4aaq2eRuKjGCZrlIc49DRoedHXotMkMdtbgRxiKGGMKiDoqgYA-GK4RJJ725WxoGgC4lxjcG8GoXYOfGuFX7AKQ4_wD5a6kA1rwK0v0idXl8v0Wn840iC40uK5SMEIOWTA3ZCN__7Ks2HkyupSYcwNql9kcxtrjUtBkYFoyJU9QDj6v91ox4zBsizhvZuLV0SSJZFKSJzKRgg9xXOBpSBVJq2kx3UXgXIDBvdilYdf9HP-GrAeYUmuy6HZYPV-FdLvZzb-xC0cufDjvF2kU_wP2Yep6jv978MZLELjOnRZ8VgYMTXajyPPyPX1UfSR7RfZfI0nDF2LvTiDzWjqXgZT1Ph55oz-xnHpXpuF_aV0Lu6aJFUdl5PiP2aeAXRix4b677ACuuaB7dOFuNBb-Vf27aHqEaBPo10wMbv0-rkyAR2AODvWv_N4aLAumfK-3yHfw36JNri8akknDGBlNYNvHb8ALXrBPg8o-0QoyPtE9Nj6New1B1XnbFWtzDAltGluN1jQIMegxVwJC5ZOY2l8mV7EJuQTuadoVzpUcDaHfh293xYi58wN8VxJQJOO4UO2DJD_l2sJQ4XiCf_mKqkn6kXIlc4XmG3kua9C6QyyAnqFxrJcsDqszT-ndXRbmLTM2CO2c1XixU7wOq6gFaIrCFJ5WnnKiKIFsknGRvgVdgog9xkk9lvzQExcTvcyNK3ug57dh2_Os88xnkMh5pE2pPK1vpvvAc9wfsgb4gHpWw_wC3wlHd5-CewUNAJCEAZt-gGMjzxXPtRRiPLKOXKncYHX8edAs6BNWcCCKUWUShgoYzSLnBJ6D9eldfDuEMrcONzd-daD0TVUUHMYsA42znGRn1rjjTRRTs1vLbMY5YmUBthnIYHoQRsflUtSaCFLMYtLd8oVfkBcq24yfdUZ_P8V0JD50iyN9t2_h4KV0FtxDErMJLkAchkjEY58tthSOx8_hXJbK92UsZ-ajemguyNNRa5ha0A2eVj5D0S4jDDHcgxvKsoMQOQBnOQx6jqDUZo5JJI3Zg0tN_AggDTTZSjc1jXCiQRX-I4YoDb4j3QU8glyMlMZ-wwG_bI7b1TNJKJczWWAcvIG69ppJHXXy2U2NZlyl1Ei_DyKknTxDKUuL9T8ThIkEfMXyOYKMHYHm33rG3HOmZmiiNuBLjmoCrbZvcirGmyuMAY6nu2NDTfnpXVNXc11G02n4mOWTmUb40QUk4wCc7k-daMLh4HhmLu7FHo6wBeoFaAdPJQmlkbmhrY39VZ6nr8pF1JJqTvcxwJzhwJGQkl2Ow939H4aswrGcNa2F7yGkaA1QA1Oo62FCZxxJLwNb1rmT8Jsn-JPBLCssckfKZC0eCrgnOAatF4qKRkha4OuqNgt0rba1CxC5pbYIq9OaTaq_Mt48sqQRycryxndSQegzU8QWEHDtaC9wsNOxArc0lHf4AUJIaDuOSY64GWz3_1s33VSPYdzvSTQ9wHYk_EU7Ql4HUb1Ep2hynHPynHnii0KDrGs6Pw_p7-rrupW2n2cf2p7iQIufIZ6n0G5qL3tjbmeaHipxxPmdljFlcO4v7AGp7Rp5dH5mPD1zrV0Dyi8mjKQg-ap9o_E4-FcPGcdihH8deZ-g_x5L0GC4BJM4CSyejfqeX41WBfhf2q-066XUOO-IJIoT0gU4VAeqgDYCvF4__Qds7Qlx8dvQL3GA-zxw475MHQan1P48lttC9nHCnC1v5Ikgjl8Eczz3JHIuBucdBXnpcZNiXUSvQQ4WGBuVgQvLzVeLJGstHDWunKeWW6dcc48seX4v846U2tZhhb5XKwkv0bsrzR9E0_RIDDZxHnf4AxZXOXlPqf-Das8krpTZTawM2VlGhdulQJTKlxwquwGT90kk6yjG_WhuppMCyAuP4OytqOra1qg36m6nIEPmqnlWuhi-6WN6K6I957x1-SvtAMdjqt1pi-75Bv9Ex_I_vr6TY-VVYgXTuqTaGZo5H96_VHxTYNaaq97Eu12vOP7uIP-gClE7M2lGswrqtnpV-t9ZQXS_5tGrn89_zzWZ7criFUNlYpIWHKflUUiOaZ4d1Lin2aanNrXs_aOW0uGMmocPTMVtro7kvDjaGb1Gx75q6uFx9js5_QrmYnAWc8Oh5jkf3XoP2ee07hT2n-dNc6BcSQ31oQl_pl0vh3dm5_jQ9V8nGVPnXSIrVc0P1LSKI5cwtQEJBHlUaVlpuSPI9RSITtMFN8gUkWg9oky82MNRVoDqTQtSpxillUs1p1Yd96aiSpEMHMw2qQCg51Kc3uIEWp7Kvcqv1Fgtswz12qt2gV8Is0F5x9tXth1SLWH5knsquEfiiWMfvPUwOaHQoD1Zj3nIxyr53IJ3wBojjiwkf3rE-g6pT8mQv66YXWQ7nk0f7AOSynCXCOkcG6Wum6UsjknnnuJm5pbiUnLSOx6sx3rzeKxUmMlMsn6F1sLhmYOIRM9TzJ5kqXxNqQs9FuGJHMUIH8VTG23LbEO-uP-tAP3JKzDdVjGfUsCa6MTu_-K8i43eS3ckkl2k2lSqhzaRzwEA5bA94H94_GsjgG94KDXZXArDaFA2j4eafMuVjuHa2b15lPL7T4K2uPawEdEpxTg4Lq5QOucb1zBqoKNNCrqUdQQwwQe4pg0dEwVRatpSzRmKbDAgpHK3T7I_5D7s2A0cwVjSG6HYqs0y8Ony_uvVUZ7ctyozfbjP4OfP5fj1k4ZxYUi2tQn5d4G0nWIuaezhu1bcN9mQeocb1ZDjJsOfw3V4LLNhoMVpO2_Hn_1C4f1jjX2eXcJ0q4_f6mWzc3_q1J-WePt9VNjt2DDrXqeG_agw0ycafD5R8l5Xif2R7ZpdhHa9Ofv2Prqu28Ce1bg_j9ms9NvJLLVoxmbSr5RFdRkdcKdnHquflXucLjoMW0GN2_wDNF84xnD4RgHFszSKWyI2AKsPl3rYsSs7X7wDlu_yM4nhyB5VwsUa4zhv6kn0XZw5_61z7APZio7-QwWVxNnHJGzEZ7gV3sxbR6LksFuAWRurSSbUZFRMh2Vww2GCBnFbMTA6fFODBoaN8qXUO6PU5kUm0g5iFPM7Eblu-f1pYyVkbBhothv8lB6KNa263M6xsCFG7HO4ArNhoTPKGqIUjVnDTFIpVKKQueYZzjO46ir6ISh82VuzdE3bqKjBQGXKtvjv7AL_1hu9AoqXZ2rwA3E6rEcZjL_Y9QK6eFgMQ7aWhW1_NSApHGbG1niImeRwRkqCFY57_0oXYaGVr45JvonpaRerpq3MiOs8e-4UAgDqcZpTtwrZHNcXA-iRpSbG3tVzcD-5c4jbJDA46b_Gr4Nho2_jMdmHK9P96IFbqNcRmeRlVJmvGPigqoVQu-fd7YGO561zZc-cmX2uaiV0EENC9lIyLyM0ikqeYuRjlz1rE5uWcYpgLrAaQDyu7-GveucHZmGI6Vr-9FIVGe3hsILySPw2aS45sgJuCNhscHpjzrnvHZzSYmSMG6DKrvWKPQixV2tLSHMbEx1VZd4f8Uebxo-SMuJluY15WZCNiem-56ds1uY9koc4DKYyRVjlrrRrnzrzVBBYQ27zAcv99VbLw-bx3ubl_3bDFEvKeX3pHUbsBsQO_QHvXnDx7_qG4eH4d7nG9dGtdqAXa661oT9rtM4QZ7mmPZMA9SRuQNPPWvJWax2ENjHLa6XHdCciTxZisYeTscY8hzEYHWuTPHj61_wBxOY6BAazvU3m3wOtbmqW5r4HFHcMWe-btNetfHluo2oa5qGlS-BJEsSTESLJBEiljncgkHzJrqt-zXDsXHHPhXlxDdQ9znAaaAhpbWoA5rPNxnFYd5YQGg82tAv33ahWvGGqi9jnurpTFCr9HhKec4P2tu561PFfY_AnDOigZ3nEa5nCtvZ15USN1RF9ocb2gdI_QXyGu-6Q0mk6s62t3bJb3JBf-Vb8EY2BAI-GxNXwYTieABlwbzJGPyP4AaobkH-c0HEYPHnssS0Mednt28AR9eSp72wnsLhrW4XDpuCNwQejA9wa76Cx0XEIBPCdDyO48COS5OLwsmClMUu_hsR1CYKAb43vGtVlUIuUD_WaYckqTizjvg7gGwF_xhxDaaXERmNZX6tm_yRjLN8hiq5JWxi3GldFC-b2Rp15e9cJ4l_al4j8lkl0r2OcEzzE5T566mnKqfzLH0HmOYk-h6VxMZx6DDDRw-Z9y76A-z4-LPcaXfBvvKylp7MONeNbxNa9qPFt5qM5yfB8UlFHkDjb7AKABXieIfaJ85_C18T5BsF7vh_2cjwoBnI8m7ep3K6VoXCOhaDAtvpthGgUdQv4Av4685LPJMbebXoo2shbkiFBXc0sFjA9zdSBI4xkk9BUAC_QIBpUb2F5xHMtxqivBYIeaG2GxfyZvI__2q4yCEUzdRHf4lcxwxW8SwwRLGiDlVFGAo8hWe7NlWDak7DAztlthRaRKlpGBsq7UlFPBcDpRaFF1S5Fnpt3dsceBBJJ-Ck1OIZngeKnF7YXJ_Z9byJpel7e_O6yt_wBb4x_KtuLOaV3gnF3YAev-q_1-E6bxlBfJtFqduUffbxYt1_FSR8qrDu0w_iE9nh3XT3bfBX6u2wvdNZ1XmeIidPlufxGaoY4NKQOVReDbjltJ7Ene0nIX7IwyP-05hqD1US0NJatOr__GqSlSlxsHAJG-KiokKt1LQvpOo23EGk6hcaPr1jvaapZkLNH7ACtnZ0PdG2PzrZhsY_D-bt6LLiMLHiR3tHcit9wb60tFpWo2nCPtihj0zUromO01q3ib-DfEd3UD-hsdunU7Cu7GW4hnaRbDcc1xZmuwrxHNz2PI_ou6W13bXkSzQSI8cozG6MHRx6MNjSFFNwLUh05HIpEaoBS0x1oUSUoqD0ppWkhBRSdqVAoUZNSAUXJcjDFFJBcP7aB9rGpcPeB7POAHV-LtWiLmcgMmk2_e4kz57H2QehwdzyK14bHh2fecRsNh1TMkmYQQavPwHXz-LkfB_B-mcG6WbCxLzzzuZr28mJaa7nJJaSRjkkkk9SevmST9rGY2THSdpJ6Douxg8HHgo8jNzueZPUq9JwMmsq1rGcdXZkgW0Q_8jhMfrV8I1taIhQtY_iSPwdIlQL1MZx8WFaYNX38K374AGT7N1uVjEaWd4x5WgQI223KwAI_IVlsm2qjcLI8RWf0LVbe56LBdxvkdhzAg_j6tXwu7pCsd38_cuigbBux37GsY0VJRPDzAletFphRJYVkVo5FBDDDAjIIqQOthTBVFqmlLInhyYIPuxyN09Fc-Xkf5m1p5hTa7L9JjRtTmsJv3ZfluXOI2b_Q9D7vpSc3MLapuYDqr6a3t7tOS4hVx2PcfA1UDSqBI2WU4l9nOna2UuAHFxBgwzxOY7iIjpyuK24XHzYQ3GVnxeDw2PblnbfjzVpwx7UfaTwEBpvEcMnGWkxjAfmEOrQKOwLfV3GBtuVbyPavZ8O-1bDTMSK8d_wB_9uvC8U-xMguXAnN4bH5PdXku8ez_j_g_2hcN6yeFdZjuZ7bwZbmykUw3lrhsHxYHw6D6bBXyJrq4rERycSwc8ZttPF8vZC8xDBLBgsVBM0tcMho6HfontWUR6bdGce54ZDDzBIB2r0BdouNF_UCx0s93ErW_jHlTAPfBx05v5atGNnazIHaLp3yUVkwARIMsN9znJ7GqM3VRVtpsEEFs1xcSFJJRzBjuoXtnvvXawQbhoDM_cqdUFAit5bqQyJyADd3GcdTnr99a5sMUmKcSPUlRGqkQeBDIILLDu2eaZh9kd8Z6VrjfHG7s8PqebjyCe2yavnZ7l-ZWVVA5FYZOOoP5ay4vEnESWDoNkibTBjRUIlRgQuQ3NnY4IPx6_jWYOI1CSkXkT3M8JVl_9hFOFOfeAx-Wa6WIYZpGOZ-YC_qmUcs7RSJBaLgKrRKUYFiSNzj7AH1qrFy94RM9lqRPJFbw3F9HJZ8pM8SF423LFQNx-G-TURM3EQlr3AEDukn8WpUTpWq6G0cBQymQoyKGYO32iSclSPTfB8jXNa6YPyuFhxoEchV0fG7GnguSchbYNEdeZtOfTVinuotOtYLq4aNvEjjYBQNiQvYKARnOdxiuFxHF4WDDRy4x7msa40dya0716E3t4FdbB4PETSlkLRbht59PDqoicQ3OmJC9lbWpuI5BGktyxkiibGTknbC4Y9M71wHvxf2ifIXkxwEE5WgCRwF69TY0JutF6FuGwvBmtFdpNtZ9kH9ae9UF7xrxHql65u9WSCNg6ZjQKu4JONi2WU4HTqK9Jg-DYTAwAQscdjRNkVVXVCgdbXKxGLlxUlykXt4enyT2ma5IiXOlXmr3H0S9jItpJgJJoHXAHMn3SRtjuBWyLtpC1zGgOa6yLysNk_mOho6-eiqayEAxv5lw06ivDkrG24mtoBam_wA_UwSRvKdsTAZ-xggg9MevTatXEMPjHYVrJWfnA0r2SRvqDfTyVRhiaczDsDv1Uwuka2zK9vcxiLxE5RzKQ46HpuKjG0YkSOGZlmrs_l0sGzoVzXOMRaHUaHz-pNrGEjncEsFhxnGME9q68Dnd9x5D5lU0AAqfynUeHWkl3l0yRVVu_hN939GvLAjh_GWsZoydpJH7ACbrfmQu0LxvCy9-roSAD7xPL0KwvGnH3B3s804alxjrkGnxuCYYj_49wfKOIZZz-gY8yK7_9GxNzPNLlRQSTGmBcN4k9uXtN45LWXs60M8L-ZJsup6ioa9lXzjjwVi_BiOvMK81xD_UYfDWyHvHwXsOGfZHEYkCSUUOp0-G_wAFm9E9jNnLfvrnFV7c6xqU7c81zeuZHdvM5JP8n9V4zGcexOLO9L22C4DgsH3iMzvH5F0fT5J0_TI1itLaOMKMDCjb8dhXFe5zzbja7N6UNAp6KzHYdajXVRJUocsCc759sDck-lAbagSo7Wf0uZLm-AZojzRRfcjPmR95vX4PWd5dAiuqebOTtVdKSVHEWPM3SighSFU9AMCilElPrsKVJIMTjNIBAKzftCumteC9YmBwforqP6rb6tacI0GYKYNBx8D4llOFIDCdNt0ByiIBg4IwlWSauc5Wv_sQHkrjj-Fl0mPUo1y2nzJOMdeUfa_4vNUMNQcWnmoH2T8a-7f8WriwmWexhkU8wKAZ8wP5KoLcpooO9qssYjp_EM0S7JcwZXyyp_sfyq13eZ5IcdbWhjlwKopLkplvJkY896KUSmtU1vStEtjd6zqNtZQgZ55pAufgOp-AqcUEkxyxtJKqdI2MW8gBcl4u_aJ4DeduH8tBl12Cc-G5eRYomPbAYEgZ-8QMV3cJwPFAdrnyn37z8rh4vjuFjd2Abnvl_PnorT2Q-1O-9j6qz4VaXHqF1wFd3RtuJNGkkE91oky4zcR8pPiKMgk7ErnIB3rpNAnIglIz4iNA75Cuc4GIHEwAmMGnt3La5g8x4r3RpWsaXxHpVlrei34F9p-owJcWt1A3NHPGwyrqfUdc7jcHBBAyvaWktdoVqY5rwHNNgqWBioUmUtaluhLRCxxQEiVICgYAp67qJK5x7XvaxFwBbw6NodrDqnFmqxM-nae7ERwxA8rXdyw3jgQ7Z6u2ETJyRdlZEwzzmmj3k_ojvlwjjFvOw8Op8PnsLK4HpOlmxlvNSvr2TUtY1SU3GpalMoWW6l-A2RF6LGNlHmck-dxmLkxr4ztANh0XawmFbhW6G3Hc9f0HQf9Nln1NY6ta0xdTiKMnPQfnRSkBawmrc19rVvbZyFBY58zWqPutJV_stVTxDEbtvo6dJbuKPHoXAqyDQk-Ck41EfT9rczRxyxPG6go2xB7isgOtqq6VHxPZLcWSyKMkKUPxXcfpVkbsrlJhsFq0-nuJ7C3kO_NEp_KqXDUqs7p7YbGo6otNyRhveXrTQCok0KyIVZAQRgg96kCQbUwaVHqWmRyL8Upwp2jlJ6HsrH5D7s2AncKxrsu-ye0i6mGbC8BE0XQn_wpOAOrUOCtVbO1V0VBIntre6Tw54lkX1HT8UCwgHKsxrvA1rfTxajbGZLy13t7u2ma3u7c46xzIQw-HTzBrdhMfPg3XE6vkqMZg8LxJnZ4tgdyvYjyI19NlbaZ7X6PeHbKTSOMrc8WWCoVF4kSwalCM5zIq4S4x5-6x79r2mB-1LZiGTtorwmP6xLoD2uBfmA5Hf8brZ8O8TaBxnCL7AEDVra5Tm-sULyvE-M8skZ95D95FenimZOLjNheVljfC7JIKPiraCOW5vUCMsZY4ZlACqO_yrVh2dtIG8tz9KIGqsb-0HL__ctvnJZT5oDoBXaxDQ9ga7Ru5-QCZVXcXHiIYogEgGAFA3OK5k-K7QZGaMUSeidQfRbc8nL88qmQgj_CZ2286bv4Abw5PzO-A6eqewUYjll91FI5iwVTtnzzWO7STXhNhgqjKglsEYx_ejMBVoAvZXNhayz2PLZ21xJLHz9Ij9sZx9kD99-3auhh8S7D8dxnc1t3ks1elu34a2VrGZwMgPios72K-Havp80E8MTRy80gAWT6MDGcjHRjvnGwFcVonc_O14y2CKG46E2Rr8BIlgGVzTda2laY7NK9naC4BZuZUQgAnYEHO_wBnIHqa0QQ5sTFMACW7nnWut7b0iNx1YOf4-StW1m91Nxai1RbK7cI0aleclN9nboTt6VhxoEbPvIdRYD1qjV2Bz4VXhIQ09nV2R0v0JT016NBiyztNq1wUbxJDzKiZ-yQeoPl6E46V4-DDu-00-Z_dwkVhrRpmNb7O916eSYcEhDG6zv1J6DomdQhVHil064jljjlaSaSBG5Y25tzIrjGRkgeg6HrXsQY2NdI5paHAAAlp0od1pHLnfUlcGQEuBab11Ou97lQpLC-NySvLeEs8iy2pV9iMk47D3gfQnPXFUxzQdkM4MY9mnb3y1vXbxsJPY_NQ73Ox_NEu_wBQeZbSeOUG-j53nPP86jBwCzbHdttugG_ksNgWxyPiLT2TuXdDSb1IA1G3gm-dxDXfmHPW_wCe9WGoKZ9KFykKG0LK8UYIyXVMe8OpPn-nvXY4XiY8QxzZwXTxjvXzF2KO3PwNDZSmByZh7PL3c07w7xFPd3MlrqzC4WZeeMBQoVgBnB7bDOKcD6yYIgBlF6dLN6Lm4o5z2j1L8p4l4Y4G0ebVuJtat7C0mVRA0pJknOfsRxjLO_4AKoJrTJkw8T3udTTzPT6BUQYaXEHLE2yVxPiP29cda5Z3eh-zvT14esrwhW1HUIRNfyKO8UGSkI79cs3T3VrwPFOOYN2IjmhBe-O6P9defiV9B4P5lMU3DvjxRDGvqxVnTl0HrqsRpHs1thqLa_r49zqWqynmlvtRkNxcOfi2y_AV5rG8XxWNP8jtOi9ZguE4Lh9GJluHM6_8WztbO3tRiCMAnqx3J-dcokrpF2bdSkU9qKSKeSMk4G5pUUiVLSMRLk7tS1UCUoLvzMct-npTSR8o6mlSLRiPJDY2oRadRM4HSklaeVQBgYxS1QlADtRqkicbZoTWL5qb84Nu4v4AvpIo9vVxWrB6SX8KR9h_l9QofDUAk1aDb7BjeTGfTA_U0PPcJ6q6fcBaPVrVbvT97dxkOpBHmKpZ3XAqMZAcCVQ8FXEh0MWkjgzWcjWz9P4AAcA_MAGrcQ3v31UWim0eWnu_ZW93EDLDdKpLQttgb8IwaqG1J0DupKPhqVJKXby8rjJpEWkV4_4AblxTqmpcfaxZRTSGK2maMMWyQg93A8l_vX0DhOGjZh2uIXz3_R4yZ2MfBGdGrDaNY3H0-Oznlkh-mJhUDlPFz0Un1P910J5RkLmDZcjBYaphFPYDh1q_AroOkfv3hidlsC1jJFhiqDGcZIyOjA7__9BI6ZrjPczE94m_HovXNgfhdIxlI_nr7Au4-xD2-yeyK-aSCCabgK_lMuq6LFl5NDnY-_cWg7wk7tHnpkjBU1ExnEDs362Nj18_FZZKwrjLCO5-Zo_L8t8PDl5be75K1XTte0y01vRr6C-sL6Fbi2uYG5o5o2GVZSOv7vWFzC00d1rY9rwHNNgqWCaVKRUqNcJlutSAUCVkvaf_RLL2dcJzay6rLf3Di0023YZ8a5YHlyP8FGWb0GOpFWRsb_TtkjYHd39ea-a_tQ9pfEja_f4A7m4r1GG_1K-a81DVGuCbm7lTKorSDHLDH5hEXCABsDBroxwsxJ7TENBAFAHYArnYuWTDjsoHkOJsu5kjx_tGwA8TzXW_Yr_U5PaDpc-m65GlvxBpIVLuMDl8ZOgmC9jnZgNgfIV5ji3DhgXiSM2x23h4fJd7hHEXY1rophUrd_HxXSHcKuTXJXZVTqU-4j4zk0wFa0LNWCifUrq7YZ5Dgf_7ABrQ7RoCm7elCtoRdavaAnIWfxj7ANIJ_Wm05WkqT7YrxWtGMYIzWalWUm7tvGs5wBnIDj8jr6VMHVDdHJ3QXLaXb42xVSDSfuUnKyZAVBFRUU302NFKSYmQ_aUU6TUOaNXBVlDKdiD3FSFjZSFBQXtstGjMedT5TKOuf8D7AH72Z7_IulYIcqCRv36NQIpCUVOObFCWiLOe1CaZurK2vVxPGGI6MNiPgaLISBrZZLVeCZYr9da0a7uLLUIjlL2zfw51_wA2NnHmD1rpYLiU-DNsPosON4fhuIjLO315q94W9rGp6Ay6f_QrJp7YkR_vmyhz8a534aAbjYDJXavoXBPtLhJfw8QcrjpfLy9V4fiX2YxWBuSHvs8Nx6LqEOv-ZxREl5peowXemzN9RPE3NGcd8g7HHb1rv8zF_eX0090beK8w460is4i3M0_Mbe36sO23MexPrtUcKwPcXyeyP9XqkExOzPIZHOef3j2xVUkrpXF7tykm2UZBUHAG2OlRut0KbaxafcqlryzfSSxbmYfVbZwgA3OcDftmszvvTpcorIaGntC6txJ0FanmrmiNza_N8PL1UppZonAtojbwRyMqSckpVnCn-scp3-998_DBt4oYnysgDsxYAK7t7i3-j0NUpNe5tlooa9fco3PyTxwXs9tKMp4knIZeu5wV3J3GfU1o_wBQlkhzCPNuBmFHTTw00VVAPpx-quJ9QtppY5PoEvOqMZGjkRXUFwVd2AJX1ODjFZZXycLw4gDmMe4gkUcrW5ayiybuuo3WouEr41HTyve7KgaHaQXGqW-TIwUGSRcdwTsPP_v81x_tHiTheFyvbuRlHmSB8rVvB4RPjo2u2Bv3a_on9tVjubtJGtUgkQryXMbgNkE8rHrzYBxXS4TgcJgsC3BytJFC9edC620JVWMxZxWIdNtrp5A6JCrGWkQ3n7KXSu08xfCsV95cjA37AFz2rqvhjwcbcrc0ZoChZbegPp8lmALibO-6h3ayRz3BtYJIIXIjIRiBy9Qu53BxmsAwz8g2PEd57eem5uq5bGknvpxyaAo7x5PEn6ltHdSSqqrcK55UIxkLsBsMKRWbDxNDGmIFjQXGut3v-6-am92pDqcTWvT6bJ355WGkaUl3qN9BZ2sULzSzXEoSNPezksTgCvSYORmHYAaGYEk7XW1nyUW26gNVyvW_bE817j2c2vOp-xqd7CfCBIIPgw7NL12Lcq98N34fxb_RYTCvIwozO8dh-q9Nw3_KT84B-I7rD_z9BUFtwzqOqagde4lv_m81CUYa5u5PEn9f8R92Jf9UAHpXgsdxLEY52ad1-GwHoveYLh-D8YzJhmUevMrR2dhbWScltEE_m7n91gLid1qc4ndPnbYCkUglxxknJpJqRHCW6bCkoWpkUQUbDFCiSnMAbkUIRYFLZCUkYJzjamknguRg0iklAAUk7QzQjdFzEdRQmlgkjp2oUVi_aZGW0KGIdGu4j6DA1owujz9K1mrXenzCLhCJTf3cpX3kiRQewyxJ_SlJowBTmPfHktHKm5HY1SqwshpiNpvE-pad0jvYluY_4y-639cprU_vxh3RTOrr-j8jQ_RaNWDqG8xms6QRdKEJ5WIAfPTem0W4DxQ0W4DxXjfiex_e_Feo3yzv8c9wzyEdXJYn4N693DN2MIbzXiZ8B99xb9b_p34VIOk2c5WS4t0kZRhSwzgf0rP272CmnddZ_D8JSHSNBI2XS-BtE_8qsYtH1EGNIpW-iXnUqwAZoj7EpG_L33rkYqUwPzs57j-rqRNa-PJJsNj08PJK414YueG71dUs7JbE43WEloJCPvR56eZRtx2JFX8TECdobdrJjcKAO0aKPht-3kt5-zP_fR7K-IIuE9fmYcE67cbIcldHu3P6JGO0LH_adFJ512yK6ckX3lt_nHxXmMxwUlj2Hcuh8Olr32kIKiUMpGMqVOQQehB7iudlXQzIMGwASAF65O3xNOidAmBegXhb5o32svxdq9zq2mXX7I2zzaRw9yt9oLtdXv84VD98nka0xsBflOw1KUjy1ts3Nhvn6Z3psOq8k65IqTTXdywByFCDoMbBR6DGK6kILu61cbFNbC0yS-7-KRwVxXrfBvEVpx1bhiYJFE8ROPHgOzofQrtv9CjFYaLExHCnpp5rLh558NIMf86-I5r2zaarY6xp1rqumzia0vIUuIZAftIwyDXzySN0Tyx-4K-jRPZMwSMOh1Cp76ZissqqScHAptGtLU1V2nJ4envN3kDSf0FWP5qkXmKicOgTalPLy7W8YTP4zHf4hTeKaB1Vkh1AWmUZqkhVclKQADlxtjFKlHmmdJASB4sY5JXX46bhqmTaswRygedQpRSXAz-U002QDtmhPVRpYgDkDY00wmGiVlKuAVYYIphNCMEEo5yR0bzHrTISUiM8p6bVFBQlgB95BjO-KKSDkyQQcEU6TtF8qKQot5p1peAmaPD5nXY_PzpgluykHluyyr4KaroF6-r4GarLpV27BpTAgaC4wc4ngPuuPUYbyNdrh_GsRgiBeZvRcniPBMHxMEuGV_UfXqtRpHtqPPHoXtB02PQ7q4mwl7E5Om3LH_IDHeBuwV9v9jXusHxyDHQiJvdO58f4LwXEuBYrhurhbeo2_nmugBYwfrU5kCh0AOQxxsAR1B8_Suha4aMNLLIOblZmO5Yco5iMDp09O2aRPRAVnYw5mOoabFdKlvyRIrNusxxnDdBlsnGNs1KB0cEUj4SReU6GrO_LcgCh71cwFxDmDb6bqRJHFp9-8Ur3cEcMSy85BmMTsThmU4VsksD2PNntivPxl-PhLm5XEkjYNDgP7kOR66K9xELwDYrXrR-XVSNM0awCPeSwu0ao_gzfSByvj3sMoGEZRvgnAI9K0Px0kA7bMLDgC2iCTtoSdWkiiaCnFAwi3DSjRv-Dn-qJHBcJOsMF4rG6uRIrh2jcgjZRkBtwTucgEjbO9Kebt_xsQw5mgjYEXdWNxd7DooNBDsrCNT11_wAItA8Fb2e4Uv8dval2JPvAnHNjy71i-0gEzcNhzs-VgPlzW3gZySSy_wBrCmbiMLfTRXYDEBlklcgZBACSLuM7YOPjXq52dnK5t7H7AB8FxAKFJEMkOn25Kt4kjyGKaMurApy5yoA3Bz1PcU8NiuwkLCLB3-a6b5VIXVpV1bxtCscagoiFoXPvF1799V6D0861YjD9m003p3f6vT05Ju2CwntC9pkXCoi0-1tF1PW72NDa6bGeQCNPdE8xH6HEP8urHYZ6jgyywcNgMjzpqfE2SVvwWBn8lMI2DUrkVweJuM-JYv4AjLURqMqQm5t7VByWloefAEcXRsDfnfLeWK8fxLjk-Oi0OVg0AHRfQuGcDw_DpBnAc6rvkDdaDn9rdaZpNtpy84QPMRu5_pXmjZXee8v3ViopUoEpe1KkkageVNCXLNb2Vs95eTLFDGMszHFNjDI7KExqU3w9qNxrNst86RpFKviRqikEISQuSepwMnp1FSmjEZpVh2bVXSrt8KpTQYedCEkKSaEk8o5RvQo2lZNJCL0zRSZR0Ukh86KTBRjOOtFBBWX89US2FvEc7zA_gRV8HtK2M6FFwlEyi_mJOGkRAPLA_wBaUo0aiQ98q8lzjPeqaULWW4nUWV7puujYW9wIpf4A7cnunPzIPyrVCMzSz1UnHu30-R0_RXe2MAY5dqzgJos5pkIUXVbx7HSry4Red4oXZF82xt-dWwMBkFozZdRyXl660-WzunikQ82RkkdTivUlwfsuVDD2baWr5nnCNxxBrUbS2JltoW3V9lZu2fNe5-VYcbiOxZTTqVriis5nbBdQ1nhxeCbkX5jldHu2T-QUG1rOv2JQOy56-hIrnRv68Np3tBXOIHfGg5_qtFzWusWLQ3VvFLFJlJoTuAw64_UH91lGeB9tNFMUQQdQuRe0L2Wz-PbXGq6NHJdaK-9xEu8tof8vUDs3boduvocBxATENdo4fFcPiHDg5hyat6cx4heof2KPbmeNeHJfZZxReK-vcNRA2cjNvd2Q6EZ6lf0P4tbcVHlIeBoVwcLI4AxO3aug_tI8bvw_wlFwbo96LbWOKxJb6Pn7AOj09Bm6uT9YQ8i_zOPKqbELDKfIefJb82ukcI2miefQcyvnLxpxQvEnEFzcaangaTpUa6fpsQ25Ilyc_h7xP4THyFbWQ9jEGO9o6lZWzDETuewdxvdb7Pj9lZv7AIUu9QjfWdQVltoJAgjzufUjzzjPoRUxi2x_hM3KHcLfij54n5luw8Op-qdubaOfT_qRYwxjQcydBynb5cVFjy14Vs8IkgdQ2C9V8HPDFwJoUVunJGmnQRKuSccq4I36FePxoJxUhPMlej8cB91jA6D8Jy8dhbOF-0Ryj8nYfrVDBra3_BNXSrb2DouwVAg_IUe05NjdQFG4TiIsJLpjn-TO8g_yj3R-lTm3roh5skrQxjJzVNKNqSgoSTVs_wDzU8GPs8rj9j7Sm4aApKeDjaoUlaWfeBopATLJTTsoiARg0J2mHhx06UItI5KdItOKuOlCRNLP-prn_g16GO7jK2d8qhZRuOfOCG3wMbEH9b921MhEsVjcKuR-Ug8vqtAyq-CCCCMg-lZardWAployNwcinSdpBHnSpFptgQeYU6TtRr3TrDU4JLe-to5o5FKurqCGHcEHr46k1zmOzN3RemU7LE2f7Gns0uryXhKf55aDayCSXRLlmIiicZ5oH3MeCGGBkbDKnt63h_HHMDWTa-K8hxP_Oxzue_D5071y1_hXVeC_aDw5x7p1xd6BcTxzw4S9sZ8LcWxO4WRQdwcHDr_pxseoHro5WSgOYdF4aWJ8DiyQUQt_q0M-mWVtbSwjweZLiSOKU_Utt7rep8z9VT2sUjPu8bjmohxIrN0I6htnbRXyB0QFjTffbw9UVpAL_x5zdXrDxIpbmZJVYiInPLkjHMDggZ-W1YmNeJo4ImtBAcACC2nDmKJ7pF2aPmmAJA5xJ5WbvT5R0TX04eJDaWUkYjhmRYLeaEGN2OVYs3bIO_njvWjiAbKTIQQA02Wk2ANRQ562deqiyQg5W7WKBGnTU8lItbW6VUk-ixLJHzxtyMDlXbK4DbjHLgEfjW3AMhbMH8h7nEjM0G9AwanTQE3snlcBoAK09_-J_RbcNY3Fw0tqmLbwwoYyM5LEYKoM5OQN89q83xpw-_4AD2OGnaEnyDd9Vu4bphcU77iB7yVBvWNxYtNOwjuIykUkWBztGufej2-yNhv3z9V6t7ZZXhsjbLASHC8paaAvkTd7clyzWTQ-nP08FDgitJJgvixxqYWZS7YCuoz_3XOcbAYzkb1imL82Zm96yPcTSgwBx6aLLe0H2kW_Amkwabp4j1PiC-BOm2UqFFhbGGnlIOfCQ5z7ABHCjcnF0nFfuOGLpxlAILddTqdNtiK95XS4dw-THzCGIWT-eu65Houk3dxc3N7e3T32o3_ia9vZcB5n6X2VUbKg2UDbvXzLiGOl4hKZZT9DovqWAwEPC4eyi1PM9T5KVvBp8NlxbblOr2Dpnu3vZqguJw5HirZNZWvPRaQAYrKpJwdKErRgZNKkWpEUOfePSgpWsn_TL6T-HZaDbNyyahKA2OyDqa3YNgAMh5JPstDRzK1HDMUkelR84wG95BjGFwAB-AH8VilOZxKDQ0Ct8kVXSAUCc9aKStGuAKdItEZN8CikJYPKM96VJIjnqe9FIQyfOghCMEmgBCVkipZUKh4u5W05JJAAqyrnNWRDXRTj3SOFkRbK4dM5e4bmJ7keVOXcBRee-VbsMjFU0gKo1iwTUtPutPc48eNkB8iRsfxq2F2V1qbRmtp5pjQ7p7zSbaeVsy-HyS_7AHF91vzFErMryAhpsa7qcQKjSLVTxVdx2HD1_dytjkgYL-sRgY_Gr4MwulACNwT8LjWncPXnEWpC5ETGEsAcDLMfIDvmurNO2FtDdVMYZXa7LufDeiQcO2KQrCEmdAH_4o_hH564r3F5sqxzr0bsFcyLDdwyW1zEssUqlXRhkMp6g1FpLDmG6V5Ta57b2V5wFrYsLud5tBv2EdncuSTCd-WKQ9iu4DHquO6kV05QzGxZ2aPG4WFrzhJKee4dj08D9bBbO2eS3kDoc5GCD0Irm7_bhbzaxmr4EvwJxXYe2r2Z24ttb0CYXk2m8_Lb3kIz80Y_gJQt6Z7DNd3h_ETIPuuI2Ox52vP4R4aHO-9wCntGo5Ec1Ve3_2xXfE9re8b3EE9hccTQR2-nWcrZksdKTdIzjbnlcs7Y8x5V0oo_vGJDT_MfxP_Lm4mb_thDKAc0mgvcN5e87rmHs44FbVo7b-VHIeX--fyLuchAO_QEmqeJY4scQz0XR4Pw0Nhb2m-58zyXQdf8XjtQsVxacqCPwyg6eGdsfLP91xcPMc2pXpHxtlZTRtp6dFyS708QXF7ZozFXjkjBx1wcg_iK9C1-YBxXnZMOAXtHMFegPZzfG94F0ksADGjxEDzDmvN8QbWJeQunw77x2-F_NaBkBO-4BzWTZbbVdxDN9G0qRx9okBfU9qnE23KbTXeU_SrcWdhb2yjAijVfnjeov_ziVAqyUYAAqNKNp5c4pEJEpqLbUmyftQr6Tf-1Ii2oJ2U8GoUo2nFwBRSd0kuM9KKRmTZWnVotFgY3opFpDLjpRSLSMb9NFJErP4f-U2pcNXEltCr3Nn5fFnrt1A-Irbgn9JMp2Ki8Z2FvNN8A6-mu6IiuSJ7bCOD1xjaoYuHspPBVwvzBaXlFZqWgFIeMN6UBFph0ZDv0opO0yw5TkVKrTUV28LUoW7TxPGfUj3h_6VTq2FRNhwcPL6fFc84j0HUuGOPbLirg24-g6hLBI0JxmKR0ILwyL0ZHU7r0OMjB96u7wvib4G0POoafh_PmuLxXhEXEngbOcDR8Rr4Qfgu58Ae0m3880W7li0fwdVjdV1Cycl2s5lUkFRj3oiPeUntkEZBr1c8jeJytxhkyjavPl67L9_iIZsA92HlZr6nMeS1ebiKyuo15PFkjS5mAGGC5GOXA23O_p8K6OIEWDbHGLzO7pPIAj2T_ufVZWlxa6vNCzUW2ntqaSvIAgZ4mCshcSHAcHqvQ_H0rKIoy77ctoXTNwT3dweg2U2W1uduumvv2-qnpfQTWt7qt48IeazSFI2J5VZWB5F6kqcHpjGcdqq-7z8bDBzbt0h7wGpbRBcToLF6A3sFcJWvzPPQaeIOw8ELSZ2sL29KiDxZLeJ1diSARguAMHYYbJ6YricXPacWw0YvRkjtPAV7jVeq14E5OHzPPNzB_PmhZPCtqYXuRPaRToORHGCHG6gsBnJAzjpXpopqlDxQeGmrs9LGh213Itc1mjcrrolYX2m-0DQ_ZVpZvtQtY7u-vnaHTrGM89xczZ2XfZVAOWYZx7uN8UY2ONlymwwaiiQDeunMjlrStgw7nyBg5-p8lxHR7HVdc1a61zXLn-TquosJLyf_qKPswx-SL0A7_nrXzzifEJOIS5nbDYL-twvh0fCocoHeO_wCi29jbR2ryxIgACofj1_tXMcLAtaybKjalHya3pV3395ID4GXI_MVYwZoi1Rf6U9P0VwvbNUUmUugJJ2JKKSJUlVGKjWqBqudXHNxNx9PyHmhswtpH-Mftn4Ob4K6LgIoA3qpO9ryXTo1SJFiQYVQFHwrnEWq7tLzuR5UqStDOKdItIeTJwKKTCVGMDmbv0opK074aVItDIxvTyotFzKKMqVlK5hiikWhzCnSLVXxFEJtMkUqDghsYzU49HKbDqmuG1YaafETlPjSY9RnrUph3lFxslWTjFVUlaizLuWHzoqlIFUOm_wDJ6rqOm9FZheRD0fZv7MPzrRKMzWuUj_R8VbKObftVBtF0sL_Sbm41C6sOFrJeYykXFxjflXOFz6Z_CuhhQImGV3om8HKGjnr-LV8JcMQ6Jaxyyx_WhcIpx9WPM-ZPesUjy82VB7gO43ZaCWMSoCOoqsBRBTSMR8RTpNKvLKz1axm06_hEtvcLyyIf1B7EHBB7EU2PdE7O3dRc1rwWuFgqJZWM1jbx208xmeIcnikYLgdGI7HGM-ucVKQh7sw0QwENDSbpVXFtxBdW68PzyFILhDPfsDgi0T_Sj1c4UfE1qwrcly8-SqxAD29l134hv_5lx_XNCu_aHxhJqbW4W3sT8VpCBkOVH2QO22QvUbV1xifuUHZN3O6wDANx2Lzv7LoBy0H4AXXeBdHsLKze7tlVixRUYdFwgzj4a4OJJzAFdXMHezoFO4l0SO_0xhAn1sHNIv4AMD5oVWw0VbHJlcuBcU2H_u1RMxDwrgkocdG7ivQ4OTtWVzWPHR9m4OGx-a6R7KXU8JCJRgRXUox5Zwa5vER-MEYHSMjxP0WuIrDS2Kq1pDc3mnaeoyJJTK_6VBn5cVYzutLlLlSvoVB7VVVKBKkDrsKSinAdulFJEokTN0kuOiFfzBqR2pIlSBnao0knQTgUqQhk-lFISWBp0mCm9xQi0RPaikWkHPxp0klBQylWGQRjFANG0XRXNYD7AMF8diE-5Z6iQOXsFY_7AItkfA11pB95gzcwqXjs3hw2K6SQRsa5FLQkH0p0mj2IwadJbJiWA4934KKTBVdejkNvIR_h3CH4Tyn7ANVWRi7Cbj3VF1m1iuDZvMufCuU3_jmBU_rUoiQCOqTwDR6H5lQSPqvBvEEHFegoGv_QFJbctypqNt96Bz2ON1b_rAdsiuvwXiRwEzXPGZljQ9eS5_GeFM4tB3NJBsevgfPquz4MazYcX-RHxPp9wW0-5Tmd2BVlJOGicD_LgjlK9iO-xP0KINxjjO823Q34l8pex8Tyx4ojdTZr2dpleI8vKpREIDBAQRgdu59d6pxNYw5TtyHTwvdIOINhO6iFiSGzQnEK5OPM_wC_zrbjnBgbA3kNUHalfWwgXRLp5phJ48kgSRm3WNQEPlvy9Pj0rx5Dp-OnL6SNvOvad5HlWi6tiPhQv4zz4AqfV9Q03Q-E7rXdbuYvodmkl4nhTDxIwCS3NkYGQBgbbsBXYsuxBDWlvVxBrKNdCDv00WKKPtabd-Frx1pvEGue1bja94915eWCy_9HS7UfYt164HqAd27sT9CuH5ouIOka2IHf8Dl717T_LYAGR2LcNGaN8-Z9F2PRdOWws0BX-yQczf0rx7l7B7_Kkq2L91z1iU_gx_vTcO6CoXqoevYjt7a6_wC4u4n6ROD6tWwC7CTz3D9q32BK-tZ6Uk5EmTk0KJKlRqcA0Ulaj-terp2m3N852hjZh8cbVKJmd4CbKLll_ZnpjLbtqlwPrZS0pY9eZz7YH4auxb_dlHJI7X1W5klSGNpX6yoztWUAqN0EiOVeUgNlgffP41MikVW6KSbAwDSyphCH-xs9hQQnspAIzn4qN1FE8wXocmikUmjIzHJb9U6T2Rhie9FJWnEcDY0UknMjzpJWmbuFbi3eE744xvQLtNpo2oujqIbFY85Idw2-d-arJBZSs6qYxzUaT3TLgb970iE7Wf1pfoWraZqY2RpDZTH6WT_JPwcD4a0Rtzxlik490Hp9f36as7iaCxtpbq4YJFAhdyewHWqWNLyAOaYGY0qHgvSZr2afi3VY_r5RcSwxH7s4htGPkN8eZq_EO1Ebdglnu3Dn4uS2JbO2KzUoHVBW-6ABSIQkOnvc2MZp0naVGeU7UyEkq4mihieedwkcSl2YnYADJNNrC85Qgarkeva35OmS1e7S2uNaYXUhdwpitVOIY9-hJy-PP8b5hseQafl-ayOffe2za-QG3v3V5YRWPC8Fvfj-IGtcsxiQeNNttGeU8rknfmI5tsZ3Oa3EztyObXiownspA9pNDX3LR8G289roMcVxD8Mpmmdos5MfM5IXPoMD9VgxGryVsHM7aq8yRuD0qik1zb2i8IxXkLmNQiSt4kTAf8Uo_p_c1tws5hdYV1CePs3fwql9kF5JGdZ0G6XkuLedZwh8jlWx8wK1cSYDlkbtSw4a2PfG7cLo4U7Vy6Wy6VbbJ9K126uD5m0iS3T7ADt7zflyirXimAdUF2tfz6bK8RQq4O1U0okpQxmikWnAfSilFORnFFISye9JCPm2p0i0pSKKStHSpK0hhTpMJonFOk0Ac0qRaMHBoq0LI-0zSvpejR6lEv1lk-SR15G2P8HBrfgn04sPNDhnZR5K38W1ldd0O2vifreXw5h5Oux_3-1mniMbyFFhsaq2K-lVUpWkYOKaVoHON6KRagaqmbRmHVWU_gwqyPdSvQqLqzCO2LkbJKh_4604xqmT3T7OYRanZR3sLW77AGgSUbyNRboptcWm1R-zXX38W42n8SvJvDseJOae3RvsJfxr_wHl4ke_qyY717PgeLfJCcOToNa_nkvEfa_BNje3FsHtb6Y_Xf3rtumxmW6Un7DjPiMD3x0r0-Cj_SWzsNV4wDVNXEhuJnn7AI2Jxjp5D4MVRNL2sjnnmonVaPXDJBZWVjFBCGJYBBGeWM5wCC24Byeted4aRLxLGz_0WsH7AKW6_NdjG9zB4aLqHO95Xm35q_i2TT_Gz8A0WZWuNUdZLpVkLF1VwsSnbo0g5sfyDtXZmkAiAOl6nawBy0vz39KOChLWlzR3ico8b0-ap_Z3wvDpNla6Wp50s0zI-P4AEkJyzfNq-f8vEHEyulPNfU8LhW8PwzMO3kNT1PM-9dCORv3NY6TUc7XsRP3o2X4MH6lTItiCdio_Eac-iXWBkonOPipB_pU4dHoOrT9KzgYSokn4ahvxFUuFGkXalRrk4qJCVqSBgbGiklmOP9ZH0yDS4G-svZljI_l71pwzaJcpDRpV9olnHZabFFGMDH-bD4hWd_eNqLjrSY1nUPBdbePdwQcDu5OFH5flUw3S0DU67KRCPo8Kx5yVG58z3qBFp3e6TztI4UbknFFIJU-JBGgB696RCgSkSTb4q_jSATTYYjY06TtKG9OkrShnOKKStLU470qQdU5zjpkUqSRFs96AE9go9ryIJQNuaVmPxJqbtUrT2Qe9JO0TYpUi1A1jT11TTbixOA0iHkb6Fxup-RANWRuyPtNpuwdiqW7tdb8lgs9L1HTJLGF3ZtTfmBRlQ_8cZ6sH23xsvNnermhkNubr0RmOWjufl--y1KhVAVAAF2AAwMVlrqo2j9gNh1NFIQBC7UUi0rOdqKTtFy4700Ws5xpdtMlnw-mcai7NOR1EEYBcfPIX91swrQwOlPLRVS94Bg_N8hv6im2fDenzwSNq1jDLNcZZlYZ8MEDCg-QAA-VVds8HulW5uiXZcKcPaXIktjpcSPH7AIbNl-T8c2cUnTSP0cdFChvSl2XurNnf--Q_nVTxqpHcqQSTSARdJi7tYb23e2uFBRxg-nrTqkB1GwuVcS8P-jwlrsXFunI7tBhZ1GyzQ53z4v0zXRgkE7Owf-JzNzkTs9objqFv5MvrPV7CHUrCXxILheZT3B7qR2IOxFYnRuY_I7dON4kotTWgwj-Gblsk3U0k5z-sQPyApy-1XRIG7Ks8jNV0mlAjOO1FKJNpwEUUhLBFIhF0hmki7QJ9alSSLnx0NFJWleL90UmSjD9opAKamYqaKTCQJD3NFKWicD9FFJJq8tY76zmspQCk0ZQ59RVkZyOBTaddVz7gK8k0nVLzRrgkDm8QA-h5X7PBrbjGZwHhVt0JBXRQxIznNc-uSkktvRSjaTnG2adIUXUR_wApLv2z6dSaNU8yia6uLCY9BlSP7EKlGO8p33SnJMcxOe9QpO1g_aXpN3JZR6zpHu6hp8sd9auOomiPMPxxj910uGYg4ecHksnEsL5_wb8ee48x-q9C6JqVrqfC1vxDZY8LV7eK6ix2SRQQPlk_hX06MjD8Qyjd3yXyai3dNxIS24blG55euK5O2igr_XIGllMi7RWypblWVirMB0znPQnc7bVyfs1A93Dvvh9qV7nEeZpvpQXW4yQcUIW7Ma1vuH-rxfruorx97a9W1uL3rDSJCIFzzLlB4UWM-qu38UcdlbCxzWbuNegGvypei-zuHMuIYXatiF-p2-Nldb0KyFnYLn_UnvGvGO1K9nI6yrHrUCKUUxMcSW8hP2ZQPxBH5am0aEJO2R30Xj2NxD154mH8inH_akErQ5fH0q1kPUxhT4Rt_SlI2nlVN2VvGvKMnrVZCCU6D-YopK1mL-NdU4rjTcpYR-9ttzH7AEzWj2I66q26C0l1dw2NpJczHlihQsQPTsKoDbKrCzWjPPf3kuo3XVG5seUjeX6VdqteMo0Uzp3VcPJvgHYVUG0hSbKLAM7fAUFQJ5J15Sx5QdqVKKbzvihO0KdJ2jDN50EIKUGJ6mlSSVTRaUKVJJROe1FJ2kRpyBhzFssSM9snOKZS2R5wfKlSVoZ79opNDmplqaUGIHQUstIJQLdwadaJIhsc96SVo8mmnaHMaKRYSw3nSpJVjGyk4iVZIS1xFacyP2VS_vD98q_hV5DhFpskfbA8P98lZ5PXNVUmgWJGKVUhRLEnlmz7AN_J-oqTwgnUqRk1GkIZNOkaJueGG4iMNxGrxtsVIyKBY1CYdlNhY2DhniDhqee24akt30--J5Ypdvokh__QfxADsNzgD1raZo5gHSaOHxUayHNH_lprG0TT_KCyWV5BBGsYd_tNgdT-msjjnJdW6kO6AE7tmkndpSgZ2opJLopFpW1KkaIiaKS0SC1SpCQZQOhopMBF4me9FIpLWTfrSpKkcvvJmilJQluCJTFKAufsEHYj69OkypCSDpRlSTynfrSISJWA4mtm0riqHUI_dSZlY8o3Ib3W_PBrpM_EhpBNOD1tNNuvpFuA32l2Pyrnkc0OFGlKJJG3eilFIbZiKdJWo2ot_wAjN6LUmjVPYWo-u5_d02f8lz78hTjHeUr_pS5FwzD1qNKQKgajALi0kQDJA5l-IpjQ2psdlIKvvY5rBn8MfhWR8zcO341ugxv5Gk-tg-QEjr70CvomGxn3nh8beYsH0XzDj2FGDx8jBsTY8jr6y3MkRQZKHlIAyR97vQVx1We1ri_7AIV4A1viRZlDfRpIrZS2DHcS-6Bg9Rk5HlXVjbHhMM10A7jWgDqKFAfXzWthOJnzu3JsrzJ7HtBZdJhuJUIl1KU3ch74n2U_4oJ_-q-d8Wl7SctGzdPqfivpPAoPu-C7QjV5v02b4BfquyDCgKBgAYA9K49LpWklsbctFItMXmTbOQN1HOPiN_-VJg1TOoUpCHGR0IyPhSAopAqPwwuLWa3b7wDZ7mWP7wA2R-tWTDvX1ULokK8G5qikIyQAc7DrTAQBZpVGgQ873GoMPeuZC4z9Hp-QH81ZIeXRSJ0VZxlqvM8emRHIT-2QD_xH2V_HenGw7psoDMeSn2MP0Gyitz5sDLnzc7k_j6lRf3ikL97p-BDPKIwcA9T9CooJVi7jARBhRtUaUEjPrRVpo80AIRcxJ2qVIS1pUhLHxopK0eR50UkUAfWlSAUuhSQ6UKJRHHanSSTkUUhFmnSdpSk460qRaGc70Ui0YIopK0C3lRSEWTTpCUD-UqT1VWij7id5O5sgv7nNXu_pAJOHeB8Fa5I6CqU7Q5iB0opJQtOdmW6z2upR8sipyNqlEGyfNS-YVXSe6HNTpCLnyaKQkyHI3opNNHfbyp0hFj0oRolLscUUml0UkjJopFhI346dItJfYUUlaZYg0UnabOxzmnSdp1GyOvSlSLSwx6E0Ui1EmhWVTExx3Vh1B86YCYdSat7h2YwygCaPY-TDzFFJlT8pOYY7ilSgqTjOxF1p0d0o9-3fcjrytt-uDWrDOolqHd4Uo_D6oO6Ru_2pFyw_mBww_I_lUHsykhSvOwO5rULhgCDkHpVVKolNSnD0UhRdQI_d9wf9elSaNVLkUzr37wCnS5_iT71rUox3vRS5EfzdP3GCxI8zVYCYOiitt8KdKdqs4Bvm0H2qCy5QYNbsZbflYbNPAfFj6ZRph8q9NwOW2ujXkvtfh80cWJHi0_MLsBA90l85yTt3ruLw64j61RrFxcafoXAFphW1rUPEcKfsohC_hlif6mrJZ-wwpcTpd-4fqunw7DuxDhG3dxAHrp8BaXwdYRW1sXjTljjVYoh5KBgD4AK-dyEuNu3K-qyAMAjZsNvLktGfU1VSrtIopO0TKHBQ9CMUwKKAUNPYvaxZ3IXkPxXY_pTcNSohHpai31fUrMff4K4HzBB_MVOTVrSou9rzVv2ziqUFM3pJtZQDykqVB-O1MDmmEzzx2FjzsQqohY-gxTqypDvOpYzSw-q60buYZAY3Lj0GyD6vyrQ45G6KT5gP94LTs2WrPlpRU21j4OPON3-_DypFK09mklaLqd_lTpK0qikWjGw-NFItGKSSUM96aEeCaSEoDFCErcdKCE7RHPehJChCSRQhEQRTpCMDNFJWjxSRaOhNChCFCEY3_0UglVq4_wCIXPcWg_4AUavd_TCHHUKzyKppK0ROdqAEr1UHS2yl374A3kw_MVbKNQk3c-ZUwbVVupBDIooo0Q5qKSJRHBFOkwm8elFItDbyoTtGMA570UlaUMUUlugfSmhEBsDTpRtIl3pUgKO1OlNIY78opNAMQciikJ1WzRSCmpNm2-dFICh3-OYvpMA-uhGV9R3FMBTaeRT5ndpcxLMh69R5HuKCKUXClLuYfptnNbA48VCoPke3902HK61Fu6w2l3D28zxv_rwyh8fHY_mD6NaZ23TlOPS2rb2UwaLkBzy4I_yncVlcFWRqlTH3gaEwFG1E_wDy6YdM4H9ipMHeRWhTHEpI0yQLsWljUfORanCO96IPsk_zcKVKN2HqcVUFIKK2MU6UlkOLrttA1XRuKUYp-7NRtbl2H7d8_JIPh4btXV4RJ2eIC5vHIRiOGyN6UR6f9XoBVCMMZ81brzDOxHyr1my-W2vNPtDvP6JvbpdBHL2_DVglsvkJ5Mlvn_zfhXO49L2cIiHgPqfiR7l7T_L8a5w8_kBPqdB8LW-0mAQafCgG5HMfnXjnaleycbKkyHlGBSpRCRkgUUnolJvjIopLml6Uv1tzGeiScw_-gD6ualJsFEmiUmQfR-JrdicC6tXi-anmH5alVxeSDyKt9jVFJbqPOFaSONhlcl2HmAP_kVKtFK9FnuLb_ktEskbDXLYPog61ZG3W1ZG3VRuG4BHZSXWPeuH537Iuw_qfnUpeQUScxJV5bQ87czfKqykSp29QpQRb9oATR75iaaClqPOhJOADzopCPbsKVJWUeTRSSGTTpFowTSpFpWT-UUEWhnfOaKCLRE5p0EwUKKTQoSJQoSQoQgfjRSaLB86E0RyO9CWyAz90VaVqAu2uucf7ALMN_wDqNWn6mgnUKw3qpK0GZI15nIVR3JwKYCY3VXptzkXSW8Rc_SpSewG4q6RuotRG7vNT80lzzySZJGORdlH5TVVg7BS1ThFKkE2iopCFJFpJBpgIQ5T3NFItGB60EJI6SEKYCRKBOBimkm2oTTDgg77KnSlabYd_KnSWZJ5vOgqYSlfB9KihCUe7zA00Wms_OhCqUkOmagYyfqJTn8etTqxas9oWr-CXBGTsahSqWT123FprrSIMLOMn8MM_k6n7AMVarzR0ps3v6fy1eaPPzQROT0-rb8dv-VnIUXiirGY-8vwqKW6jX7v2DqfvOi_6ZamzdB0B_nNROLJCunoAcF7u3UH7APirVmGFk-SHewVZTD3mHqaobsmojqQcVJSBWZ4-0wapw1eWhG8kToPmpx-eK0YZ2SUOCTmCaN8Z5gj8Lq3s310cQ8CaBrUgZ5bzTofE3xiZRyPt8VP817UnmvkMzMj3NPJedOBBNqt5rHEtyMz-1qk9wT7KG5VA9M81eb81OZpx4a-p1_RfTeBYcQYd77_nH3N0HxsrryKI1CDooA_KuIukSm3OW-FFJpNCEtM9qKSKlQKIL0DoZod_Uqf_Mak4Wy1Bx1Cja8ohl06-P7Y3aqx_lcFT6oqUQtpamTbVacxwNqprRJRJZCZ3A-6gH8nP5BTrRSWI1y5a-1WYRHPIRbRf9j1NaY20FbdNNfy1qLW3WKKOBR7kSBR8hVBNm1C1YJhVx51FRJSwcDFBCSMetKk6Sl606SSqRCEpelMISth60UlSAYUUikYwaVJUldKKQhnzFFJIA-QopCFFIR06TRfOikkR-NKkIqVIQyfOnSaPPxopFoZ9KKSQyTvTQoWMazzeduf7AFf-1YRbEHcKDf4AEE3itaaVFG8ikqZZWwoYdQB3x59BTbGN3J0ALOnmlWFpcSyLcaldmSVzygKysG2zygjYbb4ox06U3O5NCOVqZpoCrcooAAupMADAHSoya0lzKm58qrQioRaFCLQoQDaFCEKEIUIQoAUbQ6VIIRZFFIRMAaKRaQ0YIyRvTUS5R5NtqdJjqmGGDtRSsBSlPc0qpO0vIKlc0Ui01jB3oq07UDV7QT2pdftR7j8d6Y3UmOootCv7AKTbGCU_Wwe6fUdjTcEPFG1F4oUsLe4HVSYyfjuPzH91ZHsQkCl6DMHSWLscMP4Af8VBylJrqrxnLopPXGD4arpVhN3I5reND56eMf4AmFTZ1SJ0Kq-MZVWPT8j569iY_Jqvww0cUnewfNX4y-823c1lCkFEmGCD91IJ2q7U4RNZTREfaQ1NpogqyM5XAq4_ZrlN5wTeaZM-2hatc279OD8bnnRR6HmavfcNjE7BI_2Rv418v8vB2WMe3xPzI-i5pwDphsbDTbFh70McYb1Y-839k14fFydpK5_U_wCPgvpuFi-78ZkXQD3_n8kroTbKSayITHMKaaGSelCakWqc8oHbrSKiVIvgI5bS5__qcK36Vxyn4yKkzUFqrcaAP46JniOBrjRLtYx7-J4i_wCZfeH-U4T3lIa2PBSorlbi2juk-zLGsg-YzUC2iQkDeqq7q7FvZ3V8-wUufw2H-VOrICm3fVZPQ4HudThWQZMYMz792_0zVrzlaVM6UPUrbKoAAxWbwULSwT9UUhOAmikWjGc0Ukl5ycUUhKoQjBpIR0ItGBk0JWljAoSR5FCKQyKEIubyoSQyfSlqmjz4KeqSIk0ISSQKEIA0IQyKaChzCikgEA2aSaVnbFCFCk_7AFaPJwDbPn9Ff_1cPYtBNOCzVhZT28PjxN9Jty7xSS29xGriUEHkLMjgdSSMA7itkb8LqS7WOdstZm10Pn66e-kXUNtcPMZYgoVk8TkJ51bIIZMBvmqn3iMYolbETceoTgc-nNd_CNVf2DEm6OAP6ZY7eqqaxSDZavzG1K5jmoUmhk0UlohzHyopGiL3jRSLCP3qKRaVRQStCikrR7ChNJPSmhJ-dCE4ibZIp0oOdySX5KEgLTEq5GaFYFEcb01IJNNMpanNJJJcb986E0WxGDuO9CFnZObR9VEv7ZZ5X5UPQ_L6lT3Cu9tqtNbh-kWEgTBPJzqR6b0RnKVUOiqtBmC3Ma52b3P5_lQ8Kw6tWnU-6BVdKq0JiM2qedwpPyBP5Km3YpHZUXGB57qwiB3Dhh_8lH79Vow-jCm72VqZgA7Z8zWMeCFFlTINSCdpowBxynodjTqtU7VR7Brp9P80454WeUJHKbXUwepyOZDgeuVr2PD9nfdAwbFeE-07MmMLhz194B_VV3DUY-kRHvuf1ryDl9CkK00x93FVUqAmKmpo160kKfYId28tqidVBycv8GnsZ4lxzFCV-I3H9gVKPRyrOoISxIt1bCQj3Jow3yIpAZXKTDqCqTQrhxocEDnMlqZLdh6xsQPyxVsjQJCox7V5qv8ilKWFrYZAacgsPQe81DB3iVdGL06prhOHInvWAzI55T7LnA_T46JtKCCbs9VpVBPSqCopxd_lTQlChCX06UghGo76dNJKpFAKIhhQAjdKXmNFJJwKR2opF2hRSKQopJDfzpIREZp2hFymikrR8vrStNDHqakErQCE9AaEWj9eXqKSaGB5U0Whg9hQi0KVIQoKCoUhxq8B_wD-eb71JVv7AOI-ag7Vw9fos9rvA1xc30mr4NavJpF9LjxSgzHL7nXofQ9R542q6LE0MkgsfJDmB5zXTuo-o5-e6Gi8DXNteR6hr6tyajPF_hIFKRRk7FgudzjIyc0SYnM3LG2h8UgACD7P95rQ2GDPeqO04_4AQtUP2Cd6lTMY6mo2gmkknOwNAUC4FAb0EJhLpKaFCEKEIUiolCgICI5O1NNDGDvTCRKcJwoFChVlNnpQp3SaO9Okwo80eN8daFMJnl9alSaNRgjfrSpJCU5G3bahATYyelJSrRV-uWyyQJMR09x_gf4AWmFKMpGjzm4szbTnL2x8JvVe39VI7puFOVNaq9pcFM-9E_X1Bx_SpydVJuy2SYeMOOjbj8VSqEmT3ru1Ty8R_wAFx_4AlTGjSUFUmu5m1yCLGSngD9tcRD6hrTFpGU36z-fULXSx82X4zWRuyRKZMfMMAYqQQmyoRc1JMrI8CA2n_RK2CMEXWdHuI9_8kw4-fuV6rgsbp4-zHVeS-1Udujf8D8Ehf77Z2BhYYaRoZGlnZXN0SUQaQUV0TmZyYW5kb21Q8Yu-ol7l7frzuPYZviWdwXFlbGVtZW50SWRlbnRpZmllcnZ1bl9kaXN0aW5ndWlzaGluZ19zaWdubGVsZW1lbnRWYWx1ZWNVU0FqaXNzdWVyQXV0aIRDoQEmoRghWQK-MIICujCCAmGgAwIBAgIUDtWU4pfEqD-snpSJWqZdVCOmm1cwCgYIKoZIzj0EAwIwYDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRkwFwYDVQQKDBBJU09tREwgVGVzdCBSb290MSkwJwYDVQQDDCBJU08xODAxMy01IFRlc3QgQ2VydGlmaWNhdGUgUm9vdDAeFw0yMzExMTQxMDAxMDVaFw0yNDExMTMxMDAxMDVaMGsxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTEiMCAGA1UECgwZSVNPbURMIFRlc3QgSXNzdWVyIFNpZ25lcjErMCkGA1UEAwwiSVNPMTgwMTMtNSBUZXN0IENlcnRpZmljYXRlIFNpZ25lcjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCBoroqZDWZYq35WjqLF8fGyZf1iUYC5Q-Q69D-4evGCxksLoWF5HC1Te_7kYaQcPvQUk2bPsMlSJHobxNYxhkajge0wgeowHQYDVR0OBBYEFASk3SFIXoEVkkIO9nABJprp7ChfMB8GA1UdIwQYMBaAFDVGg7HPTh1C8o8XvpWEF_6My_cQMDEGCWCGSAGG-EIBDQQkFiJJU08xODAxMy01IFRlc3QgU2lnbmVyIENlcnRpZmljYXRlMA4GA1UdDwEB_wQEAwIHgDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMB0GA1UdEgQWMBSBEmV4YW1wbGVAaXNvbWRsLmNvbTAvBgNVHR8EKDAmMCSgIqAghh5odHRwczovL2V4YW1wbGUuY29tL0lTT21ETC5jcmwwCgYIKoZIzj0EAwIDRwAwRAIgZXkJDQQUaMXNUFKpZ9o9VQzOJ6xxVDhb6XHDlurucIECIF_UP1I94DTlJ1_SJYdqdbc3QUG1LsjPld1IpZPYiowoWQcr2BhZByamZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2bHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjG4JxoDk7f0WCBPSmuO_q763-NX_GdIeyLELUF0_2iMRzjXoPVHNHMXGRoGjer5WCCkCJeKAl1SpzWz9QJPclxhniskvntOiHaqrB0gN1ia-BoGwLynWCDYcrZ9GFjGw2aWhmxkPJv_3qIyMFHMB_HhQk2sMFoc6xoHXoznWCDrgBYtHuiQ3xpnVeuw46Uq_EGihjrqKGPsW0FqRCR7lxoLTBjpWCBrToKc7cK5qDH8U6y9p5SVfL-rgA0ckQmKzZAdSbpFzhoRQU_dWCB_-MpY4SPC-32PxrtLwZr6b-m-HtoYPAmSME3ZXjJ6cRoSjbolWCBiBFi2tmLjoVX_vcMc_oT7hnOQm26XDB_LSfQPRXjSphoYMgmNWCBiEvIzaXy97sGIrqHOh-63_9cfcbKlY5cWiE4QfwbQ2Roaf6a-WCC0RxCijB4L3KaAyfkEsRCvd27-_hUbVmEPI-P8HQB1Axojptt3WCDzBheYKH7WY7sRbvpN0FofFHdJkJi5ufZjZcU0c9w32xom9zkcWCBKPqccguWqZgAcFBu05m9sLyPmPEmwoMumqzJJXowe9hopgiEMWCBpgOArLy9O_SltJ0808-iGcpT6ksEyIFm1uKeX3PH-mhouJXTHWCC0ZOQedoDTZWfD1ttomls3BmspizTRsA7ZES-GczFHrBoxtd3SWCAu5rG3TeFVu4ijSeZY2NYR6dguJ909IKQ46MfMy7uxTxozUCHuWCAi4x8IXDYBmG_vrEfKzbhjD7pwwHi_wTsrw_tNk1-YKxozmJvXWCBRw-P9vyvO4X2bIqBggq5EM0HisFwfIV_lvJJqBcl3eBo065JtWCBrrwiMpgbVpKSwLNS7r_CRJ9oqe5AuWGpqy9sTp5FXqRo7FJnVWCA0MAkYbMwQnMulodKBYcpK1WDFMxKKHNAHF99NolIOLxo8g0AlWCCoq0aGlh80qV-qu7oqDKQ4aN0nGrEdxPA4qz93hkUPSho8wYijWCDe_zkMJjRZDy-PJRK032eJ8mK6bvPe2ilplhv68nO-_RpBRXROWCCn-2mK5C_-QaqVXMJp9waLKdhaW6czgopylRUYGVRLdxpBiObeWCDaV2Px8d_o38_okI-7Lh5eYmdxt_aVgkXAQgLV3KDmGRpETfSgWCAzMr5m5pcIwNdMGWAsPu2WIoa543J_MSE_SZ8VF51HWBpEVfGkWCDqI1ZOwbth6rXZmMKvDwk82nPKnsiwsLzZyIYBVmeykBpGDhO1WCB-ZsNpyb6TJzySZBlXG8O65TulDibCBTzZReHTnxwfwRpQ3we1WCD7AMUAmHV62ycglN7uMv136p5b5FxGSxswoGqadp-PThpWJCFeWCArIrhI54gzZ6dTeSpse-gaRcp_n1MX6LhaDyEJvAOmohpW_8QNWCCsmeLrkwyrBAnlajDgjTn_3tYVGHYOgs-ZGGbsJdI8YBpXWI1JWCCFCI6zjexNjHa_tEoiLwEnoJQe8ENGRwDy_8W5DiTSuRpjsV-gWCAY-WrrK4QlaTX1iFGOwWjjfCbr3ZhtmpDtt9I-c8hM9BpkUvXqWCDcqFbzlVeidSGZyzIjA5aEi3LGBJh3tcQeKf1LmONbDBpr5KYRWCBnrDGO2a_CwbAkVQsSguaYuS7XJkTYKu2ZcJGo3EPp8RpyiLecWCAyMKQ8PlqWShNwZ0-7D6YnanvD0G9oEJ8xKd_9X8RIOhpz25iKWCBvuN615ZRi71d_5P6LpRg7vA-5JkmOYo6XWFsCnpeBFxp1yrz-WCAyuVkTKG6nlc2G_d5jSJPD-9L7fl2CDEwEUscEO1H91Bp6ddmBWCDfNhKX4jnIFY-POvBZcGBrf-4uwjrQ9O5H_dH81s5VOxp6qVlHWCCAuy6AB4s2OlF0R5kqj0ewXlTaMNXyPC29SyPql_YSDxp7HCnCWCAAeHcEimElwYCroif5NsOfs3opwiFBeISyvGvZ-DcilBp_-3eSWCAilIG66ORApn56Mz5pj5SL_KQNt3pFvOyog2WOw4XnWm1kZXZpY2VLZXlJbmZvoWlkZXZpY2VLZXmkAQIgASFYIIhhLe-U3AAyXiuA8SHJ2goHqemGDg9qqgjqzxYvcym1Ilgg1NQT5vwSoMedfzXnej3Gmhx2ZOE8R4RxD5pv4TNSRxtnZG9jVHlwZXVvcmcuaXNvLjE4MDEzLjUuMS5tRExsdmFsaWRpdHlJbmZvo2ZzaWduZWTAdDIwMjQtMDItMDlUMTc6NDg6MjhaaXZhbGlkRnJvbcB0MjAyNC0wMi0wOVQxNzo0ODoyOFpqdmFsaWRVbnRpbMB0MjAyNC0wMi0wOVQxNzo0ODoyOFpYQJj-CJUOkCFirGl2BtCqESlop88ATaIX37she4fgLEy7U0pJheWhG-5V7zGj3lyGgguT7R-_Pza5GU-FRGfDaoBsZGV2aWNlU2lnbmVkompuYW1lU3BhY2Vz2BhBoGpkZXZpY2VBdXRooW9kZXZpY2VTaWduYXR1cmWEQ6EBJqD2WEA7MRQ108mj3WPVTxUYlBaEF7GpxEUKZoYUOmZqCzRzIhDbTYxGkGclI46cOUTyrtTwv_qE3X6SU7AsU1UUMOBdZnN0YXR1cwA", + "presentation_submission": { + "id": "spruceid-mDL-req", + "definition_id": "mDL", + "descriptor_map": [ + { + "id": "org.iso.18013.5.1.mDL", + "format": "mso_mdoc", + "path": "$" + } + ] + } + }, + "header": { + "typ": "JWT", + "enc": "A256GCM", + "alg": "ECDH-ES", + "apv": "SKReader", + "apu": "QG1XsWVR2rhrh5HQ", + "epk": { + "kty": "EC", + "crv": "P-256", + "x": "YRKnf1dE1yiRmZNEVnldx34AEuIXU_7kRg2DdF5D3pQ", + "y": "VJZuobSDS4ug-fDrhC6yLI4igIXjQRybgmLqGdhsciY" + } + }, + "privateKeyJwk": { + "kty": "EC", + "d": "7N8jd8HvUp3vHC7a-xitehRnYuyZLy3kqkxG7KmpfMY", + "use": "enc", + "crv": "P-256", + "kid": "A541J5yUqazgE8WBFkIyeh2OtK-udqUR_OC0kB7l3oU", + "x": "cwYyuS94hcOtcPlrMMtGtflCfbZUwz5Mf1Gfa2m0AM8", + "y": "KB7sJkFQyB8jZHO9vmWS5LNECL4id3OJO9HX9ChNonA", + "alg": "ECDH-ES" + } +} diff --git a/packages/node/src/kms/__tests__/NodeKeyManagementService.test.ts b/packages/node/src/kms/__tests__/NodeKeyManagementService.test.ts new file mode 100644 index 0000000000..810d7a8c0e --- /dev/null +++ b/packages/node/src/kms/__tests__/NodeKeyManagementService.test.ts @@ -0,0 +1,1899 @@ +import { Buffer } from 'node:buffer' +import { randomBytes } from 'node:crypto' +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { JsonEncoder, Kms, TypedArrayEncoder, ZodValidationError } from '@credo-ts/core' +import { getAgentContext } from '../../../../core/tests' +import { NodeInMemoryKeyManagementStorage } from '../NodeInMemoryKeyManagementStorage' +import { NodeKeyManagementService } from '../NodeKeyManagementService' + +const agentContext = getAgentContext({ contextCorrelationId: 'default' }) +const agentContextTenant = getAgentContext({ contextCorrelationId: 'd5d0141d-9456-49ec-9c52-338d2f4a7c60' }) + +describe('NodeKeyManagementService', () => { + let service: NodeKeyManagementService + let storage: NodeInMemoryKeyManagementStorage + + beforeEach(() => { + storage = new NodeInMemoryKeyManagementStorage() + service = new NodeKeyManagementService(storage) + }) + + it('correctly identifies backend as node', () => { + expect(service.backend).toBe('node') + }) + + describe('tenants', () => { + it('automatically handles new context correlation ids', async () => { + const { publicJwk } = await service.createKey(agentContextTenant, { + type: { kty: 'EC', crv: 'P-256' }, + keyId: 'key-1', + }) + + expect(await storage.get(agentContext, 'key-1')).toBeNull() + expect(await storage.get(agentContextTenant, 'key-1')).toEqual({ + ...publicJwk, + d: expect.any(String), + }) + }) + }) + + describe('createKey', () => { + it('throws error if key id already exists', async () => { + const keyId = 'test-key' + await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + keyId, + }) + + await expect( + service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + keyId, + }) + ).rejects.toThrow(new Kms.KeyManagementKeyExistsError('test-key', service.backend)) + }) + + it('creates EC P-256 key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'EC', + crv: 'P-256', + x: expect.any(String), + y: expect.any(String), + kid: result.keyId, + }, + }) + }) + + it('creates EC P-384 key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-384' }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'EC', + crv: 'P-384', + x: expect.any(String), + y: expect.any(String), + kid: result.keyId, + }, + }) + }) + + it('creates EC P-521 key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-521' }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'EC', + crv: 'P-521', + x: expect.any(String), + y: expect.any(String), + kid: result.keyId, + }, + }) + }) + + it('creates EC secp256k1 key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'secp256k1' }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'EC', + crv: 'secp256k1', + x: expect.any(String), + y: expect.any(String), + kid: result.keyId, + }, + }) + }) + + it('creates RSA key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 2048 }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'RSA', + n: expect.any(String), + e: expect.any(String), + kid: result.keyId, + }, + }) + }) + + it('creates OKP Ed25519 key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'Ed25519' }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'OKP', + crv: 'Ed25519', + x: expect.any(String), + kid: result.keyId, + }, + }) + }) + + it('creates OKP X25519 key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'X25519' }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'OKP', + crv: 'X25519', + x: expect.any(String), + kid: result.keyId, + }, + }) + }) + + it('creates oct aes key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'aes', length: 256 }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'oct', + kid: result.keyId, + }, + }) + }) + + it('creates oct hmac key successfully', async () => { + const result = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'hmac', length: 512 }, + }) + + const publicJwk = await service.getPublicKey(agentContext, result.keyId) + expect(result.publicJwk).toEqual(publicJwk) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kty: 'oct', + kid: result.keyId, + }, + }) + }) + + it('throws error for unsupported oct C20P key', async () => { + await expect( + service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'C20P' }, + }) + ).rejects.toThrow( + new Kms.KeyManagementAlgorithmNotSupportedError(`algorithm 'C20P' for kty 'oct'`, service.backend) + ) + }) + + it('throws error for unsupported key type', async () => { + await expect( + service.createKey(agentContext, { + // @ts-expect-error Testing invalid type + type: { kty: 'INVALID' }, + }) + ).rejects.toThrow(new Kms.KeyManagementAlgorithmNotSupportedError(`kty 'INVALID'`, service.backend)) + }) + }) + + describe('sign', () => { + it('throws error if key is not found', async () => { + await expect( + service.sign(agentContext, { + keyId: 'nonexistent', + algorithm: 'RS256', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow(new Kms.KeyManagementKeyNotFoundError('nonexistent', service.backend)) + }) + + it('signs with RS256', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 2048 }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'RS256', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with RS384', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 3072 }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'RS384', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with RS512', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 4096 }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'RS512', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with PS256', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 2048 }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'PS256', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with PS384', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 3072 }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'PS384', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with PS512', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 4096 }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'PS512', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('throws error when signing with PS512 but key has bit length shorter than 4096', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 3072 }, + }) + + await expect( + service.sign(agentContext, { + keyId, + algorithm: 'PS512', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `RSA key with bit length 3072 cannot be used with algorithm 'PS512' for signature creation or verification. Allowed algs are 'PS256', 'RS256', 'RS384', 'PS384'` + ) + ) + }) + + it('signs with ES256', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'ES256', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with ES384', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-384' }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'ES384', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with ES512', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-521' }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'ES512', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('throws error when signing with ES512 but key is for P-384', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-384' }, + }) + + await expect( + service.sign(agentContext, { + keyId, + algorithm: 'ES512', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `EC key with crv 'P-384' cannot be used with algorithm 'ES512' for signature creation or verification. Allowed algs are 'ES384'` + ) + ) + }) + + it('signs with ES256K', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'secp256k1' }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'ES256K', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with EdDSA using Ed25519 key', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'Ed25519' }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'EdDSA', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('throws error when signing with x25519 key', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'X25519' }, + }) + + await expect( + service.sign(agentContext, { + keyId, + algorithm: 'EdDSA', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `OKP key with crv 'X25519' cannot be used with algorithm 'EdDSA' for signature creation or verification.` + ) + ) + }) + + it('signs with HS256', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'hmac', length: 256 }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'HS256', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with HS384', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'hmac', length: 384 }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'HS384', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('signs with HS512', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'hmac', length: 512 }, + }) + + const result = await service.sign(agentContext, { + keyId, + algorithm: 'HS512', + data: new Uint8Array([1, 2, 3]), + }) + + expect(result).toEqual({ + signature: expect.any(Uint8Array), + }) + }) + + it('throws error when signing with HS512 but key has bit length shorter than 512', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'hmac', length: 384 }, + }) + + await expect( + service.sign(agentContext, { + keyId, + algorithm: 'HS512', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `oct key cannot be used with algorithm 'HS512' for signature creation or verification. Allowed algs are 'HS256', 'HS384'` + ) + ) + }) + + it('throws error if RSA key type does not match algorithm', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 4096 }, + }) + + await expect( + service.sign(agentContext, { + keyId, + algorithm: 'ES256', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `RSA key with bit length 4096 cannot be used with algorithm 'ES256' for signature creation or verification. Allowed algs are 'PS256', 'RS256', 'RS384', 'PS384', 'RS512', 'PS512'` + ) + ) + }) + + it('throws error if EC key type does not match algorithm', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + await expect( + service.sign(agentContext, { + keyId, + algorithm: 'RS256', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `EC key with crv 'P-256' cannot be used with algorithm 'RS256' for signature creation or verification. Allowed algs are 'ES256'` + ) + ) + }) + }) + + describe('verify', () => { + it('throws error if key is not found', async () => { + await expect( + service.verify(agentContext, { + key: 'nonexistent', + algorithm: 'RS256', + data: new Uint8Array([1, 2, 3]), + signature: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow(new Kms.KeyManagementKeyNotFoundError('nonexistent', service.backend)) + }) + + it('verifies RS256 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 2048 }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'RS256', + data, + }) + + const result = await service.verify(agentContext, { + key: publicJwk, + algorithm: 'RS256', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'RS256', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('verifies RS384 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 3072 }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'RS384', + data, + }) + + const result = await service.verify(agentContext, { + key: publicJwk, + algorithm: 'RS384', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'RS384', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('verifies RS512 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 4096 }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'RS512', + data, + }) + + const result = await service.verify(agentContext, { + key: publicJwk, + algorithm: 'RS512', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'RS512', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('throws error when verifying with RS512 but key has bit length shorter than 4096', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 2048 }, + }) + + await expect( + service.verify(agentContext, { + key: keyId, + signature: new Uint8Array([1, 2, 3]), + algorithm: 'RS512', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `RSA key with bit length 2048 cannot be used with algorithm 'RS512' for signature creation or verification. Allowed algs are 'PS256', 'RS256'` + ) + ) + }) + + it('verifies PS256 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 2048 }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'PS256', + data, + }) + + const result = await service.verify(agentContext, { + key: keyId, + algorithm: 'PS256', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'PS256', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('verifies PS384 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 3072 }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'PS384', + data, + }) + + const result = await service.verify(agentContext, { + key: keyId, + algorithm: 'PS384', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'PS384', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('verifies PS512 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 4096 }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'PS512', + data, + }) + + const result = await service.verify(agentContext, { + key: keyId, + algorithm: 'PS512', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'PS512', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('verifies ES256 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'ES256', + data, + }) + + const result = await service.verify(agentContext, { + key: publicJwk, + algorithm: 'ES256', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'ES256', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('verifies ES384 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-384' }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'ES384', + data, + }) + + const result = await service.verify(agentContext, { + key: publicJwk, + algorithm: 'ES384', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'ES384', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('verifies ES512 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-521' }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'ES512', + data, + }) + + const result = await service.verify(agentContext, { + key: publicJwk, + algorithm: 'ES512', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'ES512', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('throws error when verifying with HS512 but key has bit length shorter than 512', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'hmac', length: 384 }, + }) + + await expect( + service.verify(agentContext, { + key: keyId, + signature: new Uint8Array([1, 2, 3]), + algorithm: 'HS512', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `oct key cannot be used with algorithm 'HS512' for signature creation or verification. Allowed algs are 'HS256', 'HS384'` + ) + ) + }) + + it('verifies ECDSA Ed25519 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'Ed25519' }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'EdDSA', + data, + }) + + const result = await service.verify(agentContext, { + key: publicJwk, + algorithm: 'EdDSA', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'EdDSA', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('verifies HS256 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'hmac', length: 256 }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'HS256', + data, + }) + + const result = await service.verify(agentContext, { + key: keyId, + algorithm: 'HS256', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'HS256', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('verifies HS384 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'hmac', length: 384 }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'HS384', + data, + }) + + const result = await service.verify(agentContext, { + key: keyId, + algorithm: 'HS384', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'HS384', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('verifies HS512 signature', async () => { + const { keyId, publicJwk } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'hmac', length: 512 }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'HS512', + data, + }) + + const result = await service.verify(agentContext, { + key: keyId, + algorithm: 'HS512', + data, + signature, + }) + + expect(result).toEqual({ verified: true, publicJwk }) + + // Test invalid signature + const invalidSignature = new Uint8Array(signature.length) + signature.forEach((byte, i) => { + invalidSignature[i] = byte ^ 0xff + }) + + const invalidResult = await service.verify(agentContext, { + key: keyId, + algorithm: 'HS512', + data, + signature: invalidSignature, + }) + + expect(invalidResult).toEqual({ verified: false }) + }) + + it('throws error if key type does not match algorithm', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + await expect( + service.verify(agentContext, { + key: keyId, + algorithm: 'RS256', + data: new Uint8Array([1, 2, 3]), + signature: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + new Kms.KeyManagementError( + `EC key with crv 'P-256' cannot be used with algorithm 'RS256' for signature creation or verification. Allowed algs are 'ES256'` + ) + ) + }) + + it('throws error for x25519 key', async () => { + const { publicJwk } = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'X25519' }, + }) + + await expect( + service.verify(agentContext, { + key: publicJwk, + algorithm: 'EdDSA', + data: new Uint8Array([1, 2, 3]), + signature: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow( + `OKP key with crv 'X25519' cannot be used with algorithm 'EdDSA' for signature creation or verification.` + ) + }) + + it('returns false for modified data', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 2048 }, + }) + + const data = new Uint8Array([1, 2, 3]) + const { signature } = await service.sign(agentContext, { + keyId, + algorithm: 'RS256', + data, + }) + + const modifiedData = new Uint8Array([1, 2, 4]) + const result = await service.verify(agentContext, { + key: keyId, + algorithm: 'RS256', + data: modifiedData, + signature, + }) + + expect(result).toEqual({ verified: false }) + }) + }) + + describe('decrypt', () => { + it('decrypts with A128GCM', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'aes', length: 128 }, + }) + + const iv = randomBytes(12) + const { encrypted, tag } = await service.encrypt(agentContext, { + key: keyId, + encryption: { + algorithm: 'A128GCM', + iv, + }, + data: Buffer.from('heelllo', 'utf-8'), + }) + + const { data } = await service.decrypt(agentContext, { + key: keyId, + decryption: { + algorithm: 'A128GCM', + iv, + tag: tag as Uint8Array, + }, + encrypted, + }) + + expect(Buffer.from(data).toString('utf-8')).toEqual('heelllo') + }) + + it('decrypts with A192GCM', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'aes', length: 192 }, + }) + + const iv = randomBytes(12) + const { encrypted, tag } = await service.encrypt(agentContext, { + key: keyId, + encryption: { + algorithm: 'A192GCM', + iv, + }, + data: Buffer.from('heelllo', 'utf-8'), + }) + + const { data } = await service.decrypt(agentContext, { + key: keyId, + decryption: { + algorithm: 'A192GCM', + iv, + tag: tag as Uint8Array, + }, + encrypted, + }) + + expect(Buffer.from(data).toString('utf-8')).toEqual('heelllo') + }) + + it('decrypts with A256GCM', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'aes', length: 256 }, + }) + + const iv = randomBytes(12) + const { encrypted, tag } = await service.encrypt(agentContext, { + key: keyId, + encryption: { + algorithm: 'A256GCM', + iv, + }, + data: Buffer.from('heelllo', 'utf-8'), + }) + + const { data } = await service.decrypt(agentContext, { + key: keyId, + decryption: { + algorithm: 'A256GCM', + iv, + tag: tag as Uint8Array, + }, + encrypted, + }) + + expect(Buffer.from(data).toString('utf-8')).toEqual('heelllo') + }) + + it('decrypts with A128CBC-HS256', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'aes', length: 256 }, + }) + + const iv = randomBytes(16) + const { encrypted, tag } = await service.encrypt(agentContext, { + key: keyId, + encryption: { + algorithm: 'A128CBC-HS256', + iv, + }, + data: Buffer.from('heelllo', 'utf-8'), + }) + + const { data } = await service.decrypt(agentContext, { + key: keyId, + decryption: { + algorithm: 'A128CBC-HS256', + iv, + tag: tag as Uint8Array, + }, + encrypted, + }) + + expect(Buffer.from(data).toString('utf-8')).toEqual('heelllo') + }) + + it('decrypts JWE using ECDH-ES and A256GCM based on test vector from OpenID Conformance test', async () => { + const { + compactJwe, + decodedPayload, + privateKeyJwk, + header: expectedHeader, + } = JSON.parse( + readFileSync(path.join(__dirname, '../__fixtures__/jarm-jwe-encrypted-response.json')).toString('utf-8') + ) as { + compactJwe: string + decodedPayload: Record + privateKeyJwk: Kms.KmsJwkPrivate + header: string + } + + const [encodedHeader /* encryptionKey */, , encodedIv, encodedCiphertext, encodedTag] = compactJwe.split('.') + const header = JsonEncoder.fromBase64(encodedHeader) + + const recipientKey = await service.importKey(agentContext, { privateJwk: privateKeyJwk }) + const { data } = await service.decrypt(agentContext, { + decryption: { + algorithm: 'A256GCM', + iv: TypedArrayEncoder.fromBase64(encodedIv), + tag: TypedArrayEncoder.fromBase64(encodedTag), + aad: TypedArrayEncoder.fromString(encodedHeader), + }, + key: { + algorithm: 'ECDH-ES', + keyId: recipientKey.keyId, + externalPublicJwk: header.epk, + apu: TypedArrayEncoder.fromBase64(header.apu), + apv: TypedArrayEncoder.fromBase64(header.apv), + }, + encrypted: TypedArrayEncoder.fromBase64(encodedCiphertext), + }) + + expect(header).toEqual(expectedHeader) + expect(JsonEncoder.fromBuffer(data)).toEqual(decodedPayload) + }) + }) + + describe('getPublicKey', () => { + it('returns null if key does not exist', async () => { + const result = await service.getPublicKey(agentContext, 'nonexistent') + expect(result).toBeNull() + }) + + it('returns public key for RSA key pair', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 2048 }, + }) + + const publicKey = await service.getPublicKey(agentContext, keyId) + + // Should not contain private key (d, p, q, dp, dq, qi) components + expect(publicKey).toEqual({ + kid: keyId, + kty: 'RSA', + // Public key should have n (modulus) and e (exponent) + n: expect.any(String), + e: expect.any(String), + }) + }) + + it('returns public key for EC key pair', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'EC', crv: 'P-256' }, + }) + + const publicKey = await service.getPublicKey(agentContext, keyId) + + // Should not contain private key (d) component + expect(publicKey).toEqual({ + kid: keyId, + kty: 'EC', + crv: 'P-256', + // Public key should have x and y coordinates + x: expect.any(String), + y: expect.any(String), + }) + }) + + it('returns public key for Ed25519 key pair', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'OKP', crv: 'Ed25519' }, + }) + + const publicKey = await service.getPublicKey(agentContext, keyId) + + // Should not contain private key (d) component + expect(publicKey).toEqual({ + kid: keyId, + kty: 'OKP', + crv: 'Ed25519', + // Public key should have x coordinate + x: expect.any(String), + }) + }) + + it('returns no key material for symmetric keys', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'oct', algorithm: 'hmac', length: 256 }, + }) + + const key = await service.getPublicKey(agentContext, keyId) + + // Should not contain private key (k) component + expect(key).toEqual({ + kid: keyId, + kty: 'oct', + }) + }) + }) + + describe('importKey', () => { + it('imports RSA key pair with 2048 bit length with provided keyId', async () => { + const keyId = 'test-key-id' + const result = await service.importKey(agentContext, { + privateJwk: { + kid: keyId, + p: '8zBiIsI0_zkkHPqBKiajbKFktWs4b00sB29wx9Q1t2mY59hxka5aqrC2OdzlemEQimSKvnx6729CQd4PAU6mlMDaryS-3eiddJ7f-DoVpytpmaFvsrhsad6KwdOYf2IvjHnLIVTli5asS6Ec-aeXRi9VpJH1nM__eY5otbQfwq0', + kty: 'RSA', + q: '1EWuwEEdZZPi27yxOBJfvmo6eXzaGqvryEg1nm0hfdVKGI32dxEQabzvDUFNHdlvp2pDYs7_NdNfsKYFH9z0vsmvWt9q5whc73fvCMw3I9ryB3uAq9mrpH2m4JyvaDnCmPGD3cvTmpe-0_l9px23LASRnWdeKKjJy3dM1bb4fFc', + d: 'JCjUWV7EcxEwcXMSTjGQ9F_dNEtRAPgNMX2QQs8pwZ5hGzLWYtnvt4m_xA6jxGjJtOBLrdOopOgT7eIacA1DluXGG58CJ40LzXeilctpHYq5isnBYU5ZhwH1E_QQwbUGlNnyYtFhTWRFXDStZNRNRQL6fm_jcn86HC6VRlQ9zkMlld5cqClbCRMC-neloO2CYOJxb24Vfts86iuxj699mAZBXD78tR3FCxsmYo-QhgZpGUHm5qWfdqQkXNJ5K9XCRFEMnjjW6LPbkteSZlsQzgJMl1p-Q50q9JAedBqaG-ovtW6rvMPEu1CfAtx4myH7oia45lCgt6697_xK2UflQQ', + e: 'AQAB', + qi: 'O8L2RkhvxNIJxCJjXM0eP3XhMFIhEUpfYOPyaYT9sqWoDBQI1V9-GXM5yewuNfdM0DdpgtrwFx55V9-dNfUK7gIvV9mY4UhLnUeIBQJHpaMv-wTz5MMsn6Z3zGVduX29iQw-xJgy6wEKBvt7lNO0fGTfHZqZD6JZGrxuVU63-0g', + dp: 'a1fv-We__Og8CI6KdRCZElorGek5_-cQiDeokIwbKdpyo-PmPWe4nZ9i0CexI1O0-WFn3K0VlpqFpI1gEjOlVAPMg4K0vT7wQYnfUrJQ3HlNI4MeysSdFh4lIWlE5vVwB5G7F_thVzwq0TdMkuZm35QFOZ8zywQEwKMblRjs7AE', + dq: 'Vjv4wiGxz5JElwAQ_rZ4LuL43mHaOPuezb6ICdRLxtLfRxplBfnosQwQEVJ2AapTsa95sKpnA3bbaOgJLOiWhOtqUYBx7Wl4V9BhSzGrNOW9bUy--RF0qV5ibN06ZR0R8RAsge5MCIGdBIBWi42G3Fr-zPMxTVNEp2PP0wKB8AE', + n: 'yaZDUMEKq5pNx_ZSoXTqWIvMKJ2nZA2NihSaD6vti2riwd9FqC0EY4oBWKr0CrrGRC63SgywMo2ywTh4SsErIojo2kuVfaFFadsOaIRri1LuhN08tkdWYSSvcUybOXXPxMKkFxXfjzITLg0a3sBwFUQgyxMJScYRrOOgQm3hCOpWNX-aIv7QjTPYxQVssPotn9rVX5wk1K_a_QWhlH1QPyUUUjLnNA7Dwt3yjEpcwCQpb4u8wQhOp5zI4weJ3mHzY-yFYd0z-9fOBA5gpxglUbWbvymm2cr_lcT09Z56IuxMy6TDFIOB05EqIsiJPGNU12sSKO-Ly7nh1gIcLq70yw', + }, + }) + + expect(result).toEqual({ + keyId, + publicJwk: { + kid: keyId, + kty: 'RSA', + e: 'AQAB', + n: 'yaZDUMEKq5pNx_ZSoXTqWIvMKJ2nZA2NihSaD6vti2riwd9FqC0EY4oBWKr0CrrGRC63SgywMo2ywTh4SsErIojo2kuVfaFFadsOaIRri1LuhN08tkdWYSSvcUybOXXPxMKkFxXfjzITLg0a3sBwFUQgyxMJScYRrOOgQm3hCOpWNX-aIv7QjTPYxQVssPotn9rVX5wk1K_a_QWhlH1QPyUUUjLnNA7Dwt3yjEpcwCQpb4u8wQhOp5zI4weJ3mHzY-yFYd0z-9fOBA5gpxglUbWbvymm2cr_lcT09Z56IuxMy6TDFIOB05EqIsiJPGNU12sSKO-Ly7nh1gIcLq70yw', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, keyId) + expect(storedKey).toEqual({ + kid: keyId, + kty: 'RSA', + e: 'AQAB', + n: 'yaZDUMEKq5pNx_ZSoXTqWIvMKJ2nZA2NihSaD6vti2riwd9FqC0EY4oBWKr0CrrGRC63SgywMo2ywTh4SsErIojo2kuVfaFFadsOaIRri1LuhN08tkdWYSSvcUybOXXPxMKkFxXfjzITLg0a3sBwFUQgyxMJScYRrOOgQm3hCOpWNX-aIv7QjTPYxQVssPotn9rVX5wk1K_a_QWhlH1QPyUUUjLnNA7Dwt3yjEpcwCQpb4u8wQhOp5zI4weJ3mHzY-yFYd0z-9fOBA5gpxglUbWbvymm2cr_lcT09Z56IuxMy6TDFIOB05EqIsiJPGNU12sSKO-Ly7nh1gIcLq70yw', + }) + }) + it('imports RSA key pair with 2048 bit length with provided keyId', async () => { + const keyId = 'test-key-id' + const result = await service.importKey(agentContext, { + privateJwk: { + kid: keyId, + p: '8zBiIsI0_zkkHPqBKiajbKFktWs4b00sB29wx9Q1t2mY59hxka5aqrC2OdzlemEQimSKvnx6729CQd4PAU6mlMDaryS-3eiddJ7f-DoVpytpmaFvsrhsad6KwdOYf2IvjHnLIVTli5asS6Ec-aeXRi9VpJH1nM__eY5otbQfwq0', + kty: 'RSA', + q: '1EWuwEEdZZPi27yxOBJfvmo6eXzaGqvryEg1nm0hfdVKGI32dxEQabzvDUFNHdlvp2pDYs7_NdNfsKYFH9z0vsmvWt9q5whc73fvCMw3I9ryB3uAq9mrpH2m4JyvaDnCmPGD3cvTmpe-0_l9px23LASRnWdeKKjJy3dM1bb4fFc', + d: 'JCjUWV7EcxEwcXMSTjGQ9F_dNEtRAPgNMX2QQs8pwZ5hGzLWYtnvt4m_xA6jxGjJtOBLrdOopOgT7eIacA1DluXGG58CJ40LzXeilctpHYq5isnBYU5ZhwH1E_QQwbUGlNnyYtFhTWRFXDStZNRNRQL6fm_jcn86HC6VRlQ9zkMlld5cqClbCRMC-neloO2CYOJxb24Vfts86iuxj699mAZBXD78tR3FCxsmYo-QhgZpGUHm5qWfdqQkXNJ5K9XCRFEMnjjW6LPbkteSZlsQzgJMl1p-Q50q9JAedBqaG-ovtW6rvMPEu1CfAtx4myH7oia45lCgt6697_xK2UflQQ', + e: 'AQAB', + qi: 'O8L2RkhvxNIJxCJjXM0eP3XhMFIhEUpfYOPyaYT9sqWoDBQI1V9-GXM5yewuNfdM0DdpgtrwFx55V9-dNfUK7gIvV9mY4UhLnUeIBQJHpaMv-wTz5MMsn6Z3zGVduX29iQw-xJgy6wEKBvt7lNO0fGTfHZqZD6JZGrxuVU63-0g', + dp: 'a1fv-We__Og8CI6KdRCZElorGek5_-cQiDeokIwbKdpyo-PmPWe4nZ9i0CexI1O0-WFn3K0VlpqFpI1gEjOlVAPMg4K0vT7wQYnfUrJQ3HlNI4MeysSdFh4lIWlE5vVwB5G7F_thVzwq0TdMkuZm35QFOZ8zywQEwKMblRjs7AE', + dq: 'Vjv4wiGxz5JElwAQ_rZ4LuL43mHaOPuezb6ICdRLxtLfRxplBfnosQwQEVJ2AapTsa95sKpnA3bbaOgJLOiWhOtqUYBx7Wl4V9BhSzGrNOW9bUy--RF0qV5ibN06ZR0R8RAsge5MCIGdBIBWi42G3Fr-zPMxTVNEp2PP0wKB8AE', + n: 'yaZDUMEKq5pNx_ZSoXTqWIvMKJ2nZA2NihSaD6vti2riwd9FqC0EY4oBWKr0CrrGRC63SgywMo2ywTh4SsErIojo2kuVfaFFadsOaIRri1LuhN08tkdWYSSvcUybOXXPxMKkFxXfjzITLg0a3sBwFUQgyxMJScYRrOOgQm3hCOpWNX-aIv7QjTPYxQVssPotn9rVX5wk1K_a_QWhlH1QPyUUUjLnNA7Dwt3yjEpcwCQpb4u8wQhOp5zI4weJ3mHzY-yFYd0z-9fOBA5gpxglUbWbvymm2cr_lcT09Z56IuxMy6TDFIOB05EqIsiJPGNU12sSKO-Ly7nh1gIcLq70yw', + }, + }) + + expect(result).toEqual({ + keyId, + publicJwk: { + kid: keyId, + kty: 'RSA', + e: 'AQAB', + n: 'yaZDUMEKq5pNx_ZSoXTqWIvMKJ2nZA2NihSaD6vti2riwd9FqC0EY4oBWKr0CrrGRC63SgywMo2ywTh4SsErIojo2kuVfaFFadsOaIRri1LuhN08tkdWYSSvcUybOXXPxMKkFxXfjzITLg0a3sBwFUQgyxMJScYRrOOgQm3hCOpWNX-aIv7QjTPYxQVssPotn9rVX5wk1K_a_QWhlH1QPyUUUjLnNA7Dwt3yjEpcwCQpb4u8wQhOp5zI4weJ3mHzY-yFYd0z-9fOBA5gpxglUbWbvymm2cr_lcT09Z56IuxMy6TDFIOB05EqIsiJPGNU12sSKO-Ly7nh1gIcLq70yw', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, keyId) + expect(storedKey).toEqual({ + kid: keyId, + kty: 'RSA', + e: 'AQAB', + n: 'yaZDUMEKq5pNx_ZSoXTqWIvMKJ2nZA2NihSaD6vti2riwd9FqC0EY4oBWKr0CrrGRC63SgywMo2ywTh4SsErIojo2kuVfaFFadsOaIRri1LuhN08tkdWYSSvcUybOXXPxMKkFxXfjzITLg0a3sBwFUQgyxMJScYRrOOgQm3hCOpWNX-aIv7QjTPYxQVssPotn9rVX5wk1K_a_QWhlH1QPyUUUjLnNA7Dwt3yjEpcwCQpb4u8wQhOp5zI4weJ3mHzY-yFYd0z-9fOBA5gpxglUbWbvymm2cr_lcT09Z56IuxMy6TDFIOB05EqIsiJPGNU12sSKO-Ly7nh1gIcLq70yw', + }) + }) + + it('imports RSA key pair with 3072 bit length', async () => { + const result = await service.importKey(agentContext, { + privateJwk: { + kty: 'RSA', + p: '3y_fDcS8HyfpRe6s5CQhiwT6OieLYxU1dF-hBMtxc4pH1IHjntu7LVlM_Q6pPdhXYuZXYthRq7art8N8P8mTcflZKOoT3FapIg6vLKRRBvIMbwmHbKq8qGhbIIFEcbm4OQvaUgt8NO1umDwdfU3H05Vb3UzM_v7ivM1fvnOrmyZSzTNucbpm627vg8n9RxeNENoK4zySirraj52VYAdOJE9LFvG9N7C6XZssbXNKmIlUvhgbZPhrKlpRzBCrg6s7', + q: 'zdDYPk9uL3ASmDVBdPxz_f70MGe7RvF52f0yfhZAURApm1Go-fix4fdF4vbwHyAgP5KRpSASR5L-Lf7omiOkQfHkPNcfOSGQZ2HK8cgdCs2HUyIv7gCw4nZft9k1kvb_J2Ua5PNMYXKIcxmhxaEJ8u5OgJkznYtBpRJ1aZq0futiA_zUhd4UAiQ5gkAAczatJp32Sm0RLoBaUGFCeUQ_BcOtDX3P1OyNb38mdXgnSdTgiM2Jj5IFS-Epelv1ChMl', + d: 'IqMwCJ_APz9wXrLuzG8UJgKHRZssgUjIfZ3VqPO_olpXmMD8qe5LhbJb6XRkSsrhXzvFF0Jm4az3zJPA01oHLfIJxAa5D9MAf3tAFnuvQxtIiYfI2__LE3obLNlGdpJnO1pkOJtz7SK5MEqbEoQ2F5Fm4ysA7DYWjQbYle5A0yLXbqSORyZjGpY_NzX0rYJ4R4E3ag0_lmFLvRt2fJEqEMs4sf_EQ8Z_8OScaWFjYfEEyzlQqfcSPjC_SloyosDE0wNvKQOaSCD0UBSvPKdGpY41BP7ksWDF48R0gtO0aHYfMq9gPv_0QIfnOFZne3ucf4gO5BLdHvfZv0d25RdqV8OdSIbjSAR7ppHO0UUfP8FX0QU08AojqCQtPn8yrcLzqk5KTkaGBB24cDCFCNj-4FYGEiREjh7Fk-iUKivVfzLiz-cLGyZUPSQ2HVeUr88ZdPqkhLjgeOzl8lh24EKr2-Y1rYv4HvqVcGVseUKKp1s14i68nqTdJMr-7hq3eK65', + qi: 'gHvPnpn3YwgxSZ7BHdNm_vzT9Meg4L-LFKfjyOdAcwAehd_HsCXX2GDncIi4_SVtO_NTIcITt4YuJxmbgFdpyTeKvke_uuhfWERLUpE4g-Y0LobYx4_r18WqtzSO1pZDGSvsy4WV9ELUkY8gaCGLxh31l_1hhGIyCmIE4MSo9kO5vAyQw-yWt5gOBftkzzHBjEYxDBW7gxS4aRxGteRHwca0MDQIikRltIFaS7fjHHxNz_y-PkYux20ckM7VqV6t', + dp: 'ntAHnlqBqoHR4itF50k2fR_blooRCz5KPTbW8vx5DEg3eKW8fIvKkyhaOi-2igVpmTxirjlTVCa15hs6TIF5Y76UjSKTY1RfIZblW5TI-3I9Gr3jGZYcjJFFVsnlFC-dQSqH_Z2ikl7pNXaBXWp9aLd9GOnPbRud588T9AeG8u3AObgBPPfwyFK2KEcQ7Qd7H6Sn3q55cDIp18vAQQaxufCadAcsJ3agBn-mi3Ngf04peOLai2yhhQ-j6Ntr0FOF', + dq: 'vy83EHqccfh7bWRbD57K6LCCiMxzDO2XMUWgN7vXtvV6kMsEWmAIbU1TYAfe-irPif5OyMLH-DC1aGiYDUb6eD-IsnNqj5l8GGyhJoOrZrOQ90qUl1OQ_GzVcWSV_ZTvY9rpZrASzZqk4bZ3ratwIHf5-D9X0QrgycQhyR1qeVOR0v5zNH8cuviHa1SklmG96ldlx7EU-stEGdKe-yLIIESqZhPukW3D3ESSpyAb7tuOT8YN-I292cSo0P7G2rr5', + e: 'AQAB', + n: 's29pCfjF9uM8z4WQkfoQAhPlsk3zGVc7HbuMeI0s0uT8CSiFIMfgMIW_xMGmLZgdLpXKRnMvAj4741ZfnXhaAdiI6kG05Y2s9ot-xTYLRFi0mTrp4M9a8a1KTdXGU1j4xV62yakzRMDZ9Rvus3mROeI49FJmqMj0WL2uJIWcRE74e2_Hk8swughVQvuwBK1qGDEHc72UYTq_CCOGgZ80tnhFEYNatOvcIVv_OxqgDLdO-_mvKiUyeQdVwqKsYrzLtAJwhbRv1Lg_jm61NbSIRwtjvwA5fw4jC08Xh1Z6gg8s1ZpCzjcZhFw-nn3VyMRrzLxDQ56mUc606IQC4cjIOpC_VTSZWjmrgSwb4iktbzp1g4BxD5O51g_enW-6PsJE6M7IQ0uFzYLsTCDbHO5eYPhkdM_bymZmZ4sgyrrQ6mwUGiKAZ_4hmPqmag6lWMQQfhfGPVU9sR_TQ3tWNtrsOOh8f4dxQ8pvODohpZs1ii2_sjLRmkJv2QuebSC3CyCH', + }, + }) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kid: result.keyId, + kty: 'RSA', + e: 'AQAB', + n: 's29pCfjF9uM8z4WQkfoQAhPlsk3zGVc7HbuMeI0s0uT8CSiFIMfgMIW_xMGmLZgdLpXKRnMvAj4741ZfnXhaAdiI6kG05Y2s9ot-xTYLRFi0mTrp4M9a8a1KTdXGU1j4xV62yakzRMDZ9Rvus3mROeI49FJmqMj0WL2uJIWcRE74e2_Hk8swughVQvuwBK1qGDEHc72UYTq_CCOGgZ80tnhFEYNatOvcIVv_OxqgDLdO-_mvKiUyeQdVwqKsYrzLtAJwhbRv1Lg_jm61NbSIRwtjvwA5fw4jC08Xh1Z6gg8s1ZpCzjcZhFw-nn3VyMRrzLxDQ56mUc606IQC4cjIOpC_VTSZWjmrgSwb4iktbzp1g4BxD5O51g_enW-6PsJE6M7IQ0uFzYLsTCDbHO5eYPhkdM_bymZmZ4sgyrrQ6mwUGiKAZ_4hmPqmag6lWMQQfhfGPVU9sR_TQ3tWNtrsOOh8f4dxQ8pvODohpZs1ii2_sjLRmkJv2QuebSC3CyCH', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, result.keyId) + expect(storedKey).toEqual({ + kid: result.keyId, + kty: 'RSA', + e: 'AQAB', + n: 's29pCfjF9uM8z4WQkfoQAhPlsk3zGVc7HbuMeI0s0uT8CSiFIMfgMIW_xMGmLZgdLpXKRnMvAj4741ZfnXhaAdiI6kG05Y2s9ot-xTYLRFi0mTrp4M9a8a1KTdXGU1j4xV62yakzRMDZ9Rvus3mROeI49FJmqMj0WL2uJIWcRE74e2_Hk8swughVQvuwBK1qGDEHc72UYTq_CCOGgZ80tnhFEYNatOvcIVv_OxqgDLdO-_mvKiUyeQdVwqKsYrzLtAJwhbRv1Lg_jm61NbSIRwtjvwA5fw4jC08Xh1Z6gg8s1ZpCzjcZhFw-nn3VyMRrzLxDQ56mUc606IQC4cjIOpC_VTSZWjmrgSwb4iktbzp1g4BxD5O51g_enW-6PsJE6M7IQ0uFzYLsTCDbHO5eYPhkdM_bymZmZ4sgyrrQ6mwUGiKAZ_4hmPqmag6lWMQQfhfGPVU9sR_TQ3tWNtrsOOh8f4dxQ8pvODohpZs1ii2_sjLRmkJv2QuebSC3CyCH', + }) + }) + + it('imports RSA key pair with 4096 bit length', async () => { + const result = await service.importKey(agentContext, { + privateJwk: { + kty: 'RSA', + p: '1kePVZiE7aa8RAGR6D9oy3MXbPFKN9IfZ8OmZ5j2P9i7ScX5nqPfrYlXkc-MJ7pmaVNlOlmwiuJvPElDiIFkW8Obcd-BqK6gRQ0oMFv9y6svB48_E9RKSNzOPNtoVQG0W_ip3yd5LsMaBKbGeRGjhjCY-UeUJ8hRokw6s3b9V09gOWwACMpH23hrrNc50TQMMtWmTHv8XvNVj1tY8501PwbYqyBcD_chUEAZzpkGVfVEXKqGqIszadvTq8CvRJgxJojYrGKcOU007AdYis3x_2-Ey-jZDAlDGS_lu0Q4NWznhLaW6LMfbTPzdYJ6E0LrcvoI0MuZ_3qvEFhWF4CJtw', + q: '0gGvWRRl19KMWgjiF03jOFOPTEdOxjVXU5fCrAbohalkIcycti5bAD3A1wIk3sHBy1D3bWyBLbF5NrafRlKLlrf3segUvYMlly2Ux96J-G5F2FqxYKPa3F777cK_UPqcrefkbRCwpPsqW-CLBTRqxTXBMo8SAS4n-9umjZRb-Z44uToy5DmZWLmXsg95DkHZgEktYdo8ImeAspMsouFdCgw400SKCQ7kdW6K_RakP7M-Si2cAeQXBd9Z_O3ku_WdZE_XUuTDBR-cDIZq8bXH1ysaMcmCN6tIF3nRHfJI2Kr4wNSw_nLD75dLxkRRpyjKA8mWu4WGSqcxAY_3q11sLQ', + d: 'UZz7jfMpU3-vE5FOlzk5ZsI_V5ZBL4_gNKxWh_hiAn5x9H83Odbgc2V5n61YLoI1bad_v5CgLGsRe3WaujRACCr0Vee0KbahH829gFrd8eVTffJHmcxgRZ1qRDe3ptTWgGnN5HXTsR0r_uXcws_vbw3PIKIsK3USH7HFyivS3I2IZamiCM8Gsqxd5JLJ6eDpETwT8yNPBm6KyIo6yNTT02wpnFirTHdMzWQwG5w0ZQOneBSzDpTX5SAj_yk_i_KVjg3QGaXf9hmhl0eJu7Bhx6q9COPjmPUdct47j9N396GE4TEORjh-1m7AvGYw08G1m75TB8K5mL_9W6lqoksU5dZeaQDtQl4AV58uZwWXShevipTyJBYgyk-GQCxIXHd_gCXMPVDZvCL1rBMmqSh5XSj84XEVVZKNb3PHuHxwWMopWxSyoD6SJyPYaDpyRtrQPIi79iDneNbfJuuHAoFE2wNOiX43Vvafe9qo3kk8BUw5N7RrSXw99-R75SIzrwgY1Hh90mSVoXrf1QhJhhQwXLSL6tgmxXbmGG4jY-rBPE04XQfsEEFoNrZZsx1Kx3GFm8JwflpbRLtqvlXbyCZ9aqIoPbABizoYEGo2mwububbAZy-UKJSEEayfj_9L6K4MyCZBUNJ3zretbMNi5amm_aUrE5CYU4mgh93GWxatQeE', + qi: 'XN0HMYoftF55U-ANATf6MYh7dktdq3KV0hClJrGk0CPLW8G3PDIq-NN9w6TSMVPmcxSGbRH9tGV05oeW_L_iHMDEhKG290BMrXaPmLeomq7xgO9dA15Mv9wTZYCGaBE-vi3rYbpwUPCrvrXz0IffvRovqsudxVUGpcUEzkWYc1RfU_jD5YSn2flEshE4n8hwvd06AePD0SDyVi_rRhixzAsocuPmtkRfdUgP41iDLFPJAuIXRyu4964BdQveemhkv3V8vIby0c2T_eb8E34d02kAYVR1Kib-C2_kPXhYi4QluToevLySfGGoUrpsdYNv-kK1f2IhRVZzCbtwcOKNmg', + dp: 'OoJpsf6mdVns2EjxdVAzJjJz-AxerqVSa_vxaSJMQxzD7x3-zgGDJxh0b90TMRnlsubRokAxQ4sWwohix1hFdgUQYeScu9mK66_vBF1qDH3epprHp3t1GTYpnlZuw59mhyJ8B_H4VftyFEkRsqdNVmvYqWCRJNe-6qkT8kMQZBHJfYRu8feB7XyRMi3GnMgweIT8FrBYNfkNqMpRnJuVmXMeIIQCf12EnCwUn-QK5dfF5eOuR0FTNZmPz7saYImKCjKdr1xxuffJ9pT-6U_Yv13NDfyzn2S8DJmWii7ThksJYKSWyk12zFO-K50IBlBkiA2b8J9Xmnn-aWEliN9ROw', + dq: 'PdQvKvvdbChaGBvrbM8Kqce4Nc38vFByEHNq5jj2dnvDtkvGi8CkHDMSNns0Hb1P8Cs7XaUHd0t8E4a65_pfjJVHQMLCcHVPOO47kojLeDAHMkapWHmFc0Iny_19VDskq_LNButWBozIENrQM00Wbk-APQFwXJaZQQaPR7m5Rom1y5r95sGqizvBFLSHgJIUljd1PA0DjWGJu4mnJ6FQQigNBu5z8WzICGbuVss2umZsXWyGNOxRdvImTVhA8rHCkAkNrSMa48RFrk5Y6CcL2iafhK7-PqOYCwlbbwSpO9lCeYtlPNTPKRgTQCFXJO7WYz0Tusv6GLqWrA4V7gcIVQ', + e: 'AQAB', + n: 'r8gcoUFh0ZErAKSD25XuKeUyMT-ClvotEPc-VA58z3E2k5zjkmi_gYOGfOl8HKAWeqx8CbH9WaVSEVt84q3wSZdPID132dxZ-bsfcyF4fCP8qYaxdDzuAyTZadsmDLD-U-8GR08NfGxWys9VpzHtNodWgmIsVcJCeLaUx8dwCONEpVJYzDd00xlHQHOepZj6iFa7-vIekl76xqnXkxhNmz3_d0ClFBzil8jJpfrzuhGsCHLIqwZY_uPGPsYSaxo6fqG4yQb60TOns53gMPMW6xJyp0OlpfMyyFW8NMKUxfXxJejozo2WhZU-uPLz71RQEr6IwCQWcTkGuks797O8K1j3sqVOJMiGfUfWnK9XlW_8HJTH3jgfOgYsQn9rlFRVDt-t8JkyUUQWk3x2QiF2Yegy-JZU317iM8Z8eO-uMFAw-d3c1KJRuWRJG9QSj68GXqNXmwRIcIPD7poPUGee7mcCNz5Co7OxOwF_fC7ntqnis0PbvykPJN2w9ophrgT4vc8qE5TPboSam0hGKL2o2xKqyRSwl9vg1qEz1MIeYfJJ109J8a_T2Ltr41k2waxZb6NYsoYJHKgyZI1oMUhiESwRy7IjoDmj7X6NeKkHBKuUT4CAUMU1Ub_3_zF8CA34kDx4U4uMm5NqD2tKwka5-vWKuMkGwO7MTsZXTKAsaSs', + }, + }) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kid: result.keyId, + kty: 'RSA', + e: 'AQAB', + n: 'r8gcoUFh0ZErAKSD25XuKeUyMT-ClvotEPc-VA58z3E2k5zjkmi_gYOGfOl8HKAWeqx8CbH9WaVSEVt84q3wSZdPID132dxZ-bsfcyF4fCP8qYaxdDzuAyTZadsmDLD-U-8GR08NfGxWys9VpzHtNodWgmIsVcJCeLaUx8dwCONEpVJYzDd00xlHQHOepZj6iFa7-vIekl76xqnXkxhNmz3_d0ClFBzil8jJpfrzuhGsCHLIqwZY_uPGPsYSaxo6fqG4yQb60TOns53gMPMW6xJyp0OlpfMyyFW8NMKUxfXxJejozo2WhZU-uPLz71RQEr6IwCQWcTkGuks797O8K1j3sqVOJMiGfUfWnK9XlW_8HJTH3jgfOgYsQn9rlFRVDt-t8JkyUUQWk3x2QiF2Yegy-JZU317iM8Z8eO-uMFAw-d3c1KJRuWRJG9QSj68GXqNXmwRIcIPD7poPUGee7mcCNz5Co7OxOwF_fC7ntqnis0PbvykPJN2w9ophrgT4vc8qE5TPboSam0hGKL2o2xKqyRSwl9vg1qEz1MIeYfJJ109J8a_T2Ltr41k2waxZb6NYsoYJHKgyZI1oMUhiESwRy7IjoDmj7X6NeKkHBKuUT4CAUMU1Ub_3_zF8CA34kDx4U4uMm5NqD2tKwka5-vWKuMkGwO7MTsZXTKAsaSs', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, result.keyId) + expect(storedKey).toEqual({ + kid: result.keyId, + kty: 'RSA', + e: 'AQAB', + n: 'r8gcoUFh0ZErAKSD25XuKeUyMT-ClvotEPc-VA58z3E2k5zjkmi_gYOGfOl8HKAWeqx8CbH9WaVSEVt84q3wSZdPID132dxZ-bsfcyF4fCP8qYaxdDzuAyTZadsmDLD-U-8GR08NfGxWys9VpzHtNodWgmIsVcJCeLaUx8dwCONEpVJYzDd00xlHQHOepZj6iFa7-vIekl76xqnXkxhNmz3_d0ClFBzil8jJpfrzuhGsCHLIqwZY_uPGPsYSaxo6fqG4yQb60TOns53gMPMW6xJyp0OlpfMyyFW8NMKUxfXxJejozo2WhZU-uPLz71RQEr6IwCQWcTkGuks797O8K1j3sqVOJMiGfUfWnK9XlW_8HJTH3jgfOgYsQn9rlFRVDt-t8JkyUUQWk3x2QiF2Yegy-JZU317iM8Z8eO-uMFAw-d3c1KJRuWRJG9QSj68GXqNXmwRIcIPD7poPUGee7mcCNz5Co7OxOwF_fC7ntqnis0PbvykPJN2w9ophrgT4vc8qE5TPboSam0hGKL2o2xKqyRSwl9vg1qEz1MIeYfJJ109J8a_T2Ltr41k2waxZb6NYsoYJHKgyZI1oMUhiESwRy7IjoDmj7X6NeKkHBKuUT4CAUMU1Ub_3_zF8CA34kDx4U4uMm5NqD2tKwka5-vWKuMkGwO7MTsZXTKAsaSs', + }) + }) + + it('imports EC P-256 key pair with provided keyId', async () => { + const keyId = 'test-key-id' + + const result = await service.importKey(agentContext, { + privateJwk: { + kid: keyId, + kty: 'EC', + d: '58pb2cKWs0VmIXtHz3ayrZCGKRUnWrb9QvbfbAkGI3c', + crv: 'P-256', + x: 'wPuEY7sKE2x2rp96_QtnRhSswV2AgBk_cX5TCmvLxPs', + y: 'OG0Lm7begM02Vikg2iI70nknoWNygwlUoBGLLFDT3Zs', + }, + }) + + expect(result).toEqual({ + keyId, + publicJwk: { + kid: keyId, + kty: 'EC', + crv: 'P-256', + x: 'wPuEY7sKE2x2rp96_QtnRhSswV2AgBk_cX5TCmvLxPs', + y: 'OG0Lm7begM02Vikg2iI70nknoWNygwlUoBGLLFDT3Zs', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, keyId) + expect(storedKey).toEqual({ + kid: keyId, + kty: 'EC', + crv: 'P-256', + x: 'wPuEY7sKE2x2rp96_QtnRhSswV2AgBk_cX5TCmvLxPs', + y: 'OG0Lm7begM02Vikg2iI70nknoWNygwlUoBGLLFDT3Zs', + }) + }) + + it('imports EC P-384 key pair', async () => { + const result = await service.importKey(agentContext, { + privateJwk: { + kty: 'EC', + d: 'O2WHQQDOvifmepR3kxDRJh1TBd-LaEww5lYzrd14lzfi4IVIVm__ZQVoUQ0ws56e', + crv: 'P-384', + x: 'Vvlf4tmvKT1qTOptwSelZBoazQmrsKvg1poITeOoxqbZEgNvfa9cUObhQlbhHjGP', + y: 'gTMFQKmXdcK31ycnDULFEtCLF3vsXNnAcQcFbeapxqBpo_wMdSP-G8pN9jPMDPYS', + }, + }) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kid: result.keyId, + kty: 'EC', + crv: 'P-384', + x: 'Vvlf4tmvKT1qTOptwSelZBoazQmrsKvg1poITeOoxqbZEgNvfa9cUObhQlbhHjGP', + y: 'gTMFQKmXdcK31ycnDULFEtCLF3vsXNnAcQcFbeapxqBpo_wMdSP-G8pN9jPMDPYS', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, result.keyId) + expect(storedKey).toEqual({ + kid: result.keyId, + kty: 'EC', + crv: 'P-384', + x: 'Vvlf4tmvKT1qTOptwSelZBoazQmrsKvg1poITeOoxqbZEgNvfa9cUObhQlbhHjGP', + y: 'gTMFQKmXdcK31ycnDULFEtCLF3vsXNnAcQcFbeapxqBpo_wMdSP-G8pN9jPMDPYS', + }) + }) + + it('imports EC P-521 key pair', async () => { + const result = await service.importKey(agentContext, { + privateJwk: { + kty: 'EC', + d: 'Af8IOTaFSKF65L6vI-UTAhUpO0LbtiK-2W-Qs5-jvpLAnmalTUNX3r7mZhH1zioq26NayCFTgEZVWAwMgeEqindK', + crv: 'P-521', + x: 'AfenCyIa_2pnNYybfgdhy19fVnrBksaHgQUy4bCu3kiA8_cZujnsO6RgpIWx2ip3cdXsi2ujK-mShjIveNwdwiBF', + y: 'AVKOcCI-Zg_0IlhpCJ77wwMFjXuVpt-nilcSQY9E0JADcXQGaWSAWKWpAbCAeeevoBHepELbIJ5bX3EnU3yKMMQL', + }, + }) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kid: result.keyId, + kty: 'EC', + crv: 'P-521', + x: 'AfenCyIa_2pnNYybfgdhy19fVnrBksaHgQUy4bCu3kiA8_cZujnsO6RgpIWx2ip3cdXsi2ujK-mShjIveNwdwiBF', + y: 'AVKOcCI-Zg_0IlhpCJ77wwMFjXuVpt-nilcSQY9E0JADcXQGaWSAWKWpAbCAeeevoBHepELbIJ5bX3EnU3yKMMQL', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, result.keyId) + expect(storedKey).toEqual({ + kid: result.keyId, + kty: 'EC', + crv: 'P-521', + x: 'AfenCyIa_2pnNYybfgdhy19fVnrBksaHgQUy4bCu3kiA8_cZujnsO6RgpIWx2ip3cdXsi2ujK-mShjIveNwdwiBF', + y: 'AVKOcCI-Zg_0IlhpCJ77wwMFjXuVpt-nilcSQY9E0JADcXQGaWSAWKWpAbCAeeevoBHepELbIJ5bX3EnU3yKMMQL', + }) + }) + + it('imports EC secp256k1 key pair', async () => { + const result = await service.importKey(agentContext, { + privateJwk: { + kty: 'EC', + d: 'eGYeYMILykL1YnAZde1aSo9uQtKe-HeALQu2Yv-ZcQ0', + crv: 'secp256k1', + x: 'ZLRfyFqy_hVG_SWH7SlErOCMkztJNoZZHdJvMt6yPSE', + y: 'O89repvsgjOY9qAOZcmdIiITHU4Frk00ryKGDw7OefQ', + }, + }) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kid: result.keyId, + kty: 'EC', + crv: 'secp256k1', + x: 'ZLRfyFqy_hVG_SWH7SlErOCMkztJNoZZHdJvMt6yPSE', + y: 'O89repvsgjOY9qAOZcmdIiITHU4Frk00ryKGDw7OefQ', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, result.keyId) + expect(storedKey).toEqual({ + kid: result.keyId, + kty: 'EC', + crv: 'secp256k1', + x: 'ZLRfyFqy_hVG_SWH7SlErOCMkztJNoZZHdJvMt6yPSE', + y: 'O89repvsgjOY9qAOZcmdIiITHU4Frk00ryKGDw7OefQ', + }) + }) + + it('imports OKP Ed25519 key pair', async () => { + const result = await service.importKey(agentContext, { + privateJwk: { + kty: 'OKP', + d: 'IbJKmlKmRDoSkO0xM_DkeorvBz--1O_qGlmrb6_1Cms', + crv: 'Ed25519', + x: '4-CJ6REW9mUtp2ouh5rhQ9wvfsZE278NnPffTkLeNYI', + }, + }) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kid: result.keyId, + kty: 'OKP', + crv: 'Ed25519', + x: '4-CJ6REW9mUtp2ouh5rhQ9wvfsZE278NnPffTkLeNYI', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, result.keyId) + expect(storedKey).toEqual({ + kid: result.keyId, + kty: 'OKP', + crv: 'Ed25519', + x: '4-CJ6REW9mUtp2ouh5rhQ9wvfsZE278NnPffTkLeNYI', + }) + }) + + it('imports OKP X25519 key pair', async () => { + const result = await service.importKey(agentContext, { + privateJwk: { + kty: 'OKP', + d: '7LL0_o4FsS4w-mCFhcKlbaX8qsqgeNjTxzDV4lVj0us', + crv: 'X25519', + x: 'DdYl5R2IpY7VwLr88mgG9PBjK7jICuipVYhOzz8F3Fs', + }, + }) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kid: result.keyId, + kty: 'OKP', + crv: 'X25519', + x: 'DdYl5R2IpY7VwLr88mgG9PBjK7jICuipVYhOzz8F3Fs', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, result.keyId) + expect(storedKey).toEqual({ + kid: result.keyId, + kty: 'OKP', + crv: 'X25519', + x: 'DdYl5R2IpY7VwLr88mgG9PBjK7jICuipVYhOzz8F3Fs', + }) + }) + + // NOTE: we need to tweak the API here a bit I think. Just the JWK is not enough + // we need something of an alg. + it('imports oct key', async () => { + const result = await service.importKey(agentContext, { + privateJwk: { + kty: 'oct', + k: '7LL0_o4FsS4w-mCFhcKlbaX8qsqgeNjTxzDV4lVj0us', + }, + }) + + expect(result).toEqual({ + keyId: result.keyId, + publicJwk: { + kid: result.keyId, + kty: 'oct', + }, + }) + + // Verify key was stored + const storedKey = await service.getPublicKey(agentContext, result.keyId) + expect(storedKey).toEqual({ + kid: result.keyId, + kty: 'oct', + }) + }) + + it('error when importing invalid oct key', async () => { + const error = await service + .importKey(agentContext, { + privateJwk: { + kty: 'oct', + k: '#@$%', + }, + }) + .then(() => undefined) + .catch((e) => e) + expect(error).toBeInstanceOf(Kms.KeyManagementError) + expect(error.cause).toBeInstanceOf(ZodValidationError) + expect(error.cause.message).toContain('Must be a base64url string') + }) + + it('generates random keyId when not provided', async () => { + const privateJwk: Kms.KmsJwkPrivate = { + kty: 'EC', + d: 'ESGpJ7SIi3H7h9pkIkr-M8QDWamtiewze5_U_nP2fJg', + crv: 'P-256', + x: 'HlwSCoy8jWXx_KifMEnt4zDjPb0eyi0eH9C9awOdR70', + y: 's-Drm_bZ4eVV_UkGnLr62sI2TWibkdLFFc0dAT6ASL8', + } + + const result = await service.importKey(agentContext, { privateJwk }) + expect(result).toEqual({ + keyId: expect.any(String), + publicJwk: { + kid: expect.any(String), + kty: 'EC', + crv: 'P-256', + x: 'HlwSCoy8jWXx_KifMEnt4zDjPb0eyi0eH9C9awOdR70', + y: 's-Drm_bZ4eVV_UkGnLr62sI2TWibkdLFFc0dAT6ASL8', + }, + }) + }) + + it('throws error if invalid key data provided', async () => { + const error = await service + .importKey(agentContext, { + privateJwk: { + kty: 'EC', + crv: 'P-256', + x: 'test-x', + y: 'test-y', + d: 'test-d', + }, + }) + .then(() => undefined) + .catch((e) => e) + expect(error).toBeInstanceOf(Kms.KeyManagementError) + expect(error.cause.message).toEqual('Invalid JWK EC key') + }) + + it('throws error if key with same id already exists', async () => { + const keyId = 'existing-key' + const privateJwk: Kms.KmsJwkPrivate = { + kid: keyId, + kty: 'EC', + d: '_jBF0d-pZB_Os3CrJsPthA-CDXSy17vCdyRzuAIFbaM', + crv: 'P-256', + x: 'IcwG4MdHi8u59kc5h-cQC31ZVC50g7qlJvWkzh_j9zw', + y: 'iY57CM0fuBNx5ef2iviA2OiUtfExERAFLyYD1yno6Xo', + } + + // First import succeeds + await service.importKey(agentContext, { privateJwk }) + + // Second import with same keyId fails + await expect(service.importKey(agentContext, { privateJwk })).rejects.toThrow( + new Kms.KeyManagementKeyExistsError('existing-key', service.backend) + ) + }) + + it('throws error when key is provided with unknown kty', async () => { + await expect( + service.importKey(agentContext, { + privateJwk: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + kty: 'something', + }, + }) + ).rejects.toThrow(new Kms.KeyManagementAlgorithmNotSupportedError(`kty 'something'`, service.backend)) + }) + }) + + describe('deleteKey', () => { + it('deletes existing key', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 2048 }, + }) + + // Verify key exists + expect(await service.getPublicKey(agentContext, keyId)).toBeTruthy() + + // Delete key + expect(await service.deleteKey(agentContext, { keyId })).toBe(true) + + // Verify key no longer exists + expect(await service.getPublicKey(agentContext, keyId)).toBeNull() + }) + + it('succeeds when deleting non-existent key', async () => { + expect(await service.deleteKey(agentContext, { keyId: 'nonexistent' })).toBe(false) + }) + + it('removes key from storage completely', async () => { + const { keyId } = await service.createKey(agentContext, { + type: { kty: 'RSA', modulusLength: 2048 }, + }) + + await service.deleteKey(agentContext, { keyId }) + + // Verify we can't use the deleted key + await expect( + service.sign(agentContext, { + keyId, + algorithm: 'RS256', + data: new Uint8Array([1, 2, 3]), + }) + ).rejects.toThrow(new Kms.KeyManagementKeyNotFoundError(keyId, service.backend)) + }) + }) +}) diff --git a/packages/node/src/kms/crypto/createKey.ts b/packages/node/src/kms/crypto/createKey.ts new file mode 100644 index 0000000000..6691e59617 --- /dev/null +++ b/packages/node/src/kms/crypto/createKey.ts @@ -0,0 +1,111 @@ +import { generateKeyPair as _generateKeyPair, randomBytes } from 'node:crypto' +import { promisify } from 'node:util' +import { Kms } from '@credo-ts/core' + +const generateKeyPair = promisify(_generateKeyPair) + +const nodeSupportedEcCrvs = ['P-256', 'P-384', 'P-521', 'secp256k1'] satisfies Kms.KmsJwkPublicEc['crv'][] +export type NodeKmsSupportedEcCrvs = (typeof nodeSupportedEcCrvs)[number] +export function assertNodeSupportedEcCrv( + options: Kms.KmsCreateKeyTypeEc +): asserts options is Kms.KmsCreateKeyTypeEc & { crv: NodeKmsSupportedEcCrvs } { + if (!nodeSupportedEcCrvs.includes(options.crv as NodeKmsSupportedEcCrvs)) { + throw new Kms.KeyManagementAlgorithmNotSupportedError(`crv '${options.crv}' for kty '${options.kty}'`, 'node') + } +} + +export async function createEcKey({ crv }: Kms.KmsCreateKeyTypeEc & { crv: NodeKmsSupportedEcCrvs }) { + const { publicKey, privateKey } = await generateKeyPair('ec', { + namedCurve: crv, + }) + + const privateJwk = privateKey.export({ + format: 'jwk', + }) + + const publicJwk = publicKey.export({ + format: 'jwk', + }) + + return { + privateJwk: privateJwk as Kms.KmsJwkPrivateEc, + publicJwk: publicJwk as Kms.KmsJwkPublicEc, + } +} + +export async function createRsaKey({ modulusLength }: Kms.KmsCreateKeyTypeRsa) { + const { publicKey, privateKey } = await generateKeyPair('rsa', { + modulusLength, + }) + + const privateJwk = privateKey.export({ + format: 'jwk', + }) + + const publicJwk = publicKey.export({ + format: 'jwk', + }) + + return { + privateJwk: privateJwk as Kms.KmsJwkPrivateRsa, + publicJwk: publicJwk as Kms.KmsJwkPublicRsa, + } +} + +const nodeSupportedOkpCrvs = ['Ed25519', 'X25519'] satisfies Kms.KmsJwkPublicOkp['crv'][] +type NodeKmsSupportedOkpCrvs = (typeof nodeSupportedOkpCrvs)[number] +export function assertNodeSupportedOkpCrv( + options: Kms.KmsCreateKeyTypeOkp +): asserts options is Kms.KmsCreateKeyTypeOkp & { crv: NodeKmsSupportedOkpCrvs } { + if (!nodeSupportedOkpCrvs.includes(options.crv as NodeKmsSupportedOkpCrvs)) { + throw new Kms.KeyManagementAlgorithmNotSupportedError(`crv '${options.crv}' for kty '${options.kty}'`, 'node') + } +} + +export async function createOkpKey({ crv }: Kms.KmsCreateKeyTypeOkp & { crv: NodeKmsSupportedOkpCrvs }) { + const { publicKey, privateKey } = + crv === 'Ed25519' ? await generateKeyPair('ed25519') : await generateKeyPair('x25519') + + const privateJwk = privateKey.export({ + format: 'jwk', + }) + + const publicJwk = publicKey.export({ + format: 'jwk', + }) + + return { + privateJwk: privateJwk as Kms.KmsJwkPrivateOkp, + publicJwk: publicJwk as Kms.KmsJwkPublicOkp, + } +} + +const nodeSupportedOctAlgorithms = ['aes', 'hmac'] satisfies Kms.KmsCreateKeyTypeOct['algorithm'][] +type NodeSupportedOctAlgorithms = (typeof nodeSupportedOctAlgorithms)[number] +export function assertNodeSupportedOctAlgorithm( + options: Kms.KmsCreateKeyTypeOct +): asserts options is Kms.KmsCreateKeyTypeOct & { algorithm: NodeSupportedOctAlgorithms } { + if (!nodeSupportedOctAlgorithms.includes(options.algorithm as NodeSupportedOctAlgorithms)) { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `algorithm '${options.algorithm}' for kty '${options.kty}'`, + 'node' + ) + } +} + +export async function createOctKey(options: Kms.KmsCreateKeyTypeOct & { algorithm: NodeSupportedOctAlgorithms }) { + const secretBytes = randomBytes(options.length >> 3) + + const privateJwk = { + kty: 'oct', + k: secretBytes.toString('base64url'), + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { k, ...publicJwk } = privateJwk + + return { + privateJwk: privateJwk as Kms.KmsJwkPrivateOct, + publicJwk: publicJwk as Kms.KmsJwkPublicOct, + } +} diff --git a/packages/node/src/kms/crypto/decrypt.ts b/packages/node/src/kms/crypto/decrypt.ts new file mode 100644 index 0000000000..52bfb87d23 --- /dev/null +++ b/packages/node/src/kms/crypto/decrypt.ts @@ -0,0 +1,119 @@ +import type { DecipherGCM } from 'node:crypto' + +import { Buffer } from 'node:buffer' +import { createDecipheriv, createSecretKey, timingSafeEqual } from 'node:crypto' +import { Kms } from '@credo-ts/core' + +import { performSign } from './sign' + +export async function performDecrypt( + key: Kms.KmsJwkPrivateOct, + dataDecryption: Kms.KmsDecryptDataDecryption, + encrypted: Uint8Array +): Promise<{ data: Uint8Array }> { + const secretKeyBytes = Buffer.from(key.k, 'base64url') + const nodeKey = createSecretKey(secretKeyBytes) + + // Create decipher with key and IV + if (dataDecryption.algorithm === 'A128CBC' || dataDecryption.algorithm === 'A256CBC') { + const nodeAlgorithm = dataDecryption.algorithm === 'A128CBC' ? 'aes-128-cbc' : 'aes-256-cbc' + + const decipher = createDecipheriv(nodeAlgorithm, nodeKey, dataDecryption.iv) + + // Get decrypted data + const data = Buffer.concat([decipher.update(encrypted), decipher.final()]) + + return { data } + } + if ( + dataDecryption.algorithm === 'A128GCM' || + dataDecryption.algorithm === 'A192GCM' || + dataDecryption.algorithm === 'A256GCM' + ) { + const nodeAlgorithm = + dataDecryption.algorithm === 'A128GCM' + ? 'aes-128-gcm' + : dataDecryption.algorithm === 'A192GCM' + ? 'aes-192-gcm' + : 'aes-256-gcm' + + const decipher = createDecipheriv(nodeAlgorithm, nodeKey, dataDecryption.iv) + + // Set auth tag before decryption for authenticated modes + decipher.setAuthTag(dataDecryption.tag) + + // If AAD was used during encryption, it must be provided for decryption + if (dataDecryption.aad) { + decipher.setAAD(dataDecryption.aad) + } + + // Get decrypted data + const data = Buffer.concat([decipher.update(encrypted), decipher.final()]) + + return { data } + } + if ( + dataDecryption.algorithm === 'A128CBC-HS256' || + dataDecryption.algorithm === 'A192CBC-HS384' || + dataDecryption.algorithm === 'A256CBC-HS512' + ) { + // Map algorithms to their corresponding CBC and HMAC settings + const algSettings = { + 'A128CBC-HS256': { cbcAlg: 'aes-128-cbc', hmacAlg: 'HS256', keySize: 16 } as const, + 'A192CBC-HS384': { cbcAlg: 'aes-192-cbc', hmacAlg: 'HS384', keySize: 24 } as const, + 'A256CBC-HS512': { cbcAlg: 'aes-256-cbc', hmacAlg: 'HS512', keySize: 32 } as const, + }[dataDecryption.algorithm] + + // Split the input key into MAC and ENC keys (MAC key is first half, ENC key is second half) + const macKey = secretKeyBytes.subarray(0, algSettings.keySize) + const encKey = createSecretKey(secretKeyBytes.subarray(algSettings.keySize)) + + // Calculate authentication tag for verification + // AL (Associated Length) is 64-bit big-endian length of AAD in bits + const al = Buffer.alloc(8) + const aadLength = dataDecryption.aad ? dataDecryption.aad.length * 8 : 0 + al.writeBigUInt64BE(BigInt(aadLength)) + + // Create concatenated buffer for MAC verification + const macData = Buffer.concat([dataDecryption.aad ?? Buffer.alloc(0), dataDecryption.iv, encrypted, al]) + + // Verify the authentication tag + const hmac = await performSign({ kty: 'oct', k: macKey.toString('base64url') }, algSettings.hmacAlg, macData) + const calculatedTag = Buffer.from(hmac).subarray(0, algSettings.keySize) // Truncate to appropriate size + + if (!timingSafeEqual(calculatedTag, dataDecryption.tag)) { + throw new Kms.KeyManagementError( + `Error during verification of authentication tag with decryption algorithm '${dataDecryption.algorithm}'` + ) + } + + // After verification, perform decryption + const decipher = createDecipheriv(algSettings.cbcAlg, encKey, dataDecryption.iv) + const data = Buffer.concat([decipher.update(encrypted), decipher.final()]) + + return { data } + } + if (dataDecryption.algorithm === 'C20P') { + const decipher: DecipherGCM = createDecipheriv('chacha20-poly1305', nodeKey, dataDecryption.iv, { + authTagLength: 16, + }) + + // Set auth tag before decryption + decipher.setAuthTag(dataDecryption.tag) + + // If AAD was used during encryption, it must be provided for decryption + if (dataDecryption.aad) { + decipher.setAAD(dataDecryption.aad) + } + + // Get decrypted data + const data = Buffer.concat([decipher.update(encrypted), decipher.final()]) + + return { data } + } + + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `JWA content decryption algorithm '${dataDecryption.algorithm}'`, + 'node' + ) +} diff --git a/packages/node/src/kms/crypto/deriveKey.ts b/packages/node/src/kms/crypto/deriveKey.ts new file mode 100644 index 0000000000..4d988090a8 --- /dev/null +++ b/packages/node/src/kms/crypto/deriveKey.ts @@ -0,0 +1,316 @@ +import { Buffer } from 'node:buffer' +import type { NodeKmsSupportedEcCrvs } from './createKey' + +import { createECDH, createHash, getRandomValues, subtle } from 'node:crypto' + +import { Kms, TypedArrayEncoder } from '@credo-ts/core' + +const nodeSupportedEcdhKeyDerivationEcCrv = [ + 'P-256', + 'P-384', + 'P-521', + 'secp256k1', +] as const satisfies NodeKmsSupportedEcCrvs[] + +export const nodeSupportedKeyAgreementAlgorithms = [ + 'ECDH-ES', + 'ECDH-ES+A128KW', + 'ECDH-ES+A192KW', + 'ECDH-ES+A256KW', +] satisfies Kms.KnownJwaKeyAgreementAlgorithm[] + +function assertNodeSupportedEcdhKeyDerivationCrv( + jwk: Jwk +): asserts jwk is Jwk & { kty: 'OKP' | 'EC'; crv: (typeof nodeSupportedEcdhKeyDerivationEcCrv)[number] | 'X25519' } { + if ( + (jwk.kty === 'OKP' && jwk.crv !== 'X25519') || + (jwk.kty === 'EC' && !(nodeSupportedEcdhKeyDerivationEcCrv as string[]).includes(jwk.crv)) + ) { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `key derivation with crv '${jwk.crv}' for kty '${jwk.kty}'`, + 'node' + ) + } +} + +type NodeSupportedKeyAgreementDecryptOptions = Kms.KmsKeyAgreementDecryptOptions & { + algorithm: (typeof nodeSupportedKeyAgreementAlgorithms)[number] +} +type NodeSupportedKeyAgreementEncryptOptions = Kms.KmsKeyAgreementEncryptOptions & { + algorithm: (typeof nodeSupportedKeyAgreementAlgorithms)[number] +} + +export async function deriveEncryptionKey(options: { + keyAgreement: NodeSupportedKeyAgreementEncryptOptions + privateJwk: Kms.KmsJwkPrivateAsymmetric + encryption: Kms.KmsEncryptDataEncryption +}) { + const { keyAgreement, encryption, privateJwk } = options + + assertNodeSupportedEcdhKeyDerivationCrv(keyAgreement.externalPublicJwk) + assertNodeSupportedEcdhKeyDerivationCrv(privateJwk) + + const keyLength = + keyAgreement.algorithm === 'ECDH-ES' + ? mapContentEncryptionAlgorithmToKeyLength(encryption.algorithm) + : keyAgreement.algorithm === 'ECDH-ES+A128KW' + ? 128 + : keyAgreement.algorithm === 'ECDH-ES+A192KW' + ? 192 + : 256 + + const derivedKeyBytes = await deriveKeyEcdhEs({ + keyLength, + usageAlgorithm: keyAgreement.algorithm === 'ECDH-ES' ? encryption.algorithm : keyAgreement.algorithm, + privateJwk, + publicJwk: keyAgreement.externalPublicJwk, + apu: keyAgreement.apu, + apv: keyAgreement.apv, + }) + + if (keyAgreement.algorithm === 'ECDH-ES') { + return { + // TODO: will be more efficient to return node key instance + contentEncryptionKey: { + kty: 'oct', + k: derivedKeyBytes.toString('base64url'), + } as const, + } + } + + // Key wrapping + const derivedKey = await subtle.importKey('raw', derivedKeyBytes, 'AES-KW', true, ['wrapKey']) + const contentEncryptionKeyBytes = Buffer.from( + getRandomValues(new Uint8Array(mapContentEncryptionAlgorithmToKeyLength(encryption.algorithm) >> 3)) + ) + const contentEncryptionKey = await subtle.importKey('raw', contentEncryptionKeyBytes, 'AES-KW', true, ['wrapKey']) + const encryptedContentEncryptionKey = await subtle.wrapKey('raw', contentEncryptionKey, derivedKey, 'AES-KW') + + return { + encryptedContentEncryptionKey: { + encrypted: Buffer.from(encryptedContentEncryptionKey), + } satisfies Kms.KmsEncryptedKey, + contentEncryptionKey: { + kty: 'oct', + k: contentEncryptionKeyBytes.toString('base64url'), + } as const, + } +} + +export async function deriveDecryptionKey(options: { + keyAgreement: NodeSupportedKeyAgreementDecryptOptions + privateJwk: Kms.KmsJwkPrivateAsymmetric + decryption: Kms.KmsDecryptDataDecryption +}) { + const { keyAgreement, decryption, privateJwk } = options + + assertNodeSupportedEcdhKeyDerivationCrv(keyAgreement.externalPublicJwk) + assertNodeSupportedEcdhKeyDerivationCrv(privateJwk) + + const keyLength = + keyAgreement.algorithm === 'ECDH-ES' + ? mapContentEncryptionAlgorithmToKeyLength(decryption.algorithm) + : keyAgreement.algorithm === 'ECDH-ES+A128KW' + ? 128 + : keyAgreement.algorithm === 'ECDH-ES+A192KW' + ? 192 + : 256 + + const derivedKeyBytes = await deriveKeyEcdhEs({ + keyLength, + usageAlgorithm: keyAgreement.algorithm === 'ECDH-ES' ? decryption.algorithm : keyAgreement.algorithm, + privateJwk: privateJwk, + publicJwk: keyAgreement.externalPublicJwk, + apu: keyAgreement.apu, + apv: keyAgreement.apv, + }) + + if (keyAgreement.algorithm === 'ECDH-ES') { + return { + // TODO: will be more efficient to return node key instance + contentEncryptionKey: { + kty: 'oct', + k: derivedKeyBytes.toString('base64url'), + } as const, + } + } + + // Key wrapping + const derivedKey = await subtle.importKey('raw', derivedKeyBytes, 'AES-KW', true, ['wrapKey']) + + const contentEncryptionKey = await subtle.unwrapKey( + 'raw', + keyAgreement.encryptedKey.encrypted, + derivedKey, + 'AES-KW', + // algorithm used is irrelevant + { hash: 'SHA-256', name: 'HMAC' }, + true, + ['decrypt'] + ) + + return { + contentEncryptionKey: (await subtle.exportKey('jwk', contentEncryptionKey)) as Kms.KmsJwkPrivate, + } +} + +/** + * Derive a key using ECDH and Concat KDF + */ +async function deriveKeyEcdhEs(options: { + keyLength: number + /** + * This is only used for the AlgorithmID in KDF + */ + usageAlgorithm: string + apv?: Uint8Array + apu?: Uint8Array + privateJwk: Kms.KmsJwkPrivateEc | Kms.KmsJwkPrivateOkp + publicJwk: Kms.KmsJwkPublicEc | Kms.KmsJwkPublicOkp +}): Promise { + // const privateKey = createPrivateKey({ format: 'jwk', key: options.privateJwk }) + // const publicKey = createPublicKey({ format: 'jwk', key: options.publicJwk }) + + // Create ECDH instance based on curve + const nodeEcdhCurveName = mapCrvToNodeEcdhCurveName(options.privateJwk.crv) + const nodeConcatKdfHash = mapCrvToHashLength(options.publicJwk.crv) + + const ecdh = createECDH(nodeEcdhCurveName) + + // Set private key + ecdh.setPrivateKey(TypedArrayEncoder.fromBase64(options.privateJwk.d)) + + const publicKey = Kms.PublicJwk.fromPublicJwk(options.publicJwk).publicKey + if (publicKey.kty === 'RSA') { + throw new Kms.KeyManagementError('Key type RSA is not supported for ECDH-ES') + } + + // Compute shared secret + const sharedSecret = ecdh.computeSecret(publicKey.publicKey) + + // Prepare AlgorithmID for KDF (Datalen || Data) + const algorithmData = Buffer.from(options.usageAlgorithm) // ASCII representation of alg + const algorithmID = Buffer.concat([ + numberTo4ByteUint8Array(algorithmData.length), // Datalen: 32-bit big-endian counter + algorithmData, // Data: ASCII representation of algorithm + ]) + + // Prepare PartyUInfo with proper length prefix + const apu = options.apu || Buffer.alloc(0) + const partyUInfo = Buffer.concat([ + numberTo4ByteUint8Array(apu.length), // Datalen: 32-bit big-endian counter + apu, // Data: PartyUInfo value + ]) + + // Prepare PartyVInfo with proper length prefix + const apv = options.apv || Buffer.alloc(0) + const partyVInfo = Buffer.concat([ + numberTo4ByteUint8Array(apv.length), // Datalen: 32-bit big-endian counter + apv, // Data: PartyVInfo value + ]) + + // Prepare otherInfo for KDF + const otherInfo = Buffer.concat([ + algorithmID, // AlgorithmID: Datalen || Data + partyUInfo, // PartyUInfo: Datalen || Data + partyVInfo, // PartyVInfo: Datalen || Data + numberTo4ByteUint8Array(options.keyLength), // SuppPubInfo: 32-bit big-endian rep of keydatalen + Buffer.alloc(0), // SuppPrivInfo (empty octet sequence) + ]) + + // Derive final key using Concat KDF + return concatKDF(sharedSecret, options.keyLength, nodeConcatKdfHash, otherInfo) +} + +function numberTo4ByteUint8Array(number: number) { + const buffer = new ArrayBuffer(4) + const view = new DataView(buffer) + view.setUint32(0, number) + return new Uint8Array(buffer) +} + +/** + * Implements Concat KDF as per NIST SP 800-56A + */ +function concatKDF(secret: Buffer, length: number, hashLength: ConcatKdfHashLength, otherInfo: Buffer): Buffer { + const reps = Math.ceil((length >> 3) / (hashLength >> 3)) + const output = Buffer.alloc(reps * (hashLength >> 3)) + + for (let i = 0; i < reps; i++) { + const counter = Buffer.alloc(4 + secret.length + otherInfo.length) + counter.writeUInt32BE(i + 1) + counter.set(secret, 4) + counter.set(otherInfo, 4 + secret.length) + + createHash(`sha${hashLength}`) + .update(counter) + .digest() + .copy(output, (i * hashLength) >> 3) + } + + return output.subarray(0, length >> 3) +} + +function mapCrvToNodeEcdhCurveName(crv: Kms.KmsJwkPublicEc['crv'] | Kms.KmsJwkPublicOkp['crv']) { + switch (crv) { + case 'P-256': + return 'prime256v1' + case 'P-384': + return 'secp384r1' + case 'P-521': + return 'secp521r1' + case 'secp256k1': + return 'secp256k1' + case 'X25519': + return 'x25519' + default: + throw new Kms.KeyManagementAlgorithmNotSupportedError(`crv '${crv}' for ECDH-ES`, 'node') + } +} + +type ConcatKdfHashLength = ReturnType +function mapCrvToHashLength(crv: Kms.KmsJwkPublicEc['crv'] | Kms.KmsJwkPublicOkp['crv']) { + switch (crv) { + case 'secp256k1': + case 'X25519': + case 'P-256': + return 256 + case 'P-384': + return 384 + case 'P-521': + return 512 + default: + throw new Kms.KeyManagementAlgorithmNotSupportedError(`crv '${crv}' for ECDH-ES`, 'node') + } +} + +// TODO: might be worthwhile to add this to core? +// TODO: we might want to have a separate definition per algorithm +// defines things such as required key length. +function mapContentEncryptionAlgorithmToKeyLength( + encryptionAlgorithm: Kms.KnownJwaContentEncryptionAlgorithm | Kms.KnownJwaKeyEncryptionAlgorithm +): number { + switch (encryptionAlgorithm) { + case 'A128CBC': + case 'A128GCM': + case 'A128KW': + return 128 + case 'A192KW': + return 192 + case 'A128CBC-HS256': + case 'A256CBC': + case 'A256GCM': + case 'C20P': + case 'XC20P': + case 'A256KW': + return 256 + + case 'A192CBC-HS384': + case 'A192GCM': + return 384 + case 'A256CBC-HS512': + return 512 + case 'XSALSA20-POLY1305': + return 256 + } +} diff --git a/packages/node/src/kms/crypto/encrypt.ts b/packages/node/src/kms/crypto/encrypt.ts new file mode 100644 index 0000000000..b83a4610c7 --- /dev/null +++ b/packages/node/src/kms/crypto/encrypt.ts @@ -0,0 +1,150 @@ +import type { CipherGCM } from 'node:crypto' + +import { Buffer } from 'node:buffer' +import { createCipheriv, createSecretKey, randomBytes } from 'node:crypto' +import { Kms } from '@credo-ts/core' + +import { performSign } from './sign' + +export const nodeSupportedEncryptionAlgorithms = [ + 'A128CBC', + 'A256CBC', + 'A128CBC-HS256', + 'A192CBC-HS384', + 'A256CBC-HS512', + 'A128GCM', + 'A192GCM', + 'A256GCM', + 'C20P', +] as const satisfies Kms.KnownJwaContentEncryptionAlgorithm[] + +export async function performEncrypt( + key: Kms.KmsJwkPrivateOct, + dataEncryption: Kms.KmsEncryptDataEncryption, + data: Uint8Array +): Promise<{ encrypted: Uint8Array; tag?: Uint8Array; iv: Uint8Array }> { + const secretKeyBytes = Buffer.from(key.k, 'base64url') + const nodeKey = createSecretKey(secretKeyBytes) + + // Create cipher with key and IV + if (dataEncryption.algorithm === 'A128CBC' || dataEncryption.algorithm === 'A256CBC') { + const nodeAlgorithm = dataEncryption.algorithm === 'A128CBC' ? 'aes-128-cbc' : 'aes-256-cbc' + + // IV should be exactly 16 bytes (128 bits) for CBC mode + const iv = dataEncryption.iv ?? randomBytes(16) + + const cipher = createCipheriv(nodeAlgorithm, nodeKey, iv) + + // Get encrypted data + const encrypted = Buffer.concat([cipher.update(data), cipher.final()]) + + return { encrypted, iv } + } + if ( + dataEncryption.algorithm === 'A128CBC-HS256' || + dataEncryption.algorithm === 'A192CBC-HS384' || + dataEncryption.algorithm === 'A256CBC-HS512' + ) { + // Map algorithms to their corresponding CBC and HMAC settings + const algSettings = { + 'A128CBC-HS256': { cbcAlg: 'aes-128-cbc', hmacAlg: 'HS256', keySize: 16 } as const, + 'A192CBC-HS384': { cbcAlg: 'aes-192-cbc', hmacAlg: 'HS384', keySize: 24 } as const, + 'A256CBC-HS512': { cbcAlg: 'aes-256-cbc', hmacAlg: 'HS512', keySize: 32 } as const, + }[dataEncryption.algorithm] + + // IV should be exactly 16 bytes (128 bits) for CBC mode + const iv = dataEncryption.iv ?? randomBytes(16) + + // Split the input key into MAC and ENC keys (MAC key is first half, ENC key is second half) + const macKey = secretKeyBytes.subarray(0, algSettings.keySize) + const encKey = createSecretKey(secretKeyBytes.subarray(algSettings.keySize)) + + // Perform encryption + const cipher = createCipheriv(algSettings.cbcAlg, encKey, iv) + const encrypted = Buffer.concat([cipher.update(data), cipher.final()]) + + // Calculate authentication tag + // AL (Associated Length) is 64-bit big-endian length of AAD in bits + const al = Buffer.alloc(8) + const aadLength = dataEncryption.aad ? dataEncryption.aad.length * 8 : 0 + al.writeBigUInt64BE(BigInt(aadLength)) + + // Create concatenated buffer for MAC calculation + const macData = Buffer.concat([ + // If AAD exists, include it first, otherwise empty buffer + dataEncryption.aad ?? Buffer.alloc(0), + iv, // Initial Vector + encrypted, // Ciphertext + al, // Associated Length (AL) + ]) + + const hmac = await performSign({ kty: 'oct', k: macKey.toString('base64url') }, algSettings.hmacAlg, macData) + const tag = Buffer.from(hmac).subarray(0, algSettings.keySize) // Truncate to appropriate size + + return { encrypted, tag, iv } + } + if ( + dataEncryption.algorithm === 'A128GCM' || + dataEncryption.algorithm === 'A192GCM' || + dataEncryption.algorithm === 'A256GCM' + ) { + const nodeAlgorithm = + dataEncryption.algorithm === 'A128GCM' + ? 'aes-128-gcm' + : dataEncryption.algorithm === 'A192GCM' + ? 'aes-192-gcm' + : 'aes-256-gcm' + + // IV should be exactly 12 bytes (96 bits) for GCM + const iv = dataEncryption.iv ?? randomBytes(12) + + const cipher = createCipheriv(nodeAlgorithm, nodeKey, iv) + + // If AAD is provided, update the cipher with it before encryption + if (dataEncryption.aad) { + cipher.setAAD(dataEncryption.aad) + } + + // Get encrypted data + const encrypted = Buffer.concat([cipher.update(data), cipher.final()]) + + // Get auth tag - must be saved to verify decryption + const tag = cipher.getAuthTag() + + return { + encrypted, + tag, + iv, + } + } + if (dataEncryption.algorithm === 'C20P') { + // IV should be exactly 12 bytes (96 bits) for C20P + const iv = dataEncryption.iv ?? randomBytes(12) + + const cipher: CipherGCM = createCipheriv('chacha20-poly1305', nodeKey, iv, { + authTagLength: 16, + }) + + // If AAD is provided, update the cipher with it before encryption + if (dataEncryption.aad) { + cipher.setAAD(dataEncryption.aad) + } + + // Get encrypted data + const encrypted = Buffer.concat([cipher.update(data), cipher.final()]) + + // Get auth tag - must be saved to verify decryption + const tag = cipher.getAuthTag() + + return { + encrypted, + tag, + iv, + } + } + + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `JWA content encryption algorithm '${dataEncryption.algorithm}'`, + 'node' + ) +} diff --git a/packages/node/src/kms/crypto/sign.ts b/packages/node/src/kms/crypto/sign.ts new file mode 100644 index 0000000000..d2f6fda725 --- /dev/null +++ b/packages/node/src/kms/crypto/sign.ts @@ -0,0 +1,89 @@ +import type { CanBePromise } from '@credo-ts/core' + +import { constants, sign as _sign, createHmac, createPrivateKey, createSecretKey } from 'node:crypto' +import { promisify } from 'node:util' +import { Kms, TypedArrayEncoder } from '@credo-ts/core' + +const sign = promisify(_sign) + +export function performSign( + key: Kms.KmsJwkPrivate, + algorithm: Kms.KnownJwaSignatureAlgorithm, + data: Uint8Array +): CanBePromise { + const nodeAlgorithm = mapJwaSignatureAlgorithmToNode(algorithm) + const nodeKey = + key.kty === 'oct' ? createSecretKey(TypedArrayEncoder.fromBase64(key.k)) : createPrivateKey({ format: 'jwk', key }) + + switch (key.kty) { + case 'RSA': + case 'OKP': { + const nodeKeyInput = algorithm.startsWith('PS') + ? // For RSA-PSS, we need to set padding + { + key: nodeKey, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: Number.parseInt(algorithm.slice(2)) / 8, + } + : nodeKey + + return sign(nodeAlgorithm, data, nodeKeyInput) + } + case 'EC': { + // Node returns EC signatures as DER encoded, but we need raw + return sign(nodeAlgorithm, data, nodeKey).then((derSignature) => Kms.derEcSignatureToRaw(derSignature, key.crv)) + } + case 'oct': { + return createHmac(nodeAlgorithm as string, nodeKey) + .update(data) + .digest() + } + default: + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + throw new Kms.KeyManagementAlgorithmNotSupportedError(`kty '${key.kty}'`, 'node') + } +} + +export const nodeSupportedJwaAlgorithm = [ + 'RS256', + 'PS256', + 'HS256', + 'ES256', + 'ES256K', + 'RS384', + 'PS384', + 'HS384', + 'ES384', + 'RS512', + 'PS512', + 'HS512', + 'ES512', + 'EdDSA', +] as const satisfies Kms.KnownJwaSignatureAlgorithm[] + +export function mapJwaSignatureAlgorithmToNode(algorithm: Kms.KnownJwaSignatureAlgorithm) { + switch (algorithm) { + case 'RS256': + case 'PS256': + case 'HS256': + case 'ES256': + case 'ES256K': + return 'sha256' + case 'RS384': + case 'PS384': + case 'HS384': + case 'ES384': + return 'sha384' + case 'RS512': + case 'PS512': + case 'HS512': + case 'ES512': + return 'sha512' + // For EdDSA it's derived based on the key + case 'EdDSA': + return undefined + default: + throw new Kms.KeyManagementAlgorithmNotSupportedError(`JWA algorithm '${algorithm}'`, 'node') + } +} diff --git a/packages/node/src/kms/crypto/verify.ts b/packages/node/src/kms/crypto/verify.ts new file mode 100644 index 0000000000..a4abec8395 --- /dev/null +++ b/packages/node/src/kms/crypto/verify.ts @@ -0,0 +1,60 @@ +import { CanBePromise, Kms } from '@credo-ts/core' + +import { Buffer } from 'node:buffer' +import { + constants, + verify as _verify, + createHmac, + createPublicKey, + createSecretKey, + timingSafeEqual, +} from 'node:crypto' +import { promisify } from 'node:util' +import { TypedArrayEncoder } from '@credo-ts/core' + +import { mapJwaSignatureAlgorithmToNode } from './sign' + +const verify = promisify(_verify) + +export function performVerify( + key: Kms.KmsJwkPrivate | Kms.KmsJwkPublicEc | Kms.KmsJwkPublicOkp | Kms.KmsJwkPublicRsa, + algorithm: Kms.KnownJwaSignatureAlgorithm, + data: Uint8Array, + signature: Uint8Array +): CanBePromise { + const nodeAlgorithm = mapJwaSignatureAlgorithmToNode(algorithm) + const nodeKey = + key.kty === 'oct' ? createSecretKey(TypedArrayEncoder.fromBase64(key.k)) : createPublicKey({ format: 'jwk', key }) + + switch (key.kty) { + case 'RSA': + case 'OKP': { + const nodeKeyInput = algorithm.startsWith('PS') + ? // For RSA-PSS, we need to set padding + { + key: nodeKey, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: Number.parseInt(algorithm.slice(2)) / 8, + } + : nodeKey + + return verify(nodeAlgorithm, data, nodeKeyInput, signature) + } + case 'EC': { + // Node expects DER encoded signature, but we input raw + return verify(nodeAlgorithm, data, nodeKey, Kms.rawEcSignatureToDer(signature, key.crv)) + } + case 'oct': { + const expectedHmac = createHmac(nodeAlgorithm as string, nodeKey) + .update(data) + .digest() + + // eslint-disable-next-line no-restricted-globals + return timingSafeEqual(expectedHmac, Buffer.from(signature)) + } + default: + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + throw new Kms.KeyManagementAlgorithmNotSupportedError(`kty '${key.kty}'`, 'node') + } +} diff --git a/packages/node/tests/setup.ts b/packages/node/tests/setup.ts new file mode 100644 index 0000000000..89b8a1af96 --- /dev/null +++ b/packages/node/tests/setup.ts @@ -0,0 +1 @@ +jest.setTimeout(15000) diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 2430ed1fcd..64d1d15022 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -28,10 +28,10 @@ "class-transformer": "^0.5.1", "rxjs": "^7.8.0", "zod": "^3.24.2", - "@openid4vc/openid4vci": "0.3.0-alpha-20250330133535", - "@openid4vc/oauth2": "0.3.0-alpha-20250330133535", - "@openid4vc/openid4vp": "0.3.0-alpha-20250330133535", - "@openid4vc/utils": "0.3.0-alpha-20250330133535" + "@openid4vc/openid4vci": "0.3.0-alpha-20250511195407", + "@openid4vc/oauth2": "0.3.0-alpha-20250511195407", + "@openid4vc/openid4vp": "0.3.0-alpha-20250511195407", + "@openid4vc/utils": "0.3.0-alpha-20250511195407" }, "devDependencies": { "@credo-ts/tenants": "workspace:*", diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts index a58b32cd72..416b2bd058 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -1,8 +1,8 @@ -import type { AgentContext, JwaSignatureAlgorithm } from '@credo-ts/core' +import { AgentContext, DidsApi } from '@credo-ts/core' import { CredoError, InjectionSymbols, - Jwk, + Kms, Logger, Mdoc, MdocApi, @@ -11,14 +11,12 @@ import { W3cCredentialService, W3cJsonLdVerifiableCredential, W3cJwtVerifiableCredential, - getJwkClassFromJwaSignatureAlgorithm, - getJwkFromJson, - getJwkFromKey, inject, injectable, parseDid, } from '@credo-ts/core' import { + Jwk, Oauth2Client, RequestDpopOptions, authorizationCodeGrantIdentifier, @@ -60,7 +58,7 @@ import type { import { OpenId4VciCredentialFormatProfile } from '../shared' import { getOid4vcCallbacks } from '../shared/callbacks' import { getOfferedCredentials, getScopesFromCredentialConfigurationsSupported } from '../shared/issuerMetadataUtils' -import { getKeyFromDid, getSupportedJwaSignatureAlgorithms } from '../shared/utils' +import { getSupportedJwaSignatureAlgorithms } from '../shared/utils' import { openId4VciSupportedCredentialFormats } from './OpenId4VciHolderServiceOptions' @@ -168,8 +166,8 @@ export class OpenId4VciHolderService { // FIXME: return dpop result from this endpoint (dpop nonce) dpop: dpop ? { - alg: dpop.signer.alg as JwaSignatureAlgorithm, - jwk: getJwkFromJson(dpop.signer.publicJwk), + alg: dpop.signer.alg as Kms.KnownJwaSignatureAlgorithm, + jwk: Kms.PublicJwk.fromUnknown(dpop.signer.publicJwk), } : undefined, } @@ -183,8 +181,8 @@ export class OpenId4VciHolderService { // FIXME: return dpop result from this endpoint (dpop nonce) dpop: dpop ? { - alg: dpop.signer.alg as JwaSignatureAlgorithm, - jwk: getJwkFromJson(dpop.signer.publicJwk), + alg: dpop.signer.alg as Kms.KnownJwaSignatureAlgorithm, + jwk: Kms.PublicJwk.fromUnknown(dpop.signer.publicJwk), } : undefined, } @@ -214,18 +212,20 @@ export class OpenId4VciHolderService { jwk, dpopSigningAlgValuesSupported, nonce, - }: { dpopSigningAlgValuesSupported: string[]; jwk?: Jwk; nonce?: string } + }: { dpopSigningAlgValuesSupported: string[]; jwk?: Kms.PublicJwk; nonce?: string } ): Promise { + const kms = agentContext.resolve(Kms.KeyManagementApi) + if (jwk) { const alg = dpopSigningAlgValuesSupported.find((alg) => - jwk.supportedSignatureAlgorithms.includes(alg as JwaSignatureAlgorithm) + jwk.supportedSignatureAlgorithms.includes(alg as Kms.KnownJwaSignatureAlgorithm) ) if (!alg) { throw new CredoError( `No supported dpop signature algorithms found in dpop_signing_alg_values_supported '${dpopSigningAlgValuesSupported.join( ', ' - )}' matching key type ${jwk.keyType}` + )}' matching jwk ${jwk.jwkTypehumanDescription}` ) } @@ -233,16 +233,22 @@ export class OpenId4VciHolderService { signer: { method: 'jwk', alg, - publicJwk: jwk.toJson(), + publicJwk: jwk.toJson() as Jwk, }, nonce, } } - const alg = dpopSigningAlgValuesSupported.find((alg) => getJwkClassFromJwaSignatureAlgorithm(alg)) - const JwkClass = alg ? getJwkClassFromJwaSignatureAlgorithm(alg) : undefined + const alg = dpopSigningAlgValuesSupported.find((alg): alg is Kms.KnownJwaSignatureAlgorithm => { + try { + Kms.PublicJwk.supportedPublicJwkClassForSignatureAlgorithm(alg as Kms.KnownJwaSignatureAlgorithm) + return true + } catch { + return false + } + }) - if (!alg || !JwkClass) { + if (!alg) { throw new CredoError( `No supported dpop signature algorithms found in dpop_signing_alg_values_supported '${dpopSigningAlgValuesSupported.join( ', ' @@ -250,12 +256,12 @@ export class OpenId4VciHolderService { ) } - const key = await agentContext.wallet.createKey({ keyType: JwkClass.keyType }) + const key = await kms.createKeyForSignatureAlgorithm({ algorithm: alg }) return { signer: { method: 'jwk', alg, - publicJwk: getJwkFromKey(key).toJson(), + publicJwk: key.publicJwk as Jwk, }, nonce, } @@ -289,8 +295,8 @@ export class OpenId4VciHolderService { dpop: dpop ? { ...dpopResult, - alg: dpop.signer.alg as JwaSignatureAlgorithm, - jwk: getJwkFromJson(dpop.signer.publicJwk), + alg: dpop.signer.alg as Kms.KnownJwaSignatureAlgorithm, + jwk: Kms.PublicJwk.fromUnknown(dpop.signer.publicJwk), } : undefined, } @@ -351,8 +357,8 @@ export class OpenId4VciHolderService { dpop: dpop ? { ...result.dpop, - alg: dpop.signer.alg as JwaSignatureAlgorithm, - jwk: getJwkFromJson(dpop.signer.publicJwk), + alg: dpop.signer.alg as Kms.KnownJwaSignatureAlgorithm, + jwk: Kms.PublicJwk.fromUnknown(dpop.signer.publicJwk), } : undefined, } @@ -522,7 +528,7 @@ export class OpenId4VciHolderService { options: { metadata: OpenId4VciResolvedCredentialOffer['metadata'] credentialBindingResolver: OpenId4VciCredentialBindingResolver - allowedProofOfPossesionAlgorithms: JwaSignatureAlgorithm[] + allowedProofOfPossesionAlgorithms: Kms.KnownJwaSignatureAlgorithm[] clientId?: string cNonce: string offeredCredential: { @@ -531,6 +537,7 @@ export class OpenId4VciHolderService { } } ) { + const dids = agentContext.resolve(DidsApi) const { allowedProofOfPossesionAlgorithms, offeredCredential } = options const { configuration, id: configurationId } = offeredCredential const supportedJwaSignatureAlgorithms = getSupportedJwaSignatureAlgorithms(agentContext) @@ -621,12 +628,13 @@ export class OpenId4VciHolderService { ) } - const firstKey = await getKeyFromDid(agentContext, firstDid.didUrl) - if (!proofTypes.jwt.supportedKeyTypes.includes(firstKey.keyType)) { + const { publicJwk: firstKey } = await dids.resolveVerificationMethodFromCreatedDidRecord(firstDid.didUrl) + const algorithm = proofTypes.jwt.supportedSignatureAlgorithms.find((algorithm) => + firstKey.supportedSignatureAlgorithms.includes(algorithm) + ) + if (!algorithm) { throw new CredoError( - `Credential binding returned did url that points to key with type '${ - firstKey.keyType - }', but one of '${proofTypes.jwt.supportedKeyTypes.join(', ')}' was expected` + `Credential binding returned did url that points to key '${firstKey.jwkTypehumanDescription}' that supports signature algorithms ${firstKey.supportedSignatureAlgorithms.join(', ')}, but one of '${proofTypes.jwt.supportedSignatureAlgorithms.join(', ')}' was expected` ) } @@ -635,20 +643,14 @@ export class OpenId4VciHolderService { credentialBinding.didUrls.map(async (didUrl, index) => index === 0 ? // We already fetched the first did - { key: firstKey, didUrl: firstDid.didUrl } - : { key: await getKeyFromDid(agentContext, didUrl), didUrl } + { jwk: firstKey, didUrl: firstDid.didUrl } + : { jwk: (await dids.resolveVerificationMethodFromCreatedDidRecord(didUrl)).publicJwk, didUrl } ) ) - if (!keys.every((key) => key.key.keyType === firstKey.keyType)) { + if (!keys.every((key) => Kms.assymetricJwkKeyTypeMatches(key.jwk.toJson(), firstKey.toJson()))) { throw new CredoError('Expected all did urls to point to the same key type') } - const alg = getJwkFromKey(firstKey).supportedSignatureAlgorithms[0] - if (!alg) { - // Should not happen, to make ts happy - throw new CredoError(`Unable to determine alg for key type ${firstKey.keyType}`) - } - return { jwt: await Promise.all( keys.map((key) => @@ -659,7 +661,8 @@ export class OpenId4VciHolderService { signer: { method: 'did', didUrl: key.didUrl, - alg, + alg: algorithm, + kid: key.jwk.keyId, }, nonce: options.cNonce, clientId: options.clientId, @@ -700,23 +703,20 @@ export class OpenId4VciHolderService { } const firstJwk = credentialBinding.keys[0] - if (!credentialBinding.keys.every((key) => key.keyType === firstJwk.keyType)) { + + if (!credentialBinding.keys.every((key) => Kms.assymetricJwkKeyTypeMatches(key.toJson(), firstJwk.toJson()))) { throw new CredoError('Expected all keys for binding method jwk to use the same key type') } - if (!proofTypes.jwt.supportedKeyTypes.includes(firstJwk.keyType)) { + + const algorithm = proofTypes.jwt.supportedSignatureAlgorithms.find((algorithm) => + firstJwk.supportedSignatureAlgorithms.includes(algorithm) + ) + if (!algorithm) { throw new CredoError( - `Credential binding returned jwk with key with type '${ - firstJwk.keyType - }', but one of '${proofTypes.jwt.supportedKeyTypes.join(', ')}' was expected` + `Credential binding returned jwk that points to key '${firstJwk.jwkTypehumanDescription}' that supports signature algorithms ${firstJwk.supportedSignatureAlgorithms.join(', ')}, but one of '${proofTypes.jwt.supportedSignatureAlgorithms.join(', ')}' was expected` ) } - const alg = firstJwk.supportedSignatureAlgorithms[0] - if (!alg) { - // Should not happen, to make ts happy - throw new CredoError(`Unable to determine alg for key type ${firstJwk.keyType}`) - } - return { jwt: await Promise.all( credentialBinding.keys.map((jwk) => @@ -726,8 +726,8 @@ export class OpenId4VciHolderService { issuerMetadata: options.metadata, signer: { method: 'jwk', - publicJwk: jwk.toJson(), - alg, + publicJwk: jwk.toJson() as Jwk, + alg: algorithm, }, nonce: options.cNonce, clientId: options.clientId, @@ -758,7 +758,7 @@ export class OpenId4VciHolderService { } if (proofTypes.jwt) { - const jwk = getJwkFromJson(payload.attested_keys[0]) + const jwk = Kms.PublicJwk.fromUnknown(payload.attested_keys[0]) return { jwt: [ @@ -804,7 +804,7 @@ export class OpenId4VciHolderService { id: string configuration: OpenId4VciCredentialConfigurationSupportedWithFormats } - possibleProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] + possibleProofOfPossessionSignatureAlgorithms: Kms.KnownJwaSignatureAlgorithm[] } ): OpenId4VciProofOfPossessionRequirements { const { credentialToRequest, possibleProofOfPossessionSignatureAlgorithms, metadata } = options @@ -848,7 +848,7 @@ export class OpenId4VciHolderService { for (const [proofType, proofTypeConfig] of Object.entries(proofTypesSupported)) { if (proofType !== 'jwt' && proofType !== 'attestation') continue - let signatureAlgorithms: JwaSignatureAlgorithm[] = [] + let signatureAlgorithms: Kms.KnownJwaSignatureAlgorithm[] = [] const proofSigningAlgsSupported = proofTypeConfig?.proof_signing_alg_values_supported if (proofSigningAlgsSupported === undefined) { @@ -866,15 +866,19 @@ export class OpenId4VciHolderService { proofSigningAlgsSupported.includes(signatureAlgorithm) ) break + // FIXME: this is wrong, as the proof type is separate from the credential signing alg + // But there might be some draft 11 logic that depends on this, can be removed soon case OpenId4VciCredentialFormatProfile.LdpVc: signatureAlgorithms = options.possibleProofOfPossessionSignatureAlgorithms.filter((signatureAlgorithm) => { - const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) - if (!JwkClass) return false - - const matchingSuite = signatureSuiteRegistry.getAllByKeyType(JwkClass.keyType) - if (matchingSuite.length === 0) return false - - return proofSigningAlgsSupported.includes(matchingSuite[0].proofType) + try { + const jwkClass = Kms.PublicJwk.supportedPublicJwkClassForSignatureAlgorithm(signatureAlgorithm) + const matchingSuites = signatureSuiteRegistry.getAllByPublicJwkType(jwkClass) + if (matchingSuites.length === 0) return false + + return proofSigningAlgsSupported.includes(matchingSuites[0].proofType) + } catch { + return false + } }) break default: @@ -884,9 +888,6 @@ export class OpenId4VciHolderService { proofTypes[proofType] = { supportedSignatureAlgorithms: signatureAlgorithms, - supportedKeyTypes: signatureAlgorithms - .map((algorithm) => getJwkClassFromJwaSignatureAlgorithm(algorithm)?.keyType) - .filter((keyType) => keyType !== undefined), keyAttestationsRequired: proofTypeConfig.key_attestations_required ? { keyStorage: proofTypeConfig.key_attestations_required.key_storage, diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts index 77342b6d3c..0b568d7643 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts @@ -1,4 +1,4 @@ -import type { AgentContext, JwaSignatureAlgorithm, Jwk, KeyType, VerifiableCredential } from '@credo-ts/core' +import type { AgentContext, Kms, VerifiableCredential } from '@credo-ts/core' import type { CredentialOfferObject, IssuerMetadataResult } from '@openid4vc/openid4vci' import type { OpenId4VcCredentialHolderBinding, @@ -32,8 +32,8 @@ export const openId4VciSupportedCredentialFormats: OpenId4VciSupportedCredential ] export interface OpenId4VciDpopRequestOptions { - jwk: Jwk - alg: JwaSignatureAlgorithm + jwk: Kms.PublicJwk + alg: Kms.KnownJwaSignatureAlgorithm nonce?: string } @@ -233,7 +233,7 @@ export interface OpenId4VciAcceptCredentialOfferOptions { * for signing the credential, but this not a requirement for the spec. E.g. if the * pop uses EdDsa, the credential will most commonly also use EdDsa, or Ed25519Signature2018/2020. */ - allowedProofOfPossessionSignatureAlgorithms?: JwaSignatureAlgorithm[] + allowedProofOfPossessionSignatureAlgorithms?: Kms.KnownJwaSignatureAlgorithm[] /** * A function that should resolve key material for binding the to-be-issued credential @@ -372,14 +372,7 @@ export type OpenId4VciProofOfPressionProofTypes = Record< * to the request credential method, and the supported proof type signature * algorithms for the specific credential configuration */ - supportedSignatureAlgorithms: JwaSignatureAlgorithm[] - - /** - * The key type that can be used to create the proof of possession signature. - * This is related to the verification method and the signature algorithm, and - * is added for convenience. - */ - supportedKeyTypes: KeyType[] + supportedSignatureAlgorithms: Kms.KnownJwaSignatureAlgorithm[] /** * Whether key attestations are required and which level needs to be met. If the object diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vpHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vpHolderService.ts index 046dddff54..597efb4832 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vpHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vpHolderService.ts @@ -23,6 +23,7 @@ import { DifPresentationExchangeService, DifPresentationExchangeSubmissionLocation, Hasher, + Kms, TypedArrayEncoder, injectable, } from '@credo-ts/core' @@ -295,10 +296,11 @@ export class OpenId4VpHolderService { agentContext: AgentContext, options: OpenId4VpAcceptAuthorizationRequestOptions ) { + const kms = agentContext.resolve(Kms.KeyManagementApi) const { authorizationRequestPayload, presentationExchange, dcql, transactionData } = options const openid4vpClient = this.getOpenid4vpClient(agentContext) - const authorizationResponseNonce = await agentContext.wallet.generateNonce() + const authorizationResponseNonce = TypedArrayEncoder.toBase64URL(kms.randomBytes({ length: 32 }).bytes) const { nonce } = authorizationRequestPayload const parsedClientId = getOpenid4vpClientId({ responseMode: authorizationRequestPayload.response_mode, @@ -406,6 +408,7 @@ export class OpenId4VpHolderService { const response = await openid4vpClient.createOpenid4vpAuthorizationResponse({ authorizationRequestPayload, + origin: options.origin, authorizationResponsePayload: { vp_token: vpToken, presentation_submission: presentationSubmission, diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.test.ts index cd89e3e198..c3ab21e355 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.test.ts @@ -1,57 +1,63 @@ -import type { Key, SdJwtVc } from '@credo-ts/core' - -import { - Agent, - DidKey, - JwaSignatureAlgorithm, - KeyType, - TypedArrayEncoder, - W3cCredentialService, - W3cJwtVerifiableCredential, - getJwkFromKey, -} from '@credo-ts/core' -import nock, { cleanAll, enableNetConnect } from 'nock' +import { KeyDidCreateOptions, Kms, SdJwtVc } from '@credo-ts/core' -import { AskarModule } from '../../../../askar/src' -import { askarModuleConfig } from '../../../../askar/tests/helpers' +import { Agent, DidKey, TypedArrayEncoder, W3cCredentialService, W3cJwtVerifiableCredential } from '@credo-ts/core' +import nock, { cleanAll, enableNetConnect } from 'nock' import { agentDependencies } from '../../../../node/src' import { OpenId4VcHolderModule } from '../OpenId4VcHolderModule' import { OpenId4VciAuthorizationFlow } from '../OpenId4VciHolderServiceOptions' +import { InMemoryWalletModule } from '../../../../../tests/InMemoryWalletModule' +import { transformPrivateKeyToPrivateJwk } from '../../../../askar/src' import { animoOpenIdPlaygroundDraft11SdJwtVc, matrrLaunchpadDraft11JwtVcJson, waltIdDraft11JwtVcJson } from './fixtures' const holder = new Agent({ config: { label: 'OpenId4VcHolder Test28', - walletConfig: { id: 'openid4vc-holder-test27', key: 'openid4vc-holder-test27' }, }, dependencies: agentDependencies, modules: { openId4VcHolder: new OpenId4VcHolderModule(), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), }, }) describe('OpenId4VcHolder', () => { - let holderKey: Key + let holderKey: Kms.PublicJwk let holderDid: string let holderVerificationMethod: string beforeEach(async () => { await holder.initialize() - holderKey = await holder.wallet.createKey({ - keyType: KeyType.Ed25519, - privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + const key = await holder.kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + type: { + kty: 'OKP', + crv: 'Ed25519', + }, + }).privateJwk, }) - const holderDidKey = new DidKey(holderKey) + holderKey = Kms.PublicJwk.fromPublicJwk(key.publicJwk) + + const { + didState: { did }, + } = await holder.dids.create({ + method: 'key', + options: { + keyId: key.keyId, + }, + }) + + if (!did) throw new Error('expected did') + + const holderDidKey = DidKey.fromDid(did) holderDid = holderDidKey.did - holderVerificationMethod = `${holderDidKey.did}#${holderDidKey.key.fingerprint}` + holderVerificationMethod = `${holderDidKey.did}#${holderDidKey.publicJwk.fingerprint}` }) afterEach(async () => { await holder.shutdown() - await holder.wallet.delete() }) describe('[DRAFT 11]: Pre-authorized flow', () => { @@ -112,7 +118,7 @@ describe('OpenId4VcHolder', () => { verifyCredentialStatus: false, // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + allowedProofOfPossessionSignatureAlgorithms: [Kms.KnownJwaSignatureAlgorithms.EdDSA], credentialConfigurationIds: Object.entries(resolved.offeredCredentialConfigurations) .filter(([, configuration]) => configuration.format === 'jwt_vc_json') .map(([id]) => id), @@ -162,7 +168,7 @@ describe('OpenId4VcHolder', () => { verifyCredentialStatus: false, // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + allowedProofOfPossessionSignatureAlgorithms: [Kms.KnownJwaSignatureAlgorithms.EdDSA], credentialConfigurationIds: Object.entries(resolved.offeredCredentialConfigurations) .filter(([, configuration]) => configuration.format === 'jwt_vc_json') .map(([id]) => id), @@ -216,11 +222,11 @@ describe('OpenId4VcHolder', () => { verifyCredentialStatus: false, // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + allowedProofOfPossessionSignatureAlgorithms: [Kms.KnownJwaSignatureAlgorithms.EdDSA], credentialConfigurationIds: Object.entries(resolvedCredentialOffer.offeredCredentialConfigurations) .filter(([, configuration]) => configuration.format === 'vc+sd-jwt') .map(([id]) => id), - credentialBindingResolver: () => ({ method: 'jwk', keys: [getJwkFromKey(holderKey)] }), + credentialBindingResolver: () => ({ method: 'jwk', keys: [holderKey] }), }) if (!credentialResponse.credentials[0]?.notificationId) throw new Error("Notification metadata wasn't returned") @@ -343,7 +349,7 @@ describe('OpenId4VcHolder', () => { holder.modules.openId4VcHolder.requestCredentials({ resolvedCredentialOffer, ...tokenResponse, - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + allowedProofOfPossessionSignatureAlgorithms: [Kms.KnownJwaSignatureAlgorithms.EdDSA], credentialBindingResolver: () => ({ method: 'did', didUrls: [holderVerificationMethod] }), verifyCredentialStatus: false, }) diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index a994bbba9e..5e0afac28e 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -23,16 +23,13 @@ import { JwsService, Jwt, JwtPayload, - Key, - KeyType, + Kms, MdocApi, Query, QueryOptions, SdJwtVcApi, TypedArrayEncoder, W3cCredentialService, - getJwkFromJson, - getJwkFromKey, injectable, joinUriParts, utils, @@ -69,8 +66,8 @@ import { storeActorIdForContextCorrelationId } from '../shared/router' import { addSecondsToDate, dateToSeconds, - getKeyFromDid, - getProofTypeFromKey, + getProofTypeFromPublicJwk, + getPublicJwkFromDid, getSupportedJwaSignatureAlgorithms, } from '../shared/utils' @@ -495,11 +492,9 @@ export class OpenId4VcIssuerService { return { bindingMethod: 'jwk', keys: keyAttestation.payload.attested_keys.map((attestedKey) => { - const jwk = getJwkFromJson(attestedKey) return { method: 'jwk', - jwk, - key: jwk.key, + jwk: Kms.PublicJwk.fromUnknown(attestedKey), } }), proofType: 'attestation', @@ -645,11 +640,9 @@ export class OpenId4VcIssuerService { proofType: 'jwt', bindingMethod: 'jwk', keys: keyAttestation.payload.attested_keys.map((attestedKey) => { - const jwk = getJwkFromJson(attestedKey) return { method: 'jwk', - jwk, - key: jwk.key, + jwk: Kms.PublicJwk.fromUnknown(attestedKey), } }), keyAttestation, @@ -666,7 +659,7 @@ export class OpenId4VcIssuerService { keys: signers.map((signer) => ({ didUrl: signer.didUrl, method: 'did', - key: getJwkFromJson(signer.publicJwk).key, + jwk: Kms.PublicJwk.fromUnknown(signer.publicJwk), })), } } @@ -675,11 +668,9 @@ export class OpenId4VcIssuerService { proofType: 'jwt', bindingMethod: 'jwk', keys: (proofSigners as JwtSignerJwk[]).map((signer) => { - const jwk = getJwkFromJson(signer.publicJwk) return { method: 'jwk', - jwk, - key: jwk.key, + jwk: Kms.PublicJwk.fromUnknown(signer.publicJwk), } }), } @@ -724,18 +715,20 @@ export class OpenId4VcIssuerService { } public async createIssuer(agentContext: AgentContext, options: OpenId4VciCreateIssuerOptions) { + const kms = agentContext.resolve(Kms.KeyManagementApi) + // TODO: ideally we can store additional data with a key, such as: // - createdAt // - purpose - const accessTokenSignerKey = await agentContext.wallet.createKey({ - keyType: options.accessTokenSignerKeyType ?? KeyType.Ed25519, + const accessTokenSignerKey = await kms.createKey({ + type: options.accessTokenSignerKeyType ?? { kty: 'OKP', crv: 'Ed25519' }, }) const openId4VcIssuer = new OpenId4VcIssuerRecord({ issuerId: options.issuerId ?? utils.uuid(), display: options.display, dpopSigningAlgValuesSupported: options.dpopSigningAlgValuesSupported, - accessTokenPublicKeyFingerprint: accessTokenSignerKey.fingerprint, + accessTokenPublicJwk: accessTokenSignerKey.publicJwk, authorizationServerConfigs: options.authorizationServerConfigs, credentialConfigurationsSupported: options.credentialConfigurationsSupported, batchCredentialIssuance: options.batchCredentialIssuance, @@ -751,13 +744,20 @@ export class OpenId4VcIssuerService { issuer: OpenId4VcIssuerRecord, options?: Pick ) { - const accessTokenSignerKey = await agentContext.wallet.createKey({ - keyType: options?.accessTokenSignerKeyType ?? KeyType.Ed25519, + const kms = agentContext.resolve(Kms.KeyManagementApi) + + const previousKey = issuer.resolvedAccessTokenPublicJwk + const accessTokenSignerKey = await kms.createKey({ + type: options?.accessTokenSignerKeyType ?? { kty: 'OKP', crv: 'Ed25519' }, }) - // TODO: ideally we can remove the previous key - issuer.accessTokenPublicKeyFingerprint = accessTokenSignerKey.fingerprint + issuer.accessTokenPublicJwk = accessTokenSignerKey.publicJwk await this.openId4VcIssuerRepository.update(agentContext, issuer) + + // Remove previous key + await kms.deleteKey({ + keyId: previousKey.keyId, + }) } /** @@ -836,19 +836,17 @@ export class OpenId4VcIssuerService { const cNonceExpiresInSeconds = this.openId4VcIssuerConfig.cNonceExpiresInSeconds const cNonceExpiresAt = addSecondsToDate(new Date(), cNonceExpiresInSeconds) - const key = Key.fromFingerprint(issuer.accessTokenPublicKeyFingerprint) - const jwk = getJwkFromKey(key) - + const key = issuer.resolvedAccessTokenPublicJwk const cNonce = await jwsService.createJwsCompact(agentContext, { - key, + keyId: key.keyId, payload: JwtPayload.fromJson({ iss: issuerMetadata.credentialIssuer.credential_issuer, exp: dateToSeconds(cNonceExpiresAt), }), protectedHeaderOptions: { typ: 'credo+cnonce', - kid: issuer.accessTokenPublicKeyFingerprint, - alg: jwk.supportedSignatureAlgorithms[0], + kid: key.keyId, + alg: key.signatureAlgorithm, }, }) @@ -868,9 +866,7 @@ export class OpenId4VcIssuerService { const issuerMetadata = await this.getIssuerMetadata(agentContext, issuer) const jwsService = agentContext.dependencyManager.resolve(JwsService) - const key = Key.fromFingerprint(issuer.accessTokenPublicKeyFingerprint) - const jwk = getJwkFromKey(key) - + const key = issuer.resolvedAccessTokenPublicJwk const jwt = Jwt.fromSerializedJwt(cNonce) jwt.payload.validate() @@ -885,7 +881,7 @@ export class OpenId4VcIssuerService { jws: cNonce, jwsSigner: { method: 'jwk', - jwk, + jwk: key, }, }) @@ -965,6 +961,7 @@ export class OpenId4VcIssuerService { authorizationCodeFlowConfig?: OpenId4VciAuthorizationCodeFlowConfig } ) { + const kms = agentContext.resolve(Kms.KeyManagementApi) const { preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig, issuerMetadata } = config // TOOD: export type @@ -975,7 +972,8 @@ export class OpenId4VcIssuerService { const { txCode, authorizationServerUrl, preAuthorizedCode } = preAuthorizedCodeFlowConfig grants[preAuthorizedCodeGrantIdentifier] = { - 'pre-authorized_code': preAuthorizedCode ?? (await agentContext.wallet.generateNonce()), + 'pre-authorized_code': + preAuthorizedCode ?? TypedArrayEncoder.toBase64URL(kms.randomBytes({ length: 32 }).bytes), tx_code: txCode, authorization_server: config.issuerMetadata.credentialIssuer.authorization_servers ? authorizationServerUrl @@ -1003,7 +1001,7 @@ export class OpenId4VcIssuerService { // TODO: the issuer_state should not be guessable, so it's best if we generate it and now allow the user to provide it? // but same is true for the pre-auth code and users of credo can also provide that value. We can't easily do unique constraint with askat authorizationCodeFlowConfig.issuerState ?? - TypedArrayEncoder.toBase64URL(agentContext.wallet.getRandomValues(32)), + TypedArrayEncoder.toBase64URL(kms.randomBytes({ length: 32 }).bytes), authorization_server: config.issuerMetadata.credentialIssuer.authorization_servers ? authorizationServerUrl : undefined, @@ -1282,27 +1280,17 @@ export class OpenId4VcIssuerService { format: `${ClaimFormat.JwtVc}` | `${ClaimFormat.LdpVc}`, options: OpenId4VciSignW3cCredentials['credentials'][number] ) { - const key = await getKeyFromDid(agentContext, options.verificationMethod) + const publicJwk = await getPublicJwkFromDid(agentContext, options.verificationMethod) if (format === ClaimFormat.JwtVc) { - const supportedSignatureAlgorithms = getJwkFromKey(key).supportedSignatureAlgorithms - if (supportedSignatureAlgorithms.length === 0) { - throw new CredoError(`No supported JWA signature algorithms found for key with keyType ${key.keyType}`) - } - - const alg = supportedSignatureAlgorithms[0] - if (!alg) { - throw new CredoError(`No supported JWA signature algorithms for key type ${key.keyType}`) - } - return await this.w3cCredentialService.signCredential(agentContext, { format: ClaimFormat.JwtVc, credential: options.credential, verificationMethod: options.verificationMethod, - alg, + alg: publicJwk.signatureAlgorithm, }) } - const proofType = getProofTypeFromKey(agentContext, key) + const proofType = getProofTypeFromPublicJwk(agentContext, publicJwk) return await this.w3cCredentialService.signCredential(agentContext, { format: ClaimFormat.LdpVc, credential: options.credential, diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index 65e19dc07f..8e92a615f6 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -1,12 +1,4 @@ -import type { - AgentContext, - ClaimFormat, - JwaSignatureAlgorithm, - KeyType, - MdocSignOptions, - SdJwtVcSignOptions, - W3cCredential, -} from '@credo-ts/core' +import type { AgentContext, ClaimFormat, Kms, MdocSignOptions, SdJwtVcSignOptions, W3cCredential } from '@credo-ts/core' import type { AccessTokenProfileJwtPayload, TokenIntrospectionResponse } from '@openid4vc/oauth2' import type { OpenId4VcVerificationSessionRecord, @@ -324,13 +316,19 @@ export type OpenId4VciCreateIssuerOptions = { /** * Key type to use for signing access tokens * - * @default KeyType.Ed25519 + * @default + * ```json + * { + * kty: "OKP", + * crv: "Ed25519" + * } + * ``` */ - accessTokenSignerKeyType?: KeyType + accessTokenSignerKeyType?: Kms.KmsCreateKeyTypeAssymetric display?: OpenId4VciCredentialIssuerMetadataDisplay[] authorizationServerConfigs?: OpenId4VciAuthorizationServerConfig[] - dpopSigningAlgValuesSupported?: [JwaSignatureAlgorithm, ...JwaSignatureAlgorithm[]] + dpopSigningAlgValuesSupported?: [Kms.KnownJwaSignatureAlgorithm, ...Kms.KnownJwaSignatureAlgorithm[]] credentialConfigurationsSupported: OpenId4VciCredentialConfigurationsSupportedWithFormats diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.test.ts index 4168cbebc8..cd6afe092b 100644 --- a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.test.ts +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.test.ts @@ -21,7 +21,6 @@ import { JsonTransformer, JwsService, JwtPayload, - KeyType, SdJwtVcApi, TypedArrayEncoder, W3cCredential, @@ -31,15 +30,13 @@ import { W3cJsonLdVerifiableCredential, W3cJwtVerifiableCredential, equalsIgnoreOrder, - getJwkFromKey, w3cDate, } from '@credo-ts/core' - -import { AskarModule } from '../../../../askar/src' -import { askarModuleConfig } from '../../../../askar/tests/helpers' +import { InMemoryWalletModule } from '../../../../../tests/InMemoryWalletModule' +import { transformPrivateKeyToPrivateJwk } from '../../../../askar/src' import { agentDependencies } from '../../../../node/src' import { OpenId4VciCredentialFormatProfile } from '../../shared' -import { dateToSeconds, getKeyFromDid } from '../../shared/utils' +import { dateToSeconds } from '../../shared/utils' import { OpenId4VcIssuanceSessionState } from '../OpenId4VcIssuanceSessionState' import { OpenId4VcIssuerModule } from '../OpenId4VcIssuerModule' import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' @@ -83,7 +80,7 @@ const modules = { throw new Error('Not implemented') }, }), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), } const jwsService = new JwsService() @@ -101,16 +98,10 @@ const createCredentialRequest = async ( const { credentialConfiguration, kid, nonce, issuerMetadata, clientId } = options const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didDocument = await didsApi.resolveDidDocument(kid) - if (!didDocument.verificationMethod) { - throw new CredoError(`No verification method found for kid ${kid}`) - } - - const key = await getKeyFromDid(agentContext, kid) - const jwk = getJwkFromKey(key) + const { publicJwk } = await didsApi.resolveVerificationMethodFromCreatedDidRecord(kid) const jws = await jwsService.createJwsCompact(agentContext, { - protectedHeaderOptions: { alg: jwk.supportedSignatureAlgorithms[0], kid, typ: 'openid4vci-proof+jwt' }, + protectedHeaderOptions: { alg: publicJwk.signatureAlgorithm, kid, typ: 'openid4vci-proof+jwt' }, payload: new JwtPayload({ iat: dateToSeconds(new Date()), iss: clientId, @@ -119,7 +110,7 @@ const createCredentialRequest = async ( nonce, }, }), - key, + keyId: publicJwk.keyId, }) if (credentialConfiguration.format === OpenId4VciCredentialFormatProfile.JwtVcJson) { @@ -149,10 +140,6 @@ const createCredentialRequest = async ( const issuer = new Agent({ config: { label: 'OpenId4VcIssuer Test323', - walletConfig: { - id: 'openid4vc-Issuer-test323', - key: 'openid4vc-Issuer-test323', - }, }, dependencies: agentDependencies, modules, @@ -161,10 +148,6 @@ const issuer = new Agent({ const holder = new Agent({ config: { label: 'OpenId4VciIssuer(Holder) Test323', - walletConfig: { - id: 'openid4vc-Issuer(Holder)-test323', - key: 'openid4vc-Issuer(Holder)-test323', - }, }, dependencies: agentDependencies, modules, @@ -183,31 +166,42 @@ describe('OpenId4VcIssuer', () => { await issuer.initialize() await holder.initialize() + const { keyId } = await holder.kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + type: { kty: 'OKP', crv: 'Ed25519' }, + }).privateJwk, + }) + const holderDidCreateResult = await holder.dids.create({ method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + options: { keyId }, }) holderDid = holderDidCreateResult.didState.did as string const holderDidKey = DidKey.fromDid(holderDid) - holderKid = `${holderDid}#${holderDidKey.key.fingerprint}` + holderKid = `${holderDid}#${holderDidKey.publicJwk.fingerprint}` const _holderVerificationMethod = holderDidCreateResult.didState.didDocument?.dereferenceKey(holderKid, [ 'authentication', ]) if (!_holderVerificationMethod) throw new Error('No verification method found') holderVerificationMethod = _holderVerificationMethod + const { keyId: issuerKeyId } = await issuer.kms.importKey({ + privateJwk: transformPrivateKeyToPrivateJwk({ + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f'), + type: { kty: 'OKP', crv: 'Ed25519' }, + }).privateJwk, + }) const issuerDidCreateResult = await issuer.dids.create({ method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, + options: { keyId: issuerKeyId }, }) issuerDid = issuerDidCreateResult.didState.did as string const issuerDidKey = DidKey.fromDid(issuerDid) - const issuerKid = `${issuerDid}#${issuerDidKey.key.fingerprint}` + const issuerKid = `${issuerDid}#${issuerDidKey.publicJwk.fingerprint}` const _issuerVerificationMethod = issuerDidCreateResult.didState.didDocument?.dereferenceKey(issuerKid, [ 'authentication', ]) @@ -226,10 +220,7 @@ describe('OpenId4VcIssuer', () => { afterEach(async () => { await issuer.shutdown() - await issuer.wallet.delete() - await holder.shutdown() - await holder.wallet.delete() }) // This method is available on the holder service, diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts index d7531d262b..841490a171 100644 --- a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts +++ b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts @@ -1,4 +1,4 @@ -import type { JwaSignatureAlgorithm, RecordTags, TagsBase } from '@credo-ts/core' +import { Kms, RecordTags, TagsBase } from '@credo-ts/core' import type { OpenId4VciAuthorizationServerConfig, OpenId4VciCredentialConfigurationsSupportedWithFormats, @@ -6,7 +6,7 @@ import type { } from '../../shared' import type { OpenId4VciBatchCredentialIssuanceOptions } from '../OpenId4VcIssuerServiceOptions' -import { BaseRecord, utils } from '@credo-ts/core' +import { BaseRecord, CredoError, utils } from '@credo-ts/core' import { credentialsSupportedToCredentialConfigurationsSupported } from '@openid4vc/openid4vci' import { Transform, TransformationType } from 'class-transformer' @@ -24,16 +24,15 @@ export type OpenId4VcIssuerRecordProps = { issuerId: string /** - * The fingerprint (multibase encoded) of the public key used to sign access tokens for - * this issuer. + * The public jwk of the key used to sign access tokens for this issuer. Must include a `kid` parameter. */ - accessTokenPublicKeyFingerprint: string + accessTokenPublicJwk: Kms.KmsJwkPublicAsymmetric /** * The DPoP signing algorithms supported by this issuer. * If not provided, dPoP is considered unsupported. */ - dpopSigningAlgValuesSupported?: [JwaSignatureAlgorithm, ...JwaSignatureAlgorithm[]] + dpopSigningAlgValuesSupported?: [Kms.KnownJwaSignatureAlgorithm, ...Kms.KnownJwaSignatureAlgorithm[]] display?: OpenId4VciCredentialIssuerMetadataDisplay[] authorizationServerConfigs?: OpenId4VciAuthorizationServerConfig[] @@ -56,7 +55,13 @@ export class OpenId4VcIssuerRecord extends BaseRecord { - const nonce = await agentContext.wallet.generateNonce() - const state = await agentContext.wallet.generateNonce() + const kms = agentContext.resolve(Kms.KeyManagementApi) + const nonce = TypedArrayEncoder.toBase64URL(kms.randomBytes({ length: 32 }).bytes) + const state = TypedArrayEncoder.toBase64URL(kms.randomBytes({ length: 32 }).bytes) const responseMode = options.responseMode ?? 'direct_post.jwt' const isDcApiRequest = responseMode === 'dc_api' || responseMode === 'dc_api.jwt' @@ -331,7 +331,7 @@ export class OpenId4VpVerifierService { }) // FIXME: use JarmMode enum when new release of oid4vp - if (parsedAuthorizationResponse.jarm && parsedAuthorizationResponse.jarm.type !== 'Encrypted') { + if (parsedAuthorizationResponse.jarm && parsedAuthorizationResponse.jarm.type !== JarmMode.Encrypted) { throw new Oauth2ServerErrorResponseError({ error: Oauth2ErrorCodes.InvalidRequest, error_description: `Only encrypted JARM responses are supported, received '${parsedAuthorizationResponse.jarm.type}'.`, @@ -417,7 +417,7 @@ export class OpenId4VpVerifierService { if (result.type === 'dcql') { const dcqlPresentationEntries = Object.entries(result.dcql.presentations) if (!authorizationRequest.dcql_query) { - throw new CredoError('') + throw new CredoError('Missing required dcql query') } const dcql = agentContext.dependencyManager.resolve(DcqlService) @@ -741,22 +741,23 @@ export class OpenId4VpVerifierService { ) { const { responseMode, verifier } = options - const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + const signatureSuiteRegistry = agentContext.resolve(SignatureSuiteRegistry) + const kms = agentContext.resolve(Kms.KeyManagementApi) const supportedAlgs = getSupportedJwaSignatureAlgorithms(agentContext) const supportedMdocAlgs = supportedAlgs.filter(isMdocSupportedSignatureAlgorithm) const supportedProofTypes = signatureSuiteRegistry.supportedProofTypes - type JarmEncryptionJwk = JwkJson & { kid: string; use: 'enc' } + type JarmEncryptionJwk = Kms.Jwk & { kid: string; use: 'enc' } let jarmEncryptionJwk: JarmEncryptionJwk | undefined if (isJarmResponseMode(responseMode)) { - const key = await agentContext.wallet.createKey({ keyType: KeyType.P256 }) - jarmEncryptionJwk = { ...getJwkFromKey(key).toJson(), kid: key.fingerprint, use: 'enc' } + const key = await kms.createKey({ type: { crv: 'P-256', kty: 'EC' } }) + jarmEncryptionJwk = { ...key.publicJwk, use: 'enc' } } const jarmClientMetadata: (JarmClientMetadata & Pick) | undefined = jarmEncryptionJwk ? { - jwks: { keys: [jarmEncryptionJwk] }, + jwks: { keys: [jarmEncryptionJwk as Jwk] }, authorization_encrypted_response_alg: 'ECDH-ES', // FIXME: we need to support dynamically setting this by letting the wallet post their supported values // by posting to `request_uri` @@ -867,7 +868,7 @@ export class OpenId4VpVerifierService { this.logger.trace('Presentation response', JsonTransformer.toJSON(presentation)) let isValid: boolean - let reason: string | undefined = undefined + let cause: Error | undefined = undefined let verifiablePresentation: VerifiablePresentation if (format === ClaimFormat.SdJwtVc) { @@ -906,7 +907,7 @@ export class OpenId4VpVerifierService { }) isValid = verificationResult.verification.isValid - reason = verificationResult.isValid ? undefined : verificationResult.error.message + cause = verificationResult.isValid ? undefined : verificationResult.error verifiablePresentation = sdJwtVc } else if (format === ClaimFormat.MsoMdoc) { if (typeof presentation !== 'string') { @@ -981,7 +982,7 @@ export class OpenId4VpVerifierService { }) isValid = verificationResult.isValid - reason = verificationResult.error?.message + cause = verificationResult.error } else { verifiablePresentation = JsonTransformer.fromJSON(presentation, W3cJsonLdVerifiablePresentation) const verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { @@ -991,11 +992,13 @@ export class OpenId4VpVerifierService { }) isValid = verificationResult.isValid - reason = verificationResult.error?.message + cause = verificationResult.error } if (!isValid) { - throw new Error(reason) + throw new CredoError(`Error occured during verification of presentation.${cause ? ` ${cause.message}` : ''}`, { + cause, + }) } return { diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts index 826cabe559..3c976ce82d 100644 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.test.ts @@ -1,7 +1,5 @@ import { Jwt } from '@credo-ts/core' - -import { AskarModule } from '../../../../askar/src' -import { askarModuleConfig } from '../../../../askar/tests/helpers' +import { InMemoryWalletModule } from '../../../../../tests/InMemoryWalletModule' import { type AgentType, createAgentFromModules } from '../../../tests/utils' import { universityDegreePresentationDefinition } from '../../../tests/utilsVp' import { OpenId4VcVerifierModule } from '../OpenId4VcVerifierModule' @@ -10,7 +8,7 @@ const modules = { openId4VcVerifier: new OpenId4VcVerifierModule({ baseUrl: 'http://redirect-uri', }), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), } describe('OpenId4VcVerifier', () => { @@ -22,7 +20,6 @@ describe('OpenId4VcVerifier', () => { afterEach(async () => { await verifier.agent.shutdown() - await verifier.agent.wallet.delete() }) describe('Verification', () => { diff --git a/packages/openid4vc/src/shared/callbacks.ts b/packages/openid4vc/src/shared/callbacks.ts index 6ff62e5157..f5a8cdb0c4 100644 --- a/packages/openid4vc/src/shared/callbacks.ts +++ b/packages/openid4vc/src/shared/callbacks.ts @@ -1,9 +1,10 @@ -import { AgentContext, JwaSignatureAlgorithm, JwsSignerWithJwk } from '@credo-ts/core' +import { AgentContext, JwsSignerWithJwk, Kms } from '@credo-ts/core' import type { CallbackContext, ClientAuthenticationCallback, DecryptJweCallback, EncryptJweCallback, + Jwk, SignJwtCallback, VerifyJwtCallback, } from '@openid4vc/oauth2' @@ -16,18 +17,14 @@ import { JsonEncoder, JwsService, JwtPayload, - Key, - KeyType, TypedArrayEncoder, X509Certificate, X509ModuleConfig, X509Service, - getJwkFromJson, - getJwkFromKey, } from '@credo-ts/core' import { clientAuthenticationDynamic, decodeJwtHeader } from '@openid4vc/oauth2' -import { getKeyFromDid } from './utils' +import { getPublicJwkFromDid } from './utils' export function getOid4vcJwtVerifyCallback( agentContext: AgentContext, @@ -113,8 +110,8 @@ export function getOid4vcJwtVerifyCallback( }) } - const alg = signer.alg as JwaSignatureAlgorithm - if (!Object.values(JwaSignatureAlgorithm).includes(alg)) { + const alg = signer.alg as Kms.KnownJwaSignatureAlgorithm + if (!Object.values(Kms.KnownJwaSignatureAlgorithms).includes(alg)) { throw new CredoError(`Unsupported jwa signatre algorithm '${alg}'`) } @@ -123,18 +120,18 @@ export function getOid4vcJwtVerifyCallback( ? { method: 'did', didUrl: signer.didUrl, - jwk: getJwkFromKey(await getKeyFromDid(agentContext, signer.didUrl)), + jwk: await getPublicJwkFromDid(agentContext, signer.didUrl), } : signer.method === 'jwk' ? { method: 'jwk', - jwk: getJwkFromJson(signer.publicJwk), + jwk: Kms.PublicJwk.fromUnknown(signer.publicJwk), } : signer.method === 'x5c' ? { method: 'x5c', x5c: signer.x5c, - jwk: getJwkFromKey(X509Certificate.fromEncodedCertificate(signer.x5c[0]).publicKey), + jwk: X509Certificate.fromEncodedCertificate(signer.x5c[0]).publicJwk, } : undefined @@ -152,16 +149,14 @@ export function getOid4vcJwtVerifyCallback( return { verified: false, signerJwk: undefined } } - const signerJwk = jwsSigners[0].jwk.toJson() - if (signer.method === 'did') { - signerJwk.kid = signer.didUrl - } - + const signerJwk = jwsSigners[0].jwk.toJson() as Jwk return { verified: true, signerJwk } } } export function getOid4vcEncryptJweCallback(agentContext: AgentContext): EncryptJweCallback { + const kms = agentContext.dependencyManager.resolve(Kms.KeyManagementApi) + return async (jweEncryptor, compact) => { if (jweEncryptor.method !== 'jwk') { throw new CredoError( @@ -169,8 +164,12 @@ export function getOid4vcEncryptJweCallback(agentContext: AgentContext): Encrypt ) } - const jwk = getJwkFromJson(jweEncryptor.publicJwk) - const key = jwk.key + // TODO: we should probably add a key id or ference to the jweEncryptor/jwsSigner in + // oid4vc-ts so we can keep a reference to the key + const jwk = Kms.PublicJwk.fromUnknown(jweEncryptor.publicJwk) + if (!jwk.hasKeyId) { + throw new CredoError('Expected kid to be defined on the JWK') + } if (jweEncryptor.alg !== 'ECDH-ES') { throw new CredoError("Only 'ECDH-ES' is supported as 'alg' value for JARM response encryption") @@ -182,49 +181,139 @@ export function getOid4vcEncryptJweCallback(agentContext: AgentContext): Encrypt ) } - if (key.keyType !== KeyType.P256) { - throw new CredoError(`Only '${KeyType.P256}' key type is supported for JARM response encryption`) + const jwkJson = jwk.toJson() + if (jwkJson.kty !== 'EC' && jwkJson.kty !== 'OKP') { + throw new CredoError(`Expected EC or OKP jwk for encryption, found ${Kms.getJwkHumanDescription(jwkJson)}`) } - if (!agentContext.wallet.directEncryptCompactJweEcdhEs) { - throw new CredoError( - 'Cannot decrypt Jarm Response, wallet does not support directEncryptCompactJweEcdhEs. You need to upgrade your wallet implementation.' - ) + if (jwkJson.crv === 'Ed25519') { + throw new CredoError(`Expected ${jwkJson.kty} with crv X25519, found ${Kms.getJwkHumanDescription(jwkJson)}`) } - const jwe = await agentContext.wallet.directEncryptCompactJweEcdhEs({ - data: Buffer.from(compact), - recipientKey: key, - header: { kid: jweEncryptor.publicJwk.kid }, - encryptionAlgorithm: jweEncryptor.enc, - apu: jweEncryptor.apu ? TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString(jweEncryptor.apu)) : undefined, - apv: jweEncryptor.apv ? TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString(jweEncryptor.apv)) : undefined, + // TODO: create a JWE service that handles this + const ephmeralKey = await kms.createKey({ + type: jwkJson, }) - return { encryptionJwk: jweEncryptor.publicJwk, jwe } + try { + const header = { + kid: jweEncryptor.publicJwk.kid, + apu: jweEncryptor.apu, + apv: jweEncryptor.apv, + enc: jweEncryptor.enc, + alg: 'ECDH-ES', + epk: ephmeralKey.publicJwk, + } + const encodedHeader = JsonEncoder.toBase64URL(header) + + const encrypted = await kms.encrypt({ + key: { + // FIXME: We can make the keyId optional for ECDH-ES + // That way we don't have to store the key + keyId: ephmeralKey.keyId, + algorithm: 'ECDH-ES', + apu: jweEncryptor.apu ? TypedArrayEncoder.fromBase64(jweEncryptor.apu) : undefined, + apv: jweEncryptor.apv ? TypedArrayEncoder.fromBase64(jweEncryptor.apv) : undefined, + externalPublicJwk: jwkJson, + }, + data: Buffer.from(compact), + encryption: { + algorithm: jweEncryptor.enc, + aad: Buffer.from(encodedHeader), + }, + }) + + if (!encrypted.iv || !encrypted.tag) { + throw new CredoError("Expected 'iv' and 'tag' to be defined") + } + + const compactJwe = `${encodedHeader}..${TypedArrayEncoder.toBase64URL(encrypted.iv)}.${TypedArrayEncoder.toBase64URL( + encrypted.encrypted + )}.${TypedArrayEncoder.toBase64URL(encrypted.tag)}` + + return { encryptionJwk: jweEncryptor.publicJwk, jwe: compactJwe } + } finally { + // Delete the key + await kms.deleteKey({ + keyId: ephmeralKey.keyId, + }) + } } } export function getOid4vcDecryptJweCallback(agentContext: AgentContext): DecryptJweCallback { + const kms = agentContext.resolve(Kms.KeyManagementApi) return async (jwe, options) => { + // TODO: use custom header zod schema to limit which algorithms can be used const { header } = decodeJwtHeader({ jwt: jwe }) - const kid = options?.jwk?.kid ?? header.kid + let kid = options?.jwk?.kid ?? header.kid if (!kid) { throw new CredoError('Uanbel to decrypt jwe. No kid or jwk found') } - const key = Key.fromFingerprint(kid) - if (!agentContext.wallet.directDecryptCompactJweEcdhEs) { - throw new CredoError('Cannot decrypt Jarm Response, wallet does not support directDecryptCompactJweEcdhEs') + // Previously we used the fingerprint as the kid for JARM + // We try to parse it as fingerprint if it starts with z (base58 encoding) + // It's not 100% + if (kid.startsWith('z')) { + try { + const publicJwk = Kms.PublicJwk.fromFingerprint(kid) + if (publicJwk) kid = publicJwk.legacyKeyId + } catch { + // no-op + } + } + + // TODO: decodeJwe method in oid4vc-ts + // encryption key is not used (we don't use key wrapping) + const [encodedHeader /* encryptionKey */, , encodedIv, encodedCiphertext, encodedTag] = jwe.split('.') + + if (header.alg !== 'ECDH-ES') { + throw new CredoError("Only 'ECDH-ES' is supported as 'alg' value for JARM response decryption") + } + + if (header.enc !== 'A256GCM' && header.enc !== 'A128GCM' && header.enc !== 'A128CBC-HS256') { + throw new CredoError( + "Only 'A256GCM', 'A128GCM', and 'A128CBC-HS256' is supported as 'enc' value for JARM response decryption" + ) } let decryptedPayload: string + let publicJwk: Kms.PublicJwk + + const epk = Kms.PublicJwk.fromUnknown(header.epk) try { - const decrypted = await agentContext.wallet.directDecryptCompactJweEcdhEs({ compactJwe: jwe, recipientKey: key }) + const decrypted = await kms.decrypt({ + encrypted: TypedArrayEncoder.fromBase64(encodedCiphertext), + decryption: { + algorithm: header.enc, + // aad is the base64 encoded bytes (not just the bytes) + aad: TypedArrayEncoder.fromString(encodedHeader), + iv: TypedArrayEncoder.fromBase64(encodedIv), + tag: TypedArrayEncoder.fromBase64(encodedTag), + }, + key: { + algorithm: header.alg, + externalPublicJwk: epk.toJson() as Kms.KmsJwkPublicEcdh, + keyId: kid, + apu: typeof header.apu === 'string' ? TypedArrayEncoder.fromBase64(header.apu) : undefined, + apv: typeof header.apv === 'string' ? TypedArrayEncoder.fromBase64(header.apv) : undefined, + }, + }) + + // TODO: decrypt should return the public jwk instance + publicJwk = Kms.PublicJwk.fromUnknown( + await kms.getPublicKey({ + keyId: kid, + }) + ) + decryptedPayload = TypedArrayEncoder.toUtf8String(decrypted.data) - } catch (_error) { + } catch (error) { + agentContext.config.logger.error('Error decrypting JWE', { + error, + }) return { decrypted: false, encryptionJwk: options?.jwk, @@ -235,7 +324,7 @@ export function getOid4vcDecryptJweCallback(agentContext: AgentContext): Decrypt return { decrypted: true, - decryptionJwk: getJwkFromKey(key).toJson(), + decryptionJwk: publicJwk.toJson() as Jwk, payload: decryptedPayload, header, } @@ -254,32 +343,37 @@ export function getOid4vcJwtSignCallback(agentContext: AgentContext): SignJwtCal const leafCertificate = X509Service.getLeafCertificate(agentContext, { certificateChain: signer.x5c }) const jws = await jwsService.createJwsCompact(agentContext, { - protectedHeaderOptions: { ...header, alg: signer.alg, jwk: undefined }, + protectedHeaderOptions: { ...header, alg: signer.alg as Kms.KnownJwaSignatureAlgorithm, jwk: undefined }, payload: JwtPayload.fromJson(payload), - key: leafCertificate.publicKey, + keyId: signer.kid ?? leafCertificate.publicJwk.keyId, }) - return { jwt: jws, signerJwk: getJwkFromKey(leafCertificate.publicKey).toJson() } + return { jwt: jws, signerJwk: leafCertificate.publicJwk.toJson() as Jwk } } - const key = - signer.method === 'did' ? await getKeyFromDid(agentContext, signer.didUrl) : getJwkFromJson(signer.publicJwk).key - const jwk = getJwkFromKey(key) + // TOOD: createJwsCompact should return the Jwk, so we don't have to reoslve it here + const publicJwk = + signer.method === 'did' + ? await getPublicJwkFromDid(agentContext, signer.didUrl) + : Kms.PublicJwk.fromUnknown(signer.publicJwk) - if (!jwk.supportsSignatureAlgorithm(signer.alg)) { - throw new CredoError(`key type '${jwk.keyType}', does not support the JWS signature alg '${signer.alg}'`) + if (!publicJwk.supportedSignatureAlgorithms.includes(signer.alg as Kms.KnownJwaSignatureAlgorithm)) { + throw new CredoError( + `jwk ${publicJwk.jwkTypehumanDescription} does not support JWS signature alg '${signer.alg}'` + ) } const jwt = await jwsService.createJwsCompact(agentContext, { protectedHeaderOptions: { ...header, - jwk: header.jwk ? getJwkFromJson(header.jwk) : undefined, + jwk: header.jwk ? publicJwk : undefined, + alg: signer.alg as Kms.KnownJwaSignatureAlgorithm, }, payload: JsonEncoder.toBuffer(payload), - key, + keyId: signer.kid ?? publicJwk.keyId, }) - return { jwt, signerJwk: getJwkFromKey(key).toJson() } + return { jwt, signerJwk: publicJwk.toJson() as Jwk } } } @@ -291,9 +385,11 @@ export function getOid4vcCallbacks( issuanceSessionId?: string } ) { + const kms = agentContext.resolve(Kms.KeyManagementApi) + return { hash: (data, alg) => Hasher.hash(data, alg.toLowerCase()), - generateRandom: (length) => agentContext.wallet.getRandomValues(length), + generateRandom: (length) => kms.randomBytes({ length }).bytes, signJwt: getOid4vcJwtSignCallback(agentContext), clientAuthentication: () => { throw new CredoError('Did not expect client authentication to be called.') diff --git a/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts b/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts index 747a9e6a1e..b90fead179 100644 --- a/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts +++ b/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts @@ -1,4 +1,4 @@ -import type { Jwk, Key } from '@credo-ts/core' +import type { Kms } from '@credo-ts/core' import { Openid4vciIssuer } from '@openid4vc/openid4vci' @@ -38,7 +38,7 @@ export interface OpenId4VcCredentialHolderDidBinding { export interface OpenId4VcCredentialHolderJwkBinding { method: 'jwk' - keys: Jwk[] + keys: Kms.PublicJwk[] } export type VerifiedOpenId4VcCredentialHolderBinding = { @@ -67,7 +67,7 @@ export type VerifiedOpenId4VcCredentialHolderBinding = { */ keys: Array<{ method: 'did' - key: Key + jwk: Kms.PublicJwk didUrl: string }> } @@ -82,8 +82,7 @@ export type VerifiedOpenId4VcCredentialHolderBinding = { */ keys: Array<{ method: 'jwk' - key: Key - jwk: Jwk + jwk: Kms.PublicJwk }> } ) diff --git a/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts b/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts index 41acec5508..8e1f9a28a5 100644 --- a/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts +++ b/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts @@ -1,7 +1,13 @@ -import type { Jwk } from '@credo-ts/core' +import { Kms, X509Certificate } from '@credo-ts/core' export interface OpenId4VcJwtIssuerDid { method: 'did' + + /** + * The did url pointing to a specific verification method. + * + * Note a created DID record MUST exist for the did url, enabling extraction of the KMS key id from the did record. + */ didUrl: string } @@ -9,12 +15,12 @@ export interface OpenId4VcIssuerX5c { method: 'x5c' /** - * - * Array of base64-encoded certificate strings in the DER-format. + * Array of X.509 certificates * * The certificate containing the public key corresponding to the key used to digitally sign the JWS MUST be the first certificate. + * The first certificate MUST also have a key id configured on the public key to enable signing with the KMS. */ - x5c: string[] + x5c: X509Certificate[] /** * The issuer of the JWT. Should be a HTTPS URI. @@ -27,7 +33,7 @@ export interface OpenId4VcIssuerX5c { export interface OpenId4VcJwtIssuerJwk { method: 'jwk' - jwk: Jwk + jwk: Kms.PublicJwk } export type OpenId4VcJwtIssuer = OpenId4VcJwtIssuerDid | OpenId4VcIssuerX5c | OpenId4VcJwtIssuerJwk diff --git a/packages/openid4vc/src/shared/router/tenants.ts b/packages/openid4vc/src/shared/router/tenants.ts index f60bc2b24e..1ecb3cb44e 100644 --- a/packages/openid4vc/src/shared/router/tenants.ts +++ b/packages/openid4vc/src/shared/router/tenants.ts @@ -19,7 +19,7 @@ export async function getAgentContextForActorId(rootAgentContext: AgentContext, const agentContextProvider = rootAgentContext.dependencyManager.resolve( InjectionSymbols.AgentContextProvider ) - return agentContextProvider.getAgentContextForContextCorrelationId(tenant.id) + return agentContextProvider.getAgentContextForContextCorrelationId(`tenant-${tenant.id}`) } } @@ -44,7 +44,7 @@ export async function storeActorIdForContextCorrelationId(agentContext: AgentCon // We don't want to query the tenant record if the current context is the root context if (tenantsApi && tenantsApi.rootAgentContext.contextCorrelationId !== agentContext.contextCorrelationId) { - const tenantRecord = await tenantsApi.getTenantById(agentContext.contextCorrelationId) + const tenantRecord = await tenantsApi.getTenantById(agentContext.contextCorrelationId.replace('tenant-', '')) const currentOpenId4VcActorIds = tenantRecord.metadata.get(OPENID4VC_ACTOR_IDS_METADATA_KEY) ?? [] const openId4VcActorIds = [...currentOpenId4VcActorIds, actorId] diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index fc6fd431be..8d3e83b69b 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -1,42 +1,30 @@ -import type { AgentContext, DidPurpose, JwaSignatureAlgorithm, Key } from '@credo-ts/core' -import type { JwtSigner, JwtSignerX5c } from '@openid4vc/oauth2' +import { AgentContext, DidPurpose, Kms } from '@credo-ts/core' +import type { Jwk, JwtSigner, JwtSignerX5c } from '@openid4vc/oauth2' import type { OpenId4VcJwtIssuer } from './models' import { CredoError, DidsApi, SignatureSuiteRegistry, - X509Service, getDomainFromUrl, - getJwkClassFromKeyType, - getJwkFromKey, - getKeyFromVerificationMethod, + getPublicJwkFromVerificationMethod, } from '@credo-ts/core' /** * Returns the JWA Signature Algorithms that are supported by the wallet. - * - * This is an approximation based on the supported key types of the wallet. - * This is not 100% correct as a supporting a key type does not mean you support - * all the algorithms for that key type. However, this needs refactoring of the wallet - * that is planned for the 0.5.0 release. */ -export function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { - const supportedKeyTypes = agentContext.wallet.supportedKeyTypes - - // Extract the supported JWS algs based on the key types the wallet support. - const supportedJwaSignatureAlgorithms = supportedKeyTypes - // Map the supported key types to the supported JWK class - .map(getJwkClassFromKeyType) - // Filter out the undefined values - .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) - // Extract the supported JWA signature algorithms from the JWK class - .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) +export function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): Kms.KnownJwaSignatureAlgorithm[] { + const kms = agentContext.resolve(Kms.KeyManagementApi) + + // If we can sign with an algorithm we assume it's supported (also for verification) + const supportedJwaSignatureAlgorithms = Object.values(Kms.KnownJwaSignatureAlgorithms).filter( + (algorithm) => kms.supportedBackendsForOperation({ operation: 'sign', algorithm }).length > 0 + ) return supportedJwaSignatureAlgorithms } -export async function getKeyFromDid( +export async function getPublicJwkFromDid( agentContext: AgentContext, didUrl: string, allowedPurposes: DidPurpose[] = ['authentication'] @@ -45,7 +33,7 @@ export async function getKeyFromDid( const didDocument = await didsApi.resolveDidDocument(didUrl) const verificationMethod = didDocument.dereferenceKey(didUrl, allowedPurposes) - return getKeyFromVerificationMethod(verificationMethod) + return getPublicJwkFromVerificationMethod(verificationMethod) } export async function requestSignerToJwtIssuer( @@ -53,25 +41,20 @@ export async function requestSignerToJwtIssuer( requestSigner: OpenId4VcJwtIssuer ): Promise | (JwtSignerX5c & { issuer: string })> { if (requestSigner.method === 'did') { - const key = await getKeyFromDid(agentContext, requestSigner.didUrl) - const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] - if (!alg) throw new CredoError(`No supported signature algorithms for key type: ${key.keyType}`) + const dids = agentContext.resolve(DidsApi) + const { publicJwk } = await dids.resolveVerificationMethodFromCreatedDidRecord(requestSigner.didUrl) return { method: requestSigner.method, didUrl: requestSigner.didUrl, - alg, + alg: publicJwk.signatureAlgorithm, + kid: publicJwk.keyId, } } if (requestSigner.method === 'x5c') { - const leafCertificate = X509Service.getLeafCertificate(agentContext, { - certificateChain: requestSigner.x5c, - }) - - const jwk = getJwkFromKey(leafCertificate.publicKey) - const alg = jwk.supportedSignatureAlgorithms[0] - if (!alg) { - throw new CredoError(`No supported signature algorithms found key type: '${jwk.keyType}'`) + const leafCertificate = requestSigner.x5c[0] + if (!leafCertificate) { + throw new CredoError('Unable to extract leaf certificate, x5c certificate chain is empty') } if ( @@ -100,30 +83,28 @@ export async function requestSignerToJwtIssuer( return { ...requestSigner, - alg, + x5c: requestSigner.x5c.map((certificate) => certificate.toString('base64url')), + alg: leafCertificate.publicJwk.signatureAlgorithm, + kid: leafCertificate.publicJwk.keyId, } } if (requestSigner.method === 'jwk') { - const alg = requestSigner.jwk.supportedSignatureAlgorithms[0] - if (!alg) { - throw new CredoError(`No supported signature algorithms for key type: '${requestSigner.jwk.keyType}'`) - } return { ...requestSigner, - publicJwk: requestSigner.jwk.toJson(), - alg, + publicJwk: requestSigner.jwk.toJson() as Jwk, + alg: requestSigner.jwk.signatureAlgorithm, } } throw new CredoError(`Unsupported jwt issuer method '${(requestSigner as OpenId4VcJwtIssuer).method}'`) } -export function getProofTypeFromKey(agentContext: AgentContext, key: Key) { +export function getProofTypeFromPublicJwk(agentContext: AgentContext, key: Kms.PublicJwk) { const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) - const supportedSignatureSuites = signatureSuiteRegistry.getAllByKeyType(key.keyType) + const supportedSignatureSuites = signatureSuiteRegistry.getAllByPublicJwkType(key.jwk) if (supportedSignatureSuites.length === 0) { - throw new CredoError(`Couldn't find a supported signature suite for the given key type '${key.keyType}'.`) + throw new CredoError(`Couldn't find a supported signature suite for the given key ${key.jwkTypehumanDescription}.`) } return supportedSignatureSuites[0].proofType diff --git a/packages/openid4vc/tests/openid4vc-batch-issuance.e2e.test.ts b/packages/openid4vc/tests/openid4vc-batch-issuance.e2e.test.ts index 12b1fa750d..d6f1954a1a 100644 --- a/packages/openid4vc/tests/openid4vc-batch-issuance.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc-batch-issuance.e2e.test.ts @@ -1,12 +1,10 @@ import type { OpenId4VciCredentialBindingResolver } from '../src/openid4vc-holder' import type { AgentType } from './utils' -import { CredoError, KeyType, getJwkFromKey } from '@credo-ts/core' +import { CredoError, Kms } from '@credo-ts/core' import express, { type Express } from 'express' import { setupNockToExpress } from '../../../tests/nockToExpress' -import { AskarModule } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' import { OpenId4VcHolderModule, OpenId4VcIssuanceSessionState, @@ -14,6 +12,7 @@ import { OpenId4VciCredentialFormatProfile, } from '../src' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' import { createAgentFromModules, waitForCredentialIssuanceSessionRecordSubject } from './utils' import { universityDegreeCredentialConfigurationSupportedMdoc } from './utilsVci' @@ -26,12 +25,10 @@ describe('OpenId4Vc Batch Issuance', () => { let issuer: AgentType<{ openId4VcIssuer: OpenId4VcIssuerModule - askar: AskarModule }> let holder: AgentType<{ openId4VcHolder: OpenId4VcHolderModule - askar: AskarModule }> beforeEach(async () => { @@ -49,8 +46,8 @@ describe('OpenId4Vc Batch Issuance', () => { format: OpenId4VciCredentialFormatProfile.MsoMdoc, credentials: holderBinding.keys.map((holderBinding, index) => ({ docType: credentialRequestFormat.doctype, - holderKey: holderBinding.key, - issuerCertificate: issuer.certificate.toString('base64'), + holderKey: holderBinding.jwk, + issuerCertificate: issuer.certificate, namespaces: { [credentialRequestFormat.doctype]: { index, @@ -67,16 +64,16 @@ describe('OpenId4Vc Batch Issuance', () => { throw new Error('not supported') }, }), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), }) holder = await createAgentFromModules('holder', { openId4VcHolder: new OpenId4VcHolderModule(), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), }) - holder.agent.x509.addTrustedCertificate(issuer.certificate.toString('base64')) - issuer.agent.x509.addTrustedCertificate(issuer.certificate.toString('base64')) + holder.agent.x509.config.addTrustedCertificate(issuer.certificate.toString('base64')) + issuer.agent.x509.config.addTrustedCertificate(issuer.certificate.toString('base64')) // We let AFJ create the router, so we have a fresh one each time expressApp.use('/oid4vci', issuer.agent.modules.openId4VcIssuer.config.router) @@ -86,28 +83,30 @@ describe('OpenId4Vc Batch Issuance', () => { afterEach(async () => { clearNock() await issuer.agent.shutdown() - await issuer.agent.wallet.delete() - await holder.agent.shutdown() - await holder.agent.wallet.delete() }) const credentialBindingResolver: OpenId4VciCredentialBindingResolver = async ({ agentContext, proofTypes, issuerMaxBatchSize, - }) => ({ - method: 'jwk', - keys: await Promise.all( - new Array(issuerMaxBatchSize) - .fill(0) - .map(async () => - getJwkFromKey( - await agentContext.wallet.createKey({ keyType: proofTypes.jwt?.supportedKeyTypes[0] ?? KeyType.Ed25519 }) + }) => { + const kms = agentContext.resolve(Kms.KeyManagementApi) + return { + method: 'jwk', + keys: await Promise.all( + new Array(issuerMaxBatchSize).fill(0).map(async () => + Kms.PublicJwk.fromPublicJwk( + ( + await kms.createKeyForSignatureAlgorithm({ + algorithm: proofTypes.jwt?.supportedSignatureAlgorithms[0] ?? 'EdDSA', + }) + ).publicJwk ) ) - ), - }) + ), + } + } it('e2e flow issuing a batch of mdoc', async () => { const issuerRecord = await issuer.agent.modules.openId4VcIssuer.createIssuer({ @@ -181,18 +180,23 @@ describe('OpenId4Vc Batch Issuance', () => { holder.agent.modules.openId4VcHolder.requestCredentials({ resolvedCredentialOffer, ...tokenResponse, - credentialBindingResolver: async ({ agentContext, proofTypes }) => ({ - method: 'jwk', - keys: await Promise.all( - new Array(12).fill(0).map(async () => - getJwkFromKey( - await agentContext.wallet.createKey({ - keyType: proofTypes.jwt?.supportedKeyTypes[0] ?? KeyType.Ed25519, - }) + credentialBindingResolver: async ({ agentContext, proofTypes }) => { + const kms = agentContext.resolve(Kms.KeyManagementApi) + return { + method: 'jwk', + keys: await Promise.all( + new Array(12).fill(0).map(async () => + Kms.PublicJwk.fromPublicJwk( + ( + await kms.createKeyForSignatureAlgorithm({ + algorithm: proofTypes.jwt?.supportedSignatureAlgorithms[0] ?? 'EdDSA', + }) + ).publicJwk + ) ) - ) - ), - }), + ), + } as const + }, }) ).rejects.toThrow( 'Issuer supports issuing a batch of maximum 10 credential(s). Binding resolver returned 12 keys. Make sure the returned value does not exceed the max batch issuance.' diff --git a/packages/openid4vc/tests/openid4vc-dcapi.e2e.test.ts b/packages/openid4vc/tests/openid4vc-dcapi.e2e.test.ts index fa6f3817db..5eb5b0864a 100644 --- a/packages/openid4vc/tests/openid4vc-dcapi.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc-dcapi.e2e.test.ts @@ -1,23 +1,21 @@ -import type { DcqlQuery } from '@credo-ts/core' +import type { DcqlQuery, X509Certificate } from '@credo-ts/core' import type { OpenId4VcVerifierRecord } from '../src' import type { AgentType } from './utils' import { ClaimFormat, DateOnly, - KeyType, + Kms, MdocDeviceResponse, MdocRecord, SdJwtVcRecord, X509Service, parseDid, } from '@credo-ts/core' - -import { AskarModule } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' import { TenantsModule } from '../../tenants/src' import { OpenId4VcHolderModule, OpenId4VcVerificationSessionState, OpenId4VcVerifierModule } from '../src' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' import { createAgentFromModules } from './utils' const baseUrl = 'http://localhost:1234' @@ -123,14 +121,14 @@ describe('OpenId4VP DC API', () => { tenants: TenantsModule<{ openId4VcVerifier: OpenId4VcVerifierModule }> }> let openIdVerifier: OpenId4VcVerifierRecord - let verifierCertificate: string + let verifierCertificate: X509Certificate beforeEach(async () => { holder = (await createAgentFromModules( 'holder', { openId4VcHolder: new OpenId4VcHolderModule(), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), }, '96213c3d7fc8d4d6754c7a0fd969598e' )) as unknown as typeof holder @@ -141,7 +139,7 @@ describe('OpenId4VP DC API', () => { openId4VcVerifier: new OpenId4VcVerifierModule({ baseUrl: verificationBaseUrl, }), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), tenants: new TenantsModule(), }, '96213c3d7fc8d4d6754c7a0fd969598f' @@ -168,27 +166,29 @@ describe('OpenId4VP DC API', () => { await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) const selfSignedCertificate = await X509Service.createCertificate(verifier.agent.context, { - authorityKey: await verifier.agent.context.wallet.createKey({ keyType: KeyType.P256 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })).publicJwk + ), issuer: { commonName: 'Credo', countryName: 'DE', }, }) - await verifier.agent.x509.setTrustedCertificates([selfSignedCertificate.toString('pem')]) + verifier.agent.x509.config.setTrustedCertificates([selfSignedCertificate.toString('pem')]) const parsedDid = parseDid(verifier.kid) if (!parsedDid.fragment) { throw new Error(`didUrl '${parsedDid.didUrl}' does not contain a '#'. Unable to derive key from did document.`) } - const holderKey = await holder.agent.context.wallet.createKey({ keyType: KeyType.P256 }) + const holderKey = await holder.agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } }) const date = new DateOnly() const signedMdoc = await verifier.agent.mdoc.sign({ docType: 'org.eu.university', - holderKey, - issuerCertificate: selfSignedCertificate.toString('pem'), + holderKey: Kms.PublicJwk.fromPublicJwk(holderKey.publicJwk), + issuerCertificate: selfSignedCertificate, namespaces: { 'eu.europa.ec.eudi.pid.1': { university: 'innsbruck', @@ -200,25 +200,23 @@ describe('OpenId4VP DC API', () => { }, }) - const certificate = await verifier.agent.x509.createCertificate({ - authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + verifierCertificate = await verifier.agent.x509.createCertificate({ + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } })).publicJwk + ), extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: 'localhost' }] } }, issuer: { commonName: 'Something', countryName: 'Something' }, }) - verifierCertificate = certificate.toString('base64') await holder.agent.mdoc.store(signedMdoc) - holder.agent.x509.addTrustedCertificate(verifierCertificate) - verifier.agent.x509.addTrustedCertificate(verifierCertificate) + holder.agent.x509.config.addTrustedCertificate(verifierCertificate) + verifier.agent.x509.config.addTrustedCertificate(verifierCertificate) }) afterEach(async () => { await holder.agent.shutdown() - await holder.agent.wallet.delete() - await verifier.agent.shutdown() - await verifier.agent.wallet.delete() }) it('Digital Credentials API with dcql, mdoc, sd-jwt, transaction data. unsigned, unencrypted', async () => { diff --git a/packages/openid4vc/tests/openid4vc-draft21.e2e.test.ts b/packages/openid4vc/tests/openid4vc-draft21.e2e.test.ts index e43acf2c41..63e6ea0200 100644 --- a/packages/openid4vc/tests/openid4vc-draft21.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc-draft21.e2e.test.ts @@ -1,14 +1,12 @@ import type { DifPresentationExchangeDefinitionV2, MdocDeviceResponse, SdJwtVc } from '@credo-ts/core' import type { AgentType } from './utils' -import { ClaimFormat, KeyType, X509Module, X509Service, parseDid } from '@credo-ts/core' +import { ClaimFormat, Kms, X509Service, parseDid } from '@credo-ts/core' import express, { type Express } from 'express' - -import { AskarModule } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' import { TenantsModule } from '../../tenants/src' import { OpenId4VcHolderModule, OpenId4VcVerificationSessionState, OpenId4VcVerifierModule } from '../src' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' import { setupNockToExpress } from '../../../tests/nockToExpress' import { createAgentFromModules, waitForVerificationSessionRecordSubject } from './utils' @@ -35,8 +33,7 @@ describe('OpenID4VP Draft 21', () => { 'holder', { openId4VcHolder: new OpenId4VcHolderModule(), - askar: new AskarModule(askarModuleConfig), - x509: new X509Module(), + inMemory: new InMemoryWalletModule(), }, '96213c3d7fc8d4d6754c7a0fd969598e', global.fetch @@ -48,7 +45,7 @@ describe('OpenID4VP Draft 21', () => { openId4VcVerifier: new OpenId4VcVerifierModule({ baseUrl: verificationBaseUrl, }), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), tenants: new TenantsModule(), }, '96213c3d7fc8d4d6754c7a0fd969598f', @@ -64,10 +61,7 @@ describe('OpenID4VP Draft 21', () => { clearNock() await holder.agent.shutdown() - await holder.agent.wallet.delete() - await verifier.agent.shutdown() - await verifier.agent.wallet.delete() }) it('e2e flow with verifier endpoints verifying a sd-jwt-vc with selective disclosure', async () => { @@ -92,15 +86,16 @@ describe('OpenID4VP Draft 21', () => { const certificate = await verifier.agent.x509.createCertificate({ issuer: { commonName: 'Credo', countryName: 'NL' }, - authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } })).publicJwk + ), extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: 'localhost' }] } }, }) - const rawCertificate = certificate.toString('base64') await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) - holder.agent.x509.addTrustedCertificate(rawCertificate) - verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.config.addTrustedCertificate(certificate) + verifier.agent.x509.config.addTrustedCertificate(certificate) const presentationDefinition = { id: 'OpenBadgeCredential', @@ -136,7 +131,7 @@ describe('OpenID4VP Draft 21', () => { verifierId: openIdVerifier.verifierId, requestSigner: { method: 'x5c', - x5c: [rawCertificate], + x5c: [certificate], }, presentationExchange: { definition: presentationDefinition, @@ -335,23 +330,27 @@ describe('OpenID4VP Draft 21', () => { await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) const issuerCertificate = await X509Service.createCertificate(verifier.agent.context, { - authorityKey: await verifier.agent.context.wallet.createKey({ keyType: KeyType.P256 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })).publicJwk + ), issuer: 'C=DE', }) - await verifier.agent.x509.setTrustedCertificates([issuerCertificate.toString('pem')]) + verifier.agent.x509.config.setTrustedCertificates([issuerCertificate]) const parsedDid = parseDid(verifier.kid) if (!parsedDid.fragment) { throw new Error(`didUrl '${parsedDid.didUrl}' does not contain a '#'. Unable to derive key from did document.`) } - const holderKey = await holder.agent.context.wallet.createKey({ keyType: KeyType.P256 }) + const holderKey = Kms.PublicJwk.fromPublicJwk( + (await holder.agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })).publicJwk + ) const signedMdoc = await verifier.agent.mdoc.sign({ docType: 'org.eu.university', holderKey, - issuerCertificate: issuerCertificate.toString('pem'), + issuerCertificate, namespaces: { 'eu.europa.ec.eudi.pid.1': { university: 'innsbruck', @@ -364,15 +363,17 @@ describe('OpenID4VP Draft 21', () => { const certificate = await verifier.agent.x509.createCertificate({ issuer: { commonName: 'Credo', countryName: 'NL' }, - authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } })).publicJwk + ), extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: 'localhost' }] } }, }) const rawCertificate = certificate.toString('base64') await holder.agent.mdoc.store(signedMdoc) - holder.agent.x509.addTrustedCertificate(rawCertificate) - verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.config.addTrustedCertificate(rawCertificate) + verifier.agent.x509.config.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'mDL-sample-req', @@ -430,7 +431,7 @@ describe('OpenID4VP Draft 21', () => { verifierId: openIdVerifier.verifierId, requestSigner: { method: 'x5c', - x5c: [rawCertificate], + x5c: [certificate], }, presentationExchange: { definition: presentationDefinition, diff --git a/packages/openid4vc/tests/openid4vc-multi-mdoc-devcie-response.e2e.test.ts b/packages/openid4vc/tests/openid4vc-multi-mdoc-devcie-response.e2e.test.ts index bf08b6eaa9..03d71b89d8 100644 --- a/packages/openid4vc/tests/openid4vc-multi-mdoc-devcie-response.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc-multi-mdoc-devcie-response.e2e.test.ts @@ -1,11 +1,7 @@ -import { MdocDeviceResponse, TypedArrayEncoder } from '@credo-ts/core' -import type { AgentType } from './utils' - -import { KeyType } from '@credo-ts/core' - -import { AskarModule } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' +import { Kms, MdocDeviceResponse, TypedArrayEncoder } from '@credo-ts/core' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' import { OpenId4VcVerificationSessionState, OpenId4VcVerifierModule } from '../src' +import type { AgentType } from './utils' import { createAgentFromModules } from './utils' const baseUrl = 'https://credo.com/oid4vp' @@ -20,22 +16,21 @@ describe('OpenId4Vc', () => { openId4VcVerifier: new OpenId4VcVerifierModule({ baseUrl, }), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), })) as unknown as typeof verifier }) afterEach(async () => { await verifier.agent.shutdown() - await verifier.agent.wallet.delete() }) it('can succesfully verify a device response containing multiple mdoc documents', async () => { const openid4vcVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() const certificate = await verifier.agent.x509.createCertificate({ - authorityKey: await verifier.agent.wallet.createKey({ - keyType: KeyType.P256, - }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { crv: 'P-256', kty: 'EC' } })).publicJwk + ), issuer: { commonName: 'Credo', countryName: 'Country', @@ -46,14 +41,22 @@ describe('OpenId4Vc', () => { }, }, }) - verifier.agent.x509.addTrustedCertificate(certificate.toString('pem')) + verifier.agent.x509.config.addTrustedCertificate(certificate.toString('pem')) + const holderKey = Kms.PublicJwk.fromPublicJwk( + ( + await verifier.agent.kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, + }) + ).publicJwk + ) const mdocOne = await verifier.agent.mdoc.sign({ docType: 'one', - holderKey: await verifier.agent.wallet.createKey({ - keyType: KeyType.P256, - }), - issuerCertificate: certificate.toString('pem'), + holderKey, + issuerCertificate: certificate, namespaces: { one: { name: 'hello', @@ -61,12 +64,20 @@ describe('OpenId4Vc', () => { }, }) + const holderKey2 = Kms.PublicJwk.fromPublicJwk( + ( + await verifier.agent.kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, + }) + ).publicJwk + ) const mdocTwo = await verifier.agent.mdoc.sign({ docType: 'two', - holderKey: await verifier.agent.wallet.createKey({ - keyType: KeyType.P256, - }), - issuerCertificate: certificate.toString('pem'), + holderKey: holderKey2, + issuerCertificate: certificate, namespaces: { two: { notName: 'notHello', @@ -78,7 +89,7 @@ describe('OpenId4Vc', () => { verifierId: openid4vcVerifier.verifierId, requestSigner: { method: 'x5c', - x5c: [certificate.toString('base64url')], + x5c: [certificate], }, expectedOrigins: ['https://credo.com'], responseMode: 'dc_api', @@ -152,9 +163,9 @@ describe('OpenId4Vc', () => { const openid4vcVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() const certificate = await verifier.agent.x509.createCertificate({ - authorityKey: await verifier.agent.wallet.createKey({ - keyType: KeyType.P256, - }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { crv: 'P-256', kty: 'EC' } })).publicJwk + ), issuer: { commonName: 'Credo', countryName: 'Country', @@ -165,14 +176,14 @@ describe('OpenId4Vc', () => { }, }, }) - verifier.agent.x509.addTrustedCertificate(certificate.toString('pem')) + verifier.agent.x509.config.addTrustedCertificate(certificate) const mdocOne = await verifier.agent.mdoc.sign({ docType: 'one', - holderKey: await verifier.agent.wallet.createKey({ - keyType: KeyType.P256, - }), - issuerCertificate: certificate.toString('pem'), + holderKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { crv: 'P-256', kty: 'EC' } })).publicJwk + ), + issuerCertificate: certificate, namespaces: { one: { name: 'hello', @@ -182,10 +193,10 @@ describe('OpenId4Vc', () => { const mdocTwo = await verifier.agent.mdoc.sign({ docType: 'two', - holderKey: await verifier.agent.wallet.createKey({ - keyType: KeyType.P256, - }), - issuerCertificate: certificate.toString('pem'), + holderKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { crv: 'P-256', kty: 'EC' } })).publicJwk + ), + issuerCertificate: certificate, namespaces: { two: { notName: 'notHello', @@ -197,7 +208,7 @@ describe('OpenId4Vc', () => { verifierId: openid4vcVerifier.verifierId, requestSigner: { method: 'x5c', - x5c: [certificate.toString('base64url')], + x5c: [certificate], }, expectedOrigins: ['https://credo.com'], responseMode: 'dc_api', diff --git a/packages/openid4vc/tests/openid4vc-presentation-during-issuance.e2e.test.ts b/packages/openid4vc/tests/openid4vc-presentation-during-issuance.e2e.test.ts index 717128055d..8170e05e02 100644 --- a/packages/openid4vc/tests/openid4vc-presentation-during-issuance.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc-presentation-during-issuance.e2e.test.ts @@ -6,13 +6,11 @@ import type { import type { OpenId4VciCredentialBindingResolver } from '../src/openid4vc-holder' import type { AgentType } from './utils' -import { ClaimFormat, getJwkFromKey } from '@credo-ts/core' +import { ClaimFormat } from '@credo-ts/core' import { AuthorizationFlow } from '@openid4vc/openid4vci' import express, { type Express } from 'express' import { setupNockToExpress } from '../../../tests/nockToExpress' -import { AskarModule } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' import { OpenId4VcHolderModule, OpenId4VcIssuanceSessionState, @@ -21,6 +19,7 @@ import { getScopesFromCredentialConfigurationsSupported, } from '../src' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' import { createAgentFromModules, waitForCredentialIssuanceSessionRecordSubject } from './utils' import { universityDegreeCredentialConfigurationSupported } from './utilsVci' @@ -90,7 +89,6 @@ describe('OpenId4Vc Presentation During Issuance', () => { let issuer: AgentType<{ openId4VcIssuer: OpenId4VcIssuerModule openId4VcVerifier: OpenId4VcVerifierModule - askar: AskarModule }> const getVerificationSessionForIssuanceSessionAuthorization = @@ -101,7 +99,7 @@ describe('OpenId4Vc Presentation During Issuance', () => { verifierId: issuanceSession.issuerId, requestSigner: { method: 'x5c', - x5c: [issuer.certificate.toString('base64')], + x5c: [issuer.certificate], }, responseMode: 'direct_post.jwt', presentationExchange: @@ -129,7 +127,6 @@ describe('OpenId4Vc Presentation During Issuance', () => { let holder: AgentType<{ openId4VcHolder: OpenId4VcHolderModule - askar: AskarModule }> beforeEach(async () => { @@ -177,7 +174,7 @@ describe('OpenId4Vc Presentation During Issuance', () => { holder: holderBinding, issuer: { method: 'x5c', - x5c: [issuer.certificate.toString('base64')], + x5c: [issuer.certificate], issuer: baseUrl, }, disclosureFrame: { @@ -192,16 +189,16 @@ describe('OpenId4Vc Presentation During Issuance', () => { openId4VcVerifier: new OpenId4VcVerifierModule({ baseUrl: verifierBaseUrl, }), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), }) holder = await createAgentFromModules('holder', { openId4VcHolder: new OpenId4VcHolderModule(), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), }) - holder.agent.x509.addTrustedCertificate(issuer.certificate.toString('base64')) - issuer.agent.x509.addTrustedCertificate(issuer.certificate.toString('base64')) + holder.agent.x509.config.addTrustedCertificate(issuer.certificate) + issuer.agent.x509.config.addTrustedCertificate(issuer.certificate) // We let AFJ create the router, so we have a fresh one each time expressApp.use('/oid4vci', issuer.agent.modules.openId4VcIssuer.config.router) @@ -214,15 +211,12 @@ describe('OpenId4Vc Presentation During Issuance', () => { clearNock() await issuer.agent.shutdown() - await issuer.agent.wallet.delete() - await holder.agent.shutdown() - await holder.agent.wallet.delete() }) const credentialBindingResolver: OpenId4VciCredentialBindingResolver = () => ({ method: 'jwk', - keys: [getJwkFromKey(holder.key)], + keys: [holder.jwk], }) it('e2e flow with requesting presentation of credentials before issuance succeeds with presentation definition', async () => { @@ -235,7 +229,7 @@ describe('OpenId4Vc Presentation During Issuance', () => { const x5cIssuer = { method: 'x5c', - x5c: [issuer.certificate.toString('base64')], + x5c: [issuer.certificate], issuer: baseUrl, } satisfies SdJwtVcIssuer @@ -358,7 +352,7 @@ describe('OpenId4Vc Presentation During Issuance', () => { const x5cIssuer = { method: 'x5c', - x5c: [issuer.certificate.toString('base64')], + x5c: [issuer.certificate], issuer: baseUrl, } satisfies SdJwtVcIssuer @@ -478,7 +472,7 @@ describe('OpenId4Vc Presentation During Issuance', () => { const x5cIssuer = { method: 'x5c', - x5c: [issuer.certificate.toString('base64')], + x5c: [issuer.certificate], issuer: baseUrl, } satisfies SdJwtVcIssuer diff --git a/packages/openid4vc/tests/openid4vc-wallet-key-attestation.e2e.test.ts b/packages/openid4vc/tests/openid4vc-wallet-key-attestation.e2e.test.ts index 63f3239c97..66e01ad0bc 100644 --- a/packages/openid4vc/tests/openid4vc-wallet-key-attestation.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc-wallet-key-attestation.e2e.test.ts @@ -1,11 +1,9 @@ import type { AgentType } from './utils' -import { ClaimFormat, CredoError, JwaSignatureAlgorithm, Key, KeyType, getJwkFromKey } from '@credo-ts/core' +import { ClaimFormat, CredoError, Kms } from '@credo-ts/core' import express, { type Express } from 'express' import { setupNockToExpress } from '../../../tests/nockToExpress' -import { AskarModule } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' import { OpenId4VcHolderModule, OpenId4VcIssuanceSessionState, @@ -16,7 +14,9 @@ import { OpenId4VciCredentialFormatProfile, } from '../src' +import { Jwk } from '@openid4vc/oauth2' import { AuthorizationFlow, Openid4vciWalletProvider } from '@openid4vc/openid4vci' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' import { getOid4vcCallbacks } from '../src/shared/callbacks' import { addSecondsToDate } from '../src/shared/utils' import { createAgentFromModules, waitForCredentialIssuanceSessionRecordSubject } from './utils' @@ -49,17 +49,15 @@ describe('OpenId4Vc Wallet and Key Attestations', () => { let issuer: AgentType<{ openId4VcIssuer: OpenId4VcIssuerModule openId4VcVerifier: OpenId4VcVerifierModule - askar: AskarModule }> let issuerRecord: OpenId4VcIssuerRecord let holder: AgentType<{ openId4VcHolder: OpenId4VcHolderModule - askar: AskarModule }> let keyAttestationJwt: string - let attestedKeys: Key[] + let attestedKeys: Kms.PublicJwk[] let walletAttestationJwt: string beforeEach(async () => { @@ -77,7 +75,7 @@ describe('OpenId4Vc Wallet and Key Attestations', () => { verifierId: issuanceSession.issuerId, requestSigner: { method: 'x5c', - x5c: [issuer.certificate.toString('base64')], + x5c: [issuer.certificate], }, responseMode: 'direct_post.jwt', dcql: { @@ -133,7 +131,7 @@ describe('OpenId4Vc Wallet and Key Attestations', () => { signer: { method: 'x5c', x5c: [expect.any(String)], - alg: JwaSignatureAlgorithm.ES256, + alg: Kms.KnownJwaSignatureAlgorithms.ES256, publicJwk: expect.any(Object), }, }) @@ -142,8 +140,8 @@ describe('OpenId4Vc Wallet and Key Attestations', () => { format: OpenId4VciCredentialFormatProfile.MsoMdoc, credentials: holderBinding.keys.map((holderBinding, index) => ({ docType: credentialConfiguration.doctype, - holderKey: holderBinding.key, - issuerCertificate: issuer.certificate.toString('base64'), + holderKey: holderBinding.jwk, + issuerCertificate: issuer.certificate, namespaces: { [credentialConfiguration.doctype]: { index, @@ -160,16 +158,18 @@ describe('OpenId4Vc Wallet and Key Attestations', () => { throw new Error('not supported') }, }), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule({}), }) holder = await createAgentFromModules('holder', { openId4VcHolder: new OpenId4VcHolderModule(), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule({}), }) const walletProviderCertificate = await holder.agent.x509.createCertificate({ - authorityKey: await holder.agent.wallet.createKey({ keyType: KeyType.P256 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await holder.agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })).publicJwk + ), issuer: { commonName: 'Credo Wallet Provider', }, @@ -179,13 +179,14 @@ describe('OpenId4Vc Wallet and Key Attestations', () => { walletAttestationJwt = await walletProvider.createWalletAttestationJwt({ clientId: 'wallet', confirmation: { - jwk: getJwkFromKey(await holder.agent.wallet.createKey({ keyType: KeyType.P256 })).toJson(), + jwk: (await holder.agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })).publicJwk as Jwk, }, issuer: 'https://wallet-provider.com', signer: { method: 'x5c', - alg: JwaSignatureAlgorithm.ES256, - x5c: [walletProviderCertificate.toString('base64')], + alg: Kms.KnownJwaSignatureAlgorithms.ES256, + x5c: [walletProviderCertificate.toString('base64url')], + kid: walletProviderCertificate.publicJwk.keyId, }, walletName: 'Credo Wallet', walletLink: 'https://credo.js.org', @@ -194,19 +195,22 @@ describe('OpenId4Vc Wallet and Key Attestations', () => { }) attestedKeys = await Promise.all( - new Array(10).fill(0).map(() => - holder.agent.context.wallet.createKey({ - keyType: KeyType.P256, - }) - ) + new Array(10) + .fill(0) + .map(async () => + Kms.PublicJwk.fromPublicJwk( + (await holder.agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })).publicJwk + ) + ) ) keyAttestationJwt = await walletProvider.createKeyAttestationJwt({ - attestedKeys: attestedKeys.map((key) => getJwkFromKey(key).toJson()), + attestedKeys: attestedKeys.map((key) => key.toJson() as Jwk), signer: { method: 'x5c', - alg: JwaSignatureAlgorithm.ES256, - x5c: [walletProviderCertificate.toString('base64')], + alg: Kms.KnownJwaSignatureAlgorithms.ES256, + x5c: [walletProviderCertificate.toString('base64url')], + kid: walletProviderCertificate.publicJwk.keyId, }, use: 'proof_type.jwt', keyStorage: ['iso_18045_high'], @@ -229,7 +233,7 @@ describe('OpenId4Vc Wallet and Key Attestations', () => { const holderIdentityCredential = await issuer.agent.sdJwtVc.sign({ issuer: { method: 'x5c', - x5c: [issuer.certificate.toString('base64')], + x5c: [issuer.certificate], issuer: baseUrl, }, payload: { @@ -247,12 +251,12 @@ describe('OpenId4Vc Wallet and Key Attestations', () => { }) await holder.agent.sdJwtVc.store(holderIdentityCredential.compact) - holder.agent.x509.addTrustedCertificate(issuer.certificate.toString('base64')) - issuer.agent.x509.addTrustedCertificate(issuer.certificate.toString('base64')) + holder.agent.x509.config.addTrustedCertificate(issuer.certificate) + issuer.agent.x509.config.addTrustedCertificate(issuer.certificate) issuerRecord = await issuer.agent.modules.openId4VcIssuer.createIssuer({ issuerId: '2f9c0385-7191-4c50-aa22-40cf5839d52b', - dpopSigningAlgValuesSupported: [JwaSignatureAlgorithm.ES256], + dpopSigningAlgValuesSupported: [Kms.KnownJwaSignatureAlgorithms.ES256], batchCredentialIssuance: { batchSize: 10, }, @@ -274,10 +278,8 @@ describe('OpenId4Vc Wallet and Key Attestations', () => { afterEach(async () => { clearNock() await issuer.agent.shutdown() - await issuer.agent.wallet.delete() await holder.agent.shutdown() - await holder.agent.wallet.delete() }) it('e2e flow issuing a batch of mdoc based on wallet and key attestation', async () => { @@ -564,7 +566,7 @@ describe('OpenId4Vc Wallet and Key Attestations', () => { ...tokenResponse, credentialBindingResolver: () => ({ method: 'jwk', - keys: attestedKeys.map((key) => getJwkFromKey(key)), + keys: attestedKeys, }), }) ).rejects.toThrow( @@ -588,7 +590,7 @@ describe('OpenId4Vc Wallet and Key Attestations', () => { ...tokenResponse, credentialBindingResolver: () => ({ method: 'jwk', - keys: attestedKeys.map((key) => getJwkFromKey(key)), + keys: attestedKeys, }), }) ).rejects.toThrow( diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 05a154e128..0486a1600c 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -1,12 +1,5 @@ -import type { - DcqlQuery, - DifPresentationExchangeDefinitionV2, - JwkJson, - Mdoc, - MdocDeviceResponse, - SdJwtVc, -} from '@credo-ts/core' -import type { AuthorizationServerMetadata } from '@openid4vc/oauth2' +import type { DcqlQuery, DifPresentationExchangeDefinitionV2, Mdoc, MdocDeviceResponse, SdJwtVc } from '@credo-ts/core' +import type { AuthorizationServerMetadata, Jwk } from '@openid4vc/oauth2' import type { OpenId4VciSignMdocCredentials } from '../src' import type { OpenId4VciCredentialBindingResolver } from '../src/openid4vc-holder' import type { AgentType, TenantType } from './utils' @@ -16,22 +9,19 @@ import { CredoError, DateOnly, DidsApi, - JwaSignatureAlgorithm, - Jwk, JwsService, Jwt, JwtPayload, - KeyType, + Kms, MdocRecord, SdJwtVcRecord, W3cCredential, W3cCredentialSubject, W3cIssuer, + X509Certificate, X509Module, - X509ModuleConfig, X509Service, - getJwkFromKey, - getKeyFromVerificationMethod, + getPublicJwkFromVerificationMethod, parseDid, w3cDate, } from '@credo-ts/core' @@ -43,9 +33,6 @@ import { } from '@openid4vc/oauth2' import { AuthorizationFlow } from '@openid4vc/openid4vci' import express, { type Express } from 'express' - -import { AskarModule } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' import { TenantsModule } from '../../tenants/src' import { OpenId4VcHolderModule, @@ -56,6 +43,7 @@ import { } from '../src' import { getOid4vcCallbacks } from '../src/shared/callbacks' +import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' import { setupNockToExpress } from '../../../tests/nockToExpress' import { createAgentFromModules, @@ -66,7 +54,6 @@ import { import { universityDegreeCredentialConfigurationSupported, universityDegreeCredentialConfigurationSupportedMdoc, - universityDegreeCredentialSdJwt2, } from './utilsVci' import { openBadgePresentationDefinition, universityDegreePresentationDefinition } from './utilsVp' @@ -85,7 +72,6 @@ describe('OpenId4Vc', () => { x509: X509Module }> let issuer1: TenantType - let issuer2: TenantType let holder: AgentType<{ openId4VcHolder: OpenId4VcHolderModule @@ -100,6 +86,8 @@ describe('OpenId4Vc', () => { let verifier1: TenantType let verifier2: TenantType + let credentialIssuerCertificate: X509Certificate + beforeEach(async () => { expressApp = express() @@ -107,6 +95,7 @@ describe('OpenId4Vc', () => { 'issuer', { x509: new X509Module(), + inMemory: new InMemoryWalletModule(), openId4VcIssuer: new OpenId4VcIssuerModule({ baseUrl: issuanceBaseUrl, @@ -135,17 +124,12 @@ describe('OpenId4Vc', () => { } } if (credentialRequest.format === 'mso_mdoc') { - const trustedCertificates = agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates - if (trustedCertificates?.length !== 1) { - throw new Error('Expected exactly one trusted certificate. Received 0.') - } - return { format: ClaimFormat.MsoMdoc, credentials: holderBinding.keys.map((holderBinding) => ({ docType: universityDegreeCredentialConfigurationSupportedMdoc.doctype, - issuerCertificate: trustedCertificates[0], - holderKey: holderBinding.key, + issuerCertificate: credentialIssuerCertificate, + holderKey: holderBinding.jwk, namespaces: { 'Leopold-Franzens-University': { degree: 'bachelor', @@ -157,20 +141,18 @@ describe('OpenId4Vc', () => { throw new Error('Invalid request') }, }), - askar: new AskarModule(askarModuleConfig), tenants: new TenantsModule(), }, '96213c3d7fc8d4d6754c7a0fd969598g', global.fetch )) as unknown as typeof issuer issuer1 = await createTenantForAgent(issuer.agent, 'iTenant1') - issuer2 = await createTenantForAgent(issuer.agent, 'iTenant2') holder = (await createAgentFromModules( 'holder', { openId4VcHolder: new OpenId4VcHolderModule(), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), tenants: new TenantsModule(), x509: new X509Module(), }, @@ -185,7 +167,7 @@ describe('OpenId4Vc', () => { openId4VcVerifier: new OpenId4VcVerifierModule({ baseUrl: verificationBaseUrl, }), - askar: new AskarModule(askarModuleConfig), + inMemory: new InMemoryWalletModule(), tenants: new TenantsModule(), }, '96213c3d7fc8d4d6754c7a0fd969598f', @@ -205,13 +187,8 @@ describe('OpenId4Vc', () => { clearNock() await issuer.agent.shutdown() - await issuer.agent.wallet.delete() - await holder.agent.shutdown() - await holder.agent.wallet.delete() - await verifier.agent.shutdown() - await verifier.agent.wallet.delete() }) const credentialBindingResolver: OpenId4VciCredentialBindingResolver = ({ supportsJwk, supportedDidMethods }) => { @@ -227,7 +204,7 @@ describe('OpenId4Vc', () => { if (supportsJwk) { return { method: 'jwk', - keys: [getJwkFromKey(getKeyFromVerificationMethod(holder1.verificationMethod))], + keys: [getPublicJwkFromVerificationMethod(holder1.verificationMethod)], } } @@ -235,237 +212,17 @@ describe('OpenId4Vc', () => { throw new CredoError('Issuer does not support did:key or JWK for credential binding') } - it('e2e flow with tenants, issuer endpoints requesting a sd-jwt-vc', async () => { - const issuerTenant1 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer1.tenantId }) - const issuerTenant2 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer2.tenantId }) - - const openIdIssuerTenant1 = await issuerTenant1.modules.openId4VcIssuer.createIssuer({ - dpopSigningAlgValuesSupported: [JwaSignatureAlgorithm.EdDSA], - credentialConfigurationsSupported: { - universityDegree: universityDegreeCredentialConfigurationSupported, - }, - }) - const issuer1Record = await issuerTenant1.modules.openId4VcIssuer.getIssuerByIssuerId(openIdIssuerTenant1.issuerId) - expect(issuer1Record.dpopSigningAlgValuesSupported).toEqual(['EdDSA']) - expect(issuer1Record.credentialConfigurationsSupported).toEqual({ - universityDegree: { - format: 'vc+sd-jwt', - cryptographic_binding_methods_supported: ['did:key', 'jwk'], - proof_types_supported: { - jwt: { - proof_signing_alg_values_supported: ['EdDSA', 'ES256'], - }, - }, - vct: universityDegreeCredentialConfigurationSupported.vct, - scope: universityDegreeCredentialConfigurationSupported.scope, - }, - }) - const openIdIssuerTenant2 = await issuerTenant2.modules.openId4VcIssuer.createIssuer({ - dpopSigningAlgValuesSupported: [JwaSignatureAlgorithm.EdDSA], - credentialConfigurationsSupported: { - [universityDegreeCredentialSdJwt2.id]: universityDegreeCredentialSdJwt2, - }, - }) - - const { issuanceSession: issuanceSession1, credentialOffer: credentialOffer1 } = - await issuerTenant1.modules.openId4VcIssuer.createCredentialOffer({ - issuerId: openIdIssuerTenant1.issuerId, - credentialConfigurationIds: ['universityDegree'], - preAuthorizedCodeFlowConfig: { - txCode: { - input_mode: 'numeric', - length: 4, - }, - }, - version: 'v1.draft15', - }) - - const { issuanceSession: issuanceSession2, credentialOffer: credentialOffer2 } = - await issuerTenant2.modules.openId4VcIssuer.createCredentialOffer({ - issuerId: openIdIssuerTenant2.issuerId, - credentialConfigurationIds: [universityDegreeCredentialSdJwt2.id], - preAuthorizedCodeFlowConfig: { - txCode: {}, - }, - version: 'v1.draft11-15', - }) - - await issuerTenant2.endSession() - - await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { - state: OpenId4VcIssuanceSessionState.OfferCreated, - issuanceSessionId: issuanceSession1.id, - contextCorrelationId: issuer1.tenantId, - }) - await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { - state: OpenId4VcIssuanceSessionState.OfferCreated, - issuanceSessionId: issuanceSession2.id, - contextCorrelationId: issuer2.tenantId, - }) - - const holderTenant1 = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) - - const resolvedCredentialOffer1 = - await holderTenant1.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer1) - - expect(resolvedCredentialOffer1.metadata.credentialIssuer?.dpop_signing_alg_values_supported).toEqual(['EdDSA']) - expect(resolvedCredentialOffer1.offeredCredentialConfigurations).toEqual({ - universityDegree: { - format: 'vc+sd-jwt', - cryptographic_binding_methods_supported: ['did:key', 'jwk'], - proof_types_supported: { - jwt: { - proof_signing_alg_values_supported: ['EdDSA', 'ES256'], - }, - }, - vct: universityDegreeCredentialConfigurationSupported.vct, - scope: universityDegreeCredentialConfigurationSupported.scope, - }, - }) - - expect(resolvedCredentialOffer1.credentialOfferPayload.credential_issuer).toEqual( - `${issuanceBaseUrl}/${openIdIssuerTenant1.issuerId}` - ) - expect(resolvedCredentialOffer1.metadata.credentialIssuer?.token_endpoint).toEqual( - `${issuanceBaseUrl}/${openIdIssuerTenant1.issuerId}/token` - ) - expect(resolvedCredentialOffer1.metadata.credentialIssuer?.credential_endpoint).toEqual( - `${issuanceBaseUrl}/${openIdIssuerTenant1.issuerId}/credential` - ) - - // Bind to JWK - const tokenResponseTenant1 = await holderTenant1.modules.openId4VcHolder.requestToken({ - resolvedCredentialOffer: resolvedCredentialOffer1, - txCode: issuanceSession1.userPin, - }) - - const expectedSubject = (await issuerTenant1.modules.openId4VcIssuer.getIssuanceSessionById(issuanceSession1.id)) - .authorization?.subject - await issuerTenant1.endSession() - - expect(tokenResponseTenant1.accessToken).toBeDefined() - expect(tokenResponseTenant1.dpop?.jwk).toBeInstanceOf(Jwk) - const { payload } = Jwt.fromSerializedJwt(tokenResponseTenant1.accessToken) - expect(payload.toJson()).toEqual({ - cnf: { - jkt: await calculateJwkThumbprint({ - hashAlgorithm: HashAlgorithm.Sha256, - hashCallback: getOid4vcCallbacks(holderTenant1.context).hash, - jwk: tokenResponseTenant1.dpop?.jwk.toJson() as JwkJson, - }), - }, - 'pre-authorized_code': expect.any(String), - aud: `http://localhost:1234/oid4vci/${openIdIssuerTenant1.issuerId}`, - exp: expect.any(Number), - iat: expect.any(Number), - iss: `http://localhost:1234/oid4vci/${openIdIssuerTenant1.issuerId}`, - jti: expect.any(String), - nbf: undefined, - sub: expectedSubject, - }) - - const credentialsTenant1 = await holderTenant1.modules.openId4VcHolder.requestCredentials({ - resolvedCredentialOffer: resolvedCredentialOffer1, - ...tokenResponseTenant1, - credentialBindingResolver, - }) - - // Wait for all events - await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { - state: OpenId4VcIssuanceSessionState.AccessTokenRequested, - issuanceSessionId: issuanceSession1.id, - contextCorrelationId: issuer1.tenantId, - }) - await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { - state: OpenId4VcIssuanceSessionState.AccessTokenCreated, - issuanceSessionId: issuanceSession1.id, - contextCorrelationId: issuer1.tenantId, - }) - await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { - state: OpenId4VcIssuanceSessionState.CredentialRequestReceived, - issuanceSessionId: issuanceSession1.id, - contextCorrelationId: issuer1.tenantId, - }) - await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { - state: OpenId4VcIssuanceSessionState.Completed, - issuanceSessionId: issuanceSession1.id, - contextCorrelationId: issuer1.tenantId, - }) - - expect(credentialsTenant1.credentials).toHaveLength(1) - const compactSdJwtVcTenant1 = (credentialsTenant1.credentials[0].credentials[0] as SdJwtVc).compact - const sdJwtVcTenant1 = holderTenant1.sdJwtVc.fromCompact(compactSdJwtVcTenant1) - expect(sdJwtVcTenant1.payload.vct).toEqual('UniversityDegreeCredential') - - const resolvedCredentialOffer2 = - await holderTenant1.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer2) - - await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { - state: OpenId4VcIssuanceSessionState.OfferUriRetrieved, - issuanceSessionId: issuanceSession2.id, - contextCorrelationId: issuer2.tenantId, - }) - - expect(resolvedCredentialOffer2.credentialOfferPayload.credential_issuer).toEqual( - `${issuanceBaseUrl}/${openIdIssuerTenant2.issuerId}` - ) - expect(resolvedCredentialOffer2.metadata.credentialIssuer?.token_endpoint).toEqual( - `${issuanceBaseUrl}/${openIdIssuerTenant2.issuerId}/token` - ) - expect(resolvedCredentialOffer2.metadata.credentialIssuer?.credential_endpoint).toEqual( - `${issuanceBaseUrl}/${openIdIssuerTenant2.issuerId}/credential` - ) - - // Bind to did - const tokenResponseTenant2 = await holderTenant1.modules.openId4VcHolder.requestToken({ - resolvedCredentialOffer: resolvedCredentialOffer2, - txCode: issuanceSession2.userPin, - }) - - const credentialsTenant2 = await holderTenant1.modules.openId4VcHolder.requestCredentials({ - resolvedCredentialOffer: resolvedCredentialOffer2, - ...tokenResponseTenant2, - credentialBindingResolver, - }) - - // Wait for all events - await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { - state: OpenId4VcIssuanceSessionState.AccessTokenRequested, - issuanceSessionId: issuanceSession2.id, - contextCorrelationId: issuer2.tenantId, - }) - await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { - state: OpenId4VcIssuanceSessionState.AccessTokenCreated, - issuanceSessionId: issuanceSession2.id, - contextCorrelationId: issuer2.tenantId, - }) - await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { - state: OpenId4VcIssuanceSessionState.CredentialRequestReceived, - issuanceSessionId: issuanceSession2.id, - contextCorrelationId: issuer2.tenantId, - }) - await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { - state: OpenId4VcIssuanceSessionState.Completed, - issuanceSessionId: issuanceSession2.id, - contextCorrelationId: issuer2.tenantId, - }) - - expect(credentialsTenant2.credentials).toHaveLength(1) - const compactSdJwtVcTenant2 = (credentialsTenant2.credentials[0].credentials[0] as SdJwtVc).compact - const sdJwtVcTenant2 = holderTenant1.sdJwtVc.fromCompact(compactSdJwtVcTenant2) - expect(sdJwtVcTenant2.payload.vct).toEqual('UniversityDegreeCredential2') - - await holderTenant1.endSession() - }) - it('e2e flow with tenants, issuer endpoints requesting a sd-jwt-vc using authorization code flow', async () => { const issuerTenant = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer1.tenantId }) const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) - const authorizationServerKey = await issuer.agent.wallet.createKey({ - keyType: KeyType.P256, + const authorizationServerKey = await issuer.agent.kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, }) - const authorizationServerJwk = getJwkFromKey(authorizationServerKey).toJson() + const authorizationServerJwk = Kms.PublicJwk.fromPublicJwk(authorizationServerKey.publicJwk) const authorizationServer = new Oauth2AuthorizationServer({ callbacks: { ...getOid4vcCallbacks(issuer.agent.context), @@ -473,7 +230,7 @@ describe('OpenId4Vc', () => { signJwt: async (_signer, { header, payload }) => { const jwsService = issuer.agent.dependencyManager.resolve(JwsService) const compact = await jwsService.createJwsCompact(issuer.agent.context, { - key: authorizationServerKey, + keyId: authorizationServerKey.keyId, payload: JwtPayload.fromJson(payload), protectedHeaderOptions: { ...header, @@ -485,7 +242,7 @@ describe('OpenId4Vc', () => { return { jwt: compact, - signerJwk: authorizationServerJwk, + signerJwk: authorizationServerKey.publicJwk as Jwk, } }, }, @@ -503,7 +260,7 @@ describe('OpenId4Vc', () => { app.get('/jwks.json', (_req, res) => res.setHeader('Content-Type', 'application/jwk-set+json').send( JSON.stringify({ - keys: [{ ...authorizationServerJwk, kid: 'first' }], + keys: [{ ...authorizationServerJwk.toJson(), kid: 'first' }], }) ) ) @@ -520,7 +277,7 @@ describe('OpenId4Vc', () => { }, signer: { method: 'jwk', - publicJwk: authorizationServerJwk, + publicJwk: authorizationServerJwk.toJson() as Jwk, alg: 'ES256', }, }) @@ -584,7 +341,7 @@ describe('OpenId4Vc', () => { await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { state: OpenId4VcIssuanceSessionState.Completed, issuanceSessionId: issuanceSession.id, - contextCorrelationId: issuer1.tenantId, + contextCorrelationId: issuerTenant.context.contextCorrelationId, }) expect(credentialResponse.credentials).toHaveLength(1) @@ -612,7 +369,7 @@ describe('OpenId4Vc', () => { credentialSubject: new W3cCredentialSubject({ id: holder1.did }), issuanceDate: w3cDate(Date.now()), }), - alg: JwaSignatureAlgorithm.EdDSA, + alg: Kms.KnownJwaSignatureAlgorithms.EdDSA, verificationMethod: issuer.verificationMethod.id, }) @@ -624,7 +381,7 @@ describe('OpenId4Vc', () => { credentialSubject: new W3cCredentialSubject({ id: holder1.did }), issuanceDate: w3cDate(Date.now()), }), - alg: JwaSignatureAlgorithm.EdDSA, + alg: Kms.KnownJwaSignatureAlgorithms.EdDSA, verificationMethod: issuer.verificationMethod.id, }) @@ -855,15 +612,17 @@ describe('OpenId4Vc', () => { const certificate = await verifier.agent.x509.createCertificate({ issuer: { commonName: 'Credo', countryName: 'NL' }, - authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } })).publicJwk + ), extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: 'localhost' }] } }, }) const rawCertificate = certificate.toString('base64') await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) - holder.agent.x509.addTrustedCertificate(rawCertificate) - verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.config.addTrustedCertificate(rawCertificate) + verifier.agent.x509.config.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'OpenBadgeCredential', @@ -900,7 +659,7 @@ describe('OpenId4Vc', () => { responseMode: 'direct_post.jwt', requestSigner: { method: 'x5c', - x5c: [rawCertificate], + x5c: [certificate], }, transactionData: [ { @@ -1133,15 +892,17 @@ describe('OpenId4Vc', () => { const certificate = await verifier.agent.x509.createCertificate({ issuer: { commonName: 'Credo', countryName: 'NL' }, - authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } })).publicJwk + ), extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: 'localhost' }] } }, }) const rawCertificate = certificate.toString('base64') await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) - holder.agent.x509.addTrustedCertificate(rawCertificate) - verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.config.addTrustedCertificate(rawCertificate) + verifier.agent.x509.config.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'OpenBadgeCredential', @@ -1177,7 +938,7 @@ describe('OpenId4Vc', () => { verifierId: openIdVerifier.verifierId, requestSigner: { method: 'x5c', - x5c: [rawCertificate], + x5c: [certificate], }, transactionData: [ { @@ -1408,7 +1169,9 @@ describe('OpenId4Vc', () => { const certificate = await verifier.agent.x509.createCertificate({ issuer: { commonName: 'Credo', countryName: 'NL' }, - authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } })).publicJwk + ), extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: 'localhost' }] } }, }) @@ -1416,8 +1179,8 @@ describe('OpenId4Vc', () => { await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) await holder.agent.sdJwtVc.store(signedSdJwtVc2.compact) - holder.agent.x509.addTrustedCertificate(rawCertificate) - verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.config.addTrustedCertificate(rawCertificate) + verifier.agent.x509.config.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'OpenBadgeCredentials', @@ -1477,7 +1240,7 @@ describe('OpenId4Vc', () => { requestSigner: { method: 'x5c', - x5c: [rawCertificate], + x5c: [certificate], }, presentationExchange: { definition: presentationDefinition, @@ -1820,14 +1583,15 @@ describe('OpenId4Vc', () => { const issuerTenant1 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer1.tenantId }) const issuerCertificate = await issuerTenant1.x509.createCertificate({ - authorityKey: await issuerTenant1.wallet.createKey({ keyType: KeyType.P256 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await issuerTenant1.kms.createKey({ type: { crv: 'P-256', kty: 'EC' } })).publicJwk + ), issuer: 'C=DE', }) - const issuerCertificatePem = issuerCertificate.toString('pem') - await issuerTenant1.x509.setTrustedCertificates([issuerCertificatePem]) + credentialIssuerCertificate = issuerCertificate const openIdIssuerTenant1 = await issuerTenant1.modules.openId4VcIssuer.createIssuer({ - dpopSigningAlgValuesSupported: [JwaSignatureAlgorithm.ES256], + dpopSigningAlgValuesSupported: [Kms.KnownJwaSignatureAlgorithms.ES256], credentialConfigurationsSupported: { universityDegree: universityDegreeCredentialConfigurationSupportedMdoc, }, @@ -1862,11 +1626,11 @@ describe('OpenId4Vc', () => { await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { state: OpenId4VcIssuanceSessionState.OfferCreated, issuanceSessionId: issuanceSession1.id, - contextCorrelationId: issuer1.tenantId, + contextCorrelationId: issuerTenant1.context.contextCorrelationId, }) const holderTenant1 = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) - await holderTenant1.x509.setTrustedCertificates([issuerCertificatePem]) + holderTenant1.x509.config.setTrustedCertificates([issuerCertificate.toString('pem')]) const resolvedCredentialOffer1 = await holderTenant1.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer1) @@ -1902,7 +1666,7 @@ describe('OpenId4Vc', () => { }) expect(tokenResponseTenant1.accessToken).toBeDefined() - expect(tokenResponseTenant1.dpop?.jwk).toBeInstanceOf(Jwk) + expect(tokenResponseTenant1.dpop?.jwk).toBeInstanceOf(Kms.PublicJwk) const { payload } = Jwt.fromSerializedJwt(tokenResponseTenant1.accessToken) expect(payload.toJson()).toEqual({ @@ -1910,7 +1674,7 @@ describe('OpenId4Vc', () => { jkt: await calculateJwkThumbprint({ hashAlgorithm: HashAlgorithm.Sha256, hashCallback: getOid4vcCallbacks(holderTenant1.context).hash, - jwk: tokenResponseTenant1.dpop?.jwk.toJson() as JwkJson, + jwk: tokenResponseTenant1.dpop?.jwk.toJson() as Jwk, }), }, 'pre-authorized_code': @@ -1937,22 +1701,22 @@ describe('OpenId4Vc', () => { await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { state: OpenId4VcIssuanceSessionState.AccessTokenRequested, issuanceSessionId: issuanceSession1.id, - contextCorrelationId: issuer1.tenantId, + contextCorrelationId: issuerTenant1.context.contextCorrelationId, }) await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { state: OpenId4VcIssuanceSessionState.AccessTokenCreated, issuanceSessionId: issuanceSession1.id, - contextCorrelationId: issuer1.tenantId, + contextCorrelationId: issuerTenant1.context.contextCorrelationId, }) await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { state: OpenId4VcIssuanceSessionState.CredentialRequestReceived, issuanceSessionId: issuanceSession1.id, - contextCorrelationId: issuer1.tenantId, + contextCorrelationId: issuerTenant1.context.contextCorrelationId, }) await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { state: OpenId4VcIssuanceSessionState.Completed, issuanceSessionId: issuanceSession1.id, - contextCorrelationId: issuer1.tenantId, + contextCorrelationId: issuerTenant1.context.contextCorrelationId, }) expect(credentialsTenant1.credentials).toHaveLength(1) @@ -1967,17 +1731,28 @@ describe('OpenId4Vc', () => { const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() const issuerCertificate = await X509Service.createCertificate(issuer.agent.context, { - authorityKey: await issuer.agent.context.wallet.createKey({ keyType: KeyType.P256 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await issuer.agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })).publicJwk + ), issuer: 'C=DE', }) - await verifier.agent.x509.setTrustedCertificates([issuerCertificate.toString('pem')]) + verifier.agent.x509.config.setTrustedCertificates([issuerCertificate.toString('pem')]) - const holderKey = await holder.agent.context.wallet.createKey({ keyType: KeyType.P256 }) + const holderKey = Kms.PublicJwk.fromPublicJwk( + ( + await holder.agent.kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, + }) + ).publicJwk + ) const signedMdoc = await issuer.agent.mdoc.sign({ docType: 'org.eu.university', holderKey, - issuerCertificate: issuerCertificate.toString('pem'), + issuerCertificate, namespaces: { 'eu.europa.ec.eudi.pid.1': { university: 'innsbruck', @@ -1989,7 +1764,9 @@ describe('OpenId4Vc', () => { }) const certificate = await verifier.agent.x509.createCertificate({ - authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } })).publicJwk + ), extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: 'localhost' }] } }, issuer: { commonName: 'Credo', countryName: 'NL' }, }) @@ -1997,8 +1774,8 @@ describe('OpenId4Vc', () => { const rawCertificate = certificate.toString('base64') await holder.agent.mdoc.store(signedMdoc) - holder.agent.x509.addTrustedCertificate(rawCertificate) - verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.config.addTrustedCertificate(rawCertificate) + verifier.agent.x509.config.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'mDL-sample-req', @@ -2032,7 +1809,7 @@ describe('OpenId4Vc', () => { verifierId: openIdVerifier.verifierId, requestSigner: { method: 'x5c', - x5c: [rawCertificate], + x5c: [certificate], }, presentationExchange: { definition: presentationDefinition }, }) @@ -2088,23 +1865,34 @@ describe('OpenId4Vc', () => { await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) const issuerCertificate = await X509Service.createCertificate(issuer.agent.context, { - authorityKey: await issuer.agent.context.wallet.createKey({ keyType: KeyType.P256 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await issuer.agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })).publicJwk + ), issuer: 'C=DE', }) - await verifier.agent.x509.setTrustedCertificates([issuerCertificate.toString('pem')]) + verifier.agent.x509.config.setTrustedCertificates([issuerCertificate.toString('pem')]) const parsedDid = parseDid(issuer.kid) if (!parsedDid.fragment) { throw new Error(`didUrl '${parsedDid.didUrl}' does not contain a '#'. Unable to derive key from did document.`) } - const holderKey = await holder.agent.context.wallet.createKey({ keyType: KeyType.P256 }) + const holderKey = Kms.PublicJwk.fromPublicJwk( + ( + await holder.agent.kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, + }) + ).publicJwk + ) const signedMdoc = await issuer.agent.mdoc.sign({ docType: 'org.eu.university', holderKey, - issuerCertificate: issuerCertificate.toString('pem'), + issuerCertificate, namespaces: { 'eu.europa.ec.eudi.pid.1': { university: 'innsbruck', @@ -2117,15 +1905,17 @@ describe('OpenId4Vc', () => { const certificate = await verifier.agent.x509.createCertificate({ issuer: { commonName: 'Credo', countryName: 'NL' }, - authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } })).publicJwk + ), extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: 'localhost' }] } }, }) const rawCertificate = certificate.toString('base64') await holder.agent.mdoc.store(signedMdoc) - holder.agent.x509.addTrustedCertificate(rawCertificate) - verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.config.addTrustedCertificate(rawCertificate) + verifier.agent.x509.config.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'mDL-sample-req', @@ -2183,7 +1973,7 @@ describe('OpenId4Vc', () => { verifierId: openIdVerifier.verifierId, requestSigner: { method: 'x5c', - x5c: [rawCertificate], + x5c: [certificate], }, presentationExchange: { definition: presentationDefinition, @@ -2456,7 +2246,9 @@ describe('OpenId4Vc', () => { const certificate = await verifier.agent.x509.createCertificate({ issuer: { commonName: 'Credo', countryName: 'NL' }, - authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } })).publicJwk + ), extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: 'localhost' }] } }, }) @@ -2464,8 +2256,8 @@ describe('OpenId4Vc', () => { await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) await holder.agent.sdJwtVc.store(signedSdJwtVc2.compact) - holder.agent.x509.addTrustedCertificate(rawCertificate) - verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.config.addTrustedCertificate(rawCertificate) + verifier.agent.x509.config.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'OpenBadgeCredentials', @@ -2525,7 +2317,7 @@ describe('OpenId4Vc', () => { requestSigner: { method: 'x5c', - x5c: [rawCertificate], + x5c: [certificate], }, presentationExchange: { definition: presentationDefinition, @@ -2806,27 +2598,38 @@ describe('OpenId4Vc', () => { await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) const selfSignedCertificate = await X509Service.createCertificate(issuer.agent.context, { - authorityKey: await issuer.agent.context.wallet.createKey({ keyType: KeyType.P256 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await issuer.agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })).publicJwk + ), issuer: { countryName: 'DE', }, }) - await verifier.agent.x509.setTrustedCertificates([selfSignedCertificate.toString('pem')]) + verifier.agent.x509.config.setTrustedCertificates([selfSignedCertificate.toString('pem')]) const parsedDid = parseDid(issuer.kid) if (!parsedDid.fragment) { throw new Error(`didUrl '${parsedDid.didUrl}' does not contain a '#'. Unable to derive key from did document.`) } - const holderKey = await holder.agent.context.wallet.createKey({ keyType: KeyType.P256 }) + const holderKey = Kms.PublicJwk.fromPublicJwk( + ( + await holder.agent.kms.createKey({ + type: { + kty: 'EC', + crv: 'P-256', + }, + }) + ).publicJwk + ) const date = new DateOnly(new DateOnly().toISOString()) const signedMdoc = await issuer.agent.mdoc.sign({ docType: 'org.eu.university', holderKey, - issuerCertificate: selfSignedCertificate.toString('pem'), + issuerCertificate: selfSignedCertificate, namespaces: { 'eu.europa.ec.eudi.pid.1': { university: 'innsbruck', @@ -2839,7 +2642,9 @@ describe('OpenId4Vc', () => { }) const certificate = await verifier.agent.x509.createCertificate({ - authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + authorityKey: Kms.PublicJwk.fromPublicJwk( + (await verifier.agent.kms.createKey({ type: { kty: 'OKP', crv: 'Ed25519' } })).publicJwk + ), issuer: { commonName: 'Test' }, extensions: { subjectAlternativeName: { @@ -2851,8 +2656,8 @@ describe('OpenId4Vc', () => { const rawCertificate = certificate.toString('base64') await holder.agent.mdoc.store(signedMdoc) - holder.agent.x509.addTrustedCertificate(rawCertificate) - verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.config.addTrustedCertificate(rawCertificate) + verifier.agent.x509.config.addTrustedCertificate(rawCertificate) const dcqlQuery = { credentials: [ @@ -2881,7 +2686,7 @@ describe('OpenId4Vc', () => { verifierId: openIdVerifier.verifierId, requestSigner: { method: 'x5c', - x5c: [rawCertificate], + x5c: [certificate], }, dcql: { query: dcqlQuery, diff --git a/packages/openid4vc/tests/setup.ts b/packages/openid4vc/tests/setup.ts index 34e38c9705..1c93cfbb19 100644 --- a/packages/openid4vc/tests/setup.ts +++ b/packages/openid4vc/tests/setup.ts @@ -1 +1,2 @@ +import '@openwallet-foundation/askar-nodejs' jest.setTimeout(120000) diff --git a/packages/openid4vc/tests/utils.ts b/packages/openid4vc/tests/utils.ts index fa2c0934dc..ffc48ed44a 100644 --- a/packages/openid4vc/tests/utils.ts +++ b/packages/openid4vc/tests/utils.ts @@ -8,7 +8,7 @@ import type { OpenId4VcVerificationSessionStateChangedEvent, } from '../src' -import { Agent, LogLevel, getDomainFromUrl, getJwkFromKey, utils } from '@credo-ts/core' +import { Agent, LogLevel, getDomainFromUrl } from '@credo-ts/core' import { ReplaySubject, catchError, filter, lastValueFrom, map, take, timeout } from 'rxjs' import { @@ -29,7 +29,6 @@ export async function createAgentFromModules( const agent = new Agent({ config: { label, - walletConfig: { id: utils.uuid(), key: utils.uuid() }, allowInsecureHttpUrls: true, logger: new TestLogger(LogLevel.off), }, @@ -49,7 +48,7 @@ export async function createAgentFromModules( await agent.initialize() const data = await createDidKidVerificationMethod(agent.context, secretKey) - const certificate = await createX509Certificate(agent.context, dns, data.key) + const certificate = await createX509Certificate(agent.context, dns, data.publicJwk) const [replaySubject] = setupEventReplaySubjects( [agent], @@ -58,7 +57,7 @@ export async function createAgentFromModules( return { ...data, - jwk: getJwkFromKey(data.key), + jwk: data.publicJwk, certificate: certificate.certificate, agent, replaySubject, diff --git a/packages/openid4vc/tests/utilsVci.ts b/packages/openid4vc/tests/utilsVci.ts index f872089dd1..5d87c3916d 100644 --- a/packages/openid4vc/tests/utilsVci.ts +++ b/packages/openid4vc/tests/utilsVci.ts @@ -1,4 +1,4 @@ -import { JwaSignatureAlgorithm } from '@credo-ts/core' +import { Kms } from '@credo-ts/core' import type { OpenId4VciCredentialConfigurationSupportedWithFormats } from '../src' import { OpenId4VciCredentialFormatProfile } from '../src' @@ -10,7 +10,12 @@ export const openBadgeCredential = { type: ['VerifiableCredential', 'OpenBadgeCredential'], }, proof_types_supported: { - jwt: { proof_signing_alg_values_supported: [JwaSignatureAlgorithm.EdDSA, JwaSignatureAlgorithm.ES256] }, + jwt: { + proof_signing_alg_values_supported: [ + Kms.KnownJwaSignatureAlgorithms.EdDSA, + Kms.KnownJwaSignatureAlgorithms.ES256, + ], + }, }, } satisfies OpenId4VciCredentialConfigurationSupportedWithFormats @@ -21,7 +26,12 @@ export const universityDegreeCredential = { type: ['VerifiableCredential', 'UniversityDegreeCredential'], }, proof_types_supported: { - jwt: { proof_signing_alg_values_supported: [JwaSignatureAlgorithm.EdDSA, JwaSignatureAlgorithm.ES256] }, + jwt: { + proof_signing_alg_values_supported: [ + Kms.KnownJwaSignatureAlgorithms.EdDSA, + Kms.KnownJwaSignatureAlgorithms.ES256, + ], + }, }, } satisfies OpenId4VciCredentialConfigurationSupportedWithFormats @@ -33,7 +43,12 @@ export const universityDegreeCredentialLd = { '@context': ['context'], }, proof_types_supported: { - jwt: { proof_signing_alg_values_supported: [JwaSignatureAlgorithm.EdDSA, JwaSignatureAlgorithm.ES256] }, + jwt: { + proof_signing_alg_values_supported: [ + Kms.KnownJwaSignatureAlgorithms.EdDSA, + Kms.KnownJwaSignatureAlgorithms.ES256, + ], + }, }, } satisfies OpenId4VciCredentialConfigurationSupportedWithFormats @@ -43,7 +58,12 @@ export const universityDegreeCredentialSdJwt = { vct: 'UniversityDegreeCredential', cryptographic_binding_methods_supported: ['did:key'], proof_types_supported: { - jwt: { proof_signing_alg_values_supported: [JwaSignatureAlgorithm.EdDSA, JwaSignatureAlgorithm.ES256] }, + jwt: { + proof_signing_alg_values_supported: [ + Kms.KnownJwaSignatureAlgorithms.EdDSA, + Kms.KnownJwaSignatureAlgorithms.ES256, + ], + }, }, } satisfies OpenId4VciCredentialConfigurationSupportedWithFormats @@ -52,7 +72,12 @@ export const universityDegreeCredentialConfigurationSupported = { scope: 'UniversityDegreeCredential', vct: 'UniversityDegreeCredential', proof_types_supported: { - jwt: { proof_signing_alg_values_supported: [JwaSignatureAlgorithm.EdDSA, JwaSignatureAlgorithm.ES256] }, + jwt: { + proof_signing_alg_values_supported: [ + Kms.KnownJwaSignatureAlgorithms.EdDSA, + Kms.KnownJwaSignatureAlgorithms.ES256, + ], + }, }, cryptographic_binding_methods_supported: ['did:key', 'jwk'], } satisfies OpenId4VciCredentialConfigurationSupportedWithFormats @@ -72,7 +97,12 @@ export const universityDegreeCredentialSdJwt2 = { format: OpenId4VciCredentialFormatProfile.SdJwtVc, vct: 'UniversityDegreeCredential2', proof_types_supported: { - jwt: { proof_signing_alg_values_supported: [JwaSignatureAlgorithm.EdDSA, JwaSignatureAlgorithm.ES256] }, + jwt: { + proof_signing_alg_values_supported: [ + Kms.KnownJwaSignatureAlgorithms.EdDSA, + Kms.KnownJwaSignatureAlgorithms.ES256, + ], + }, }, // FIXME: should this be dynamically generated? I think static is fine for now cryptographic_binding_methods_supported: ['jwk'], diff --git a/packages/openid4vc/tests/utilsVp.ts b/packages/openid4vc/tests/utilsVp.ts index 981c5e3720..3bef59884a 100644 --- a/packages/openid4vc/tests/utilsVp.ts +++ b/packages/openid4vc/tests/utilsVp.ts @@ -1,68 +1,4 @@ -import type { AgentContext, DifPresentationExchangeDefinitionV2, VerificationMethod } from '@credo-ts/core' - -import { - CREDENTIALS_CONTEXT_V1_URL, - ClaimFormat, - W3cCredential, - W3cCredentialService, - W3cCredentialSubject, - W3cIssuer, - getKeyFromVerificationMethod, -} from '@credo-ts/core' - -import { getProofTypeFromKey } from '../src/shared/utils' - -export const waltPortalOpenBadgeJwt = - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6e319LCJpc3MiOiJkaWQ6a2V5Ono2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSIsInN1YiI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwibmJmIjoxNzAwNzQzMzM1fQ.OcKPyaWeVV-78BWr8N4h2Cyvjtc9jzknAqvTA77hTbKCNCEbhGboo-S6yXHLC-3NWYQ1vVcqZmdPlIOrHZ7MDw' - -export const waltUniversityDegreeJwt = - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnt9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdGlRUUVxbTJ5YXBYQkR0MVdFVkIzZHFndnl6aTk2RnVGQU5ZbXJnVHJLVjkiLCJzdWIiOiJkaWQ6a2V5Ono2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyIsIm5iZiI6MTcwMDc0MzM5NH0.EhMnE349oOvzbu0rFl-m_7FOoRsB5VucLV5tUUIW0jPxkJ7J0qVLOJTXVX4KNv_N9oeP8pgTUvydd6nxB_0KCQ' - -export const getOpenBadgeCredentialLdpVc = async ( - agentContext: AgentContext, - issuerVerificationMethod: VerificationMethod, - holderVerificationMethod: VerificationMethod -) => { - const credential = new W3cCredential({ - context: [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], - type: ['VerifiableCredential', 'OpenBadgeCredential'], - id: 'http://example.edu/credentials/3732', - issuer: new W3cIssuer({ - id: issuerVerificationMethod.controller, - }), - issuanceDate: '2017-10-22T12:23:48Z', - expirationDate: '2027-10-22T12:23:48Z', - credentialSubject: new W3cCredentialSubject({ - id: holderVerificationMethod.controller, - }), - }) - - const w3cs = agentContext.dependencyManager.resolve(W3cCredentialService) - const key = getKeyFromVerificationMethod(holderVerificationMethod) - const proofType = getProofTypeFromKey(agentContext, key) - const signedLdpVc = await w3cs.signCredential(agentContext, { - format: ClaimFormat.LdpVc, - credential, - verificationMethod: issuerVerificationMethod.id, - proofType, - }) - - return signedLdpVc -} -export const openBadgeCredentialPresentationDefinitionLdpVc: DifPresentationExchangeDefinitionV2 = { - id: 'OpenBadgeCredential', - input_descriptors: [ - { - id: 'OpenBadgeCredential', - // changed jwt_vc_json to jwt_vc - format: { ldp_vc: { proof_type: ['Ed25519Signature2018'] } }, - // changed $.type to $.vc.type - constraints: { - fields: [{ path: ['$.type.*', '$.vc.type'], filter: { type: 'string', pattern: 'OpenBadgeCredential' } }], - }, - }, - ], -} +import type { DifPresentationExchangeDefinitionV2 } from '@credo-ts/core' export const universityDegreePresentationDefinition: DifPresentationExchangeDefinitionV2 = { id: 'UniversityDegreeCredential', @@ -93,29 +29,3 @@ export const openBadgePresentationDefinition: DifPresentationExchangeDefinitionV }, ], } - -export const combinePresentationDefinitions = ( - presentationDefinitions: DifPresentationExchangeDefinitionV2[] -): DifPresentationExchangeDefinitionV2 => { - return { - id: 'Combined', - input_descriptors: presentationDefinitions.flatMap((p) => p.input_descriptors), - } -} - -// biome-ignore lint/suspicious/noExplicitAny: -export function waitForMockFunction(mockFn: jest.Mock) { - return new Promise((resolve, reject) => { - const intervalId = setInterval(() => { - if (mockFn.mock.calls.length > 0) { - clearInterval(intervalId) - resolve(0) - } - }, 100) - - setTimeout(() => { - clearInterval(intervalId) - reject(new Error('Timeout Callback')) - }, 10000) - }) -} diff --git a/packages/question-answer/src/__tests__/QuestionAnswerService.test.ts b/packages/question-answer/src/__tests__/QuestionAnswerService.test.ts index 59710c4920..7d2d552045 100644 --- a/packages/question-answer/src/__tests__/QuestionAnswerService.test.ts +++ b/packages/question-answer/src/__tests__/QuestionAnswerService.test.ts @@ -1,4 +1,4 @@ -import type { AgentConfig, AgentContext, Repository, Wallet } from '@credo-ts/core' +import type { AgentConfig, AgentContext, Repository } from '@credo-ts/core' import type { QuestionAnswerStateChangedEvent, ValidResponse } from '@credo-ts/question-answer' import { EventEmitter } from '@credo-ts/core' @@ -6,7 +6,6 @@ import { DidExchangeState, InboundMessageContext } from '@credo-ts/didcomm' import { agentDependencies } from '@credo-ts/node' import { Subject } from 'rxjs' -import { InMemoryWallet } from '../../../../tests/InMemoryWallet' import { getAgentConfig, getAgentContext, getMockConnection, mockFunction } from '../../../core/tests/helpers' import { @@ -19,6 +18,7 @@ import { QuestionAnswerState, QuestionMessage, } from '@credo-ts/question-answer' +import { InMemoryStorageService } from '../../../../tests/InMemoryStorageService' jest.mock('../repository/QuestionAnswerRepository') const QuestionAnswerRepositoryMock = QuestionAnswerRepository as jest.Mock @@ -30,7 +30,6 @@ describe('QuestionAnswerService', () => { state: DidExchangeState.Completed, }) - let wallet: Wallet let agentConfig: AgentConfig let questionAnswerRepository: Repository let questionAnswerService: QuestionAnswerService @@ -61,10 +60,9 @@ describe('QuestionAnswerService', () => { beforeAll(async () => { agentConfig = getAgentConfig('QuestionAnswerServiceTest') - wallet = new InMemoryWallet() - agentContext = getAgentContext() - // biome-ignore lint/style/noNonNullAssertion: - await wallet.createAndOpen(agentConfig.walletConfig!) + agentContext = getAgentContext({ + registerInstances: [[InMemoryStorageService, new InMemoryStorageService()]], + }) }) beforeEach(async () => { @@ -73,10 +71,6 @@ describe('QuestionAnswerService', () => { questionAnswerService = new QuestionAnswerService(questionAnswerRepository, eventEmitter, agentConfig.logger) }) - afterAll(async () => { - await wallet.delete() - }) - describe('create question', () => { it('emits a question with question text, valid responses, and question answer record', async () => { const eventListenerMock = jest.fn() diff --git a/packages/question-answer/tests/question-answer.test.ts b/packages/question-answer/tests/question-answer.test.ts index d469dff7ba..046a0e8674 100644 --- a/packages/question-answer/tests/question-answer.test.ts +++ b/packages/question-answer/tests/question-answer.test.ts @@ -2,7 +2,7 @@ import type { ConnectionRecord } from '@credo-ts/didcomm' import { Agent } from '@credo-ts/core' -import { getInMemoryAgentOptions, makeConnection, setupSubjectTransports, testLogger } from '../../core/tests' +import { getAgentOptions, makeConnection, setupSubjectTransports, testLogger } from '../../core/tests' import { waitForQuestionAnswerRecord } from './helpers' @@ -12,22 +12,24 @@ const modules = { questionAnswer: new QuestionAnswerModule(), } -const bobAgentOptions = getInMemoryAgentOptions( +const bobAgentOptions = getAgentOptions( 'Bob Question Answer', { endpoints: ['rxjs:bob'], }, {}, - modules + modules, + { requireDidcomm: true } ) -const aliceAgentOptions = getInMemoryAgentOptions( +const aliceAgentOptions = getAgentOptions( 'Alice Question Answer', { endpoints: ['rxjs:alice'], }, {}, - modules + modules, + { requireDidcomm: true } ) describe('Question Answer', () => { @@ -47,9 +49,7 @@ describe('Question Answer', () => { afterEach(async () => { await bobAgent.shutdown() - await bobAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice sends a question and Bob answers', async () => { diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 7e10a697ac..1d7b63dd2e 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -38,6 +38,12 @@ "peerDependencies": { "react-native": ">=0.71.4", "react-native-fs": "^2.20.0", - "react-native-get-random-values": "^1.8.0" + "react-native-get-random-values": "^1.8.0", + "@animo-id/expo-secure-environment": "^0.1.1" + }, + "peerDependenciesMeta": { + "@animo-id/expo-secure-environment": { + "optional": true + } } } diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index d1da43a1ce..f92ed0a30e 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -7,6 +7,8 @@ import { EventEmitter } from 'events' import { ReactNativeFileSystem } from './ReactNativeFileSystem' +export { SecureEnvironmentKeyManagementService } from './kms/SecureEnvironmentKeyManagementService' + const fetch = global.fetch as unknown as AgentDependencies['fetch'] const WebSocket = global.WebSocket as unknown as AgentDependencies['WebSocketClass'] diff --git a/packages/react-native/src/kms/SecureEnvironmentKeyManagementService.ts b/packages/react-native/src/kms/SecureEnvironmentKeyManagementService.ts new file mode 100644 index 0000000000..c9f50c0080 --- /dev/null +++ b/packages/react-native/src/kms/SecureEnvironmentKeyManagementService.ts @@ -0,0 +1,161 @@ +import type { AgentContext } from '@credo-ts/core' + +import { Kms, utils } from '@credo-ts/core' + +import { importSecureEnvironment } from './secureEnvironment' + +export class SecureEnvironmentKeyManagementService implements Kms.KeyManagementService { + public readonly backend = 'secureEnvironment' + private readonly secureEnvironment = importSecureEnvironment() + + public isOperationSupported(_agentContext: AgentContext, operation: Kms.KmsOperation): boolean { + if (operation.operation === 'createKey') { + return operation.type.kty === 'EC' && operation.type.crv === 'P-256' + } + + if (operation.operation === 'sign') { + return operation.algorithm === 'ES256' + } + + if (operation.operation === 'deleteKey') { + return true + } + + return false + } + + public randomBytes(_agentContext: AgentContext, _options: Kms.KmsRandomBytesOptions): Kms.KmsRandomBytesReturn { + throw new Kms.KeyManagementError(`Generating random bytes is not supported for backend '${this.backend}'`) + } + + public async getPublicKey(_agentContext: AgentContext, keyId: string): Promise { + try { + return await this.getKeyAsserted(keyId) + } catch (error) { + if (error instanceof Kms.KeyManagementKeyNotFoundError) return null + throw error + } + } + + public async importKey(): Promise> { + throw new Kms.KeyManagementError(`Importing a key is not supported for backend '${this.backend}'`) + } + + public async deleteKey(_agentContext: AgentContext, options: Kms.KmsDeleteKeyOptions): Promise { + try { + await this.secureEnvironment.deleteKey(options.keyId) + return true + } catch (error) { + if (error instanceof this.secureEnvironment.KeyNotFoundError) { + return false + } + + throw new Kms.KeyManagementError(`Error deleting key with id '${options.keyId}' in backend '${this.backend}'`, { + cause: error, + }) + } + } + + public async encrypt(): Promise { + throw new Kms.KeyManagementError(`Encryption is not supported for backend '${this.backend}'`) + } + + public async decrypt(): Promise { + throw new Kms.KeyManagementError(`Decryption is not supported for backend '${this.backend}'`) + } + + public async createKey( + _agentContext: AgentContext, + options: Kms.KmsCreateKeyOptions + ): Promise { + if (options.type.kty !== 'EC') { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `kty ${options.type.kty}. Only EC P-256 supported.`, + this.backend + ) + } + if (options.type.crv !== 'P-256') { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `kty ${options.type.kty} with crv ${options.type.crv}. Only EC P-256 supported.`, + this.backend + ) + } + + const keyId = options.keyId ?? utils.uuid() + + try { + await this.secureEnvironment.generateKeypair(keyId) + + return { + keyId, + publicJwk: await this.getKeyAsserted(keyId), + } + } catch (error) { + if (error instanceof Kms.KeyManagementError) throw error + if (error instanceof this.secureEnvironment.KeyAlreadyExistsError) { + throw new Kms.KeyManagementKeyExistsError(keyId, this.backend) + } + + throw new Kms.KeyManagementError('Error creating key', { cause: error }) + } + } + + public async sign(_agentContext: AgentContext, options: Kms.KmsSignOptions): Promise { + if (options.algorithm !== 'ES256') { + throw new Kms.KeyManagementAlgorithmNotSupportedError( + `algorithm '${options.algorithm}'. Only 'ES256' supported.`, + this.backend + ) + } + + try { + // TODO: can we store something like 'use' for the key in secure environment? + // Kms.assertKeyAllowsSign(publicJwk) + + // Perform the signing operation + const signature = await this.secureEnvironment.sign(options.keyId, options.data) + + return { + signature, + } + } catch (error) { + if (error instanceof this.secureEnvironment.KeyNotFoundError) { + throw new Kms.KeyManagementKeyNotFoundError(options.keyId, this.backend) + } + + throw new Kms.KeyManagementError('Error signing with key', { cause: error }) + } + } + + public async verify(): Promise { + throw new Kms.KeyManagementError(`verification of signatures is not supported for backend '${this.backend}'`) + } + + private publicJwkFromPublicKeyBytes(key: Uint8Array, keyId: string) { + const publicJwk = Kms.PublicJwk.fromPublicKey({ + kty: 'EC', + crv: 'P-256', + publicKey: key, + }).toJson() + + return { + ...publicJwk, + kid: keyId, + } satisfies Kms.KmsJwkPublicEc + } + + private async getKeyAsserted(keyId: string) { + try { + const publicKeyBytes = await this.secureEnvironment.getPublicBytesForKeyId(keyId) + return this.publicJwkFromPublicKeyBytes(publicKeyBytes, keyId) + } catch (error) { + if (error instanceof this.secureEnvironment.KeyNotFoundError) { + throw new Kms.KeyManagementKeyNotFoundError(keyId, this.backend) + } + + throw new Kms.KeyManagementError(`Error retrieving key with id '${keyId}' from backend ${this.backend}`, { + cause: error, + }) + } + } +} diff --git a/packages/react-native/src/kms/secureEnvironment.ts b/packages/react-native/src/kms/secureEnvironment.ts new file mode 100644 index 0000000000..29fbb3238e --- /dev/null +++ b/packages/react-native/src/kms/secureEnvironment.ts @@ -0,0 +1,15 @@ +export function importSecureEnvironment(): { + sign: (id: string, message: Uint8Array) => Promise + getPublicBytesForKeyId: (id: string) => Promise + generateKeypair: (id: string) => Promise + deleteKey: (id: string) => Promise + KeyAlreadyExistsError: typeof Error + KeyNotFoundError: typeof Error +} { + try { + const secureEnvironment = require('@animo-id/expo-secure-environment') + return secureEnvironment + } catch (_error) { + throw new Error('@animo-id/expo-secure-environment must be installed as a peer dependency') + } +} diff --git a/packages/bbs-signatures/jest.config.ts b/packages/redis-cache-nodejs/jest.config.ts similarity index 84% rename from packages/bbs-signatures/jest.config.ts rename to packages/redis-cache-nodejs/jest.config.ts index 8641cf4d67..2556d19c61 100644 --- a/packages/bbs-signatures/jest.config.ts +++ b/packages/redis-cache-nodejs/jest.config.ts @@ -6,9 +6,7 @@ import packageJson from './package.json' const config: Config.InitialOptions = { ...base, - displayName: packageJson.name, - setupFilesAfterEnv: ['./tests/setup.ts'], } export default config diff --git a/packages/bbs-signatures/package.json b/packages/redis-cache-nodejs/package.json similarity index 53% rename from packages/bbs-signatures/package.json rename to packages/redis-cache-nodejs/package.json index 5bb688825a..f4bfd91f56 100644 --- a/packages/bbs-signatures/package.json +++ b/packages/redis-cache-nodejs/package.json @@ -1,5 +1,5 @@ { - "name": "@credo-ts/bbs-signatures", + "name": "@credo-ts/redis-cache-nodejs", "main": "src/index", "types": "src/index", "version": "0.5.13", @@ -10,11 +10,11 @@ "types": "build/index", "access": "public" }, - "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/bbs-signatures", + "homepage": "https://github.com/openwallet-foundation/credo-ts/tree/main/packages/redis-cache-nodejs", "repository": { "type": "git", "url": "https://github.com/openwallet-foundation/credo-ts", - "directory": "packages/bbs-signatures" + "directory": "packages/redis-cache-nodejs" }, "scripts": { "build": "pnpm run clean && pnpm run compile", @@ -25,22 +25,7 @@ }, "dependencies": { "@credo-ts/core": "workspace:*", - "@mattrglobal/bbs-signatures": "^1.0.0", - "@mattrglobal/bls12381-key-pair": "^1.0.0", - "@stablelib/random": "^1.0.2" - }, - "peerDependencies": { - "@animo-id/react-native-bbs-signatures": "^0.1.0" - }, - "devDependencies": { - "@credo-ts/node": "workspace:*", - "reflect-metadata": "^0.1.13", - "rimraf": "^4.4.0", - "typescript": "~5.5.2" - }, - "peerDependenciesMeta": { - "@animo-id/react-native-bbs-signatures": { - "optional": true - } + "ioredis": "^5.6.1", + "redis": "^5.0.1" } } diff --git a/packages/redis-cache-nodejs/src/RedisCache.ts b/packages/redis-cache-nodejs/src/RedisCache.ts new file mode 100644 index 0000000000..3dff975b4c --- /dev/null +++ b/packages/redis-cache-nodejs/src/RedisCache.ts @@ -0,0 +1,99 @@ +import { AgentContext, Cache, CacheModuleConfig } from '@credo-ts/core' +import Redis, { RedisOptions } from 'ioredis' + +export type RedisCacheOptions = RedisOptions + +export class RedisCache implements Cache { + private readonly _client: Redis + + constructor(options: RedisCacheOptions = {}) { + this._client = new Redis(options) + } + + private async client() { + try { + await this._client.ping() + return this._client + } catch { + await this._client.connect() + return this._client + } + } + + private getNamespacedKey(agentContext: AgentContext, key: string): string { + return `${agentContext.contextCorrelationId}:${key}` + } + + private serialize(value: CacheValue): string { + return JSON.stringify(value) + } + + private deserialize(value: string | null): CacheValue | null { + return value === null ? value : (JSON.parse(value) as CacheValue) + } + + private getDefaultExpiryInSeconds(agentContext: AgentContext) { + try { + return agentContext.resolve(CacheModuleConfig).defaultExpiryInSeconds + } catch { + return undefined + } + } + + public async get(agentContext: AgentContext, key: string): Promise { + const client = await this.client() + const namespacedKey = this.getNamespacedKey(agentContext, key) + const value = await client.get(namespacedKey) + return this.deserialize(value) + } + + public async set( + agentContext: AgentContext, + key: string, + value: CacheValue, + expiresInSeconds: number | undefined = this.getDefaultExpiryInSeconds(agentContext) + ): Promise { + const client = await this.client() + const namespacedKey = this.getNamespacedKey(agentContext, key) + const serializedValue = this.serialize(value) + + if (expiresInSeconds) { + await client.set(namespacedKey, serializedValue, 'EX', expiresInSeconds) + } else { + await client.set(namespacedKey, serializedValue) + } + } + + public async remove(agentContext: AgentContext, key: string): Promise { + const client = await this.client() + const namespacedKey = this.getNamespacedKey(agentContext, key) + await client.del(namespacedKey) + } + + public async destroy(agentContext: AgentContext) { + const client = await this.client() + await this.removeTenantKeys(agentContext) + client.disconnect() + } + + private async removeTenantKeys(agentContext: AgentContext): Promise { + const client = await this.client() + let cursor = '0' + + do { + const [nextCursor, keys] = await client.scan( + cursor, + 'MATCH', + `${agentContext.contextCorrelationId}:*`, + 'COUNT', + '100' + ) + + cursor = nextCursor + + if (keys.length > 0) { + await client.del(...keys) + } + } while (cursor !== '0') + } +} diff --git a/packages/redis-cache-nodejs/src/index.ts b/packages/redis-cache-nodejs/src/index.ts new file mode 100644 index 0000000000..5985639d92 --- /dev/null +++ b/packages/redis-cache-nodejs/src/index.ts @@ -0,0 +1 @@ +export { RedisCache, RedisCacheOptions } from './RedisCache' diff --git a/packages/redis-cache-nodejs/tests/redisCache.test.ts b/packages/redis-cache-nodejs/tests/redisCache.test.ts new file mode 100644 index 0000000000..3f906f5277 --- /dev/null +++ b/packages/redis-cache-nodejs/tests/redisCache.test.ts @@ -0,0 +1,73 @@ +import { getAgentContext } from '../../core/tests/helpers' +import { RedisCache } from '../src/' + +describe('RedisCache', () => { + const agentContext = getAgentContext() + const agentContextTwo = getAgentContext({ contextCorrelationId: 'abba' }) + let redisCache: RedisCache + + beforeAll(async () => { + redisCache = new RedisCache() + }) + + afterAll(async () => { + await redisCache.destroy(agentContext) + }) + + it('should initialize the redis cache', () => { + expect(redisCache).toBeDefined() + }) + + it('should set key "1" and value "one"', async () => { + await expect(redisCache.set(agentContext, '1', 'one')).resolves.toBeUndefined() + }) + + it('should get key "2" and value "two"', async () => { + await expect(redisCache.set(agentContext, '2', 'two')).resolves.toBeUndefined() + await expect(redisCache.get(agentContext, '2')).resolves.toStrictEqual('two') + }) + + it('should get key "3" and value "{ a: "b" }"', async () => { + await expect(redisCache.set(agentContext, '3', { a: 'b' })).resolves.toBeUndefined() + await expect(redisCache.get(agentContext, '3')).resolves.toEqual({ a: 'b' }) + }) + + it('should set key "4" and delete', async () => { + await expect(redisCache.set(agentContext, '4', 'a')).resolves.toBeUndefined() + await expect(redisCache.remove(agentContext, '4')).resolves.toBeUndefined() + await expect(redisCache.get(agentContext, '4')).resolves.toBeNull() + }) + + it('should set key "5" and delete after ttl', async () => { + await expect(redisCache.set(agentContext, '5', 'a', 2)).resolves.toBeUndefined() + await new Promise((r) => setTimeout(r, 2100)) + await expect(redisCache.get(agentContext, '5')).resolves.toBeNull() + }) + + it('should not get key "6" set by other agent', async () => { + await expect(redisCache.set(agentContext, '6', 'a')).resolves.toBeUndefined() + + await expect(redisCache.get(agentContextTwo, '6')).resolves.toBeNull() + }) + + it('should not remove all keys when agent is destoryed', async () => { + await expect(redisCache.set(agentContext, '7', 'a')).resolves.toBeUndefined() + await expect(redisCache.set(agentContext, '8', 'a')).resolves.toBeUndefined() + + await expect(redisCache.set(agentContextTwo, '7', 'a')).resolves.toBeUndefined() + await expect(redisCache.set(agentContextTwo, '8', 'a')).resolves.toBeUndefined() + + await redisCache.destroy(agentContext) + + await expect(redisCache.get(agentContext, '7')).resolves.toBeNull() + await expect(redisCache.get(agentContext, '8')).resolves.toBeNull() + + await expect(redisCache.get(agentContextTwo, '7')).resolves.toEqual('a') + await expect(redisCache.get(agentContextTwo, '8')).resolves.toEqual('a') + + await redisCache.destroy(agentContextTwo) + + await expect(redisCache.get(agentContextTwo, '7')).resolves.toBeNull() + await expect(redisCache.get(agentContextTwo, '8')).resolves.toBeNull() + }) +}) diff --git a/packages/bbs-signatures/tsconfig.build.json b/packages/redis-cache-nodejs/tsconfig.build.json similarity index 98% rename from packages/bbs-signatures/tsconfig.build.json rename to packages/redis-cache-nodejs/tsconfig.build.json index 9c30e30bd2..2b75d0adab 100644 --- a/packages/bbs-signatures/tsconfig.build.json +++ b/packages/redis-cache-nodejs/tsconfig.build.json @@ -1,9 +1,7 @@ { "extends": "../../tsconfig.build.json", - "compilerOptions": { "outDir": "./build" }, - "include": ["src/**/*"] } diff --git a/packages/bbs-signatures/tsconfig.json b/packages/redis-cache-nodejs/tsconfig.json similarity index 58% rename from packages/bbs-signatures/tsconfig.json rename to packages/redis-cache-nodejs/tsconfig.json index 46efe6f721..c1aca0e050 100644 --- a/packages/bbs-signatures/tsconfig.json +++ b/packages/redis-cache-nodejs/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "types": ["jest"] + "types": ["jest"], + "skipLibCheck": true } } diff --git a/packages/tenants/src/TenantAgent.ts b/packages/tenants/src/TenantAgent.ts index 535e018333..a521970d0c 100644 --- a/packages/tenants/src/TenantAgent.ts +++ b/packages/tenants/src/TenantAgent.ts @@ -14,7 +14,6 @@ export class TenantAgent throw new CredoError("Can't initialize agent after tenant sessions has been ended.") } - await super.initialize() this._isInitialized = true } diff --git a/packages/tenants/src/TenantsApi.ts b/packages/tenants/src/TenantsApi.ts index 4484c7b60f..867b1b2b50 100644 --- a/packages/tenants/src/TenantsApi.ts +++ b/packages/tenants/src/TenantsApi.ts @@ -41,15 +41,7 @@ export class TenantsApi { } public async getTenantAgent({ tenantId }: GetTenantAgentOptions): Promise> { - this.logger.debug(`Getting tenant agent for tenant '${tenantId}'`) - const tenantContext = await this.agentContextProvider.getAgentContextForContextCorrelationId(tenantId) - - this.logger.trace(`Got tenant context for tenant '${tenantId}'`) - const tenantAgent = new TenantAgent(tenantContext) - await tenantAgent.initialize() - this.logger.trace(`Initializing tenant agent for tenant '${tenantId}'`) - - return tenantAgent + return this._getTenantAgent({ tenantId }) } public async withTenantAgent( @@ -74,10 +66,15 @@ export class TenantsApi { public async createTenant(options: CreateTenantOptions) { this.logger.debug(`Creating tenant with label ${options.config.label}`) + const tenantRecord = await this.tenantRecordService.createTenant(this.rootAgentContext, options.config) // This initializes the tenant agent, creates the wallet etc... - const tenantAgent = await this.getTenantAgent({ tenantId: tenantRecord.id }) + const tenantAgent = await this._getTenantAgent({ + tenantId: tenantRecord.id, + // When creating a tenant we need to provision the context + provisionContext: true, + }) await tenantAgent.endSession() this.logger.info(`Successfully created tenant '${tenantRecord.id}'`) @@ -97,13 +94,12 @@ export class TenantsApi { public async deleteTenantById(tenantId: string) { this.logger.debug(`Deleting tenant by id '${tenantId}'`) - // TODO: force remove context from the context provider (or session manager) const tenantAgent = await this.getTenantAgent({ tenantId }) this.logger.trace(`Deleting wallet for tenant '${tenantId}'`) - await tenantAgent.wallet.delete() - this.logger.trace(`Shutting down agent for tenant '${tenantId}'`) - await tenantAgent.endSession() + + // Deleting agent context will also end the session since there is no session anymore if the agent context is deleted + await this.agentContextProvider.deleteAgentContext(tenantAgent.context) return this.tenantRecordService.deleteTenantById(this.rootAgentContext, tenantId) } @@ -142,4 +138,23 @@ export class TenantsApi { return outdatedTenants } + + private async _getTenantAgent({ + tenantId, + provisionContext = false, + }: GetTenantAgentOptions & { provisionContext?: boolean }): Promise> { + this.logger.debug(`Getting tenant agent for tenant '${tenantId}'`) + const tenantContext = await this.agentContextProvider.getAgentContextForContextCorrelationId( + this.agentContextProvider.getContextCorrelationIdForTenantId(tenantId), + { provisionContext } + ) + + this.logger.trace(`Got tenant context for tenant '${tenantId}'`) + const tenantAgent = new TenantAgent(tenantContext) + + await tenantAgent.initialize() + this.logger.trace(`Initializing tenant agent for tenant '${tenantId}'`) + + return tenantAgent + } } diff --git a/packages/tenants/src/TenantsApiOptions.ts b/packages/tenants/src/TenantsApiOptions.ts index a422228f67..1efa864133 100644 --- a/packages/tenants/src/TenantsApiOptions.ts +++ b/packages/tenants/src/TenantsApiOptions.ts @@ -11,7 +11,7 @@ export type WithTenantAgentCallback = ( ) => Promise export interface CreateTenantOptions { - config: Omit + config: TenantConfig } export interface UpdateTenantStorageOptions { diff --git a/packages/tenants/src/__tests__/TenantAgent.test.ts b/packages/tenants/src/__tests__/TenantAgent.test.ts index 6989bfc47c..2ac45b2d1b 100644 --- a/packages/tenants/src/__tests__/TenantAgent.test.ts +++ b/packages/tenants/src/__tests__/TenantAgent.test.ts @@ -1,11 +1,11 @@ import { Agent, AgentContext } from '@credo-ts/core' -import { getAgentConfig, getAgentContext, getInMemoryAgentOptions } from '../../../core/tests/helpers' +import { getAgentConfig, getAgentContext, getAgentOptions } from '../../../core/tests/helpers' import { TenantAgent } from '../TenantAgent' describe('TenantAgent', () => { test('possible to construct a TenantAgent instance', () => { - const agent = new Agent(getInMemoryAgentOptions('TenantAgentRoot')) + const agent = new Agent(getAgentOptions('TenantAgentRoot')) const tenantDependencyManager = agent.dependencyManager.createChild() diff --git a/packages/tenants/src/__tests__/TenantsApi.test.ts b/packages/tenants/src/__tests__/TenantsApi.test.ts index 2943f4ab8f..03e6617394 100644 --- a/packages/tenants/src/__tests__/TenantsApi.test.ts +++ b/packages/tenants/src/__tests__/TenantsApi.test.ts @@ -1,6 +1,6 @@ import { Agent, AgentContext, InjectionSymbols } from '@credo-ts/core' -import { getAgentContext, getInMemoryAgentOptions, mockFunction } from '../../../core/tests' +import { getAgentContext, getAgentOptions, mockFunction } from '../../../core/tests' import { TenantAgent } from '../TenantAgent' import { TenantsApi } from '../TenantsApi' import { TenantAgentContextProvider } from '../context/TenantAgentContextProvider' @@ -15,7 +15,8 @@ const AgentContextProviderMock = TenantAgentContextProvider as jest.Mock `tenant-${tenantId}` +const agentOptions = getAgentOptions('TenantsApi', undefined, { autoUpdateStorageOnStartup: true }) const rootAgent = new Agent(agentOptions) rootAgent.dependencyManager.registerInstance(InjectionSymbols.AgentContextProvider, agentContextProvider) @@ -30,10 +31,6 @@ describe('TenantsApi', () => { dependencyManager: tenantDependencyManager, agentConfig: rootAgent.config.extend({ label: 'tenant-agent', - walletConfig: { - id: 'Wallet: TenantsApi: tenant-id', - key: 'Wallet: TenantsApi: tenant-id', - }, }), }) tenantDependencyManager.registerInstance(AgentContext, tenantAgentContext) @@ -43,16 +40,14 @@ describe('TenantsApi', () => { const tenantAgent = await tenantsApi.getTenantAgent({ tenantId: 'tenant-id' }) expect(tenantAgent.isInitialized).toBe(true) - expect(tenantAgent.wallet.walletConfig).toEqual({ - id: 'Wallet: TenantsApi: tenant-id', - key: 'Wallet: TenantsApi: tenant-id', - }) + expect(tenantAgent.config.label).toEqual('tenant-agent') - expect(agentContextProvider.getAgentContextForContextCorrelationId).toHaveBeenCalledWith('tenant-id') + expect(agentContextProvider.getAgentContextForContextCorrelationId).toHaveBeenCalledWith('tenant-tenant-id', { + provisionContext: false, + }) expect(tenantAgent).toBeInstanceOf(TenantAgent) expect(tenantAgent.context).toBe(tenantAgentContext) - await tenantAgent.wallet.delete() await tenantAgent.endSession() }) }) @@ -67,10 +62,6 @@ describe('TenantsApi', () => { dependencyManager: tenantDependencyManager, agentConfig: rootAgent.config.extend({ label: 'tenant-agent', - walletConfig: { - id: 'Wallet: TenantsApi: tenant-id', - key: 'Wallet: TenantsApi: tenant-id', - }, }), }) tenantDependencyManager.registerInstance(AgentContext, tenantAgentContext) @@ -81,16 +72,13 @@ describe('TenantsApi', () => { await tenantsApi.withTenantAgent({ tenantId: 'tenant-id' }, async (tenantAgent) => { endSessionSpy = jest.spyOn(tenantAgent, 'endSession') expect(tenantAgent.isInitialized).toBe(true) - expect(tenantAgent.wallet.walletConfig).toEqual({ - id: 'Wallet: TenantsApi: tenant-id', - key: 'Wallet: TenantsApi: tenant-id', - }) + expect(tenantAgent.config.label).toEqual('tenant-agent') - expect(agentContextProvider.getAgentContextForContextCorrelationId).toHaveBeenCalledWith('tenant-id') + expect(agentContextProvider.getAgentContextForContextCorrelationId).toHaveBeenCalledWith('tenant-tenant-id', { + provisionContext: false, + }) expect(tenantAgent).toBeInstanceOf(TenantAgent) expect(tenantAgent.context).toBe(tenantAgentContext) - - await tenantAgent.wallet.delete() }) expect(endSessionSpy).toHaveBeenCalled() @@ -105,10 +93,6 @@ describe('TenantsApi', () => { dependencyManager: tenantDependencyManager, agentConfig: rootAgent.config.extend({ label: 'tenant-agent', - walletConfig: { - id: 'Wallet: TenantsApi: tenant-id', - key: 'Wallet: TenantsApi: tenant-id', - }, }), }) tenantDependencyManager.registerInstance(AgentContext, tenantAgentContext) @@ -120,17 +104,14 @@ describe('TenantsApi', () => { tenantsApi.withTenantAgent({ tenantId: 'tenant-id' }, async (tenantAgent) => { endSessionSpy = jest.spyOn(tenantAgent, 'endSession') expect(tenantAgent.isInitialized).toBe(true) - expect(tenantAgent.wallet.walletConfig).toEqual({ - id: 'Wallet: TenantsApi: tenant-id', - key: 'Wallet: TenantsApi: tenant-id', - }) + expect(tenantAgent.config.label).toEqual('tenant-agent') - expect(agentContextProvider.getAgentContextForContextCorrelationId).toHaveBeenCalledWith('tenant-id') + expect(agentContextProvider.getAgentContextForContextCorrelationId).toHaveBeenCalledWith('tenant-tenant-id', { + provisionContext: false, + }) expect(tenantAgent).toBeInstanceOf(TenantAgent) expect(tenantAgent.context).toBe(tenantAgentContext) - await tenantAgent.wallet.delete() - throw new Error('Uh oh something went wrong') }) ).rejects.toThrow('Uh oh something went wrong') @@ -146,23 +127,18 @@ describe('TenantsApi', () => { id: 'tenant-id', config: { label: 'test', - walletConfig: { - id: 'Wallet: TenantsApi: tenant-id', - key: 'Wallet: TenantsApi: tenant-id', - }, }, storageVersion: '0.5', }) const tenantAgentMock = { - wallet: { - delete: jest.fn(), - }, endSession: jest.fn(), } as unknown as TenantAgent mockFunction(tenantRecordService.createTenant).mockResolvedValue(tenantRecord) - const getTenantAgentSpy = jest.spyOn(tenantsApi, 'getTenantAgent').mockResolvedValue(tenantAgentMock) + + // @ts-ignore + const getTenantAgentSpy = jest.spyOn(tenantsApi, '_getTenantAgent').mockResolvedValue(tenantAgentMock) const createdTenantRecord = await tenantsApi.createTenant({ config: { @@ -170,7 +146,7 @@ describe('TenantsApi', () => { }, }) - expect(getTenantAgentSpy).toHaveBeenCalledWith({ tenantId: 'tenant-id' }) + expect(getTenantAgentSpy).toHaveBeenCalledWith({ tenantId: 'tenant-id', provisionContext: true }) expect(createdTenantRecord).toBe(tenantRecord) expect(tenantAgentMock.endSession).toHaveBeenCalled() expect(tenantRecordService.createTenant).toHaveBeenCalledWith(rootAgent.context, { @@ -194,18 +170,19 @@ describe('TenantsApi', () => { describe('deleteTenantById', () => { test('deletes the tenant and removes the wallet', async () => { const tenantAgentMock = { - wallet: { - delete: jest.fn(), - }, endSession: jest.fn(), + context: { + dependencyManager: { + deleteAgentContext: jest.fn(), + }, + }, } as unknown as TenantAgent const getTenantAgentSpy = jest.spyOn(tenantsApi, 'getTenantAgent').mockResolvedValue(tenantAgentMock) await tenantsApi.deleteTenantById('tenant-id') expect(getTenantAgentSpy).toHaveBeenCalledWith({ tenantId: 'tenant-id' }) - expect(tenantAgentMock.wallet.delete).toHaveBeenCalled() - expect(tenantAgentMock.endSession).toHaveBeenCalled() + expect(agentContextProvider.deleteAgentContext).toHaveBeenCalled() expect(tenantRecordService.deleteTenantById).toHaveBeenCalledWith(rootAgent.context, 'tenant-id') }) }) diff --git a/packages/tenants/src/context/TenantAgentContextProvider.ts b/packages/tenants/src/context/TenantAgentContextProvider.ts index 34c3ea224b..d283669596 100644 --- a/packages/tenants/src/context/TenantAgentContextProvider.ts +++ b/packages/tenants/src/context/TenantAgentContextProvider.ts @@ -1,4 +1,4 @@ -import type { AgentContextProvider, UpdateAssistantUpdateOptions } from '@credo-ts/core' +import { AgentContextProvider, Kms, TypedArrayEncoder, UpdateAssistantUpdateOptions } from '@credo-ts/core' import type { EncryptedMessage, RoutingCreatedEvent } from '@credo-ts/didcomm' import type { TenantRecord } from '../repository' @@ -8,8 +8,6 @@ import { EventEmitter, InjectionSymbols, JsonEncoder, - Key, - KeyType, Logger, UpdateAssistant, inject, @@ -49,14 +47,25 @@ export class TenantAgentContextProvider implements AgentContextProvider { this.listenForRoutingKeyCreatedEvents() } - public async getAgentContextForContextCorrelationId(contextCorrelationId: string) { + public getContextCorrelationIdForTenantId(tenantId: string) { + return this.tenantSessionCoordinator.getContextCorrelationIdForTenantId(tenantId) + } + + public async getAgentContextForContextCorrelationId( + contextCorrelationId: string, + { provisionContext = false }: { provisionContext?: boolean } = {} + ) { // It could be that the root agent context is requested, in that case we return the root agent context if (contextCorrelationId === this.rootAgentContext.contextCorrelationId) { return this.rootAgentContext } + // If not the root agent context, we require it to be a tenant context correlation id + this.tenantSessionCoordinator.assertTenantContextCorrelationId(contextCorrelationId) + const tenantId = this.tenantSessionCoordinator.getTenantIdForContextCorrelationId(contextCorrelationId) + // TODO: maybe we can look at not having to retrieve the tenant record if there's already a context available. - const tenantRecord = await this.tenantRecordService.getTenantById(this.rootAgentContext, contextCorrelationId) + const tenantRecord = await this.tenantRecordService.getTenantById(this.rootAgentContext, tenantId) const shouldUpdate = !isStorageUpToDate(tenantRecord.storageVersion) // If the tenant storage is not up to date, and autoUpdate is disabled we throw an error @@ -67,10 +76,11 @@ export class TenantAgentContextProvider implements AgentContextProvider { } const agentContext = await this.tenantSessionCoordinator.getContextForSession(tenantRecord, { + provisionContext, runInMutex: shouldUpdate ? (agentContext) => this._updateTenantStorage(tenantRecord, agentContext) : undefined, }) - this.logger.debug(`Created tenant agent context for tenant '${contextCorrelationId}'`) + this.logger.debug(`Created tenant agent context for context correlation id '${contextCorrelationId}'`) return agentContext } @@ -80,8 +90,13 @@ export class TenantAgentContextProvider implements AgentContextProvider { contextCorrelationId: options?.contextCorrelationId, }) - let tenantId = options?.contextCorrelationId - let recipientKeys: Key[] = [] + // TODO: what if context is for root agent context? + let tenantId = + options?.contextCorrelationId && + this.tenantSessionCoordinator.isTenantContextCorrelationId(options.contextCorrelationId) + ? this.tenantSessionCoordinator.getTenantIdForContextCorrelationId(options.contextCorrelationId) + : undefined + let recipientKeys: Kms.PublicJwk[] = [] if (!tenantId && isValidJweStructure(inboundMessage)) { this.logger.trace("Inbound message is a JWE, extracting tenant id from JWE's protected header") @@ -116,7 +131,8 @@ export class TenantAgentContextProvider implements AgentContextProvider { throw new CredoError("Couldn't determine tenant id for inbound message. Unable to create context") } - const agentContext = await this.getAgentContextForContextCorrelationId(tenantId) + const contextCorrelationId = this.tenantSessionCoordinator.getContextCorrelationIdForTenantId(tenantId) + const agentContext = await this.getAgentContextForContextCorrelationId(contextCorrelationId) return agentContext } @@ -125,26 +141,35 @@ export class TenantAgentContextProvider implements AgentContextProvider { await this.tenantSessionCoordinator.endAgentContextSession(agentContext) } - private getRecipientKeysFromEncryptedMessage(jwe: EncryptedMessage): Key[] { + public async deleteAgentContext(agentContext: AgentContext): Promise { + await this.tenantSessionCoordinator.deleteAgentContext(agentContext) + } + + private getRecipientKeysFromEncryptedMessage(jwe: EncryptedMessage): Kms.PublicJwk[] { const jweProtected = JsonEncoder.fromBase64(jwe.protected) if (!Array.isArray(jweProtected.recipients)) return [] - const recipientKeys: Key[] = [] + const recipientKeys: Kms.PublicJwk[] = [] for (const recipient of jweProtected.recipients) { // Check if recipient.header.kid is a string if (isJsonObject(recipient) && isJsonObject(recipient.header) && typeof recipient.header.kid === 'string') { // This won't work with other key types, we should detect what the encoding is of kid, and based on that // determine how we extract the key from the message - const key = Key.fromPublicKeyBase58(recipient.header.kid, KeyType.Ed25519) - recipientKeys.push(key) + const publicJwk = Kms.PublicJwk.fromPublicKey({ + crv: 'Ed25519', + kty: 'OKP', + publicKey: TypedArrayEncoder.fromBase58(recipient.header.kid), + }) + + recipientKeys.push(publicJwk) } } return recipientKeys } - private async registerRecipientKeyForTenant(tenantId: string, recipientKey: Key) { + private async registerRecipientKeyForTenant(tenantId: string, recipientKey: Kms.PublicJwk) { this.logger.debug(`Registering recipient key ${recipientKey.fingerprint} for tenant ${tenantId}`) const tenantRecord = await this.tenantRecordService.getTenantById(this.rootAgentContext, tenantId) await this.tenantRecordService.addTenantRoutingRecord(this.rootAgentContext, tenantRecord.id, recipientKey) @@ -159,10 +184,15 @@ export class TenantAgentContextProvider implements AgentContextProvider { // We don't want to register the key if it's for the root agent context if (contextCorrelationId === this.rootAgentContext.contextCorrelationId) return + this.tenantSessionCoordinator.assertTenantContextCorrelationId(contextCorrelationId) + this.logger.debug( - `Received routing key created event for tenant ${contextCorrelationId}, registering recipient key ${recipientKey.fingerprint} in base wallet` + `Received routing key created event for tenant context ${contextCorrelationId}, registering recipient key ${recipientKey.fingerprint} in base wallet` + ) + await this.registerRecipientKeyForTenant( + this.tenantSessionCoordinator.getTenantIdForContextCorrelationId(contextCorrelationId), + recipientKey ) - await this.registerRecipientKeyForTenant(contextCorrelationId, recipientKey) }) } @@ -204,11 +234,7 @@ export class TenantAgentContextProvider implements AgentContextProvider { const tenantAgent = new TenantAgent(agentContext) const updateAssistant = new UpdateAssistant(tenantAgent) await updateAssistant.initialize() - await updateAssistant.update({ - ...updateOptions, - backupBeforeStorageUpdate: - updateOptions?.backupBeforeStorageUpdate ?? agentContext.config.backupBeforeStorageUpdate, - }) + await updateAssistant.update(updateOptions) // Update the storage version in the tenant record tenantRecord.storageVersion = await updateAssistant.getCurrentAgentStorageVersion() diff --git a/packages/tenants/src/context/TenantSessionCoordinator.ts b/packages/tenants/src/context/TenantSessionCoordinator.ts index 8954d0e7a7..d214de53a3 100644 --- a/packages/tenants/src/context/TenantSessionCoordinator.ts +++ b/packages/tenants/src/context/TenantSessionCoordinator.ts @@ -1,17 +1,7 @@ import type { MutexInterface } from 'async-mutex' import type { TenantRecord } from '../repository' -import { - AgentConfig, - AgentContext, - CredoError, - InjectionSymbols, - Logger, - WalletApi, - WalletError, - inject, - injectable, -} from '@credo-ts/core' +import { AgentConfig, AgentContext, CredoError, InjectionSymbols, Logger, inject, injectable } from '@credo-ts/core' import { Mutex, withTimeout } from 'async-mutex' import { TenantsModuleConfig } from '../TenantsModuleConfig' @@ -55,7 +45,8 @@ export class TenantSessionCoordinator { } public getSessionCountForTenant(tenantId: string) { - return this.tenantAgentContextMapping[tenantId]?.sessionCount ?? 0 + const contextCorrelationId = this.getContextCorrelationIdForTenantId(tenantId) + return this.tenantAgentContextMapping[contextCorrelationId]?.sessionCount ?? 0 } /** @@ -69,9 +60,11 @@ export class TenantSessionCoordinator { tenantRecord: TenantRecord, { runInMutex, + provisionContext = false, }: { /** optional callback that will be run inside the mutex lock */ runInMutex?: (agentContext: AgentContext) => Promise + provisionContext?: boolean } = {} ): Promise { this.logger.debug(`Getting context for session with tenant '${tenantRecord.id}'`) @@ -80,14 +73,15 @@ export class TenantSessionCoordinator { await this.sessionMutex.acquireSession() try { - return await this.mutexForTenant(tenantRecord.id).runExclusive(async () => { + const contextCorrelationId = this.getContextCorrelationIdForTenantId(tenantRecord.id) + return await this.mutexForTenant(contextCorrelationId).runExclusive(async () => { this.logger.debug(`Acquired lock for tenant '${tenantRecord.id}' to get context`) - const tenantSessions = this.getTenantSessionsMapping(tenantRecord.id) + const tenantSessions = this.getTenantSessionsMapping(contextCorrelationId) // If we don't have an agent context already, create one and initialize it if (!tenantSessions.agentContext) { this.logger.debug(`No agent context has been initialized for tenant '${tenantRecord.id}', creating one`) - tenantSessions.agentContext = await this.createAgentContext(tenantRecord) + tenantSessions.agentContext = await this.createAgentContext(tenantRecord, { provisionContext }) } // If we already have a context with sessions in place return the context and increment @@ -104,13 +98,13 @@ export class TenantSessionCoordinator { // If the runInMutex failed we should release the session again tenantSessions.sessionCount-- this.logger.debug( - `Decreased agent context session count for tenant '${tenantSessions.agentContext.contextCorrelationId}' to ${tenantSessions.sessionCount} due to failure in mutex script`, + `Decreased agent context session count for tenant context '${contextCorrelationId}' to ${tenantSessions.sessionCount} due to failure in mutex script`, error ) if (tenantSessions.sessionCount <= 0 && tenantSessions.agentContext) { await this.closeAgentContext(tenantSessions.agentContext) - delete this.tenantAgentContextMapping[tenantSessions.agentContext.contextCorrelationId] + delete this.tenantAgentContextMapping[contextCorrelationId] } throw error @@ -142,51 +136,145 @@ export class TenantSessionCoordinator { this.logger.debug( `Ending session for agent context with contextCorrelationId ${agentContext.contextCorrelationId}'` ) - const hasTenantSessionMapping = this.hasTenantSessionMapping(agentContext.contextCorrelationId) // Custom handling for the root agent context. We don't keep track of the total number of sessions for the root // agent context, and we always keep the dependency manager intact. - if (!hasTenantSessionMapping && agentContext.contextCorrelationId === this.rootAgentContext.contextCorrelationId) { + if (agentContext.contextCorrelationId === this.rootAgentContext.contextCorrelationId) { this.logger.debug('Ending session for root agent context. Not disposing dependency manager') return } + const contextCorrelationId = agentContext.contextCorrelationId + this.assertTenantContextCorrelationId(contextCorrelationId) + const hasTenantSessionMapping = this.hasTenantSessionMapping(contextCorrelationId) + // This should not happen if (!hasTenantSessionMapping) { this.logger.error( - `Unknown agent context with contextCorrelationId '${agentContext.contextCorrelationId}'. Cannot end session` + `Unknown agent context with contextCorrelationId '${contextCorrelationId}'. Cannot end session` ) throw new CredoError( - `Unknown agent context with contextCorrelationId '${agentContext.contextCorrelationId}'. Cannot end session` + `Unknown agent context with contextCorrelationId '${contextCorrelationId}'. Cannot end session` ) } - await this.mutexForTenant(agentContext.contextCorrelationId).runExclusive(async () => { - this.logger.debug(`Acquired lock for tenant '${agentContext.contextCorrelationId}' to end session context`) - const tenantSessions = this.getTenantSessionsMapping(agentContext.contextCorrelationId) + await this.mutexForTenant(contextCorrelationId) + .runExclusive(async () => { + this.logger.debug(`Acquired lock for tenant '${contextCorrelationId}' to end session context`) + const tenantSessions = this.getTenantSessionsMapping(contextCorrelationId) - // TODO: check if session count is already 0 - tenantSessions.sessionCount-- - this.logger.debug( - `Decreased agent context session count for tenant '${agentContext.contextCorrelationId}' to ${tenantSessions.sessionCount}` + // TODO: check if session count is already 0 + tenantSessions.sessionCount-- + this.logger.debug( + `Decreased agent context session count for tenant '${contextCorrelationId}' to ${tenantSessions.sessionCount}` + ) + + if (tenantSessions.sessionCount <= 0 && tenantSessions.agentContext) { + await this.closeAgentContext(tenantSessions.agentContext) + delete this.tenantAgentContextMapping[contextCorrelationId] + } + }) + .finally(() => { + // Release a session so new sessions can be acquired + this.sessionMutex.releaseSession() + }) + } + + /** + * Delete the provided agent context. All opens sessions will be disposed and not usable anymore + */ + public async deleteAgentContext(agentContext: AgentContext): Promise { + this.logger.debug(`Deleting agent context with contextCorrelationId ${agentContext.contextCorrelationId}'`) + + // Custom handling for the root agent context. We don't keep track of the total number of sessions for the root + // agent context, and we always keep the dependency manager intact. + if (agentContext.contextCorrelationId === this.rootAgentContext.contextCorrelationId) { + this.logger.debug('Deleting agent context for root agent context.') + await agentContext.dependencyManager.deleteAgentContext(agentContext) + return + } + + const contextCorrelationId = agentContext.contextCorrelationId + this.assertTenantContextCorrelationId(contextCorrelationId) + const hasTenantSessionMapping = this.hasTenantSessionMapping(contextCorrelationId) + + // This should not happen + if (!hasTenantSessionMapping) { + this.logger.error( + `Unknown agent context with contextCorrelationId '${contextCorrelationId}'. Cannot delete agent context` ) + throw new CredoError( + `Unknown agent context with contextCorrelationId '${contextCorrelationId}'. Cannot delete agent context` + ) + } - if (tenantSessions.sessionCount <= 0 && tenantSessions.agentContext) { - await this.closeAgentContext(tenantSessions.agentContext) - delete this.tenantAgentContextMapping[agentContext.contextCorrelationId] - } - }) + await this.mutexForTenant(contextCorrelationId) + .runExclusive(async () => { + this.logger.debug(`Acquired lock for tenant '${contextCorrelationId}' to delete agent context`) + const tenantSessions = this.getTenantSessionsMapping(contextCorrelationId) - // Release a session so new sessions can be acquired - this.sessionMutex.releaseSession() + this.logger.debug( + `Deleting agent context for tenant '${contextCorrelationId}' with ${tenantSessions.sessionCount} active sessions.` + ) + if (!tenantSessions.agentContext) { + throw new CredoError( + `Unable to delete agent context for tenant '${contextCorrelationId}' as there are no active sessions.` + ) + } + + await agentContext.dependencyManager.deleteAgentContext(tenantSessions.agentContext) + delete this.tenantAgentContextMapping[contextCorrelationId] + }) + .finally(() => { + // Release a session so new sessions can be acquired + this.sessionMutex.releaseSession() + }) + } + + /** + * The context correlation id for a tenant is the tenant id prefixed with tenant- + */ + public getContextCorrelationIdForTenantId(tenantId: string): TenantContextCorrelationId { + if (tenantId.startsWith('tenant-')) { + throw new CredoError(`Tenant id already starts with 'tenant-'. You are probalby passing a context correlation id`) + } + + return `tenant-${tenantId}` + } + + /** + * The context correlation id for a tenant is the tenant id prefixed with tenant- + */ + public getTenantIdForContextCorrelationId(contextCorrelationId: TenantContextCorrelationId) { + if (!contextCorrelationId.startsWith('tenant-')) { + throw new CredoError( + `Could not extract tenant id from context correlation id. Context correlation id should start with 'tenant-'` + ) + } + + return contextCorrelationId.replace('tenant-', '') + } + + public isTenantContextCorrelationId( + contextCorrelationId: string + ): contextCorrelationId is TenantContextCorrelationId { + return contextCorrelationId.startsWith('tenant-') + } + + public assertTenantContextCorrelationId( + contextCorrelationId: string + ): asserts contextCorrelationId is TenantContextCorrelationId { + if (!this.isTenantContextCorrelationId(contextCorrelationId)) { + throw new CredoError(`Expected context correlation id for tenant to start with 'tenant-'`) + } } - private hasTenantSessionMapping(tenantId: T): boolean { - return this.tenantAgentContextMapping[tenantId] !== undefined + private hasTenantSessionMapping(contextCorrelationId: TenantContextCorrelationId): boolean { + return this.tenantAgentContextMapping[contextCorrelationId] !== undefined } - private getTenantSessionsMapping(tenantId: string): TenantContextSessions { - let tenantSessionMapping = this.tenantAgentContextMapping[tenantId] + private getTenantSessionsMapping(contextCorrelationId: TenantContextCorrelationId): TenantContextSessions { + let tenantSessionMapping = this.tenantAgentContextMapping[contextCorrelationId] if (tenantSessionMapping) return tenantSessionMapping tenantSessionMapping = { @@ -197,56 +285,49 @@ export class TenantSessionCoordinator { // be fast enough to not cause a problem. This wil also only be problem when the wallet is being created // for the first time or being acquired while wallet initialization is in progress. this.tenantsModuleConfig.sessionAcquireTimeout, - new CredoError(`Error acquiring lock for tenant ${tenantId}. Wallet initialization or shutdown took too long.`) + new CredoError( + `Error acquiring lock for tenant context ${contextCorrelationId}. Wallet initialization or shutdown took too long.` + ) ), } - this.tenantAgentContextMapping[tenantId] = tenantSessionMapping + this.tenantAgentContextMapping[contextCorrelationId] = tenantSessionMapping return tenantSessionMapping } - private mutexForTenant(tenantId: string) { - const tenantSessions = this.getTenantSessionsMapping(tenantId) + private mutexForTenant(contextCorrelationId: TenantContextCorrelationId) { + const tenantSessions = this.getTenantSessionsMapping(contextCorrelationId) return tenantSessions.mutex } - private async createAgentContext(tenantRecord: TenantRecord) { + private async createAgentContext(tenantRecord: TenantRecord, { provisionContext }: { provisionContext: boolean }) { const tenantDependencyManager = this.rootAgentContext.dependencyManager.createChild() - - const { id, key, keyDerivationMethod, ...strippedWalletConfig } = this.rootAgentContext.config?.walletConfig ?? {} const tenantConfig = this.rootAgentContext.config.extend({ - ...tenantRecord.config, - walletConfig: { - ...strippedWalletConfig, - ...tenantRecord.config.walletConfig, - }, + label: tenantRecord.config.label, }) const agentContext = new AgentContext({ - contextCorrelationId: tenantRecord.id, + contextCorrelationId: this.getContextCorrelationIdForTenantId(tenantRecord.id), dependencyManager: tenantDependencyManager, + isRootAgentContext: false, }) tenantDependencyManager.registerInstance(AgentContext, agentContext) tenantDependencyManager.registerInstance(AgentConfig, tenantConfig) - // NOTE: we're using the wallet api here because that correctly handle creating if it doesn't exist yet - // and will also write the storage version to the storage, which is needed by the update assistant. We either - // need to move this out of the module, or just keep using the module here. - const walletApi = agentContext.dependencyManager.resolve(WalletApi) - - if (!tenantConfig.walletConfig) { - throw new WalletError('Cannot initialize tenant without Wallet config.') + if (provisionContext) { + await tenantDependencyManager.provisionAgentContext(agentContext) } - await walletApi.initialize(tenantConfig.walletConfig) + + await tenantDependencyManager.initializeAgentContext(agentContext) return agentContext } private async closeAgentContext(agentContext: AgentContext) { this.logger.debug(`Closing agent context for tenant '${agentContext.contextCorrelationId}'`) - await agentContext.dependencyManager.dispose() + await agentContext.dependencyManager.closeAgentContext(agentContext) } } @@ -256,6 +337,8 @@ interface TenantContextSessions { mutex: MutexInterface } +export type TenantContextCorrelationId = `tenant-${string}` + export interface TenantAgentContextMapping { - [tenantId: string]: TenantContextSessions | undefined + [contextCorrelationId: TenantContextCorrelationId]: TenantContextSessions | undefined } diff --git a/packages/tenants/src/context/__tests__/TenantAgentContextProvider.test.ts b/packages/tenants/src/context/__tests__/TenantAgentContextProvider.test.ts index 2b84f82b3b..54905e8edf 100644 --- a/packages/tenants/src/context/__tests__/TenantAgentContextProvider.test.ts +++ b/packages/tenants/src/context/__tests__/TenantAgentContextProvider.test.ts @@ -1,13 +1,13 @@ import type { AgentContext } from '@credo-ts/core' -import { Key } from '@credo-ts/core' +import { Kms } from '@credo-ts/core' import { EventEmitter } from '../../../../core/src/agent/EventEmitter' import { getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' import { TenantRecord, TenantRoutingRecord } from '../../repository' import { TenantRecordService } from '../../services/TenantRecordService' import { TenantAgentContextProvider } from '../TenantAgentContextProvider' -import { TenantSessionCoordinator } from '../TenantSessionCoordinator' +import { TenantContextCorrelationId, TenantSessionCoordinator } from '../TenantSessionCoordinator' jest.mock('../../../../core/src/agent/EventEmitter') jest.mock('../../services/TenantRecordService') @@ -24,6 +24,11 @@ const rootAgentContext = getAgentContext() const agentConfig = getAgentConfig('TenantAgentContextProvider') const eventEmitter = new EventEmitterMock() +tenantSessionCoordinator.getTenantIdForContextCorrelationId = (id) => id.replace('tenant-', '') +tenantSessionCoordinator.getContextCorrelationIdForTenantId = (tenantId) => `tenant-${tenantId}` +tenantSessionCoordinator.isTenantContextCorrelationId = (id): id is TenantContextCorrelationId => + id.startsWith('tenant-') + const tenantAgentContextProvider = new TenantAgentContextProvider( tenantRecordService, rootAgentContext, @@ -52,10 +57,6 @@ describe('TenantAgentContextProvider', () => { id: 'tenant1', config: { label: 'Test Tenant', - walletConfig: { - id: 'test-wallet', - key: 'test-wallet-key', - }, }, storageVersion: '0.5', }) @@ -70,6 +71,7 @@ describe('TenantAgentContextProvider', () => { expect(tenantRecordService.getTenantById).toHaveBeenCalledWith(rootAgentContext, 'tenant1') expect(tenantSessionCoordinator.getContextForSession).toHaveBeenCalledWith(tenantRecord, { runInMutex: undefined, + provisionContext: false, }) expect(returnedAgentContext).toBe(tenantAgentContext) }) @@ -81,10 +83,6 @@ describe('TenantAgentContextProvider', () => { id: 'tenant1', config: { label: 'Test Tenant', - walletConfig: { - id: 'test-wallet', - key: 'test-wallet-key', - }, }, storageVersion: '0.5', }) @@ -96,12 +94,13 @@ describe('TenantAgentContextProvider', () => { const returnedAgentContext = await tenantAgentContextProvider.getContextForInboundMessage( {}, - { contextCorrelationId: 'tenant1' } + { contextCorrelationId: 'tenant-tenant1' } ) expect(tenantRecordService.getTenantById).toHaveBeenCalledWith(rootAgentContext, 'tenant1') expect(tenantSessionCoordinator.getContextForSession).toHaveBeenCalledWith(tenantRecord, { runInMutex: undefined, + provisionContext: false, }) expect(returnedAgentContext).toBe(tenantAgentContext) expect(tenantRecordService.findTenantRoutingRecordByRecipientKey).not.toHaveBeenCalled() @@ -126,10 +125,6 @@ describe('TenantAgentContextProvider', () => { id: 'tenant1', config: { label: 'Test Tenant', - walletConfig: { - id: 'test-wallet', - key: 'test-wallet-key', - }, }, storageVersion: '0.5', }) @@ -145,11 +140,12 @@ describe('TenantAgentContextProvider', () => { expect(tenantRecordService.getTenantById).toHaveBeenCalledWith(rootAgentContext, 'tenant1') expect(tenantSessionCoordinator.getContextForSession).toHaveBeenCalledWith(tenantRecord, { runInMutex: undefined, + provisionContext: false, }) expect(returnedAgentContext).toBe(tenantAgentContext) expect(tenantRecordService.findTenantRoutingRecordByRecipientKey).toHaveBeenCalledWith( rootAgentContext, - expect.any(Key) + expect.any(Kms.PublicJwk) ) const actualKey = mockFunction(tenantRecordService.findTenantRoutingRecordByRecipientKey).mock.calls[0][1] diff --git a/packages/tenants/src/context/__tests__/TenantSessionCoordinator.test.ts b/packages/tenants/src/context/__tests__/TenantSessionCoordinator.test.ts index 7be697af4f..b01092de1b 100644 --- a/packages/tenants/src/context/__tests__/TenantSessionCoordinator.test.ts +++ b/packages/tenants/src/context/__tests__/TenantSessionCoordinator.test.ts @@ -1,10 +1,10 @@ -import type { DependencyManager } from '@credo-ts/core' +import type { DependencyManager, Module } from '@credo-ts/core' import type { TenantAgentContextMapping } from '../TenantSessionCoordinator' -import { AgentConfig, AgentContext, WalletApi } from '@credo-ts/core' +import { AgentConfig, AgentContext } from '@credo-ts/core' import { Mutex, withTimeout } from 'async-mutex' -import { getAgentConfig, getAgentContext, mockFunction } from '../../../../core/tests/helpers' +import { getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' import testLogger from '../../../../core/tests/logger' import { TenantsModuleConfig } from '../../TenantsModuleConfig' import { TenantRecord } from '../../repository' @@ -20,15 +20,10 @@ type PublicTenantAgentContextMapping = Omit { afterEach(() => { tenantSessionCoordinator.tenantAgentContextMapping = {} + jest.resetAllMocks() jest.clearAllMocks() }) @@ -53,17 +49,13 @@ describe('TenantSessionCoordinator', () => { sessionCount: 1, } tenantSessionCoordinator.tenantAgentContextMapping = { - tenant1, + 'tenant-tenant1': tenant1, } const tenantRecord = new TenantRecord({ id: 'tenant1', config: { label: 'Test Tenant', - walletConfig: { - id: 'test-wallet', - key: 'test-wallet-key', - }, }, storageVersion: '0.5', }) @@ -75,14 +67,22 @@ describe('TenantSessionCoordinator', () => { }) test('creates a new agent context, initializes the wallet and stores it in the tenant agent context mapping', async () => { + const agentContext = getAgentContext({ + agentConfig: getAgentConfig('TenantSessionCoordinator'), + }) + + const tenantSessionCoordinator = new TenantSessionCoordinator( + agentContext, + testLogger, + new TenantsModuleConfig() + ) as unknown as PublicTenantAgentContextMapping + + const tenantSessionMutexMock = TenantSessionMutexMock.mock.instances[0] + const tenantRecord = new TenantRecord({ id: 'tenant1', config: { label: 'Test Tenant', - walletConfig: { - id: 'test-wallet', - key: 'test-wallet-key', - }, }, storageVersion: '0.5', }) @@ -91,27 +91,20 @@ describe('TenantSessionCoordinator', () => { const tenantDependencyManager = { registerInstance: jest.fn(), - resolve: jest.fn(() => wallet), + initializeAgentContext: jest.fn(), } as unknown as DependencyManager createChildSpy.mockReturnValue(tenantDependencyManager) const tenantAgentContext = await tenantSessionCoordinator.getContextForSession(tenantRecord) - expect(wallet.initialize).toHaveBeenCalledWith({ - ...tenantRecord.config.walletConfig, - storage: { config: { inMemory: true }, type: 'sqlite' }, - }) expect(tenantSessionMutexMock.acquireSession).toHaveBeenCalledTimes(1) - expect(extendSpy).toHaveBeenCalledWith({ - ...tenantRecord.config, - walletConfig: { ...tenantRecord.config.walletConfig, storage: { config: { inMemory: true }, type: 'sqlite' } }, - }) + expect(extendSpy).toHaveBeenCalledWith(tenantRecord.config) expect(createChildSpy).toHaveBeenCalledWith() expect(tenantDependencyManager.registerInstance).toHaveBeenCalledWith(AgentContext, expect.any(AgentContext)) expect(tenantDependencyManager.registerInstance).toHaveBeenCalledWith(AgentConfig, expect.any(AgentConfig)) - expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toEqual({ + expect(tenantSessionCoordinator.tenantAgentContextMapping['tenant-tenant1']).toEqual({ agentContext: tenantAgentContext, mutex: expect.objectContaining({ acquire: expect.any(Function), @@ -124,59 +117,42 @@ describe('TenantSessionCoordinator', () => { sessionCount: 1, }) - expect(tenantAgentContext.contextCorrelationId).toBe('tenant1') + expect(tenantAgentContext.contextCorrelationId).toBe('tenant-tenant1') + createChildSpy.mockClear() + createChildSpy.mockReset() }) - test('rethrows error and releases session if error is throw while getting agent context', async () => { + test('locks and waits for lock to release when initialization is already in progress', async () => { const tenantRecord = new TenantRecord({ id: 'tenant1', config: { label: 'Test Tenant', - walletConfig: { - id: 'test-wallet', - key: 'test-wallet-key', - }, }, storageVersion: '0.5', }) - // Throw error during wallet initialization - mockFunction(wallet.initialize).mockRejectedValue(new Error('Test error')) + let hasBeenCalledTimes = 0 - await expect(tenantSessionCoordinator.getContextForSession(tenantRecord)).rejects.toThrowError('Test error') + const { ...originalModules } = agentContext.dependencyManager.registeredModules + agentContext.dependencyManager.registerModules({ + test2: new (class implements Module { + async onInitializeContext(_agentContext: AgentContext): Promise { + hasBeenCalledTimes++ + await new Promise((res) => setTimeout(res, 500)) + } - expect(wallet.initialize).toHaveBeenCalledWith({ - ...tenantRecord.config.walletConfig, - storage: { config: { inMemory: true }, type: 'sqlite' }, + register(_dependencyManager: DependencyManager): void {} + })(), }) - expect(tenantSessionMutexMock.acquireSession).toHaveBeenCalledTimes(1) - expect(tenantSessionMutexMock.releaseSession).toHaveBeenCalledTimes(1) - }) - - test('locks and waits for lock to release when initialization is already in progress', async () => { - const tenantRecord = new TenantRecord({ - id: 'tenant1', - config: { - label: 'Test Tenant', - walletConfig: { - id: 'test-wallet', - key: 'test-wallet-key', - }, - }, - storageVersion: '0.5', - }) - - // Add timeout to mock the initialization and we can test that the mutex is used. - mockFunction(wallet.initialize).mockReturnValueOnce(new Promise((resolve) => setTimeout(resolve, 100))) // Start two context session creations (but don't await). It should set the mutex property on the tenant agent context mapping. const tenantAgentContext1Promise = tenantSessionCoordinator.getContextForSession(tenantRecord) const tenantAgentContext2Promise = tenantSessionCoordinator.getContextForSession(tenantRecord) - expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toBeUndefined() + expect(tenantSessionCoordinator.tenantAgentContextMapping['tenant-tenant1']).toBeUndefined() // Await first session promise, should have 1 session const tenantAgentContext1 = await tenantAgentContext1Promise - expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toEqual({ + expect(tenantSessionCoordinator.tenantAgentContextMapping['tenant-tenant1']).toEqual({ agentContext: tenantAgentContext1, sessionCount: 1, mutex: expect.objectContaining({ @@ -191,7 +167,7 @@ describe('TenantSessionCoordinator', () => { // There should be two sessions active now const tenantAgentContext2 = await tenantAgentContext2Promise - expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toEqual({ + expect(tenantSessionCoordinator.tenantAgentContextMapping['tenant-tenant1']).toEqual({ agentContext: tenantAgentContext1, sessionCount: 2, mutex: expect.objectContaining({ @@ -205,12 +181,10 @@ describe('TenantSessionCoordinator', () => { }) // Initialize should only be called once - expect(wallet.initialize).toHaveBeenCalledWith({ - ...tenantRecord.config.walletConfig, - storage: { config: { inMemory: true }, type: 'sqlite' }, - }) - expect(wallet.initialize).toHaveBeenCalledTimes(1) + expect(hasBeenCalledTimes).toEqual(1) + // @ts-ignore + agentContext.dependencyManager.registeredModules = originalModules expect(tenantAgentContext1).toBe(tenantAgentContext2) }) }) @@ -220,21 +194,22 @@ describe('TenantSessionCoordinator', () => { const rootAgentContextMock = { contextCorrelationId: 'mock', dependencyManager: { dispose: jest.fn() }, + isRootAgentContext: true, } as unknown as AgentContext await tenantSessionCoordinator.endAgentContextSession(rootAgentContextMock) expect(tenantSessionMutexMock.releaseSession).not.toHaveBeenCalled() }) - test('throws an error if not agent context session exists for the tenant', async () => { - const tenantAgentContextMock = { contextCorrelationId: 'does-not-exist' } as unknown as AgentContext - expect(tenantSessionCoordinator.endAgentContextSession(tenantAgentContextMock)).rejects.toThrowError( - `Unknown agent context with contextCorrelationId 'does-not-exist'. Cannot end session` + test('throws an error if no agent context session exists for the tenant', async () => { + const tenantAgentContextMock = { contextCorrelationId: 'tenant-does-not-exist' } as unknown as AgentContext + expect(tenantSessionCoordinator.endAgentContextSession(tenantAgentContextMock)).rejects.toThrow( + `Unknown agent context with contextCorrelationId 'tenant-does-not-exist'. Cannot end session` ) }) test('decreases the tenant session count and calls release session', async () => { - const tenant1AgentContext = { contextCorrelationId: 'tenant1' } as unknown as AgentContext + const tenant1AgentContext = { contextCorrelationId: 'tenant-tenant1' } as unknown as AgentContext const tenant1 = { agentContext: tenant1AgentContext, @@ -242,13 +217,13 @@ describe('TenantSessionCoordinator', () => { sessionCount: 2, } tenantSessionCoordinator.tenantAgentContextMapping = { - tenant1, + 'tenant-tenant1': tenant1, } await tenantSessionCoordinator.endAgentContextSession(tenant1AgentContext) // Should have reduced session count by one - expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toEqual({ + expect(tenantSessionCoordinator.tenantAgentContextMapping['tenant-tenant1']).toEqual({ agentContext: tenant1AgentContext, mutex: tenant1.mutex, sessionCount: 1, @@ -258,8 +233,8 @@ describe('TenantSessionCoordinator', () => { test('closes the agent context and removes the agent context mapping if the number of sessions reaches 0', async () => { const tenant1AgentContext = { - dependencyManager: { dispose: jest.fn() }, - contextCorrelationId: 'tenant1', + dependencyManager: { closeAgentContext: jest.fn() }, + contextCorrelationId: 'tenant-tenant1', } as unknown as AgentContext const tenant1 = { @@ -268,14 +243,14 @@ describe('TenantSessionCoordinator', () => { sessionCount: 1, } tenantSessionCoordinator.tenantAgentContextMapping = { - tenant1, + 'tenant-tenant1': tenant1, } await tenantSessionCoordinator.endAgentContextSession(tenant1AgentContext) // Should have removed tenant1 - expect(tenantSessionCoordinator.tenantAgentContextMapping.tenant1).toBeUndefined() - expect(tenant1AgentContext.dependencyManager.dispose).toHaveBeenCalledTimes(1) + expect(tenantSessionCoordinator.tenantAgentContextMapping['tenant-tenant1']).toBeUndefined() + expect(tenant1AgentContext.dependencyManager.closeAgentContext).toHaveBeenCalledTimes(1) expect(tenantSessionMutexMock.releaseSession).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/tenants/src/models/TenantConfig.ts b/packages/tenants/src/models/TenantConfig.ts index 5858f4e236..f90af9210a 100644 --- a/packages/tenants/src/models/TenantConfig.ts +++ b/packages/tenants/src/models/TenantConfig.ts @@ -1,6 +1,4 @@ -import type { InitConfig, WalletConfig } from '@credo-ts/core' +import type { InitConfig } from '@credo-ts/core' -// FIXME: decide what to do with connectionImageUrl, since this would make this module dependant on didcomm -export type TenantConfig = Pick & { - walletConfig: Pick -} +// TODO: remove label from tenant config +export type TenantConfig = Pick diff --git a/packages/tenants/src/repository/TenantRoutingRepository.ts b/packages/tenants/src/repository/TenantRoutingRepository.ts index 6b3a33f46f..b44ca7a182 100644 --- a/packages/tenants/src/repository/TenantRoutingRepository.ts +++ b/packages/tenants/src/repository/TenantRoutingRepository.ts @@ -1,4 +1,4 @@ -import type { AgentContext, Key } from '@credo-ts/core' +import type { AgentContext, Kms } from '@credo-ts/core' import { EventEmitter, InjectionSymbols, Repository, StorageService, inject, injectable } from '@credo-ts/core' @@ -13,9 +13,9 @@ export class TenantRoutingRepository extends Repository { super(TenantRoutingRecord, storageService, eventEmitter) } - public findByRecipientKey(agentContext: AgentContext, key: Key) { + public findByRecipientKey(agentContext: AgentContext, publicJwk: Kms.PublicJwk) { return this.findSingleByQuery(agentContext, { - recipientKeyFingerprint: key.fingerprint, + recipientKeyFingerprint: publicJwk.fingerprint, }) } } diff --git a/packages/tenants/src/repository/__tests__/TenantRecord.test.ts b/packages/tenants/src/repository/__tests__/TenantRecord.test.ts index 6ba6b23344..70f9ecbdf3 100644 --- a/packages/tenants/src/repository/__tests__/TenantRecord.test.ts +++ b/packages/tenants/src/repository/__tests__/TenantRecord.test.ts @@ -13,10 +13,6 @@ describe('TenantRecord', () => { }, config: { label: 'test', - walletConfig: { - id: 'test', - key: 'test', - }, }, storageVersion: '0.5', }) @@ -26,10 +22,6 @@ describe('TenantRecord', () => { expect(tenantRecord.createdAt).toBe(createdAt) expect(tenantRecord.config).toEqual({ label: 'test', - walletConfig: { - id: 'test', - key: 'test', - }, }) expect(tenantRecord.getTags()).toEqual({ label: 'test', @@ -48,10 +40,6 @@ describe('TenantRecord', () => { }, config: { label: 'test', - walletConfig: { - id: 'test', - key: 'test', - }, }, storageVersion: '0.5', }) @@ -67,10 +55,6 @@ describe('TenantRecord', () => { }, config: { label: 'test', - walletConfig: { - id: 'test', - key: 'test', - }, }, }) @@ -81,10 +65,6 @@ describe('TenantRecord', () => { expect(instance.createdAt.getTime()).toBe(createdAt.getTime()) expect(instance.config).toEqual({ label: 'test', - walletConfig: { - id: 'test', - key: 'test', - }, }) expect(instance.getTags()).toEqual({ label: 'test', diff --git a/packages/tenants/src/repository/__tests__/TenantRoutingRepository.test.ts b/packages/tenants/src/repository/__tests__/TenantRoutingRepository.test.ts index 14d8c2f6b8..0cadc1e9e1 100644 --- a/packages/tenants/src/repository/__tests__/TenantRoutingRepository.test.ts +++ b/packages/tenants/src/repository/__tests__/TenantRoutingRepository.test.ts @@ -1,6 +1,4 @@ -import type { EventEmitter, StorageService } from '@credo-ts/core' - -import { Key } from '@credo-ts/core' +import { type EventEmitter, Kms, type StorageService } from '@credo-ts/core' import { getAgentContext, mockFunction } from '../../../../core/tests/helpers' import { TenantRoutingRecord } from '../TenantRoutingRecord' @@ -21,7 +19,7 @@ describe('TenantRoutingRepository', () => { describe('findByRecipientKey', () => { test('it should correctly transform the key to a fingerprint and return the routing record', async () => { - const key = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + const key = Kms.PublicJwk.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') const tenantRoutingRecord = new TenantRoutingRecord({ recipientKeyFingerprint: key.fingerprint, tenantId: 'tenant-id', diff --git a/packages/tenants/src/services/TenantRecordService.ts b/packages/tenants/src/services/TenantRecordService.ts index fc7cfc55c1..a33163d7df 100644 --- a/packages/tenants/src/services/TenantRecordService.ts +++ b/packages/tenants/src/services/TenantRecordService.ts @@ -1,7 +1,7 @@ -import type { AgentContext, Key, Query, QueryOptions } from '@credo-ts/core' +import type { AgentContext, Kms, Query, QueryOptions } from '@credo-ts/core' import type { TenantConfig } from '../models/TenantConfig' -import { KeyDerivationMethod, UpdateAssistant, injectable, utils } from '@credo-ts/core' +import { UpdateAssistant, injectable, utils } from '@credo-ts/core' import { TenantRecord, TenantRepository, TenantRoutingRecord, TenantRoutingRepository } from '../repository' @@ -15,22 +15,12 @@ export class TenantRecordService { this.tenantRoutingRepository = tenantRoutingRepository } - public async createTenant(agentContext: AgentContext, config: Omit) { + public async createTenant(agentContext: AgentContext, config: TenantConfig) { const tenantId = utils.uuid() - const walletId = `tenant-${tenantId}` - const walletKey = await agentContext.wallet.generateWalletKey() - const tenantRecord = new TenantRecord({ id: tenantId, - config: { - ...config, - walletConfig: { - id: walletId, - key: walletKey, - keyDerivationMethod: KeyDerivationMethod.Raw, - }, - }, + config, storageVersion: UpdateAssistant.frameworkStorageVersion, }) @@ -79,7 +69,7 @@ export class TenantRecordService { public async findTenantRoutingRecordByRecipientKey( agentContext: AgentContext, - recipientKey: Key + recipientKey: Kms.PublicJwk ): Promise { return this.tenantRoutingRepository.findByRecipientKey(agentContext, recipientKey) } @@ -87,7 +77,7 @@ export class TenantRecordService { public async addTenantRoutingRecord( agentContext: AgentContext, tenantId: string, - recipientKey: Key + recipientKey: Kms.PublicJwk ): Promise { const tenantRoutingRecord = new TenantRoutingRecord({ tenantId, diff --git a/packages/tenants/src/services/__tests__/TenantService.test.ts b/packages/tenants/src/services/__tests__/TenantService.test.ts index da055efb0d..58b193d711 100644 --- a/packages/tenants/src/services/__tests__/TenantService.test.ts +++ b/packages/tenants/src/services/__tests__/TenantService.test.ts @@ -1,7 +1,4 @@ -import type { Wallet } from '@credo-ts/core' - -import { Key } from '@credo-ts/core' - +import { Kms } from '@credo-ts/core' import { getAgentContext, mockFunction } from '../../../../core/tests/helpers' import { TenantRecord, TenantRoutingRecord } from '../../repository' import { TenantRepository } from '../../repository/TenantRepository' @@ -13,13 +10,9 @@ const TenantRepositoryMock = TenantRepository as jest.Mock jest.mock('../../repository/TenantRoutingRepository') const TenantRoutingRepositoryMock = TenantRoutingRepository as jest.Mock -const wallet = { - generateWalletKey: jest.fn(() => Promise.resolve('walletKey')), -} as unknown as Wallet - const tenantRepository = new TenantRepositoryMock() const tenantRoutingRepository = new TenantRoutingRepositoryMock() -const agentContext = getAgentContext({ wallet }) +const agentContext = getAgentContext({}) const tenantRecordService = new TenantRecordService(tenantRepository, tenantRoutingRepository) @@ -42,14 +35,9 @@ describe('TenantRecordService', () => { config: { label: 'Test Tenant', //connectionImageUrl: 'https://example.com/connection.png', - walletConfig: { - id: expect.any(String), - key: 'walletKey', - }, }, }) - expect(agentContext.wallet.generateWalletKey).toHaveBeenCalled() expect(tenantRepository.save).toHaveBeenCalledWith(agentContext, tenantRecord) }) }) @@ -70,10 +58,6 @@ describe('TenantRecordService', () => { id: 'tenant-id', config: { label: 'Test Tenant', - walletConfig: { - id: 'tenant-wallet-id', - key: 'tenant-wallet-key', - }, }, storageVersion: '0.5', }) @@ -90,10 +74,6 @@ describe('TenantRecordService', () => { id: 'tenant-id', config: { label: 'Test Tenant', - walletConfig: { - id: 'tenant-wallet-id', - key: 'tenant-wallet-key', - }, }, storageVersion: '0.5', }) @@ -128,7 +108,7 @@ describe('TenantRecordService', () => { const tenantRoutingRecord = jest.fn() as unknown as TenantRoutingRecord mockFunction(tenantRoutingRepository.findByRecipientKey).mockResolvedValue(tenantRoutingRecord) - const recipientKey = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + const recipientKey = Kms.PublicJwk.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') const returnedTenantRoutingRecord = await tenantRecordService.findTenantRoutingRecordByRecipientKey( agentContext, recipientKey @@ -141,7 +121,7 @@ describe('TenantRecordService', () => { describe('addTenantRoutingRecord', () => { test('creates a tenant routing record and stores it in the tenant routing repository', async () => { - const recipientKey = Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + const recipientKey = Kms.PublicJwk.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') const tenantRoutingRecord = await tenantRecordService.addTenantRoutingRecord( agentContext, 'tenant-id', diff --git a/packages/tenants/src/updates/__tests__/0.4.test.ts b/packages/tenants/src/updates/__tests__/0.4.test.ts index ba7b988dea..ffb4ede30a 100644 --- a/packages/tenants/src/updates/__tests__/0.4.test.ts +++ b/packages/tenants/src/updates/__tests__/0.4.test.ts @@ -5,7 +5,6 @@ import { MediatorRoutingRecord } from '@credo-ts/didcomm' import { agentDependencies } from '@credo-ts/node' import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' -import { RegisteredAskarTestWallet } from '../../../../askar/tests/helpers' import { TenantsModule } from '../../TenantsModule' // Backup date / time is the unique identifier for a backup, needs to be unique for every test @@ -23,17 +22,11 @@ describe('UpdateAssistant | Tenants | v0.4 - v0.5', () => { const dependencyManager = new DependencyManager() const storageService = new InMemoryStorageService() dependencyManager.registerInstance(InjectionSymbols.StorageService, storageService) - // If we register the AskarModule it will register the storage service, but we use in memory storage here - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, RegisteredAskarTestWallet) const agent = new Agent( { config: { label: 'Test Agent', - walletConfig: { - id: 'Wallet: 0.5 Update Tenants', - key: 'Key: 0.5 Update Tenants', - }, }, dependencies: agentDependencies, modules: { @@ -79,7 +72,6 @@ describe('UpdateAssistant | Tenants | v0.4 - v0.5', () => { expect(storageService.contextCorrelationIdToRecords[agent.context.contextCorrelationId].records).toMatchSnapshot() await agent.shutdown() - await agent.wallet.delete() uuidSpy.mockReset() }) diff --git a/packages/tenants/tests/tenant-sessions.test.ts b/packages/tenants/tests/tenant-sessions.test.ts index bf1dcdd15e..fef1b72560 100644 --- a/packages/tenants/tests/tenant-sessions.test.ts +++ b/packages/tenants/tests/tenant-sessions.test.ts @@ -5,7 +5,6 @@ import { ConnectionsModule } from '@credo-ts/didcomm' import { agentDependencies } from '@credo-ts/node' import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' -import { uuid } from '../../core/src/utils/uuid' import { testLogger } from '../../core/tests' import { getDefaultDidcommModules } from '../../didcomm/src/util/modules' @@ -13,10 +12,6 @@ import { TenantsModule } from '@credo-ts/tenants' const agentConfig: InitConfig = { label: 'Tenant Agent 1', - walletConfig: { - id: `tenant sessions e2e agent 1 - ${uuid().slice(0, 4)}`, - key: 'tenant sessions e2e agent 1', - }, logger: testLogger, } @@ -28,6 +23,7 @@ const agent = new Agent({ ...getDefaultDidcommModules({ endpoints: ['rxjs:tenant-agent1'] }), tenants: new TenantsModule({ sessionAcquireTimeout: 10000 }), inMemory: new InMemoryWalletModule(), + connections: new ConnectionsModule({ autoAcceptConnections: true, }), @@ -40,7 +36,6 @@ describe('Tenants Sessions E2E', () => { }) afterAll(async () => { - await agent.wallet.delete() await agent.shutdown() }) diff --git a/packages/tenants/tests/tenants-askar-profiles.test.ts b/packages/tenants/tests/tenants-askar-profiles.test.ts index c2702784c2..c4aeb755cd 100644 --- a/packages/tenants/tests/tenants-askar-profiles.test.ts +++ b/packages/tenants/tests/tenants-askar-profiles.test.ts @@ -3,17 +3,17 @@ import type { InitConfig } from '@credo-ts/core' import { Agent } from '@credo-ts/core' import { agentDependencies } from '@credo-ts/node' -import { AskarModule, AskarMultiWalletDatabaseScheme, AskarProfileWallet, AskarWallet } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' -import { getAskarWalletConfig, testLogger } from '../../core/tests' +import { AskarModule, AskarMultiWalletDatabaseScheme } from '../../askar/src' +import { getAskarStoreConfig, testLogger } from '../../core/tests' import { TenantsModule } from '@credo-ts/tenants' +import { Store, askar } from '@openwallet-foundation/askar-nodejs' +import { AskarStoreManager } from '../../askar/src/AskarStoreManager' describe('Tenants Askar database schemes E2E', () => { test('uses AskarWallet for all wallets and tenants when database schema is DatabasePerWallet', async () => { const agentConfig: InitConfig = { label: 'Tenant Agent 1', - walletConfig: getAskarWalletConfig('askar tenants without profiles e2e agent 1', { inMemory: false }), logger: testLogger, } @@ -23,7 +23,8 @@ describe('Tenants Askar database schemes E2E', () => { modules: { tenants: new TenantsModule(), askar: new AskarModule({ - askar: askarModuleConfig.askar, + askar, + store: getAskarStoreConfig('askar tenants without profiles e2e agent 1', { inMemory: false }), // Database per wallet multiWalletDatabaseScheme: AskarMultiWalletDatabaseScheme.DatabasePerWallet, }), @@ -33,10 +34,6 @@ describe('Tenants Askar database schemes E2E', () => { await agent.initialize() - // main wallet should use AskarWallet - expect(agent.context.wallet).toBeInstanceOf(AskarWallet) - const mainWallet = agent.context.wallet as AskarWallet - // Create tenant const tenantRecord = await agent.modules.tenants.createTenant({ config: { @@ -49,13 +46,13 @@ describe('Tenants Askar database schemes E2E', () => { tenantId: tenantRecord.id, }) - expect(tenantAgent.context.wallet).toBeInstanceOf(AskarWallet) - const tenantWallet = tenantAgent.context.wallet as AskarWallet + const rootStore = agent.dependencyManager.resolve(Store) + const tenantStore = tenantAgent.dependencyManager.resolve(Store) // By default, profile is the same as the wallet id - expect(tenantWallet.profile).toEqual(`tenant-${tenantRecord.id}`) + expect(await tenantStore.getDefaultProfile()).toEqual(`tenant-${tenantRecord.id}`) // But the store should be different - expect(tenantWallet.store).not.toBe(mainWallet.store) + expect(tenantStore).not.toBe(rootStore) // Insert and end await tenantAgent.genericRecords.save({ content: { name: 'hello' }, id: 'hello' }) @@ -64,14 +61,12 @@ describe('Tenants Askar database schemes E2E', () => { const tenantAgent2 = await agent.modules.tenants.getTenantAgent({ tenantId: tenantRecord.id }) expect(await tenantAgent2.genericRecords.findById('hello')).not.toBeNull() - await agent.wallet.delete() await agent.shutdown() }) test('uses AskarWallet for main agent, and ProfileAskarWallet for tenants', async () => { const agentConfig: InitConfig = { label: 'Tenant Agent 1', - walletConfig: getAskarWalletConfig('askar tenants with profiles e2e agent 1'), logger: testLogger, } @@ -81,7 +76,8 @@ describe('Tenants Askar database schemes E2E', () => { modules: { tenants: new TenantsModule(), askar: new AskarModule({ - askar: askarModuleConfig.askar, + askar, + store: getAskarStoreConfig('askar tenants with profiles e2e agent 1'), // Profile per wallet multiWalletDatabaseScheme: AskarMultiWalletDatabaseScheme.ProfilePerWallet, }), @@ -91,10 +87,6 @@ describe('Tenants Askar database schemes E2E', () => { await agent.initialize() - // main wallet should use AskarWallet - expect(agent.context.wallet).toBeInstanceOf(AskarWallet) - const mainWallet = agent.context.wallet as AskarWallet - // Create tenant const tenantRecord = await agent.modules.tenants.createTenant({ config: { @@ -107,14 +99,22 @@ describe('Tenants Askar database schemes E2E', () => { tenantId: tenantRecord.id, }) - expect(tenantAgent.context.wallet).toBeInstanceOf(AskarProfileWallet) - const tenantWallet = tenantAgent.context.wallet as AskarProfileWallet + const rootStore = agent.dependencyManager.resolve(Store) + const tenantStore = tenantAgent.dependencyManager.resolve(Store) + + const storeManager = agent.dependencyManager.resolve(AskarStoreManager) + + const rootStoreWithProfile = await storeManager.getInitializedStoreWithProfile(agent.context) + const tenantStoreWithProfile = await storeManager.getInitializedStoreWithProfile(tenantAgent.context) + + expect(tenantStoreWithProfile.profile).toEqual(`tenant-${tenantRecord.id}`) + expect(tenantStoreWithProfile.store).toEqual(rootStoreWithProfile.store) + + expect(rootStoreWithProfile.profile).toEqual(undefined) - expect(tenantWallet.profile).toEqual(`tenant-${tenantRecord.id}`) // When using profile, the wallets should share the same store - expect(tenantWallet.store).toBe(mainWallet.store) + expect(tenantStore).toBe(rootStore) - await agent.wallet.delete() await agent.shutdown() }) }) diff --git a/packages/tenants/tests/tenants-storage-update.test.ts b/packages/tenants/tests/tenants-storage-update.test.ts index f355cad1a7..cddc449834 100644 --- a/packages/tenants/tests/tenants-storage-update.test.ts +++ b/packages/tenants/tests/tenants-storage-update.test.ts @@ -15,10 +15,6 @@ import { TenantsModule } from '@credo-ts/tenants' const agentConfig = { label: 'Tenant Agent', - walletConfig: { - id: 'tenants-agent-04', - key: 'tenants-agent-04', - }, logger: testLogger, } satisfies InitConfig @@ -26,8 +22,12 @@ const modules = { ...getDefaultDidcommModules(), tenants: new TenantsModule(), askar: new AskarModule({ - askar: askar, + askar, multiWalletDatabaseScheme: AskarMultiWalletDatabaseScheme.ProfilePerWallet, + store: { + id: 'tenants-agent-04', + key: 'tenants-agent-04', + }, }), connections: new ConnectionsModule({ autoAcceptConnections: true, @@ -44,10 +44,6 @@ describe('Tenants Storage Update', () => { config: { ...agentConfig, autoUpdateStorageOnStartup: true, - - // export not supported for askar profile wallet - // so we skip creating a backup - backupBeforeStorageUpdate: false, }, modules, dependencies: agentDependencies, @@ -55,14 +51,17 @@ describe('Tenants Storage Update', () => { // Delete existing wallet at this path const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) - await fileSystem.delete(path.join(fileSystem.dataPath, 'wallet', agentConfig.walletConfig.id)) - - // Import the wallet - await agent.wallet.import(agentConfig.walletConfig, { - key: agentConfig.walletConfig.key, - path: path.join(__dirname, 'tenants-04.db'), + await fileSystem.delete(path.join(fileSystem.dataPath, 'wallet', modules.askar.config.store.id)) + + const askarStoreConfig = agent.modules.askar.config.store + await agent.modules.askar.importStore({ + importFromStore: { + id: askarStoreConfig.id, + key: askarStoreConfig.key, + keyDerivationMethod: askarStoreConfig.keyDerivationMethod, + database: { type: 'sqlite', config: { path: path.join(__dirname, 'tenants-04.db') } }, + }, }) - await agent.initialize() // Expect tenant storage version to be still 0.4 @@ -78,41 +77,6 @@ describe('Tenants Storage Update', () => { const updatedTenant = await agent.modules.tenants.getTenantById('1d45d3c2-3480-4375-ac6f-47c322f091b0') expect(updatedTenant.storageVersion).toBe('0.5') - await agent.wallet.delete() - await agent.shutdown() - }) - - test('error when trying to open session for tenant when backupBeforeStorageUpdate is not disabled because profile cannot be exported', async () => { - // Create multi-tenant agents - const agent = new Agent({ - config: { ...agentConfig, autoUpdateStorageOnStartup: true, backupBeforeStorageUpdate: true }, - modules, - dependencies: agentDependencies, - }) - - // Delete existing wallet at this path - const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) - await fileSystem.delete(path.join(fileSystem.dataPath, 'wallet', agentConfig.walletConfig.id)) - - // Import the wallet - await agent.wallet.import(agentConfig.walletConfig, { - key: agentConfig.walletConfig.key, - path: path.join(__dirname, 'tenants-04.db'), - }) - - // Initialize agent - await agent.initialize() - - // Expect tenant storage version to be still 0.4 - const tenant = await agent.modules.tenants.getTenantById('1d45d3c2-3480-4375-ac6f-47c322f091b0') - expect(tenant.storageVersion).toBe('0.4') - - // Should throw error because not up to date and backupBeforeStorageUpdate is true - await expect( - agent.modules.tenants.getTenantAgent({ tenantId: '1d45d3c2-3480-4375-ac6f-47c322f091b0' }) - ).rejects.toThrow(/the wallet backend does not support exporting/) - - await agent.wallet.delete() await agent.shutdown() }) @@ -126,12 +90,16 @@ describe('Tenants Storage Update', () => { // Delete existing wallet at this path const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) - await fileSystem.delete(path.join(fileSystem.dataPath, 'wallet', agentConfig.walletConfig.id)) - - // Import the wallet - await agent.wallet.import(agentConfig.walletConfig, { - key: agentConfig.walletConfig.key, - path: path.join(__dirname, 'tenants-04.db'), + await fileSystem.delete(path.join(fileSystem.dataPath, 'wallet', modules.askar.config.store.id)) + + const askarStoreConfig = agent.modules.askar.config.store + await agent.modules.askar.importStore({ + importFromStore: { + id: askarStoreConfig.id, + key: askarStoreConfig.key, + keyDerivationMethod: askarStoreConfig.keyDerivationMethod, + database: { type: 'sqlite', config: { path: path.join(__dirname, 'tenants-04.db') } }, + }, }) // Update root agent (but not tenants) @@ -151,7 +119,6 @@ describe('Tenants Storage Update', () => { agent.modules.tenants.getTenantAgent({ tenantId: '1d45d3c2-3480-4375-ac6f-47c322f091b0' }) ).rejects.toThrow(/Current agent storage for tenant 1d45d3c2-3480-4375-ac6f-47c322f091b0 is not up to date/) - await agent.wallet.delete() await agent.shutdown() }) @@ -165,12 +132,16 @@ describe('Tenants Storage Update', () => { // Delete existing wallet at this path const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) - await fileSystem.delete(path.join(fileSystem.dataPath, 'wallet', agentConfig.walletConfig.id)) - - // Import the wallet - await agent.wallet.import(agentConfig.walletConfig, { - key: agentConfig.walletConfig.key, - path: path.join(__dirname, 'tenants-04.db'), + await fileSystem.delete(path.join(fileSystem.dataPath, 'wallet', modules.askar.config.store.id)) + + const askarStoreConfig = agent.modules.askar.config.store + await agent.modules.askar.importStore({ + importFromStore: { + id: askarStoreConfig.id, + key: askarStoreConfig.key, + keyDerivationMethod: askarStoreConfig.keyDerivationMethod, + database: { type: 'sqlite', config: { path: path.join(__dirname, 'tenants-04.db') } }, + }, }) // Update root agent (but not tenants) @@ -196,9 +167,6 @@ describe('Tenants Storage Update', () => { // Update tenant await agent.modules.tenants.updateTenantStorage({ tenantId: tenant.id, - updateOptions: { - backupBeforeStorageUpdate: false, - }, }) // Should have closed session after upgrade @@ -222,9 +190,6 @@ describe('Tenants Storage Update', () => { const updatePromises = outdatedTenants.map((tenant) => agent.modules.tenants.updateTenantStorage({ tenantId: tenant.id, - updateOptions: { - backupBeforeStorageUpdate: false, - }, }) ) @@ -234,7 +199,6 @@ describe('Tenants Storage Update', () => { const outdatedTenantsAfterUpdate = await agent.modules.tenants.getTenantsWithOutdatedStorage() expect(outdatedTenantsAfterUpdate).toHaveLength(0) - await agent.wallet.delete() await agent.shutdown() }) }) diff --git a/packages/tenants/tests/tenants.test.ts b/packages/tenants/tests/tenants.test.ts index 83b642a190..9c0458b833 100644 --- a/packages/tenants/tests/tenants.test.ts +++ b/packages/tenants/tests/tenants.test.ts @@ -12,19 +12,16 @@ import { } from '@credo-ts/didcomm' import { agentDependencies } from '@credo-ts/node' +import { askar } from '@openwallet-foundation/askar-nodejs' import { InMemoryWalletModule } from '../../../tests/InMemoryWalletModule' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { uuid } from '../../core/src/utils/uuid' -import { testLogger } from '../../core/tests' +import { AskarModule } from '../../askar/src' +import { getAskarStoreConfig, testLogger } from '../../core/tests' import { TenantsModule } from '../src/TenantsModule' const agent1Config: InitConfig = { label: 'Tenant Agent 1', - walletConfig: { - id: `tenants e2e agent 1 - ${uuid().slice(0, 4)}`, - key: 'tenants e2e agent 1', - }, logger: testLogger, } @@ -34,10 +31,6 @@ const agent1DidcommConfig: DidCommModuleConfigOptions = { const agent2Config: InitConfig = { label: 'Tenant Agent 2', - walletConfig: { - id: `tenants e2e agent 2 - ${uuid().slice(0, 4)}`, - key: 'tenants e2e agent 2', - }, logger: testLogger, } @@ -51,7 +44,7 @@ const getTenantsAgentModules = (didcommConfig: DidCommModuleConfigOptions) => oob: new OutOfBandModule(), messagePickup: new MessagePickupModule(), tenants: new TenantsModule(), - inMemory: new InMemoryWalletModule(), + inMemory: new InMemoryWalletModule({ enableKms: false }), connections: new ConnectionsModule({ autoAcceptConnections: true, }), @@ -63,13 +56,27 @@ const getTenantsAgentModules = (didcommConfig: DidCommModuleConfigOptions) => // Create multi-tenant agents const agent1 = new Agent({ config: agent1Config, - modules: getTenantsAgentModules(agent1DidcommConfig), + modules: { + ...getTenantsAgentModules(agent1DidcommConfig), + askar: new AskarModule({ + enableStorage: false, + askar, + store: getAskarStoreConfig('tenants.test.ts', { inMemory: false }), + }), + }, dependencies: agentDependencies, }) const agent2 = new Agent({ config: agent2Config, - modules: getTenantsAgentModules(agent2DidcommConfig), + modules: { + ...getTenantsAgentModules(agent2DidcommConfig), + askar: new AskarModule({ + enableStorage: false, + askar, + store: getAskarStoreConfig('tenants.test.ts', { inMemory: false }), + }), + }, dependencies: agentDependencies, }) @@ -100,9 +107,7 @@ describe('Tenants E2E', () => { }) afterAll(async () => { - await agent1.wallet.delete() await agent1.shutdown() - await agent2.wallet.delete() await agent2.shutdown() }) @@ -127,9 +132,19 @@ describe('Tenants E2E', () => { }) await tenantAgent.endSession() + // Create session but do not close it yet + const tenantAgent1 = await agent1.modules.tenants.getTenantAgent({ + tenantId: tenantRecord1.id, + }) + // Delete tenant agent await agent1.modules.tenants.deleteTenantById(tenantRecord1.id) + // Should not be able to use the session anymore + await expect(tenantAgent1.dids.getCreatedDids({})).rejects.toThrow( + `Storage for agent context ${tenantAgent1.context.contextCorrelationId} does not exist` + ) + // Can not get tenant agent again await expect(agent1.modules.tenants.getTenantAgent({ tenantId: tenantRecord1.id })).rejects.toThrow( `TenantRecord: record with id ${tenantRecord1.id} not found.` @@ -200,8 +215,8 @@ describe('Tenants E2E', () => { await tenantAgent2.endSession() // Delete tenants (will also delete wallets) - await agent1.modules.tenants.deleteTenantById(tenantAgent1.context.contextCorrelationId) - await agent1.modules.tenants.deleteTenantById(tenantAgent2.context.contextCorrelationId) + await agent1.modules.tenants.deleteTenantById(tenantAgent1.context.contextCorrelationId.replace('tenant-', '')) + await agent1.modules.tenants.deleteTenantById(tenantAgent2.context.contextCorrelationId.replace('tenant-', '')) }) test('create a connection between two tenants within different agents', async () => { @@ -258,7 +273,7 @@ describe('Tenants E2E', () => { ).modules.oob.createInvitation() expect(outOfBandRecord).toBeInstanceOf(OutOfBandRecord) - expect(tenantAgent.context.contextCorrelationId).toBe(tenantRecord.id) + expect(tenantAgent.context.contextCorrelationId).toBe(`tenant-${tenantRecord.id}`) expect(tenantAgent.config.label).toBe('Agent 1 Tenant 1') }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f907a5ecb..f004f40463 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -296,9 +296,9 @@ importers: specifier: ^4.8.0 version: 4.8.0 devDependencies: - '@animo-id/expo-secure-environment': - specifier: ^0.1.0 - version: 0.1.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(react@18.3.1) + '@credo-ts/tenants': + specifier: workspace:* + version: link:../tenants '@openwallet-foundation/askar-nodejs': specifier: ^0.3.1 version: 0.3.1 @@ -321,37 +321,6 @@ importers: specifier: ~5.5.2 version: 5.5.4 - packages/bbs-signatures: - dependencies: - '@animo-id/react-native-bbs-signatures': - specifier: ^0.1.0 - version: 0.1.0(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(react@18.3.1) - '@credo-ts/core': - specifier: workspace:* - version: link:../core - '@mattrglobal/bbs-signatures': - specifier: ^1.0.0 - version: 1.4.0 - '@mattrglobal/bls12381-key-pair': - specifier: ^1.0.0 - version: 1.2.1 - '@stablelib/random': - specifier: ^1.0.2 - version: 1.0.2 - devDependencies: - '@credo-ts/node': - specifier: workspace:* - version: link:../node - reflect-metadata: - specifier: ^0.1.13 - version: 0.1.14 - rimraf: - specifier: ^4.4.0 - version: 4.4.1 - typescript: - specifier: ~5.5.2 - version: 5.5.4 - packages/cheqd: dependencies: '@cheqd/sdk': @@ -411,13 +380,13 @@ importers: version: 1.1.2 '@digitalcredentials/jsonld': specifier: ^6.0.0 - version: 6.0.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) + version: 6.0.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) '@digitalcredentials/jsonld-signatures': specifier: ^9.4.0 - version: 9.4.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) + version: 9.4.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) '@digitalcredentials/vc': specifier: ^6.0.1 - version: 6.0.1(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) + version: 6.0.1(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) '@multiformats/base-x': specifier: ^4.0.1 version: 4.0.1 @@ -428,8 +397,11 @@ importers: specifier: ^1.7.1 version: 1.7.1 '@peculiar/asn1-ecc': - specifier: ^2.3.13 + specifier: ^2.3.14 version: 2.3.14 + '@peculiar/asn1-rsa': + specifier: ^2.3.15 + version: 2.3.15 '@peculiar/asn1-schema': specifier: ^2.3.13 version: 2.3.13 @@ -523,6 +495,9 @@ importers: webcrypto-core: specifier: ^1.8.0 version: 1.8.0 + zod: + specifier: ^3.24.2 + version: 3.24.2 devDependencies: '@types/events': specifier: ^3.0.0 @@ -728,17 +703,17 @@ importers: specifier: workspace:* version: link:../core '@openid4vc/oauth2': - specifier: 0.3.0-alpha-20250330133535 - version: 0.3.0-alpha-20250330133535 + specifier: 0.3.0-alpha-20250511195407 + version: 0.3.0-alpha-20250511195407 '@openid4vc/openid4vci': - specifier: 0.3.0-alpha-20250330133535 - version: 0.3.0-alpha-20250330133535 + specifier: 0.3.0-alpha-20250511195407 + version: 0.3.0-alpha-20250511195407 '@openid4vc/openid4vp': - specifier: 0.3.0-alpha-20250330133535 - version: 0.3.0-alpha-20250330133535 + specifier: 0.3.0-alpha-20250511195407 + version: 0.3.0-alpha-20250511195407 '@openid4vc/utils': - specifier: 0.3.0-alpha-20250330133535 - version: 0.3.0-alpha-20250330133535 + specifier: 0.3.0-alpha-20250511195407 + version: 0.3.0-alpha-20250511195407 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -801,6 +776,9 @@ importers: packages/react-native: dependencies: + '@animo-id/expo-secure-environment': + specifier: ^0.1.1 + version: 0.1.1(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(react@18.3.1) '@azure/core-asynciterator-polyfill': specifier: ^1.0.2 version: 1.0.2 @@ -827,6 +805,18 @@ importers: specifier: ~5.5.2 version: 5.5.4 + packages/redis-cache-nodejs: + dependencies: + '@credo-ts/core': + specifier: workspace:* + version: link:../core + ioredis: + specifier: ^5.6.1 + version: 5.6.1 + redis: + specifier: ^5.0.1 + version: 5.0.1 + packages/tenants: dependencies: '@credo-ts/core': @@ -934,8 +924,8 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@animo-id/expo-secure-environment@0.1.0': - resolution: {integrity: sha512-JNCyj+vY+1I/BXBCGQBtgyegNp78RnGHnmfUgwmGATq8Y6oLC/NeMAyggam5WWW+GTMg1NlR7Gl3nUnEe1uLaw==} + '@animo-id/expo-secure-environment@0.1.1': + resolution: {integrity: sha512-4vgRA5XNeDekyj7a4LGoErie5VaZdb6Y8rTyyfuVlMSgQUeAz4L61WZJvhNWgWFjGqur6WgU2AYfGIy2aTZm2w==} peerDependencies: expo: '*' react: '*' @@ -948,12 +938,6 @@ packages: resolution: {integrity: sha512-qlfbTAASA3B8DFwKV9nIe6ZSTg/UkgyQ2fUKkyO/D+sNPDgAP33JXtUmTG/uPDRJrhxhodSpwQ6I9seAlSTmUA==} engines: {node: '>=18'} - '@animo-id/react-native-bbs-signatures@0.1.0': - resolution: {integrity: sha512-7qvsiWhGfUev8ngE8YzF6ON9PtCID5LiYVYM4EC5eyj80gCdhx3R46CI7K1qbqIlGsoTYQ/Xx5Ubo5Ji9eaUEA==} - peerDependencies: - react: '>= 16' - react-native: '>= 0.66.0' - '@astronautlabs/jsonpath@1.1.2': resolution: {integrity: sha512-FqL/muoreH7iltYC1EB5Tvox5E8NSOOPGkgns4G+qxRKl6k5dxEVljUjB5NcKESzkqwnUqWjSZkL61XGYOuV+A==} @@ -976,6 +960,9 @@ packages: resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} engines: {node: '>=6.9.0'} + '@babel/generator@7.2.0': + resolution: {integrity: sha512-BA75MVfRlFQG2EZgFYIwyT1r6xSkwfP2bdkY/kLZusEYWiJs4xCowab/alaEaT0wSvmVuXGqiefeBlP+7V1yKg==} + '@babel/generator@7.26.3': resolution: {integrity: sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==} engines: {node: '>=6.9.0'} @@ -2177,21 +2164,21 @@ packages: resolution: {integrity: sha512-Ydf4LidRB/EBI+YrB+cVLqIseiRfjUI/AeHBgjGMtq3GroraDu81OV7zqophRgupngoL3iS3JUMDMnxO7g39qA==} engines: {'0': node >=0.10.0} - '@expo/cli@0.18.19': - resolution: {integrity: sha512-8Rj18cTofpLl+7D++auMVS71KungldHbrArR44fpE8loMVAvYZA+U932lmd0K2lOYBASPhm7SVP9wzls//ESFQ==} + '@expo/cli@0.18.29': + resolution: {integrity: sha512-X810C48Ss+67RdZU39YEO1khNYo1RmjouRV+vVe0QhMoTe8R6OA3t+XYEdwaNbJ5p/DJN7szfHfNmX2glpC7xg==} hasBin: true '@expo/code-signing-certificates@0.0.5': resolution: {integrity: sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw==} - '@expo/config-plugins@8.0.5': - resolution: {integrity: sha512-VGseKX1dYvaf2qHUDGzIQwSOJrO5fomH0gE5cKSQyi6wn+Q6rcV2Dj2E5aga+9aKNPL6FxZ0dqRFC3t2sbhaSA==} + '@expo/config-plugins@8.0.8': + resolution: {integrity: sha512-Fvu6IO13EUw0R9WeqxUO37FkM62YJBNcZb9DyJAOgMz7Ez/vaKQGEjKt9cwT+Q6uirtCATMgaq6VWAW7YW8xXw==} '@expo/config-types@51.0.1': resolution: {integrity: sha512-5JuzUFobFImrUgnq93LeucP44ZMxq8WMXmCtIUf3ZC3mJSwjvvHJBMO2fS/sIlmgvvQk9eq4VnX06/7tgDFMSg==} - '@expo/config@9.0.1': - resolution: {integrity: sha512-0tjaXBstTbXmD4z+UMFBkh2SZFwilizSQhW6DlaTMnPG5ezuw93zSFEWAuEC3YzkpVtNQTmYzxAYjxwh6seOGg==} + '@expo/config@9.0.3': + resolution: {integrity: sha512-eOTNM8eOC8gZNHgenySRlc/lwmYY1NOgvjwA8LHuvPT7/eUwD93zrxu3lPD1Cc/P6C/2BcVdfH4hf0tLmDxnsg==} '@expo/devcert@1.1.2': resolution: {integrity: sha512-FyWghLu7rUaZEZSTLt/XNRukm0c9GFfwP0iFaswoDWpV6alvVg+zRAfCLdIVQEz1SVcQ3zo1hMZFDrnKGvkCuQ==} @@ -2205,8 +2192,8 @@ packages: '@expo/json-file@8.3.3': resolution: {integrity: sha512-eZ5dld9AD0PrVRiIWpRkm5aIoWBw3kAyd8VkuWEy92sEthBKDDDHAnK2a0dw0Eil6j7rK7lS/Qaq/Zzngv2h5A==} - '@expo/metro-config@0.18.7': - resolution: {integrity: sha512-MzAyFP0fvoyj9IUc6SPnpy6/HLT23j/p5J+yWjGug2ddOpSuKNDHOOqlwWZbJp5KfZCEIVWNHeUoE+TaC/yhaQ==} + '@expo/metro-config@0.18.11': + resolution: {integrity: sha512-/uOq55VbSf9yMbUO1BudkUM2SsGW1c5hr9BnhIqYqcsFv0Jp5D3DtJ4rljDKaUeNLbwr6m7pqIrkSMq5NrYf4Q==} '@expo/osascript@2.1.3': resolution: {integrity: sha512-aOEkhPzDsaAfolSswObGiYW0Pf0ROfR9J2NBRLQACdQ6uJlyAMiPF45DVEVknAU9juKh0y8ZyvC9LXqLEJYohA==} @@ -2218,8 +2205,8 @@ packages: '@expo/plist@0.1.3': resolution: {integrity: sha512-GW/7hVlAylYg1tUrEASclw1MMk9FP4ZwyFAY/SUTJIhPDQHtfOlXREyWV3hhrHdX/K+pS73GNgdfT6E/e+kBbg==} - '@expo/prebuild-config@7.0.6': - resolution: {integrity: sha512-Hts+iGBaG6OQ+N8IEMMgwQElzJeSTb7iUJ26xADEHkaexsucAK+V52dM8M4ceicvbZR9q8M+ebJEGj0MCNA3dQ==} + '@expo/prebuild-config@7.0.8': + resolution: {integrity: sha512-wH9NVg6HiwF5y9x0TxiMEeBF+ITPGDXy5/i6OUheSrKpPgb0lF1Mwzl/f2fLPXBEpl+ZXOQ8LlLW32b7K9lrNg==} peerDependencies: expo-modules-autolinking: '>=0.8.1' @@ -2269,6 +2256,9 @@ packages: '@hyperledger/indy-vdr-shared@0.2.2': resolution: {integrity: sha512-9425MHU3K+/ahccCRjOIX3Z/51gqxvp3Nmyujyqlx9cd7PWG2Rianx7iNWecFBkdAEqS0DfHsb6YqqH39YZp/A==} + '@ioredis/commands@1.2.0': + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2355,6 +2345,10 @@ packages: resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/types@24.9.0': + resolution: {integrity: sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==} + engines: {node: '>= 6'} + '@jest/types@26.6.2': resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==} engines: {node: '>= 10.14.2'} @@ -2423,22 +2417,6 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true - '@mattrglobal/bbs-signatures@1.3.1': - resolution: {integrity: sha512-syZGkapPpktD2el4lPTCQRw/LSia6/NwBS83hzCKu4dTlaJRO636qo5NCiiQb+iBYWyZQQEzN0jdRik8N9EUGA==} - engines: {node: '>=14'} - - '@mattrglobal/bbs-signatures@1.4.0': - resolution: {integrity: sha512-uBK1IWw48fqloO9W/yoDncTs9rfwfbG/53cOrrCQL7XkyZe4DtB40HcLbi3i+yxTYs5wytf1Qr4Z5RpzpW10jw==} - engines: {node: '>=16'} - - '@mattrglobal/bls12381-key-pair@1.2.1': - resolution: {integrity: sha512-Xh63NP1iSGBLW10N5uRpDyoPo2LtNHHh/TRGVJEHRgo+07yxgl8tS06Q2zO9gN9+b+GU5COKvR3lACwrvn+MYw==} - engines: {node: '>=14.0.0'} - - '@mattrglobal/node-bbs-signatures@0.18.1': - resolution: {integrity: sha512-s9ccL/1TTvCP1N//4QR84j/d5D/stx/AI1kPcRgiE4O3KrxyF7ZdL9ca8fmFuN6yh9LAbn/OiGRnOXgvn38Dgg==} - engines: {node: '>=14', yarn: 1.x} - '@mswjs/interceptors@0.37.5': resolution: {integrity: sha512-AAwRb5vXFcY4L+FvZ7LZusDuZ0vEe0Zm8ohn1FM6/X7A3bj4mqmkAcGRWuvC2JwSygNwHAAmMnAI73vPHeqsHA==} engines: {node: '>=18'} @@ -2487,17 +2465,17 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@openid4vc/oauth2@0.3.0-alpha-20250330133535': - resolution: {integrity: sha512-A5UgxQDJobddp0utxQqALG4dyzrQHo8DCjaHuKtrnoAcZWJFXmYFBvKbiKoiHjSLuTcME5mDC/+m46hchGiIQA==} + '@openid4vc/oauth2@0.3.0-alpha-20250511195407': + resolution: {integrity: sha512-H4SYmrszAm/qk+P35jk1vEVIIRTkhLTZOzTO0pTKBzDMordgAPyD06EDxw40mVEY3vY1IHICloUq8AzNtBPKOA==} - '@openid4vc/openid4vci@0.3.0-alpha-20250330133535': - resolution: {integrity: sha512-QNgpoPOQ/Viyq24xFbSlFccayChP6kcthjiyEuK7TPA5cU693BVgjGfyyzStgWf0MoL3aoJ47OQwYels7+MBvA==} + '@openid4vc/openid4vci@0.3.0-alpha-20250511195407': + resolution: {integrity: sha512-OMQbQNym2hDWrfldZgYdzaVKwE83WE7aExQgL299WBd8nr9fvUaoG74+GtTHX/bgDXcLR7XwoUmcLsWByqSXpA==} - '@openid4vc/openid4vp@0.3.0-alpha-20250330133535': - resolution: {integrity: sha512-yJ/8ZnSFC3GSokGTafXYIsdCM/h38PhHWaGan7T6TuJaeDrZW8l44HV2Cbod82drgyQ3IL10bpaX1bm+K3lQyw==} + '@openid4vc/openid4vp@0.3.0-alpha-20250511195407': + resolution: {integrity: sha512-bOTzFCv7gDcuDJ1JoWUPDlqVVjGkGVuQRWlnpl792bbsflkKQ7OCPIFuOH6oqxutPwE0ehV0nt8Pc0lUTwutTw==} - '@openid4vc/utils@0.3.0-alpha-20250330133535': - resolution: {integrity: sha512-yx/dar8DqqXhhJ2oyKTFPHG0vj73kgQdBLH5oR6IrAZ5b8MSSaRSOjnYKWAvEDtS7ZLBIEbYnC+OF34fcIOW+g==} + '@openid4vc/utils@0.3.0-alpha-20250511195407': + resolution: {integrity: sha512-S9c7GVEoohMbWY0CJsIchqcMy6le2hGUAEqAIF5R9PaaADviXOFoyaitXIa8rMIqNuPMk66Fs2IW1PvwnbI7EQ==} '@openwallet-foundation/askar-nodejs@0.3.1': resolution: {integrity: sha512-m3L8KEPC+qgA3MAFssMtjSqJiAQtrawZEWPmW6eiB7OPjZvkKjodMhx/cuUV5YTl4eQlSix2EY4vXMzk4vt+cQ==} @@ -2524,18 +2502,24 @@ packages: '@peculiar/asn1-pkcs9@2.3.13': resolution: {integrity: sha512-rIwQXmHpTo/dgPiWqUgby8Fnq6p1xTJbRMxCiMCk833kQCeZrC5lbSKg6NDnJTnX2kC6IbXBB9yCS2C73U2gJg==} - '@peculiar/asn1-rsa@2.3.13': - resolution: {integrity: sha512-wBNQqCyRtmqvXkGkL4DR3WxZhHy8fDiYtOjTeCd7SFE5F6GBeafw3EJ94PX/V0OJJrjQ40SkRY2IZu3ZSyBqcg==} + '@peculiar/asn1-rsa@2.3.15': + resolution: {integrity: sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==} '@peculiar/asn1-schema@2.3.13': resolution: {integrity: sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g==} + '@peculiar/asn1-schema@2.3.15': + resolution: {integrity: sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==} + '@peculiar/asn1-x509-attr@2.3.13': resolution: {integrity: sha512-WpEos6CcnUzJ6o2Qb68Z7Dz5rSjRGv/DtXITCNBtjZIRWRV12yFVci76SVfOX8sisL61QWMhpLKQibrG8pi2Pw==} '@peculiar/asn1-x509@2.3.13': resolution: {integrity: sha512-PfeLQl2skXmxX2/AFFCVaWU8U6FKW1Db43mgBhShCOFS1bVxqtvusq1hVjfuEcuSQGedrLdCSvTgabluwN/M9A==} + '@peculiar/asn1-x509@2.3.15': + resolution: {integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==} + '@peculiar/json-schema@1.1.12': resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} engines: {node: '>=8.0.0'} @@ -2623,16 +2607,16 @@ packages: resolution: {integrity: sha512-SegfYQFuut05EQIQIVB/6QMGaxJ29jEtPmzFWJdIp/yc2mmhIq7MfWRjwOe6qbONzIdp6Ca8p835hiGiAGyeKQ==} engines: {node: '>=18'} - '@react-native/babel-plugin-codegen@0.74.84': - resolution: {integrity: sha512-UR4uiii5szIJA84mSC6GJOfYKDq7/ThyetOQT62+BBcyGeHVtHlNLNRzgaMeLqIQaT8Fq4pccMI+7QqLOMXzdw==} + '@react-native/babel-plugin-codegen@0.74.87': + resolution: {integrity: sha512-+vJYpMnENFrwtgvDfUj+CtVJRJuUnzAUYT0/Pb68Sq9RfcZ5xdcCuUgyf7JO+akW2VTBoJY427wkcxU30qrWWw==} engines: {node: '>=18'} '@react-native/babel-plugin-codegen@0.78.1': resolution: {integrity: sha512-rD0tnct/yPEtoOc8eeFHIf8ZJJJEzLkmqLs8HZWSkt3w9VYWngqLXZxiDGqv0ngXjunAlC/Hpq+ULMVOvOnByw==} engines: {node: '>=18'} - '@react-native/babel-preset@0.74.84': - resolution: {integrity: sha512-WUfu6Y4aGuVdocQZvx33BJiQWFH6kRCHYbZfBn2psgFrSRLgQWEQrDCxqPFObNAVSayM0rNhp2FvI5K/Eyeqlg==} + '@react-native/babel-preset@0.74.87': + resolution: {integrity: sha512-hyKpfqzN2nxZmYYJ0tQIHG99FQO0OWXp/gVggAfEUgiT+yNKas1C60LuofUsK7cd+2o9jrpqgqW4WzEDZoBlTg==} engines: {node: '>=18'} peerDependencies: '@babel/core': '*' @@ -2643,8 +2627,8 @@ packages: peerDependencies: '@babel/core': '*' - '@react-native/codegen@0.74.84': - resolution: {integrity: sha512-0hXlnu9i0o8v+gXKQi+x6T471L85kCDwW4WrJiYAeOheWrQdNNW6rC3g8+LL7HXAf7QcHGU/8/d57iYfdVK2BQ==} + '@react-native/codegen@0.74.87': + resolution: {integrity: sha512-GMSYDiD+86zLKgMMgz9z0k6FxmRn+z6cimYZKkucW4soGbxWsbjUAZoZ56sJwt2FJ3XVRgXCrnOCgXoH/Bkhcg==} engines: {node: '>=18'} peerDependencies: '@babel/preset-env': ^7.1.6 @@ -2664,16 +2648,16 @@ packages: '@react-native-community/cli': optional: true - '@react-native/debugger-frontend@0.74.84': - resolution: {integrity: sha512-YUEA03UNFbiYzHpYxlcS2D9+3eNT5YLGkl5yRg3nOSN6KbCc/OttGnNZme+tuSOJwjMN/vcvtDKYkTqjJw8U0A==} + '@react-native/debugger-frontend@0.74.85': + resolution: {integrity: sha512-gUIhhpsYLUTYWlWw4vGztyHaX/kNlgVspSvKe2XaPA7o3jYKUoNLc3Ov7u70u/MBWfKdcEffWq44eSe3j3s5JQ==} engines: {node: '>=18'} '@react-native/debugger-frontend@0.78.1': resolution: {integrity: sha512-xev/B++QLxSDpEBWsc74GyCuq9XOHYTBwcGSpsuhOJDUha6WZIbEEvZe3LpVW+OiFso4oGIdnVSQntwippZdWw==} engines: {node: '>=18'} - '@react-native/dev-middleware@0.74.84': - resolution: {integrity: sha512-veYw/WmyrAOQHUiIeULzn2duJQnXDPiKq2jZ/lcmDo6jsLirpp+Q73lx09TYgy/oVoPRuV0nfmU3x9B6EV/7qQ==} + '@react-native/dev-middleware@0.74.85': + resolution: {integrity: sha512-BRmgCK5vnMmHaKRO+h8PKJmHHH3E6JFuerrcfE3wG2eZ1bcSr+QTu8DAlpxsDWvJvHpCi8tRJGauxd+Ssj/c7w==} engines: {node: '>=18'} '@react-native/dev-middleware@0.78.1': @@ -2694,8 +2678,8 @@ packages: peerDependencies: '@babel/core': '*' - '@react-native/normalize-colors@0.74.84': - resolution: {integrity: sha512-Y5W6x8cC5RuakUcTVUFNAIhUZ/tYpuqHZlRBoAuakrTwVuoNHXfQki8lj1KsYU7rW6e3VWgdEx33AfOQpdNp6A==} + '@react-native/normalize-colors@0.74.85': + resolution: {integrity: sha512-pcE4i0X7y3hsAE0SpIl7t6dUc0B0NZLd1yv7ssm4FrLhWG+CGyIq4eFDXpmPU1XHmL5PPySxTAjEMiwv6tAmOw==} '@react-native/normalize-colors@0.78.1': resolution: {integrity: sha512-h4wARnY4iBFgigN1NjnaKFtcegWwQyE9+CEBVG4nHmwMtr8lZBmc7ZKIM6hUc6lxqY/ugHg48aSQSynss7mJUg==} @@ -2711,6 +2695,34 @@ packages: '@types/react': optional: true + '@redis/bloom@5.0.1': + resolution: {integrity: sha512-F7L+rnuJvq/upKaVoEgsf8VT7g5pLQYWRqSUOV3uO4vpVtARzSKJ7CLyJjVsQS+wZVCGxsLMh8DwAIDcny1B+g==} + engines: {node: '>= 18'} + peerDependencies: + '@redis/client': ^5.0.1 + + '@redis/client@5.0.1': + resolution: {integrity: sha512-k0EJvlMGEyBqUD3orKe0UMZ66fPtfwqPIr+ZSd853sXj2EyhNtPXSx+J6sENXJNgAlEBhvD+57Dwt0qTisKB0A==} + engines: {node: '>= 18'} + + '@redis/json@5.0.1': + resolution: {integrity: sha512-t94HOTk5myfhvaHZzlUzk2hoUvH2jsjftcnMgJWuHL/pzjAJQoZDCUJzjkoXIUjWXuyJixTguaaDyOZWwqH2Kg==} + engines: {node: '>= 18'} + peerDependencies: + '@redis/client': ^5.0.1 + + '@redis/search@5.0.1': + resolution: {integrity: sha512-wipK6ZptY7K68B7YLVhP5I/wYCDUU+mDJMyJiUcQLuOs7/eKOBc8lTXKUSssor8QnzZSPy4A5ulcC5PZY22Zgw==} + engines: {node: '>= 18'} + peerDependencies: + '@redis/client': ^5.0.1 + + '@redis/time-series@5.0.1': + resolution: {integrity: sha512-k6PgbrakhnohsEWEAdQZYt3e5vSKoIzpKvgQt8//lnWLrTZx+c3ed2sj0+pKIF4FvnSeuXLo4bBWcH0Z7Urg1A==} + engines: {node: '>= 18'} + peerDependencies: + '@redis/client': ^5.0.1 + '@rnx-kit/chromium-edge-launcher@1.0.0': resolution: {integrity: sha512-lzD84av1ZQhYUS+jsGqJiCMaJO2dn9u+RTT9n9q6D3SaKVwWqv+7AoRKqBu19bkwyE+iFRl1ymr40QS90jVFYg==} engines: {node: '>=14.15'} @@ -2789,7 +2801,6 @@ packages: '@sphereon/kmp-mdoc-core@0.2.0-SNAPSHOT.26': resolution: {integrity: sha512-QXJ6R8ENiZV2rPMbn06cw5JKwqUYN1kzVRbYfONqE1PEXx1noQ4md7uxr2zSczi0ubKkNcbyYDNtIMTZIhGzmQ==} - bundledDependencies: [] '@sphereon/pex-models@2.3.2': resolution: {integrity: sha512-foFxfLkRwcn/MOp/eht46Q7wsvpQGlO7aowowIIb5Tz9u97kYZ2kz6K2h2ODxWuv5CRA7Q0MY8XUBGE2lfOhOQ==} @@ -2821,9 +2832,6 @@ packages: '@stablelib/int@2.0.1': resolution: {integrity: sha512-Ht63fQp3wz/F8U4AlXEPb7hfJOIILs8Lq55jgtD7KueWtyjhVuzcsGLSTAWtZs3XJDZYdF1WcSKn+kBtbzupww==} - '@stablelib/random@1.0.0': - resolution: {integrity: sha512-G9vwwKrNCGMI/uHL6XeWe2Nk4BuxkYyWZagGaDU9wrsuV+9hUwNI1lok2WVo8uJDa2zx7ahNwN7Ij983hOUFEw==} - '@stablelib/random@1.0.2': resolution: {integrity: sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==} @@ -2931,6 +2939,9 @@ packages: '@types/istanbul-lib-report@3.0.3': resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + '@types/istanbul-reports@1.1.2': + resolution: {integrity: sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==} + '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} @@ -3033,6 +3044,9 @@ packages: '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + '@types/yargs@13.0.12': + resolution: {integrity: sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ==} + '@types/yargs@15.0.19': resolution: {integrity: sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==} @@ -3189,14 +3203,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-back@3.1.0: - resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} - engines: {node: '>=6'} - - array-back@4.0.2: - resolution: {integrity: sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==} - engines: {node: '>=8'} - array-buffer-byte-length@1.0.1: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} engines: {node: '>= 0.4'} @@ -3311,6 +3317,9 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-react-compiler@0.0.0-experimental-fe484b5-20240912: + resolution: {integrity: sha512-iGtEbwQeiLXba8o8ESTjogmQ8rTP6xHi+w3JIxR8HmKAb+SYZ3cljRhpOEsrxZIXuk3L9w9o98BJFIcxaVyFag==} + babel-plugin-react-native-web@0.19.12: resolution: {integrity: sha512-eYZ4+P6jNcB37lObWIg0pUbi7+3PKoU1Oie2j0C8UF3cXyXoR74tO2NBjI/FORb2LJyItJZEAmjU5pSaJYEL1w==} @@ -3328,8 +3337,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - babel-preset-expo@11.0.10: - resolution: {integrity: sha512-YBg40Om31gw9IPlRw5v8elzgtPUtNEh4GSibBi5MsmmYddGg4VPjWtCZIFJChN543qRmbGb/fa/kejvLX567hQ==} + babel-preset-expo@11.0.14: + resolution: {integrity: sha512-4BVYR0Sc2sSNxYTiE/OLSnPiOp+weFNy8eV+hX3aD6YAIbBnw+VubKRWqJV/sOJauzOLz0SgYAYyFciYMqizRA==} babel-preset-fbjs@3.4.0: resolution: {integrity: sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==} @@ -3348,9 +3357,6 @@ packages: base-64@0.1.0: resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==} - base-x@3.0.9: - resolution: {integrity: sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -3441,9 +3447,6 @@ packages: resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} engines: {node: '>= 6'} - bs58@4.0.1: - resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} - bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -3629,6 +3632,10 @@ packages: resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} engines: {node: '>=0.8'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + co-body@6.2.0: resolution: {integrity: sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==} engines: {node: '>=8.0.0'} @@ -3667,18 +3674,6 @@ packages: command-exists@1.2.9: resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} - command-line-args@5.2.1: - resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} - engines: {node: '>=4.0.0'} - - command-line-commands@3.0.2: - resolution: {integrity: sha512-ac6PdCtdR6q7S3HN+JiVLIWGHY30PRYIEl2qPo+FuEuzwAUk0UYyimrngrg7FvF/mCr4Jgoqv5ZnHZgads50rw==} - engines: {node: '>=8'} - - command-line-usage@6.1.3: - resolution: {integrity: sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==} - engines: {node: '>=8.0.0'} - commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -3976,6 +3971,10 @@ packages: denodeify@1.2.1: resolution: {integrity: sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg==} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -4224,8 +4223,8 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - expo-asset@10.0.9: - resolution: {integrity: sha512-KX7LPtVf9eeMidUvYZafXZldrVdzfjZNKKFAjFvDy2twg7sTa2R0L4VdCXp32eGLWZyk+i/rpOUSbyD1YFyJnA==} + expo-asset@10.0.10: + resolution: {integrity: sha512-0qoTIihB79k+wGus9wy0JMKq7DdenziVx3iUkGvMAy2azscSgWH6bd2gJ9CGnhC6JRd3qTMFBL0ou/fx7WZl7A==} peerDependencies: expo: '*' @@ -4239,8 +4238,8 @@ packages: peerDependencies: expo: '*' - expo-font@12.0.7: - resolution: {integrity: sha512-rbSdpjtT/A3M+u9xchR9tdD+5VGSxptUis7ngX5zfAVp3O5atOcPNSA82Jeo15HkrQE+w/upfFBOvi56lsGdsQ==} + expo-font@12.0.9: + resolution: {integrity: sha512-seTCyf0tbgkAnp3ZI9ZfK9QVtURQUgFnuj+GuJ5TSnN0XsOtVe1s2RxTvmMgkfuvfkzcjJ69gyRpsZS1cC8hjw==} peerDependencies: expo: '*' @@ -4253,12 +4252,12 @@ packages: resolution: {integrity: sha512-azkCRYj/DxbK4udDuDxA9beYzQTwpJ5a9QA0bBgha2jHtWdFGF4ZZWSY+zNA5mtU3KqzYt8jWHfoqgSvKyu1Aw==} hasBin: true - expo-modules-autolinking@1.11.1: - resolution: {integrity: sha512-2dy3lTz76adOl7QUvbreMCrXyzUiF8lygI7iFJLjgIQIVH+43KnFWE5zBumpPbkiaq0f0uaFpN9U0RGQbnKiMw==} + expo-modules-autolinking@1.11.2: + resolution: {integrity: sha512-fdcaNO8ucHA3yLNY52ZUENBcAG7KEx8QyMmnVNavO1JVBGRMZG8JyVcbrhYQDtVtpxkbai5YzwvLutINvbDZDQ==} hasBin: true - expo-modules-core@1.12.15: - resolution: {integrity: sha512-VjDPIgUyhCZzf692NF4p2iFTsKAQMcU3jc0pg33eNvN/kdrJqkeucqCDuuwoNxg0vIBKtoqAJDuPnWiemldsTg==} + expo-modules-core@1.12.21: + resolution: {integrity: sha512-UQxRljqPcowS1+bECW9tnuVGfvWL18GAKPiKMnu9sZwJssAN9FU/JhED50DJzdzICLR0hL17FZAgV4rbMG3IWQ==} expo-random@14.0.1: resolution: {integrity: sha512-gX2mtR9o+WelX21YizXUCD/y+a4ZL+RDthDmFkHxaYbdzjSYTn8u/igoje/l3WEO+/RYspmqUFa8w/ckNbt6Vg==} @@ -4266,8 +4265,8 @@ packages: peerDependencies: expo: '*' - expo@51.0.14: - resolution: {integrity: sha512-99BAMSYBH1aq1TIEJqM03kRpsZjN8OqZXDqYHRq9/PXT67axRUOvRjwMMLprnCmqkAVM7m7FpiECNWN4U0gvLQ==} + expo@51.0.29: + resolution: {integrity: sha512-bW8JR3RAw5hQhEGbwDqO3UxtjEq8noCYfqQ9v3aUfdtCoWtAp4jwB+xtwfDZPvRh1b8ebSJ/WI2jK/RljZw3mA==} hasBin: true exponential-backoff@3.1.2: @@ -4384,10 +4383,6 @@ packages: resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} engines: {node: '>=6'} - find-replace@3.0.0: - resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} - engines: {node: '>=4.0.0'} - find-up@3.0.0: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} @@ -4561,9 +4556,6 @@ packages: resolution: {integrity: sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==} engines: {node: '>=6'} - git-config@0.0.7: - resolution: {integrity: sha512-LidZlYZXWzVjS+M3TEwhtYBaYwLeOZrXci1tBgqp/vDdZTBMl02atvwb6G35L64ibscYoPnxfbwwUS+VZAISLA==} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -4624,11 +4616,6 @@ packages: resolution: {integrity: sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==} engines: {node: '>= 10.x'} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} - engines: {node: '>=0.4.7'} - hasBin: true - hard-rejection@2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} @@ -4794,13 +4781,6 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - iniparser@1.0.5: - resolution: {integrity: sha512-i40MWqgTU6h/70NtMsDVVDLjDYWwcIR1yIEVDPfxZIJno9z9L4s83p/V7vAu2i48Vj0gpByrkGFub7ko9XvPrw==} - - inquirer@7.3.3: - resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} - engines: {node: '>=8.0.0'} - inquirer@8.2.6: resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} engines: {node: '>=12.0.0'} @@ -4816,6 +4796,10 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ioredis@5.6.1: + resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==} + engines: {node: '>=12.22.0'} + ip-regex@2.1.0: resolution: {integrity: sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==} engines: {node: '>=4'} @@ -5246,6 +5230,11 @@ packages: '@babel/preset-env': optional: true + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -5460,12 +5449,15 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -5546,9 +5538,6 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - make-promises-safe@5.1.0: - resolution: {integrity: sha512-AfdZ49rtyhQR/6cqVKGoH7y4ql7XkS5HJI1lZm0/5N6CQosy1eYbBJ/qbhkKHzo17UH7M918Bysf6XB9f3kS1g==} - makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} @@ -5906,11 +5895,6 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - neon-cli@0.10.1: - resolution: {integrity: sha512-kOd9ELaYETe1J1nBEOYD7koAZVj6xR9TGwOPccAsWmwL5amkaXXXwXHCUHkBAWujlgSZY5f2pT+pFGkzoHExYQ==} - engines: {node: '>=8'} - hasBin: true - nested-error-stacks@2.0.1: resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} @@ -5941,6 +5925,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} @@ -6286,6 +6271,10 @@ packages: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} + pretty-format@24.9.0: + resolution: {integrity: sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==} + engines: {node: '>= 6'} + pretty-format@26.6.2: resolution: {integrity: sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==} engines: {node: '>= 10'} @@ -6349,6 +6338,9 @@ packages: pvtsutils@1.3.5: resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + pvutils@1.1.3: resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} engines: {node: '>=6.0.0'} @@ -6502,9 +6494,17 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} - reduce-flatten@2.0.0: - resolution: {integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==} - engines: {node: '>=6'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + redis@5.0.1: + resolution: {integrity: sha512-J8nqUjrfSq0E8NQkcHDZ4HdEQk5RMYjP3jZq02PE+ERiRxolbDNxPaTT4xh6tdrme+lJ86Goje9yMt9uzh23hQ==} + engines: {node: '>= 18'} ref-array-di@1.2.2: resolution: {integrity: sha512-jhCmhqWa7kvCVrWhR/d7RemkppqPUdxEil1CtTtm7FkZV8LcHHCK3Or9GinUiFP5WY3k0djUkMvhBhx49Jb2iA==} @@ -6612,9 +6612,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfc4648@1.5.2: - resolution: {integrity: sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg==} - rimraf@2.2.8: resolution: {integrity: sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -6656,10 +6653,6 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rxjs@6.6.7: - resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} - engines: {npm: '>=2.0.0'} - rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} @@ -6866,6 +6859,9 @@ packages: resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} engines: {node: '>=6'} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + static-eval@2.0.2: resolution: {integrity: sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==} @@ -7030,10 +7026,6 @@ packages: resolution: {integrity: sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==} engines: {node: '>=0.10'} - table-layout@1.0.2: - resolution: {integrity: sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==} - engines: {node: '>=8.0.0'} - tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} @@ -7132,9 +7124,6 @@ packages: resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==} engines: {node: '>=14.16'} - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -7146,6 +7135,10 @@ packages: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} + trim-right@1.0.1: + resolution: {integrity: sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==} + engines: {node: '>=0.10.0'} + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -7187,9 +7180,6 @@ packages: '@swc/wasm': optional: true - ts-typed-json@0.3.2: - resolution: {integrity: sha512-Tdu3BWzaer7R5RvBIJcg9r8HrTZgpJmsX+1meXMJzYypbkj8NK2oJN0yvm4Dp/Iv6tzFa/L5jKRmEVTga6K3nA==} - tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -7293,14 +7283,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - typical@4.0.0: - resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} - engines: {node: '>=8'} - - typical@5.2.0: - resolution: {integrity: sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==} - engines: {node: '>=8'} - ua-parser-js@1.0.38: resolution: {integrity: sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==} @@ -7310,11 +7292,6 @@ packages: deprecated: support for ECMAScript is superseded by `uglify-js` as of v3.13.0 hasBin: true - uglify-js@3.18.0: - resolution: {integrity: sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==} - engines: {node: '>=0.8.0'} - hasBin: true - uint8array-extras@1.4.0: resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} engines: {node: '>=18'} @@ -7532,13 +7509,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - - wordwrapjs@4.0.1: - resolution: {integrity: sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==} - engines: {node: '>=8.0.0'} - wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -7675,6 +7645,12 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-validation-error@2.1.0: + resolution: {integrity: sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.18.0 + zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} @@ -7705,12 +7681,12 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@animo-id/expo-secure-environment@0.1.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(react@18.3.1)': + '@animo-id/expo-secure-environment@0.1.1(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(react@18.3.1)': dependencies: '@peculiar/asn1-ecc': 2.3.14 '@peculiar/asn1-schema': 2.3.13 '@peculiar/asn1-x509': 2.3.13 - expo: 51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + expo: 51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) react: 18.3.1 react-native: 0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1) @@ -7735,11 +7711,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@animo-id/react-native-bbs-signatures@0.1.0(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(react@18.3.1)': - dependencies: - react: 18.3.1 - react-native: 0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1) - '@astronautlabs/jsonpath@1.1.2': dependencies: static-eval: 2.0.2 @@ -7778,6 +7749,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/generator@7.2.0': + dependencies: + '@babel/types': 7.26.3 + jsesc: 2.5.2 + lodash: 4.17.21 + source-map: 0.5.7 + trim-right: 1.0.1 + '@babel/generator@7.26.3': dependencies: '@babel/parser': 7.26.3 @@ -9216,11 +9195,11 @@ snapshots: '@digitalcredentials/base64url-universal': 2.0.6 pako: 2.1.0 - '@digitalcredentials/ed25519-signature-2020@3.0.2(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': + '@digitalcredentials/ed25519-signature-2020@3.0.2(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': dependencies: '@digitalcredentials/base58-universal': 1.0.1 '@digitalcredentials/ed25519-verification-key-2020': 3.2.2 - '@digitalcredentials/jsonld-signatures': 9.4.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/jsonld-signatures': 9.4.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) ed25519-signature-2018-context: 1.1.0 ed25519-signature-2020-context: 1.1.0 transitivePeerDependencies: @@ -9244,12 +9223,12 @@ snapshots: - domexception - web-streams-polyfill - '@digitalcredentials/jsonld-signatures@9.4.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': + '@digitalcredentials/jsonld-signatures@9.4.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': dependencies: '@digitalbazaar/security-context': 1.0.1 - '@digitalcredentials/jsonld': 6.0.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/jsonld': 6.0.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) fast-text-encoding: 1.0.6 - isomorphic-webcrypto: 2.3.8(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1)) + isomorphic-webcrypto: 2.3.8(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1)) serialize-error: 8.1.0 transitivePeerDependencies: - domexception @@ -9257,10 +9236,10 @@ snapshots: - react-native - web-streams-polyfill - '@digitalcredentials/jsonld@5.2.2(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': + '@digitalcredentials/jsonld@5.2.2(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': dependencies: '@digitalcredentials/http-client': 1.2.2(web-streams-polyfill@3.3.3) - '@digitalcredentials/rdf-canonize': 1.0.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1)) + '@digitalcredentials/rdf-canonize': 1.0.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1)) canonicalize: 1.0.8 lru-cache: 6.0.0 transitivePeerDependencies: @@ -9269,10 +9248,10 @@ snapshots: - react-native - web-streams-polyfill - '@digitalcredentials/jsonld@6.0.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': + '@digitalcredentials/jsonld@6.0.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': dependencies: '@digitalcredentials/http-client': 1.2.2(web-streams-polyfill@3.3.3) - '@digitalcredentials/rdf-canonize': 1.0.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1)) + '@digitalcredentials/rdf-canonize': 1.0.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1)) canonicalize: 1.0.8 lru-cache: 6.0.0 transitivePeerDependencies: @@ -9283,19 +9262,19 @@ snapshots: '@digitalcredentials/open-badges-context@2.1.0': {} - '@digitalcredentials/rdf-canonize@1.0.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))': + '@digitalcredentials/rdf-canonize@1.0.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))': dependencies: fast-text-encoding: 1.0.6 - isomorphic-webcrypto: 2.3.8(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1)) + isomorphic-webcrypto: 2.3.8(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1)) transitivePeerDependencies: - expo - react-native - '@digitalcredentials/vc-status-list@5.0.2(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': + '@digitalcredentials/vc-status-list@5.0.2(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': dependencies: '@digitalbazaar/vc-status-list-context': 3.1.1 '@digitalcredentials/bitstring': 2.0.1 - '@digitalcredentials/vc': 4.2.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/vc': 4.2.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) credentials-context: 2.0.0 transitivePeerDependencies: - domexception @@ -9303,10 +9282,10 @@ snapshots: - react-native - web-streams-polyfill - '@digitalcredentials/vc@4.2.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': + '@digitalcredentials/vc@4.2.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': dependencies: - '@digitalcredentials/jsonld': 5.2.2(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) - '@digitalcredentials/jsonld-signatures': 9.4.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/jsonld': 5.2.2(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/jsonld-signatures': 9.4.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) credentials-context: 2.0.0 transitivePeerDependencies: - domexception @@ -9314,14 +9293,14 @@ snapshots: - react-native - web-streams-polyfill - '@digitalcredentials/vc@6.0.1(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': + '@digitalcredentials/vc@6.0.1(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3)': dependencies: '@digitalbazaar/vc-status-list': 7.1.0(web-streams-polyfill@3.3.3) - '@digitalcredentials/ed25519-signature-2020': 3.0.2(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) - '@digitalcredentials/jsonld': 6.0.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) - '@digitalcredentials/jsonld-signatures': 9.4.0(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/ed25519-signature-2020': 3.0.2(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/jsonld': 6.0.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/jsonld-signatures': 9.4.0(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) '@digitalcredentials/open-badges-context': 2.1.0 - '@digitalcredentials/vc-status-list': 5.0.2(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) + '@digitalcredentials/vc-status-list': 5.0.2(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1))(web-streams-polyfill@3.3.3) credentials-context: 2.0.0 fix-esm: 1.0.1 transitivePeerDependencies: @@ -9410,25 +9389,25 @@ snapshots: mv: 2.1.1 safe-json-stringify: 1.2.0 - '@expo/cli@0.18.19(expo-modules-autolinking@1.11.1)': + '@expo/cli@0.18.29(expo-modules-autolinking@1.11.2)': dependencies: '@babel/runtime': 7.26.10 '@expo/code-signing-certificates': 0.0.5 - '@expo/config': 9.0.1 - '@expo/config-plugins': 8.0.5 + '@expo/config': 9.0.3 + '@expo/config-plugins': 8.0.8 '@expo/devcert': 1.1.2 '@expo/env': 0.3.0 '@expo/image-utils': 0.5.1 '@expo/json-file': 8.3.3 - '@expo/metro-config': 0.18.7 + '@expo/metro-config': 0.18.11 '@expo/osascript': 2.1.3 '@expo/package-manager': 1.5.2 '@expo/plist': 0.1.3 - '@expo/prebuild-config': 7.0.6(expo-modules-autolinking@1.11.1) + '@expo/prebuild-config': 7.0.8(expo-modules-autolinking@1.11.2) '@expo/rudder-sdk-node': 1.1.1 '@expo/spawn-async': 1.7.2 '@expo/xcpretty': 4.3.1 - '@react-native/dev-middleware': 0.74.84 + '@react-native/dev-middleware': 0.74.85 '@urql/core': 2.3.6(graphql@15.8.0) '@urql/exchange-retry': 0.3.0(graphql@15.8.0) accepts: 1.3.8 @@ -9501,7 +9480,7 @@ snapshots: node-forge: 1.3.1 nullthrows: 1.1.1 - '@expo/config-plugins@8.0.5': + '@expo/config-plugins@8.0.8': dependencies: '@expo/config-types': 51.0.1 '@expo/json-file': 8.3.3 @@ -9523,10 +9502,10 @@ snapshots: '@expo/config-types@51.0.1': {} - '@expo/config@9.0.1': + '@expo/config@9.0.3': dependencies: '@babel/code-frame': 7.10.4 - '@expo/config-plugins': 8.0.5 + '@expo/config-plugins': 8.0.8 '@expo/config-types': 51.0.1 '@expo/json-file': 8.3.3 getenv: 1.0.0 @@ -9588,13 +9567,13 @@ snapshots: json5: 2.2.3 write-file-atomic: 2.4.3 - '@expo/metro-config@0.18.7': + '@expo/metro-config@0.18.11': dependencies: '@babel/core': 7.26.0 '@babel/generator': 7.26.3 '@babel/parser': 7.26.3 '@babel/types': 7.26.3 - '@expo/config': 9.0.1 + '@expo/config': 9.0.3 '@expo/env': 0.3.0 '@expo/json-file': 8.3.3 '@expo/spawn-async': 1.7.2 @@ -9637,16 +9616,16 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 14.0.0 - '@expo/prebuild-config@7.0.6(expo-modules-autolinking@1.11.1)': + '@expo/prebuild-config@7.0.8(expo-modules-autolinking@1.11.2)': dependencies: - '@expo/config': 9.0.1 - '@expo/config-plugins': 8.0.5 + '@expo/config': 9.0.3 + '@expo/config-plugins': 8.0.8 '@expo/config-types': 51.0.1 '@expo/image-utils': 0.5.1 '@expo/json-file': 8.3.3 - '@react-native/normalize-colors': 0.74.84 + '@react-native/normalize-colors': 0.74.85 debug: 4.4.0 - expo-modules-autolinking: 1.11.1 + expo-modules-autolinking: 1.11.2 fs-extra: 9.1.0 resolve-from: 5.0.0 semver: 7.6.2 @@ -9726,6 +9705,8 @@ snapshots: '@hyperledger/indy-vdr-shared@0.2.2': {} + '@ioredis/commands@1.2.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -9908,6 +9889,12 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/types@24.9.0': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 1.1.2 + '@types/yargs': 13.0.12 + '@jest/types@26.6.2': dependencies: '@types/istanbul-lib-coverage': 2.0.6 @@ -10016,42 +10003,6 @@ snapshots: - encoding - supports-color - '@mattrglobal/bbs-signatures@1.3.1': - dependencies: - '@stablelib/random': 1.0.0 - optionalDependencies: - '@mattrglobal/node-bbs-signatures': 0.18.1 - transitivePeerDependencies: - - encoding - - supports-color - - '@mattrglobal/bbs-signatures@1.4.0': - dependencies: - '@stablelib/random': 1.0.0 - optionalDependencies: - '@mattrglobal/node-bbs-signatures': 0.18.1 - transitivePeerDependencies: - - encoding - - supports-color - - '@mattrglobal/bls12381-key-pair@1.2.1': - dependencies: - '@mattrglobal/bbs-signatures': 1.3.1 - bs58: 4.0.1 - rfc4648: 1.5.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@mattrglobal/node-bbs-signatures@0.18.1': - dependencies: - '@mapbox/node-pre-gyp': 1.0.11 - neon-cli: 0.10.1 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - '@mswjs/interceptors@0.37.5': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -10098,24 +10049,24 @@ snapshots: '@open-draft/until@2.1.0': {} - '@openid4vc/oauth2@0.3.0-alpha-20250330133535': + '@openid4vc/oauth2@0.3.0-alpha-20250511195407': dependencies: - '@openid4vc/utils': 0.3.0-alpha-20250330133535 + '@openid4vc/utils': 0.3.0-alpha-20250511195407 zod: 3.24.2 - '@openid4vc/openid4vci@0.3.0-alpha-20250330133535': + '@openid4vc/openid4vci@0.3.0-alpha-20250511195407': dependencies: - '@openid4vc/oauth2': 0.3.0-alpha-20250330133535 - '@openid4vc/utils': 0.3.0-alpha-20250330133535 + '@openid4vc/oauth2': 0.3.0-alpha-20250511195407 + '@openid4vc/utils': 0.3.0-alpha-20250511195407 zod: 3.24.2 - '@openid4vc/openid4vp@0.3.0-alpha-20250330133535': + '@openid4vc/openid4vp@0.3.0-alpha-20250511195407': dependencies: - '@openid4vc/oauth2': 0.3.0-alpha-20250330133535 - '@openid4vc/utils': 0.3.0-alpha-20250330133535 + '@openid4vc/oauth2': 0.3.0-alpha-20250511195407 + '@openid4vc/utils': 0.3.0-alpha-20250511195407 zod: 3.24.2 - '@openid4vc/utils@0.3.0-alpha-20250330133535': + '@openid4vc/utils@0.3.0-alpha-20250511195407': dependencies: buffer: 6.0.3 zod: 3.24.2 @@ -10162,7 +10113,7 @@ snapshots: dependencies: '@peculiar/asn1-cms': 2.3.13 '@peculiar/asn1-pkcs8': 2.3.13 - '@peculiar/asn1-rsa': 2.3.13 + '@peculiar/asn1-rsa': 2.3.15 '@peculiar/asn1-schema': 2.3.13 asn1js: 3.0.5 tslib: 2.8.1 @@ -10185,10 +10136,10 @@ snapshots: asn1js: 3.0.5 tslib: 2.8.1 - '@peculiar/asn1-rsa@2.3.13': + '@peculiar/asn1-rsa@2.3.15': dependencies: - '@peculiar/asn1-schema': 2.3.13 - '@peculiar/asn1-x509': 2.3.13 + '@peculiar/asn1-schema': 2.3.15 + '@peculiar/asn1-x509': 2.3.15 asn1js: 3.0.5 tslib: 2.8.1 @@ -10198,6 +10149,12 @@ snapshots: pvtsutils: 1.3.5 tslib: 2.8.1 + '@peculiar/asn1-schema@2.3.15': + dependencies: + asn1js: 3.0.5 + pvtsutils: 1.3.6 + tslib: 2.8.1 + '@peculiar/asn1-x509-attr@2.3.13': dependencies: '@peculiar/asn1-schema': 2.3.13 @@ -10213,6 +10170,13 @@ snapshots: pvtsutils: 1.3.5 tslib: 2.8.1 + '@peculiar/asn1-x509@2.3.15': + dependencies: + '@peculiar/asn1-schema': 2.3.15 + asn1js: 3.0.5 + pvtsutils: 1.3.6 + tslib: 2.8.1 + '@peculiar/json-schema@1.1.12': dependencies: tslib: 2.8.1 @@ -10231,7 +10195,7 @@ snapshots: '@peculiar/asn1-csr': 2.3.13 '@peculiar/asn1-ecc': 2.3.14 '@peculiar/asn1-pkcs9': 2.3.13 - '@peculiar/asn1-rsa': 2.3.13 + '@peculiar/asn1-rsa': 2.3.15 '@peculiar/asn1-schema': 2.3.13 '@peculiar/asn1-x509': 2.3.13 pvtsutils: 1.3.5 @@ -10436,9 +10400,9 @@ snapshots: '@react-native/assets-registry@0.78.1': {} - '@react-native/babel-plugin-codegen@0.74.84(@babel/preset-env@7.26.0(@babel/core@7.26.0))': + '@react-native/babel-plugin-codegen@0.74.87(@babel/preset-env@7.26.0(@babel/core@7.26.0))': dependencies: - '@react-native/codegen': 0.74.84(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + '@react-native/codegen': 0.74.87(@babel/preset-env@7.26.0(@babel/core@7.26.0)) transitivePeerDependencies: - '@babel/preset-env' - supports-color @@ -10451,7 +10415,7 @@ snapshots: - '@babel/preset-env' - supports-color - '@react-native/babel-preset@0.74.84(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))': + '@react-native/babel-preset@0.74.87(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.26.0) @@ -10493,7 +10457,7 @@ snapshots: '@babel/plugin-transform-typescript': 7.26.8(@babel/core@7.26.0) '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) '@babel/template': 7.25.9 - '@react-native/babel-plugin-codegen': 0.74.84(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + '@react-native/babel-plugin-codegen': 0.74.87(@babel/preset-env@7.26.0(@babel/core@7.26.0)) babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.26.0) react-refresh: 0.14.2 transitivePeerDependencies: @@ -10551,7 +10515,7 @@ snapshots: - '@babel/preset-env' - supports-color - '@react-native/codegen@0.74.84(@babel/preset-env@7.26.0(@babel/core@7.26.0))': + '@react-native/codegen@0.74.87(@babel/preset-env@7.26.0(@babel/core@7.26.0))': dependencies: '@babel/parser': 7.26.3 '@babel/preset-env': 7.26.0(@babel/core@7.26.0) @@ -10598,14 +10562,14 @@ snapshots: - supports-color - utf-8-validate - '@react-native/debugger-frontend@0.74.84': {} + '@react-native/debugger-frontend@0.74.85': {} '@react-native/debugger-frontend@0.78.1': {} - '@react-native/dev-middleware@0.74.84': + '@react-native/dev-middleware@0.74.85': dependencies: '@isaacs/ttlcache': 1.4.1 - '@react-native/debugger-frontend': 0.74.84 + '@react-native/debugger-frontend': 0.74.85 '@rnx-kit/chromium-edge-launcher': 1.0.0 chrome-launcher: 0.15.2 connect: 3.7.0 @@ -10656,7 +10620,7 @@ snapshots: - '@babel/preset-env' - supports-color - '@react-native/normalize-colors@0.74.84': {} + '@react-native/normalize-colors@0.74.85': {} '@react-native/normalize-colors@0.78.1': {} @@ -10667,6 +10631,26 @@ snapshots: react: 18.3.1 react-native: 0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1) + '@redis/bloom@5.0.1(@redis/client@5.0.1)': + dependencies: + '@redis/client': 5.0.1 + + '@redis/client@5.0.1': + dependencies: + cluster-key-slot: 1.1.2 + + '@redis/json@5.0.1(@redis/client@5.0.1)': + dependencies: + '@redis/client': 5.0.1 + + '@redis/search@5.0.1(@redis/client@5.0.1)': + dependencies: + '@redis/client': 5.0.1 + + '@redis/time-series@5.0.1(@redis/client@5.0.1)': + dependencies: + '@redis/client': 5.0.1 + '@rnx-kit/chromium-edge-launcher@1.0.0': dependencies: '@types/node': 18.18.8 @@ -10807,11 +10791,6 @@ snapshots: '@stablelib/int@2.0.1': {} - '@stablelib/random@1.0.0': - dependencies: - '@stablelib/binary': 1.0.1 - '@stablelib/wipe': 1.0.1 - '@stablelib/random@1.0.2': dependencies: '@stablelib/binary': 1.0.1 @@ -10948,6 +10927,11 @@ snapshots: dependencies: '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports@1.1.2': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-lib-report': 3.0.3 + '@types/istanbul-reports@3.0.4': dependencies: '@types/istanbul-lib-report': 3.0.3 @@ -11071,6 +11055,10 @@ snapshots: '@types/yargs-parser@21.0.3': {} + '@types/yargs@13.0.12': + dependencies: + '@types/yargs-parser': 21.0.3 + '@types/yargs@15.0.19': dependencies: '@types/yargs-parser': 21.0.3 @@ -11218,12 +11206,6 @@ snapshots: argparse@2.0.1: {} - array-back@3.1.0: - optional: true - - array-back@4.0.2: - optional: true - array-buffer-byte-length@1.0.1: dependencies: call-bind: 1.0.7 @@ -11378,6 +11360,16 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-react-compiler@0.0.0-experimental-fe484b5-20240912: + dependencies: + '@babel/generator': 7.2.0 + '@babel/types': 7.26.3 + chalk: 4.1.2 + invariant: 2.2.4 + pretty-format: 24.9.0 + zod: 3.24.2 + zod-validation-error: 2.1.0(zod@3.24.2) + babel-plugin-react-native-web@0.19.12: {} babel-plugin-syntax-hermes-parser@0.25.1: @@ -11409,7 +11401,7 @@ snapshots: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) - babel-preset-expo@11.0.10(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)): + babel-preset-expo@11.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)): dependencies: '@babel/plugin-proposal-decorators': 7.24.7(@babel/core@7.26.0) '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.0) @@ -11417,7 +11409,8 @@ snapshots: '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) '@babel/preset-react': 7.24.7(@babel/core@7.26.0) '@babel/preset-typescript': 7.24.7(@babel/core@7.26.0) - '@react-native/babel-preset': 0.74.84(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + '@react-native/babel-preset': 0.74.87(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + babel-plugin-react-compiler: 0.0.0-experimental-fe484b5-20240912 babel-plugin-react-native-web: 0.19.12 react-refresh: 0.14.2 transitivePeerDependencies: @@ -11469,10 +11462,6 @@ snapshots: base-64@0.1.0: {} - base-x@3.0.9: - dependencies: - safe-buffer: 5.2.1 - base64-js@1.5.1: {} base64url-universal@1.1.0: @@ -11582,10 +11571,6 @@ snapshots: dependencies: fast-json-stable-stringify: 2.1.0 - bs58@4.0.1: - dependencies: - base-x: 3.0.9 - bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -11783,6 +11768,8 @@ snapshots: clone@2.1.2: {} + cluster-key-slot@1.1.2: {} + co-body@6.2.0: dependencies: '@hapi/bourne': 3.0.0 @@ -11818,27 +11805,6 @@ snapshots: command-exists@1.2.9: {} - command-line-args@5.2.1: - dependencies: - array-back: 3.1.0 - find-replace: 3.0.0 - lodash.camelcase: 4.3.0 - typical: 4.0.0 - optional: true - - command-line-commands@3.0.2: - dependencies: - array-back: 4.0.2 - optional: true - - command-line-usage@6.1.3: - dependencies: - array-back: 4.0.2 - chalk: 2.4.2 - table-layout: 1.0.2 - typical: 5.2.0 - optional: true - commander@12.1.0: {} commander@2.13.0: @@ -12137,6 +12103,8 @@ snapshots: denodeify@1.2.1: optional: true + denque@2.1.0: {} + depd@1.1.2: {} depd@2.0.0: {} @@ -12445,35 +12413,35 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - expo-asset@10.0.9(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))): + expo-asset@10.0.10(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))): dependencies: - expo: 51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) - expo-constants: 16.0.2(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) + expo: 51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + expo-constants: 16.0.2(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) invariant: 2.2.4 md5-file: 3.2.3 transitivePeerDependencies: - supports-color - expo-constants@16.0.2(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))): + expo-constants@16.0.2(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))): dependencies: - '@expo/config': 9.0.1 + '@expo/config': 9.0.3 '@expo/env': 0.3.0 - expo: 51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + expo: 51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) transitivePeerDependencies: - supports-color - expo-file-system@17.0.1(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))): + expo-file-system@17.0.1(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))): dependencies: - expo: 51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + expo: 51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) - expo-font@12.0.7(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))): + expo-font@12.0.9(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))): dependencies: - expo: 51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + expo: 51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) fontfaceobserver: 2.3.0 - expo-keep-awake@13.0.2(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))): + expo-keep-awake@13.0.2(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))): dependencies: - expo: 51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + expo: 51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) expo-modules-autolinking@0.0.3: dependencies: @@ -12484,39 +12452,41 @@ snapshots: fs-extra: 9.1.0 optional: true - expo-modules-autolinking@1.11.1: + expo-modules-autolinking@1.11.2: dependencies: chalk: 4.1.2 commander: 7.2.0 fast-glob: 3.3.2 find-up: 5.0.0 fs-extra: 9.1.0 + require-from-string: 2.0.2 + resolve-from: 5.0.0 - expo-modules-core@1.12.15: + expo-modules-core@1.12.21: dependencies: invariant: 2.2.4 - expo-random@14.0.1(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))): + expo-random@14.0.1(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))): dependencies: base64-js: 1.5.1 - expo: 51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + expo: 51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) optional: true - expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)): + expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)): dependencies: '@babel/runtime': 7.26.10 - '@expo/cli': 0.18.19(expo-modules-autolinking@1.11.1) - '@expo/config': 9.0.1 - '@expo/config-plugins': 8.0.5 - '@expo/metro-config': 0.18.7 + '@expo/cli': 0.18.29(expo-modules-autolinking@1.11.2) + '@expo/config': 9.0.3 + '@expo/config-plugins': 8.0.8 + '@expo/metro-config': 0.18.11 '@expo/vector-icons': 14.0.2 - babel-preset-expo: 11.0.10(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) - expo-asset: 10.0.9(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) - expo-file-system: 17.0.1(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) - expo-font: 12.0.7(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) - expo-keep-awake: 13.0.2(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) - expo-modules-autolinking: 1.11.1 - expo-modules-core: 1.12.15 + babel-preset-expo: 11.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + expo-asset: 10.0.10(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) + expo-file-system: 17.0.1(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) + expo-font: 12.0.9(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) + expo-keep-awake: 13.0.2(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) + expo-modules-autolinking: 1.11.2 + expo-modules-core: 1.12.21 fbemitter: 3.0.0 whatwg-url-without-unicode: 8.0.0-3 transitivePeerDependencies: @@ -12698,11 +12668,6 @@ snapshots: make-dir: 2.1.0 pkg-dir: 3.0.0 - find-replace@3.0.0: - dependencies: - array-back: 3.1.0 - optional: true - find-up@3.0.0: dependencies: locate-path: 3.0.0 @@ -12885,11 +12850,6 @@ snapshots: getenv@1.0.0: {} - git-config@0.0.7: - dependencies: - iniparser: 1.0.5 - optional: true - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -12982,16 +12942,6 @@ snapshots: graphql@15.8.0: {} - handlebars@4.7.8: - dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.18.0 - optional: true - hard-rejection@2.1.0: {} has-bigints@1.0.2: {} @@ -13147,26 +13097,6 @@ snapshots: ini@1.3.8: {} - iniparser@1.0.5: - optional: true - - inquirer@7.3.3: - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - run-async: 2.4.1 - rxjs: 6.6.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - optional: true - inquirer@8.2.6: dependencies: ansi-escapes: 4.3.2 @@ -13200,6 +13130,20 @@ snapshots: dependencies: loose-envify: 1.4.0 + ioredis@5.6.1: + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.4.0 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-regex@2.1.0: {} ipaddr.js@1.9.1: {} @@ -13349,7 +13293,7 @@ snapshots: isobject@3.0.1: {} - isomorphic-webcrypto@2.3.8(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1)): + isomorphic-webcrypto@2.3.8(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)))(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1)): dependencies: '@peculiar/webcrypto': 1.5.0 asmcrypto.js: 0.22.0 @@ -13361,7 +13305,7 @@ snapshots: optionalDependencies: '@unimodules/core': 7.1.2 '@unimodules/react-native-adapter': 6.3.9 - expo-random: 14.0.1(expo@51.0.14(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) + expo-random: 14.0.1(expo@51.0.29(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) react-native-securerandom: 0.1.1(react-native@0.78.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli@10.2.7(@babel/core@7.26.0))(react@18.3.1)) transitivePeerDependencies: - expo @@ -13846,6 +13790,8 @@ snapshots: transitivePeerDependencies: - supports-color + jsesc@2.5.2: {} + jsesc@3.0.2: {} jsesc@3.1.0: {} @@ -14066,11 +14012,12 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.camelcase@4.3.0: - optional: true - lodash.debounce@4.0.8: {} + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} @@ -14143,9 +14090,6 @@ snapshots: make-error@1.3.6: {} - make-promises-safe@5.1.0: - optional: true - makeerror@1.0.12: dependencies: tmpl: 1.0.5 @@ -14790,24 +14734,6 @@ snapshots: neo-async@2.6.2: {} - neon-cli@0.10.1: - dependencies: - chalk: 4.1.2 - command-line-args: 5.2.1 - command-line-commands: 3.0.2 - command-line-usage: 6.1.3 - git-config: 0.0.7 - handlebars: 4.7.8 - inquirer: 7.3.3 - make-promises-safe: 5.1.0 - rimraf: 3.0.2 - semver: 7.6.2 - toml: 3.0.0 - ts-typed-json: 0.3.2 - validate-npm-package-license: 3.0.4 - validate-npm-package-name: 3.0.0 - optional: true - nested-error-stacks@2.0.1: {} next-tick@1.1.0: {} @@ -15163,6 +15089,13 @@ snapshots: pretty-bytes@5.6.0: {} + pretty-format@24.9.0: + dependencies: + '@jest/types': 24.9.0 + ansi-regex: 4.1.1 + ansi-styles: 3.2.1 + react-is: 16.13.1 + pretty-format@26.6.2: dependencies: '@jest/types': 26.6.2 @@ -15255,6 +15188,10 @@ snapshots: dependencies: tslib: 2.8.1 + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + pvutils@1.1.3: {} qrcode-terminal@0.11.0: {} @@ -15465,8 +15402,19 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 - reduce-flatten@2.0.0: - optional: true + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + redis@5.0.1: + dependencies: + '@redis/bloom': 5.0.1(@redis/client@5.0.1) + '@redis/client': 5.0.1 + '@redis/json': 5.0.1(@redis/client@5.0.1) + '@redis/search': 5.0.1(@redis/client@5.0.1) + '@redis/time-series': 5.0.1(@redis/client@5.0.1) ref-array-di@1.2.2: dependencies: @@ -15575,8 +15523,6 @@ snapshots: reusify@1.0.4: {} - rfc4648@1.5.2: {} - rimraf@2.2.8: optional: true @@ -15611,11 +15557,6 @@ snapshots: dependencies: queue-microtask: 1.2.3 - rxjs@6.6.7: - dependencies: - tslib: 1.14.1 - optional: true - rxjs@7.8.1: dependencies: tslib: 2.8.1 @@ -15853,6 +15794,8 @@ snapshots: dependencies: type-fest: 0.7.1 + standard-as-callback@2.1.0: {} + static-eval@2.0.2: dependencies: escodegen: 1.14.3 @@ -16021,14 +15964,6 @@ snapshots: symbol-observable@2.0.3: {} - table-layout@1.0.2: - dependencies: - array-back: 4.0.2 - deep-extend: 0.6.0 - typical: 5.2.0 - wordwrapjs: 4.0.1 - optional: true - tar@6.2.1: dependencies: chownr: 2.0.0 @@ -16141,9 +16076,6 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - toml@3.0.0: - optional: true - tr46@0.0.3: {} traverse@0.6.9: @@ -16154,6 +16086,8 @@ snapshots: trim-newlines@3.0.1: {} + trim-right@1.0.1: {} + ts-interface-checker@0.1.13: {} ts-jest@29.1.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@18.18.8)(ts-node@10.9.2(@types/node@18.18.8)(typescript@5.5.4)))(typescript@5.5.4): @@ -16192,9 +16126,6 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-typed-json@0.3.2: - optional: true - tslib@1.14.1: {} tslib@2.8.1: {} @@ -16298,12 +16229,6 @@ snapshots: typescript@5.5.4: {} - typical@4.0.0: - optional: true - - typical@5.2.0: - optional: true - ua-parser-js@1.0.38: {} uglify-es@3.3.9: @@ -16312,9 +16237,6 @@ snapshots: source-map: 0.6.1 optional: true - uglify-js@3.18.0: - optional: true - uint8array-extras@1.4.0: {} uint8arrays@3.1.1: @@ -16513,15 +16435,6 @@ snapshots: word-wrap@1.2.5: {} - wordwrap@1.0.0: - optional: true - - wordwrapjs@4.0.1: - dependencies: - reduce-flatten: 2.0.0 - typical: 5.2.0 - optional: true - wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -16638,4 +16551,8 @@ snapshots: yocto-queue@0.1.0: {} + zod-validation-error@2.1.0(zod@3.24.2): + dependencies: + zod: 3.24.2 + zod@3.24.2: {} diff --git a/samples/extension-module/requester.ts b/samples/extension-module/requester.ts index de78bd8ea8..55018abade 100644 --- a/samples/extension-module/requester.ts +++ b/samples/extension-module/requester.ts @@ -26,14 +26,16 @@ const run = async () => { const agent = new Agent({ config: { label: 'Dummy-powered agent - requester', - walletConfig: { - id: 'requester', - key: 'requester', - }, logger: new ConsoleLogger(LogLevel.info), }, modules: { - askar: new AskarModule({ askar }), + askar: new AskarModule({ + askar, + store: { + id: 'requester', + key: 'requester', + }, + }), didcomm: new DidCommModule(), oob: new OutOfBandModule(), messagePickup: new MessagePickupModule(), diff --git a/samples/extension-module/responder.ts b/samples/extension-module/responder.ts index 23612f1fec..400b9fb264 100644 --- a/samples/extension-module/responder.ts +++ b/samples/extension-module/responder.ts @@ -25,14 +25,16 @@ const run = async () => { const agent = new Agent({ config: { label: 'Dummy-powered agent - responder', - walletConfig: { - id: 'responder', - key: 'responder', - }, logger: new ConsoleLogger(LogLevel.debug), }, modules: { - askar: new AskarModule({ askar }), + askar: new AskarModule({ + askar, + store: { + id: 'responder', + key: 'responder', + }, + }), didcomm: new DidCommModule({ endpoints: [`http://localhost:${port}`] }), oob: new OutOfBandModule(), messagePickup: new MessagePickupModule(), diff --git a/samples/extension-module/tests/dummy.test.ts b/samples/extension-module/tests/dummy.test.ts index b55c95087c..6cb5d5b459 100644 --- a/samples/extension-module/tests/dummy.test.ts +++ b/samples/extension-module/tests/dummy.test.ts @@ -1,9 +1,7 @@ import type { ConnectionRecord } from '@credo-ts/didcomm' import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' -import { AskarModule } from '@credo-ts/askar' import { Agent } from '@credo-ts/core' -import { askar } from '@openwallet-foundation/askar-nodejs' import { Subject } from 'rxjs' import { getAgentOptions, makeConnection } from '../../../packages/core/tests/helpers' @@ -17,9 +15,6 @@ import { waitForDummyRecord } from './helpers' const modules = { dummy: new DummyModule(), - askar: new AskarModule({ - askar: askar, - }), } const bobAgentOptions = getAgentOptions( @@ -28,7 +23,8 @@ const bobAgentOptions = getAgentOptions( endpoints: ['rxjs:bob'], }, {}, - modules + modules, + { requireDidcomm: true } ) const aliceAgentOptions = getAgentOptions( @@ -37,7 +33,8 @@ const aliceAgentOptions = getAgentOptions( endpoints: ['rxjs:alice'], }, {}, - modules + modules, + { requireDidcomm: true } ) describe('Dummy extension module test', () => { @@ -68,9 +65,7 @@ describe('Dummy extension module test', () => { afterEach(async () => { await bobAgent.shutdown() - await bobAgent.wallet.delete() await aliceAgent.shutdown() - await aliceAgent.wallet.delete() }) test('Alice sends a request and Bob answers', async () => { diff --git a/samples/mediator.ts b/samples/mediator.ts index c733f38cd3..9756aae6da 100644 --- a/samples/mediator.ts +++ b/samples/mediator.ts @@ -48,10 +48,6 @@ const logger = new TestLogger(LogLevel.info) const agentConfig: InitConfig = { label: process.env.AGENT_LABEL || 'Credo Mediator', - walletConfig: { - id: process.env.WALLET_NAME || 'Credo', - key: process.env.WALLET_KEY || 'Credo', - }, logger, } @@ -60,7 +56,13 @@ const agent = new Agent({ config: agentConfig, dependencies: agentDependencies, modules: { - askar: new AskarModule({ askar }), + askar: new AskarModule({ + askar, + store: { + id: process.env.WALLET_NAME || 'Credo', + key: process.env.WALLET_KEY || 'Credo', + }, + }), didcomm: new DidCommModule({ endpoints }), oob: new OutOfBandModule(), messagePickup: new MessagePickupModule(), diff --git a/tests/InMemoryStorageService.ts b/tests/InMemoryStorageService.ts index e9dc82797b..377a850425 100644 --- a/tests/InMemoryStorageService.ts +++ b/tests/InMemoryStorageService.ts @@ -7,9 +7,13 @@ import type { StorageService, } from '../packages/core/src/storage/StorageService' -import { InMemoryWallet } from './InMemoryWallet' - -import { JsonTransformer, RecordDuplicateError, RecordNotFoundError, injectable } from '@credo-ts/core' +import { + JsonTransformer, + RecordDuplicateError, + RecordNotFoundError, + StorageVersionRecord, + injectable, +} from '@credo-ts/core' interface StorageRecord { value: Record @@ -44,33 +48,59 @@ export class InMemoryStorageService = BaseRe return instance } + public deleteRecordsForContext(agentContext: AgentContext) { + const contextCorrelationId = agentContext.contextCorrelationId + + // Be strict so that we can catch bugs in how credo handles context lifecycle + if (!this.contextCorrelationIdToRecords[contextCorrelationId]) { + throw new Error(`Storage for agent context ${contextCorrelationId} does not exist`) + } + + delete this.contextCorrelationIdToRecords[contextCorrelationId] + } + + public createRecordsForContext(agentContext: AgentContext) { + const contextCorrelationId = agentContext.contextCorrelationId + + // Be strict so that we can catch bugs in how credo handles context lifecycle + if (this.contextCorrelationIdToRecords[contextCorrelationId]) { + throw new Error(`Storage for agent context ${contextCorrelationId} already exists`) + } + + this.contextCorrelationIdToRecords[contextCorrelationId] = { + records: {}, + creationDate: new Date(), + } + this.setCurrentFrameworkStorageVersionForContext(agentContext) + } + private getRecordsForContext(agentContext: AgentContext): InMemoryRecords { const contextCorrelationId = agentContext.contextCorrelationId + // Be strict so that we can catch bugs in how credo handles context lifecycle if (!this.contextCorrelationIdToRecords[contextCorrelationId]) { - this.contextCorrelationIdToRecords[contextCorrelationId] = { - records: {}, - creationDate: new Date(), - } - } else if (agentContext.wallet instanceof InMemoryWallet && agentContext.wallet.activeWalletId) { - const walletCreationDate = agentContext.wallet.inMemoryWallets[agentContext.wallet.activeWalletId].creationDate - const storageCreationDate = this.contextCorrelationIdToRecords[contextCorrelationId].creationDate - - // If the storage was created before the wallet, it means the wallet has been deleted in the meantime - // and thus we need to recreate the storage as we don't want to serve records from the previous wallet - // FIXME: this is a flaw in our wallet/storage model. I think wallet should be for keys, and storage - // for records and you can create them separately. But that's a bigger change. - if (storageCreationDate < walletCreationDate) { - this.contextCorrelationIdToRecords[contextCorrelationId] = { - records: {}, - creationDate: new Date(), - } + if (agentContext.isRootAgentContext) { + this.createRecordsForContext(agentContext) + } else { + throw new Error(`Storage for agent context ${contextCorrelationId} does not exist`) } } return this.contextCorrelationIdToRecords[contextCorrelationId].records } + /** + * When we create storage for a context we need to store the version record + */ + private async setCurrentFrameworkStorageVersionForContext(agentContext: AgentContext) { + await this.save( + agentContext, + new StorageVersionRecord({ + storageVersion: StorageVersionRecord.frameworkStorageVersion, + }) as unknown as T + ) + } + /** @inheritDoc */ public async save(agentContext: AgentContext, record: T) { record.updatedAt = new Date() diff --git a/tests/InMemoryWallet.ts b/tests/InMemoryWallet.ts deleted file mode 100644 index f362371d21..0000000000 --- a/tests/InMemoryWallet.ts +++ /dev/null @@ -1,379 +0,0 @@ -import type { - EncryptedMessage, - UnpackedMessageContext, - Wallet, - WalletConfig, - WalletCreateKeyOptions, - WalletSignOptions, - WalletVerifyOptions, -} from '@credo-ts/core' - -import { Key as AskarKey, CryptoBox, Store, keyAlgorithmFromString } from '@openwallet-foundation/askar-nodejs' - -import { convertToAskarKeyBackend } from '../packages/askar/src/utils/askarKeyBackend' -import { didcommV1Pack, didcommV1Unpack } from '../packages/askar/src/wallet/didcommV1' -import { - Buffer, - CredoError, - JsonEncoder, - Key, - KeyBackend, - KeyType, - TypedArrayEncoder, - WalletError, - WalletNotFoundError, - injectable, - isValidPrivateKey, - isValidSeed, -} from '../packages/core' - -const inMemoryWallets: InMemoryWallets = {} - -const isError = (error: unknown): error is Error => error instanceof Error - -interface InMemoryKey { - publicKeyBytes: Uint8Array - secretKeyBytes: Uint8Array - keyType: KeyType -} - -interface InMemoryKeys { - [id: string]: InMemoryKey -} - -interface InMemoryWallets { - [id: string]: { - keys: InMemoryKeys - creationDate: Date - } -} - -@injectable() -export class InMemoryWallet implements Wallet { - // activeWalletId can be set even if wallet is closed. So make sure to also look at - // isInitialized to see if the wallet is actually open - public activeWalletId?: string - - public get inMemoryWallets() { - return inMemoryWallets - } - /** - * Abstract methods that need to be implemented by subclasses - */ - public isInitialized = false - public isProvisioned = false - - public get supportedKeyTypes() { - return [KeyType.Ed25519, KeyType.P256, KeyType.P384, KeyType.K256] - } - - private getInMemoryKeys(): InMemoryKeys { - if (!this.activeWalletId || !this.isInitialized) { - throw new WalletError('No active wallet') - } - - if (!this.inMemoryWallets[this.activeWalletId]) { - throw new WalletError('wallet does not exist') - } - - return this.inMemoryWallets[this.activeWalletId].keys - } - - public async create(walletConfig: WalletConfig) { - if (this.inMemoryWallets[walletConfig.id]) { - throw new WalletError('Wallet already exists') - } - - this.inMemoryWallets[walletConfig.id] = { - keys: {}, - creationDate: new Date(), - } - } - - public async createAndOpen(walletConfig: WalletConfig) { - await this.create(walletConfig) - await this.open(walletConfig) - } - - public async open(walletConfig: WalletConfig) { - if (this.isInitialized) { - throw new WalletError('A wallet is already open') - } - - if (!this.inMemoryWallets[walletConfig.id]) { - throw new WalletNotFoundError('Wallet does not exist', { walletType: 'InMemoryWallet' }) - } - - this.activeWalletId = walletConfig.id - this.isProvisioned = true - this.isInitialized = true - } - - public rotateKey(): Promise { - throw new Error('Method not implemented.') - } - - public async close() { - this.isInitialized = false - } - - public async delete() { - if (!this.activeWalletId) { - throw new WalletError('wallet is not provisioned') - } - - delete this.inMemoryWallets[this.activeWalletId] - this.activeWalletId = undefined - this.isProvisioned = false - } - - public async export() { - throw new Error('Method not implemented.') - } - - public async import() { - throw new Error('Method not implemented.') - } - - public async dispose() { - this.isInitialized = false - } - - /** - * Create a key with an optional seed and keyType. - * The keypair is also automatically stored in the wallet afterwards - */ - public async createKey({ - seed, - privateKey, - keyType, - keyBackend = KeyBackend.Software, - }: WalletCreateKeyOptions): Promise { - try { - if (keyBackend !== KeyBackend.Software) { - throw new WalletError('Only Software backend is allowed for the in-memory wallet') - } - if (seed && privateKey) { - throw new WalletError('Only one of seed and privateKey can be set') - } - - if (seed && !isValidSeed(seed, keyType)) { - throw new WalletError('Invalid seed provided') - } - - if (privateKey && !isValidPrivateKey(privateKey, keyType)) { - throw new WalletError('Invalid private key provided') - } - - if (!this.supportedKeyTypes.includes(keyType)) { - throw new WalletError(`Unsupported key type: '${keyType}'`) - } - - const algorithm = keyAlgorithmFromString(keyType) - - // Create key - let key: AskarKey | undefined - try { - key = privateKey - ? AskarKey.fromSecretBytes({ secretKey: privateKey, algorithm }) - : seed - ? AskarKey.fromSeed({ seed, algorithm }) - : AskarKey.generate(algorithm, convertToAskarKeyBackend(keyBackend)) - - const keyPublicBytes = key.publicBytes - - // Store key - const _key = new Key(keyPublicBytes, keyType) - this.getInMemoryKeys()[TypedArrayEncoder.toBase58(_key.publicKey)] = { - publicKeyBytes: keyPublicBytes, - secretKeyBytes: key.secretBytes, - keyType, - } - - return Key.fromPublicKey(keyPublicBytes, keyType) - } finally { - key?.handle.free() - } - } catch (error) { - // If already instance of `WalletError`, re-throw - if (error instanceof WalletError) throw error - - if (!isError(error)) { - throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError(`Error creating key with key type '${keyType}': ${error.message}`, { cause: error }) - } - } - - /** - * sign a Buffer with an instance of a Key class - * - * @param data Buffer The data that needs to be signed - * @param key Key The key that is used to sign the data - * - * @returns A signature for the data - */ - public async sign({ data, key }: WalletSignOptions): Promise { - const inMemoryKey = this.getInMemoryKeys()[key.publicKeyBase58] - if (!inMemoryKey) { - throw new WalletError('Key not found in wallet') - } - - if (!TypedArrayEncoder.isTypedArray(data)) { - throw new WalletError('Currently not supporting signing of multiple messages') - } - - let askarKey: AskarKey | undefined - try { - const inMemoryKey = this.getInMemoryKeys()[key.publicKeyBase58] - askarKey = AskarKey.fromSecretBytes({ - algorithm: keyAlgorithmFromString(inMemoryKey.keyType), - secretKey: inMemoryKey.secretKeyBytes, - }) - - const signed = askarKey.signMessage({ message: data as Buffer }) - - return Buffer.from(signed) - } finally { - askarKey?.handle.free() - } - } - - /** - * Verify the signature with the data and the used key - * - * @param data Buffer The data that has to be confirmed to be signed - * @param key Key The key that was used in the signing process - * @param signature Buffer The signature that was created by the signing process - * - * @returns A boolean whether the signature was created with the supplied data and key - * - * @throws {WalletError} When it could not do the verification - * @throws {WalletError} When an unsupported keytype is used - */ - public async verify({ data, key, signature }: WalletVerifyOptions): Promise { - if (!TypedArrayEncoder.isTypedArray(data)) { - throw new WalletError('Currently not supporting signing of multiple messages') - } - - let askarKey: AskarKey | undefined - try { - askarKey = AskarKey.fromPublicBytes({ - algorithm: keyAlgorithmFromString(key.keyType), - publicKey: key.compressedPublicKey, - }) - return askarKey.verifySignature({ message: data as Buffer, signature }) - } finally { - askarKey?.handle.free() - } - } - - /** - * Pack a message using DIDComm V1 algorithm - * - * @param payload message to send - * @param recipientKeys array containing recipient keys in base58 - * @param senderVerkey sender key in base58 - * @returns JWE Envelope to send - */ - public async pack( - payload: Record, - recipientKeys: string[], - senderVerkey?: string // in base58 - ): Promise { - const senderKey = senderVerkey ? this.getInMemoryKeys()[senderVerkey] : undefined - - if (senderVerkey && !senderKey) { - throw new WalletError('Sender key not found') - } - - const askarSenderKey = senderKey - ? AskarKey.fromSecretBytes({ - algorithm: keyAlgorithmFromString(senderKey.keyType), - secretKey: senderKey.secretKeyBytes, - }) - : undefined - - try { - const envelope = didcommV1Pack(payload, recipientKeys, askarSenderKey) - return envelope - } finally { - askarSenderKey?.handle.free() - } - } - - /** - * Unpacks a JWE Envelope coded using DIDComm V1 algorithm - * - * @param messagePackage JWE Envelope - * @returns UnpackedMessageContext with plain text message, sender key and recipient key - */ - public async unpack(messagePackage: EncryptedMessage): Promise { - const protectedJson = JsonEncoder.fromBase64(messagePackage.protected) - // biome-ignore lint/suspicious/noExplicitAny: - const recipientKids: string[] = protectedJson.recipients.map((r: any) => r.header.kid) - - for (const recipientKid of recipientKids) { - const recipientKey = this.getInMemoryKeys()[recipientKid] - const recipientAskarKey = recipientKey - ? AskarKey.fromSecretBytes({ - algorithm: keyAlgorithmFromString(recipientKey.keyType), - secretKey: recipientKey.secretKeyBytes, - }) - : undefined - try { - if (recipientAskarKey) { - const unpacked = didcommV1Unpack(messagePackage, recipientAskarKey) - return unpacked - } - } finally { - recipientAskarKey?.handle.free() - } - } - - throw new WalletError('No corresponding recipient key found') - } - - public async generateNonce(): Promise { - try { - // generate an 80-bit nonce suitable for AnonCreds proofs - const nonce = CryptoBox.randomNonce().slice(0, 10) - return nonce.reduce((acc, byte) => (acc << 8n) | BigInt(byte), 0n).toString() - } catch (error) { - if (!isError(error)) { - throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError('Error generating nonce', { cause: error }) - } - } - - public getRandomValues(length: number): Uint8Array { - try { - const buffer = new Uint8Array(length) - const CBOX_NONCE_LENGTH = 24 - - const genCount = Math.ceil(length / CBOX_NONCE_LENGTH) - const buf = new Uint8Array(genCount * CBOX_NONCE_LENGTH) - for (let i = 0; i < genCount; i++) { - const randomBytes = CryptoBox.randomNonce() - buf.set(randomBytes, CBOX_NONCE_LENGTH * i) - } - buffer.set(buf.subarray(0, length)) - - return buffer - } catch (error) { - if (!isError(error)) { - throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error }) - } - throw new WalletError('Error generating nonce', { cause: error }) - } - } - - public async generateWalletKey() { - try { - return Store.generateRawKey() - } catch (error) { - throw new WalletError('Error generating wallet key', { cause: error }) - } - } -} diff --git a/tests/InMemoryWalletModule.ts b/tests/InMemoryWalletModule.ts index 068922109c..a173afa724 100644 --- a/tests/InMemoryWalletModule.ts +++ b/tests/InMemoryWalletModule.ts @@ -1,20 +1,38 @@ -import type { DependencyManager, Module } from '@credo-ts/core' +import type { AgentContext, DependencyManager, Module } from '@credo-ts/core' import { InMemoryStorageService } from './InMemoryStorageService' -import { InMemoryWallet } from './InMemoryWallet' -import { CredoError, InjectionSymbols } from '@credo-ts/core' +import { CredoError, InjectionSymbols, Kms } from '@credo-ts/core' +import { NodeInMemoryKeyManagementStorage, NodeKeyManagementService } from '../packages/node/src' export class InMemoryWalletModule implements Module { - public register(dependencyManager: DependencyManager) { - if (dependencyManager.isRegistered(InjectionSymbols.Wallet)) { - throw new CredoError('There is an instance of Wallet already registered') - } - dependencyManager.registerContextScoped(InjectionSymbols.Wallet, InMemoryWallet) + private inMemoryStorageService = new InMemoryStorageService() + private enableKms: boolean + + public constructor(config: { enableKms?: boolean } = {}) { + this.enableKms = config.enableKms ?? true + } + public register(dependencyManager: DependencyManager) { if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { throw new CredoError('There is an instance of StorageService already registered') } - dependencyManager.registerSingleton(InjectionSymbols.StorageService, InMemoryStorageService) + + dependencyManager.registerInstance(InjectionSymbols.StorageService, this.inMemoryStorageService) + + if (this.enableKms) { + const kmsConfig = dependencyManager.resolve(Kms.KeyManagementModuleConfig) + + // TODO: prevent double registration + kmsConfig.registerBackend(new NodeKeyManagementService(new NodeInMemoryKeyManagementStorage())) + } + } + + public async onProvisionContext(agentContext: AgentContext): Promise { + this.inMemoryStorageService.createRecordsForContext(agentContext) + } + + public async onDeleteContext(agentContext: AgentContext): Promise { + this.inMemoryStorageService.deleteRecordsForContext(agentContext) } } diff --git a/tests/e2e-askar-indy-vdr-anoncreds-rs.e2e.test.ts b/tests/e2e-askar-indy-vdr-anoncreds-rs.e2e.test.ts index cde516182c..a673a0bffb 100644 --- a/tests/e2e-askar-indy-vdr-anoncreds-rs.e2e.test.ts +++ b/tests/e2e-askar-indy-vdr-anoncreds-rs.e2e.test.ts @@ -4,7 +4,6 @@ import type { SubjectMessage } from './transport/SubjectInboundTransport' import { Subject } from 'rxjs' import { getAnonCredsModules } from '../packages/anoncreds/tests/anoncredsSetup' -import { askarModule } from '../packages/askar/tests/helpers' import { getAgentOptions } from '../packages/core/tests/helpers' import { e2eTest } from './e2e-test' @@ -30,8 +29,8 @@ const recipientAgentOptions = getAgentOptions( mediationRecipient: new MediationRecipientModule({ mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - askar: askarModule, - } + }, + { requireDidcomm: true } ) const mediatorAgentOptions = getAgentOptions( 'E2E Askar Subject Mediator', @@ -44,8 +43,8 @@ const mediatorAgentOptions = getAgentOptions( autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediator: new MediatorModule({ autoAcceptMediationRequests: true }), - askar: askarModule, - } + }, + { requireDidcomm: true } ) const senderAgentOptions = getAgentOptions( 'E2E Askar Subject Sender', @@ -61,8 +60,8 @@ const senderAgentOptions = getAgentOptions( mediatorPollingInterval: 1000, mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - askar: askarModule, - } + }, + { requireDidcomm: true } ) describe('E2E Askar-AnonCredsRS-IndyVDR Subject tests', () => { @@ -78,11 +77,8 @@ describe('E2E Askar-AnonCredsRS-IndyVDR Subject tests', () => { afterEach(async () => { await recipientAgent.shutdown() - await recipientAgent.wallet.delete() await mediatorAgent.shutdown() - await mediatorAgent.wallet.delete() await senderAgent.shutdown() - await senderAgent.wallet.delete() }) test('Full Subject flow (connect, request mediation, issue, verify)', async () => { diff --git a/tests/e2e-http.e2e.test.ts b/tests/e2e-http.e2e.test.ts index 90017d6f4a..bfb55f7963 100644 --- a/tests/e2e-http.e2e.test.ts +++ b/tests/e2e-http.e2e.test.ts @@ -1,7 +1,7 @@ import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/anoncredsSetup' import { getAnonCredsModules } from '../packages/anoncreds/tests/anoncredsSetup' -import { getInMemoryAgentOptions } from '../packages/core/tests/helpers' +import { getAgentOptions } from '../packages/core/tests/helpers' import { e2eTest } from './e2e-test' @@ -15,7 +15,7 @@ import { } from '@credo-ts/didcomm' import { HttpInboundTransport } from '@credo-ts/node' -const recipientAgentOptions = getInMemoryAgentOptions( +const recipientAgentOptions = getAgentOptions( 'E2E HTTP Recipient', {}, {}, @@ -27,11 +27,12 @@ const recipientAgentOptions = getInMemoryAgentOptions( mediatorPollingInterval: 500, mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - } + }, + { requireDidcomm: true } ) const mediatorPort = 3000 -const mediatorAgentOptions = getInMemoryAgentOptions( +const mediatorAgentOptions = getAgentOptions( 'E2E HTTP Mediator', { endpoints: [`http://localhost:${mediatorPort}`], @@ -44,11 +45,12 @@ const mediatorAgentOptions = getInMemoryAgentOptions( mediator: new MediatorModule({ autoAcceptMediationRequests: true, }), - } + }, + { requireDidcomm: true } ) const senderPort = 3001 -const senderAgentOptions = getInMemoryAgentOptions( +const senderAgentOptions = getAgentOptions( 'E2E HTTP Sender', { endpoints: [`http://localhost:${senderPort}`], @@ -56,7 +58,8 @@ const senderAgentOptions = getInMemoryAgentOptions( {}, getAnonCredsModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, - }) + }), + { requireDidcomm: true } ) describe('E2E HTTP tests', () => { @@ -72,11 +75,8 @@ describe('E2E HTTP tests', () => { afterEach(async () => { await recipientAgent.shutdown() - await recipientAgent.wallet.delete() await mediatorAgent.shutdown() - await mediatorAgent.wallet.delete() await senderAgent.shutdown() - await senderAgent.wallet.delete() }) test('Full HTTP flow (connect, request mediation, issue, verify)', async () => { diff --git a/tests/e2e-subject.e2e.test.ts b/tests/e2e-subject.e2e.test.ts index 64279da151..f2f9be8213 100644 --- a/tests/e2e-subject.e2e.test.ts +++ b/tests/e2e-subject.e2e.test.ts @@ -4,7 +4,7 @@ import type { SubjectMessage } from './transport/SubjectInboundTransport' import { Subject } from 'rxjs' import { getAnonCredsModules } from '../packages/anoncreds/tests/anoncredsSetup' -import { getInMemoryAgentOptions } from '../packages/core/tests/helpers' +import { getAgentOptions } from '../packages/core/tests/helpers' import { e2eTest } from './e2e-test' import { SubjectInboundTransport } from './transport/SubjectInboundTransport' @@ -18,7 +18,7 @@ import { MediatorPickupStrategy, } from '@credo-ts/didcomm' -const recipientAgentOptions = getInMemoryAgentOptions( +const recipientAgentOptions = getAgentOptions( 'E2E Subject Recipient', {}, {}, @@ -29,9 +29,10 @@ const recipientAgentOptions = getInMemoryAgentOptions( mediationRecipient: new MediationRecipientModule({ mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - } + }, + { requireDidcomm: true } ) -const mediatorAgentOptions = getInMemoryAgentOptions( +const mediatorAgentOptions = getAgentOptions( 'E2E Subject Mediator', { endpoints: ['rxjs:mediator'], @@ -42,9 +43,10 @@ const mediatorAgentOptions = getInMemoryAgentOptions( autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediator: new MediatorModule({ autoAcceptMediationRequests: true }), - } + }, + { requireDidcomm: true } ) -const senderAgentOptions = getInMemoryAgentOptions( +const senderAgentOptions = getAgentOptions( 'E2E Subject Sender', { endpoints: ['rxjs:sender'], @@ -58,7 +60,8 @@ const senderAgentOptions = getInMemoryAgentOptions( mediatorPollingInterval: 1000, mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - } + }, + { requireDidcomm: true } ) describe('E2E Subject tests', () => { @@ -74,11 +77,8 @@ describe('E2E Subject tests', () => { afterEach(async () => { await recipientAgent.shutdown() - await recipientAgent.wallet.delete() await mediatorAgent.shutdown() - await mediatorAgent.wallet.delete() await senderAgent.shutdown() - await senderAgent.wallet.delete() }) test('Full Subject flow (connect, request mediation, issue, verify)', async () => { diff --git a/tests/e2e-ws-pickup-v2.e2e.test.ts b/tests/e2e-ws-pickup-v2.e2e.test.ts index d2b0fe4e3e..6a15af8a3d 100644 --- a/tests/e2e-ws-pickup-v2.e2e.test.ts +++ b/tests/e2e-ws-pickup-v2.e2e.test.ts @@ -1,7 +1,6 @@ import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/anoncredsSetup' import { getAnonCredsModules } from '../packages/anoncreds/tests/anoncredsSetup' -import { askarModule } from '../packages/askar/tests/helpers' import { getAgentOptions } from '../packages/core/tests/helpers' import { AutoAcceptCredential, @@ -36,8 +35,8 @@ const mediatorOptions = getAgentOptions( autoAcceptMediationRequests: true, messageForwardingStrategy: MessageForwardingStrategy.QueueAndLiveModeDelivery, }), - askar: askarModule, - } + }, + { requireDidcomm: true } ) const senderPort = 4101 @@ -51,8 +50,8 @@ const senderOptions = getAgentOptions( ...getAnonCredsModules({ autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), - askar: askarModule, - } + }, + { requireDidcomm: true } ) describe('E2E WS Pickup V2 tests', () => { @@ -69,11 +68,8 @@ describe('E2E WS Pickup V2 tests', () => { // NOTE: the order is important here, as the recipient sends pickup messages to the mediator // so we first want the recipient to fully be finished with the sending of messages await recipientAgent.shutdown() - await recipientAgent.wallet.delete() await mediatorAgent.shutdown() - await mediatorAgent.wallet.delete() await senderAgent.shutdown() - await senderAgent.wallet.delete() }) test('Full WS flow (connect, request mediation, issue, verify) using Message Pickup V2 polling mode', async () => { @@ -89,8 +85,8 @@ describe('E2E WS Pickup V2 tests', () => { mediatorPickupStrategy: MediatorPickupStrategy.PickUpV2, mediatorPollingInterval: 500, }), - askar: askarModule, - } + }, + { requireDidcomm: true } ) recipientAgent = new Agent(recipientOptions) as unknown as AnonCredsTestsAgent @@ -128,8 +124,8 @@ describe('E2E WS Pickup V2 tests', () => { mediationRecipient: new MediationRecipientModule({ mediatorPickupStrategy: MediatorPickupStrategy.PickUpV2LiveMode, }), - askar: askarModule, - } + }, + { requireDidcomm: true } ) recipientAgent = new Agent(recipientOptions) as unknown as AnonCredsTestsAgent diff --git a/tests/e2e-ws.e2e.test.ts b/tests/e2e-ws.e2e.test.ts index db5bc54e00..d74e9263e0 100644 --- a/tests/e2e-ws.e2e.test.ts +++ b/tests/e2e-ws.e2e.test.ts @@ -1,7 +1,6 @@ import type { AnonCredsTestsAgent } from '../packages/anoncreds/tests/anoncredsSetup' import { getAnonCredsModules } from '../packages/anoncreds/tests/anoncredsSetup' -import { askarModule } from '../packages/askar/tests/helpers' import { getAgentOptions } from '../packages/core/tests/helpers' import { e2eTest } from './e2e-test' @@ -29,8 +28,8 @@ const recipientAgentOptions = getAgentOptions( mediationRecipient: new MediationRecipientModule({ mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - askar: askarModule, - } + }, + { requireDidcomm: true } ) const mediatorPort = 4000 @@ -45,8 +44,8 @@ const mediatorAgentOptions = getAgentOptions( autoAcceptCredentials: AutoAcceptCredential.ContentApproved, }), mediator: new MediatorModule({ autoAcceptMediationRequests: true }), - askar: askarModule, - } + }, + { requireDidcomm: true } ) const senderPort = 4001 @@ -64,8 +63,8 @@ const senderAgentOptions = getAgentOptions( mediatorPollingInterval: 1000, mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }), - askar: askarModule, - } + }, + { requireDidcomm: true } ) describe('E2E WS tests', () => { @@ -81,11 +80,8 @@ describe('E2E WS tests', () => { afterEach(async () => { await recipientAgent.shutdown() - await recipientAgent.wallet.delete() await mediatorAgent.shutdown() - await mediatorAgent.wallet.delete() await senderAgent.shutdown() - await senderAgent.wallet.delete() }) test('Full WS flow (connect, request mediation, issue, verify)', async () => {