From 19cefa54596a4e4848bdbe89306a884a5ce2e991 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Sat, 11 Mar 2023 07:20:49 -0300 Subject: [PATCH] feat(askar): import/export wallet support for SQLite (#1377) Signed-off-by: Ariel Gentile --- packages/askar/src/wallet/AskarWallet.ts | 376 ++++++++++++------ .../src/wallet/__tests__/AskarWallet.test.ts | 8 +- packages/askar/tests/askar-sqlite.e2e.test.ts | 187 +++++++++ packages/askar/tests/helpers.ts | 22 +- packages/core/src/storage/FileSystem.ts | 1 + packages/node/src/NodeFileSystem.ts | 6 +- .../react-native/src/ReactNativeFileSystem.ts | 4 + 7 files changed, 471 insertions(+), 133 deletions(-) create mode 100644 packages/askar/tests/askar-sqlite.e2e.test.ts diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index 06985912ef..4084d5a415 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -9,8 +9,9 @@ import type { WalletConfigRekey, KeyPair, KeyDerivationMethod, + WalletExportImportConfig, } from '@aries-framework/core' -import type { Session } from '@hyperledger/aries-askar-shared' +import type { KeyEntryObject, Session } from '@hyperledger/aries-askar-shared' import { WalletKeyExistsError, @@ -131,6 +132,14 @@ export class AskarWallet implements Wallet { 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 { this._store = await Store.provision({ recreate: false, @@ -223,7 +232,9 @@ export class AskarWallet implements Wallet { if (rekey) { await this._store.rekey({ passKey: rekey, - keyMethod: keyDerivationMethodToStoreKeyMethod(rekeyDerivation) ?? StoreKeyMethod.Raw, + keyMethod: + keyDerivationMethodToStoreKeyMethod(rekeyDerivation) ?? + (`${StoreKeyMethod.Kdf}:argon2i:int` as StoreKeyMethod), }) } this._session = await this._store.openSession() @@ -246,10 +257,7 @@ export class AskarWallet implements Wallet { cause: error, }) } - throw new WalletError( - `Error opening wallet ${walletConfig.id}. ERROR CODE ${error.code} MESSAGE ${error.message}`, - { 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}'`) @@ -267,7 +275,6 @@ export class AskarWallet implements Wallet { } this.logger.info(`Deleting wallet '${this.walletConfig.id}'`) - if (this._store) { await this.close() } @@ -286,14 +293,89 @@ export class AskarWallet implements Wallet { } } - public async export() { - // TODO - throw new WalletError('AskarWallet Export not yet implemented') + 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 (!sourcePath) { + throw new WalletError('Export is only supported for SQLite backend') + } + + try { + // This method ensures that destination directory is created + const exportedWalletConfig = await this.getAskarWalletConfig({ + ...this.walletConfig, + storage: { type: 'sqlite', path: destinationPath }, + }) + + // Close this wallet before copying + await this.close() + + // Copy wallet to the destination path + await this.fileSystem.copyFile(sourcePath, destinationPath) + + // Open exported wallet and rotate its key to the one requested + const exportedWalletStore = await Store.open({ + uri: exportedWalletConfig.uri, + keyMethod: exportedWalletConfig.keyMethod, + passKey: exportedWalletConfig.passKey, + }) + await exportedWalletStore.rekey({ keyMethod: exportedWalletConfig.keyMethod, passKey: exportKey }) + + await exportedWalletStore.close() + + await this._open(this.walletConfig) + } catch (error) { + const errorMessage = `Error exporting wallet '${this.walletConfig.id}': ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } } - public async import() { - // TODO - throw new WalletError('AskarWallet Import not yet implemented') + 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') + } + + try { + // This method ensures that destination directory is created + const importWalletConfig = await this.getAskarWalletConfig(walletConfig) + + // Copy wallet to the destination path + await this.fileSystem.copyFile(sourcePath, destinationPath) + + // Open imported wallet and rotate its key to the one requested + const importedWalletStore = await Store.open({ + uri: importWalletConfig.uri, + keyMethod: importWalletConfig.keyMethod, + passKey: importKey, + }) + + await importedWalletStore.rekey({ keyMethod: importWalletConfig.keyMethod, passKey: importWalletConfig.passKey }) + + await importedWalletStore.close() + } catch (error) { + const errorMessage = `Error importing wallet '${walletConfig.id}': ${error.message}` + this.logger.error(errorMessage, { + error, + errorMessage: error.message, + }) + + throw new WalletError(errorMessage, { cause: error }) + } } /** @@ -343,17 +425,21 @@ export class AskarWallet implements Wallet { const algorithm = keyAlgFromString(keyType) // Create key - const key = privateKey - ? AskarKey.fromSecretBytes({ secretKey: privateKey, algorithm }) - : seed - ? AskarKey.fromSeed({ seed, algorithm }) - : AskarKey.generate(algorithm) - - // Store key + let key: AskarKey | undefined try { - await this.session.insertKey({ key, name: TypedArrayEncoder.toBase58(key.publicBytes) }) - return Key.fromPublicKey(key.publicBytes, keyType) + const key = privateKey + ? AskarKey.fromSecretBytes({ secretKey: privateKey, algorithm }) + : seed + ? AskarKey.fromSeed({ seed, algorithm }) + : AskarKey.generate(algorithm) + + const keyPublicBytes = key.publicBytes + // Store key + await this.session.insertKey({ key, name: TypedArrayEncoder.toBase58(keyPublicBytes) }) + key.handle.free() + return Key.fromPublicKey(keyPublicBytes, keyType) } catch (error) { + key?.handle.free() // Handle case where key already exists if (isAskarError(error, AskarErrorCode.Duplicate)) { throw new WalletKeyExistsError('Key already exists') @@ -393,12 +479,13 @@ export class AskarWallet implements Wallet { * @returns A signature for the data */ public async sign({ data, key }: WalletSignOptions): Promise { + let keyEntry: KeyEntryObject | null | undefined try { if (keyTypeSupportedByAskar(key.keyType)) { if (!TypedArrayEncoder.isTypedArray(data)) { throw new WalletError(`Currently not supporting signing of multiple messages`) } - const keyEntry = await this.session.fetchKey({ name: key.publicKeyBase58 }) + keyEntry = await this.session.fetchKey({ name: key.publicKeyBase58 }) if (!keyEntry) { throw new WalletError('Key entry not found') @@ -406,6 +493,8 @@ export class AskarWallet implements Wallet { const signed = keyEntry.key.signMessage({ message: data as Buffer }) + keyEntry.key.handle.free() + return Buffer.from(signed) } else { // Check if there is a signing key provider for the specified key type. @@ -424,6 +513,7 @@ export class AskarWallet implements Wallet { throw new WalletError(`Unsupported keyType: ${key.keyType}`) } } catch (error) { + keyEntry?.key.handle.free() if (!isError(error)) { throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) } @@ -444,6 +534,7 @@ export class AskarWallet implements Wallet { * @throws {WalletError} When an unsupported keytype is used */ public async verify({ data, key, signature }: WalletVerifyOptions): Promise { + let askarKey: AskarKey | undefined try { if (keyTypeSupportedByAskar(key.keyType)) { if (!TypedArrayEncoder.isTypedArray(data)) { @@ -454,7 +545,9 @@ export class AskarWallet implements Wallet { algorithm: keyAlgFromString(key.keyType), publicKey: key.publicKey, }) - return askarKey.verifySignature({ message: data as Buffer, signature }) + const verified = askarKey.verifySignature({ message: data as Buffer, signature }) + askarKey.handle.free() + return verified } else { // Check if there is a signing key provider for the specified key type. if (this.signingKeyProviderRegistry.hasProviderForKeyType(key.keyType)) { @@ -471,6 +564,7 @@ export class AskarWallet implements Wallet { throw new WalletError(`Unsupported keyType: ${key.keyType}`) } } catch (error) { + askarKey?.handle.free() if (!isError(error)) { throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error', { cause: error }) } @@ -493,79 +587,92 @@ export class AskarWallet implements Wallet { recipientKeys: string[], senderVerkey?: string // in base58 ): Promise { - const cek = AskarKey.generate(KeyAlgs.Chacha20C20P) + let cek: AskarKey | undefined + let senderKey: KeyEntryObject | null | undefined + let senderExchangeKey: AskarKey | undefined - const senderKey = senderVerkey ? await this.session.fetchKey({ name: senderVerkey }) : undefined + try { + cek = AskarKey.generate(KeyAlgs.Chacha20C20P) + senderKey = senderVerkey ? await this.session.fetchKey({ name: senderVerkey }) : undefined + senderExchangeKey = senderKey ? senderKey.key.convertkey({ algorithm: KeyAlgs.X25519 }) : undefined - const senderExchangeKey = senderKey ? senderKey.key.convertkey({ algorithm: KeyAlgs.X25519 }) : undefined + const recipients: JweRecipient[] = [] - const recipients: JweRecipient[] = [] + for (const recipientKey of recipientKeys) { + let targetExchangeKey: AskarKey | undefined + try { + targetExchangeKey = AskarKey.fromPublicBytes({ + publicKey: Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519).publicKey, + algorithm: KeyAlgs.Ed25519, + }).convertkey({ algorithm: KeyAlgs.X25519 }) - for (const recipientKey of recipientKeys) { - const targetExchangeKey = AskarKey.fromPublicBytes({ - publicKey: Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519).publicKey, - algorithm: KeyAlgs.Ed25519, - }).convertkey({ algorithm: KeyAlgs.X25519 }) + if (senderVerkey && senderExchangeKey) { + const encryptedSender = CryptoBox.seal({ + recipientKey: targetExchangeKey, + message: Buffer.from(senderVerkey), + }) + const nonce = CryptoBox.randomNonce() + const encryptedCek = CryptoBox.cryptoBox({ + recipientKey: targetExchangeKey, + senderKey: senderExchangeKey, + message: cek.secretBytes, + nonce, + }) - if (senderVerkey && senderExchangeKey) { - const encryptedSender = CryptoBox.seal({ - recipientKey: targetExchangeKey, - message: Buffer.from(senderVerkey), - }) - 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() + } + } - 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, - }, - }) - ) + const protectedJson = { + enc: 'xchacha20poly1305_ietf', + typ: 'JWM/1.0', + alg: senderVerkey ? 'Authcrypt' : 'Anoncrypt', + recipients: recipients.map((item) => JsonTransformer.toJSON(item)), } - } - const protectedJson = { - enc: 'xchacha20poly1305_ietf', - typ: 'JWM/1.0', - alg: senderVerkey ? '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() + senderKey?.key.handle.free() + senderExchangeKey?.handle.free() } - - 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 } /** @@ -607,41 +714,43 @@ export class AskarWallet implements Wallet { let payloadKey, senderKey, recipientKey for (const recipient of recipients) { - let recipientKeyEntry + let recipientKeyEntry: KeyEntryObject | null | undefined + let sender_x: AskarKey | undefined + let recip_x: AskarKey | undefined + try { recipientKeyEntry = await this.session.fetchKey({ name: recipient.kid }) - } catch (error) { - // TODO: Currently Askar wrapper throws error when key is not found - // In this case we don't need to throw any error because we should - // try with other recipient keys - continue - } - if (recipientKeyEntry) { - const recip_x = recipientKeyEntry.key.convertkey({ algorithm: KeyAlgs.X25519 }) - recipientKey = recipient.kid - - if (recipient.sender && recipient.iv) { - senderKey = TypedArrayEncoder.toUtf8String( - CryptoBox.sealOpen({ + if (recipientKeyEntry) { + const recip_x = recipientKeyEntry.key.convertkey({ algorithm: KeyAlgs.X25519 }) + recipientKey = recipient.kid + + if (recipient.sender && recipient.iv) { + senderKey = TypedArrayEncoder.toUtf8String( + CryptoBox.sealOpen({ + recipientKey: recip_x, + ciphertext: recipient.sender, + }) + ) + const sender_x = AskarKey.fromPublicBytes({ + algorithm: KeyAlgs.Ed25519, + publicKey: TypedArrayEncoder.fromBase58(senderKey), + }).convertkey({ algorithm: KeyAlgs.X25519 }) + + payloadKey = CryptoBox.open({ recipientKey: recip_x, - ciphertext: recipient.sender, + senderKey: sender_x, + message: recipient.encrypted_key, + nonce: recipient.iv, }) - ) - const sender_x = AskarKey.fromPublicBytes({ - algorithm: KeyAlgs.Ed25519, - publicKey: TypedArrayEncoder.fromBase58(senderKey), - }).convertkey({ algorithm: KeyAlgs.X25519 }) - - payloadKey = CryptoBox.open({ - recipientKey: recip_x, - senderKey: sender_x, - message: recipient.encrypted_key, - nonce: recipient.iv, - }) - } else { - payloadKey = CryptoBox.sealOpen({ ciphertext: recipient.encrypted_key, recipientKey: recip_x }) + } else { + payloadKey = CryptoBox.sealOpen({ ciphertext: recipient.encrypted_key, recipientKey: recip_x }) + } + break } - break + } finally { + recipientKeyEntry?.key.handle.free() + sender_x?.handle.free() + recip_x?.handle.free() } } if (!payloadKey) { @@ -652,17 +761,22 @@ export class AskarWallet implements Wallet { throw new WalletError('Sender public key not provided for Authcrypt') } - const cek = AskarKey.fromSecretBytes({ algorithm: KeyAlgs.Chacha20C20P, secretKey: payloadKey }) - const message = cek.aeadDecrypt({ - ciphertext: TypedArrayEncoder.fromBase64(messagePackage.ciphertext as any), - nonce: TypedArrayEncoder.fromBase64(messagePackage.iv as any), - tag: TypedArrayEncoder.fromBase64(messagePackage.tag as any), - aad: TypedArrayEncoder.fromString(messagePackage.protected), - }) - return { - plaintextMessage: JsonEncoder.fromBuffer(message), - senderKey, - recipientKey, + let cek: AskarKey | undefined + try { + cek = AskarKey.fromSecretBytes({ algorithm: KeyAlgs.Chacha20C20P, secretKey: payloadKey }) + const message = cek.aeadDecrypt({ + ciphertext: TypedArrayEncoder.fromBase64(messagePackage.ciphertext as any), + nonce: TypedArrayEncoder.fromBase64(messagePackage.iv as any), + tag: TypedArrayEncoder.fromBase64(messagePackage.tag as any), + aad: TypedArrayEncoder.fromString(messagePackage.protected), + }) + return { + plaintextMessage: JsonEncoder.fromBuffer(message), + senderKey, + recipientKey, + } + } finally { + cek?.handle.free() } } @@ -699,7 +813,9 @@ export class AskarWallet implements Wallet { uri, profile: walletConfig.id, // FIXME: Default derivation method should be set somewhere in either agent config or some constants - keyMethod: keyDerivationMethodToStoreKeyMethod(walletConfig.keyDerivationMethod) ?? StoreKeyMethod.None, + keyMethod: + keyDerivationMethodToStoreKeyMethod(walletConfig.keyDerivationMethod) ?? + (`${StoreKeyMethod.Kdf}:argon2i:int` as StoreKeyMethod), passKey: walletConfig.key, } } diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts index ffbb648999..efeccd9fcd 100644 --- a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -267,7 +267,13 @@ describeRunInNodeVersion([18], 'AskarWallet management', () => { await askarWallet.close() const newKey = Store.generateRawKey() - await askarWallet.rotateKey({ ...walletConfig, id: 'AskarWallet Key Rotation', key: initialKey, rekey: newKey }) + await askarWallet.rotateKey({ + ...walletConfig, + id: 'AskarWallet Key Rotation', + key: initialKey, + rekey: newKey, + rekeyDerivationMethod: KeyDerivationMethod.Raw, + }) await askarWallet.close() diff --git a/packages/askar/tests/askar-sqlite.e2e.test.ts b/packages/askar/tests/askar-sqlite.e2e.test.ts new file mode 100644 index 0000000000..3de47f3183 --- /dev/null +++ b/packages/askar/tests/askar-sqlite.e2e.test.ts @@ -0,0 +1,187 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { + Agent, + BasicMessageRecord, + BasicMessageRepository, + BasicMessageRole, + KeyDerivationMethod, + TypedArrayEncoder, + utils, + WalletDuplicateError, + WalletInvalidKeyError, + WalletNotFoundError, +} from '@aries-framework/core' +import { Store } from '@hyperledger/aries-askar-shared' +import { tmpdir } from 'os' +import path from 'path' + +import { describeRunInNodeVersion } from '../../../tests/runInVersion' + +import { getSqliteAgentOptions } from './helpers' + +const aliceAgentOptions = getSqliteAgentOptions('AgentsAlice') +const bobAgentOptions = getSqliteAgentOptions('AgentsBob') + +// FIXME: Re-include in tests when Askar NodeJS wrapper performance is improved +describeRunInNodeVersion([18], '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 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await bobAgent.wallet.initialize(bobAgentOptions.config.walletConfig!) + expect(await bobBasicMessageRepository.findById(bobAgent.context, basicMessageRecord.id)).toBeNull() + await bobAgent.wallet.delete() + + // Import backup with different wallet id and initialize + await bobAgent.wallet.import({ id: backupWalletName, key: backupWalletName }, { path: backupPath, key: backupKey }) + await bobAgent.wallet.initialize({ id: backupWalletName, key: backupWalletName }) + + // Expect same basic message record to exist in new wallet + expect(await bobBasicMessageRepository.getById(bobAgent.context, basicMessageRecord.id)).toMatchObject( + basicMessageRecord + ) + }) + + 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/helpers.ts b/packages/askar/tests/helpers.ts index 8be4b2a833..9321aca39d 100644 --- a/packages/askar/tests/helpers.ts +++ b/packages/askar/tests/helpers.ts @@ -20,7 +20,7 @@ export function getPostgresAgentOptions( extraConfig: Partial = {} ) { const config: InitConfig = { - label: `Agent: ${name}`, + label: `Agent: ${name} Postgres`, walletConfig: { id: `Wallet${name}`, key: `Key${name}`, @@ -37,3 +37,23 @@ export function getPostgresAgentOptions( modules: { askar: new AskarModule() }, } as const } + +export function getSqliteAgentOptions(name: string, extraConfig: Partial = {}) { + const config: InitConfig = { + label: `Agent: ${name} SQLite`, + walletConfig: { + id: `Wallet${name}`, + key: `Key${name}`, + storage: { type: 'sqlite' }, + }, + autoAcceptConnections: true, + autoUpdateStorageOnStartup: false, + logger: new TestLogger(LogLevel.off, name), + ...extraConfig, + } + return { + config, + dependencies: agentDependencies, + modules: { askar: new AskarModule() }, + } as const +} diff --git a/packages/core/src/storage/FileSystem.ts b/packages/core/src/storage/FileSystem.ts index a6eeb08e48..9a5710835e 100644 --- a/packages/core/src/storage/FileSystem.ts +++ b/packages/core/src/storage/FileSystem.ts @@ -11,6 +11,7 @@ export interface FileSystem { exists(path: string): Promise createDirectory(path: string): Promise + copyFile(sourcePath: string, destinationPath: string): Promise write(path: string, data: string): Promise read(path: string): Promise delete(path: string): Promise diff --git a/packages/node/src/NodeFileSystem.ts b/packages/node/src/NodeFileSystem.ts index 75ebb01b73..33af5391d6 100644 --- a/packages/node/src/NodeFileSystem.ts +++ b/packages/node/src/NodeFileSystem.ts @@ -8,7 +8,7 @@ import https from 'https' import { tmpdir, homedir } from 'os' import { dirname } from 'path' -const { access, readFile, writeFile, mkdir, rm, unlink } = promises +const { access, readFile, writeFile, mkdir, rm, unlink, copyFile } = promises export class NodeFileSystem implements FileSystem { public readonly dataPath @@ -44,6 +44,10 @@ export class NodeFileSystem implements FileSystem { await mkdir(dirname(path), { recursive: true }) } + public async copyFile(sourcePath: string, destinationPath: string): Promise { + await copyFile(sourcePath, destinationPath) + } + public async write(path: string, data: string): Promise { // Make sure parent directories exist await mkdir(dirname(path), { recursive: true }) diff --git a/packages/react-native/src/ReactNativeFileSystem.ts b/packages/react-native/src/ReactNativeFileSystem.ts index 48588fad88..a14ba4bd40 100644 --- a/packages/react-native/src/ReactNativeFileSystem.ts +++ b/packages/react-native/src/ReactNativeFileSystem.ts @@ -43,6 +43,10 @@ export class ReactNativeFileSystem implements FileSystem { await RNFS.mkdir(getDirFromFilePath(path)) } + public async copyFile(sourcePath: string, destinationPath: string): Promise { + await RNFS.copyFile(sourcePath, destinationPath) + } + public async write(path: string, data: string): Promise { // Make sure parent directories exist await RNFS.mkdir(getDirFromFilePath(path))