-
Notifications
You must be signed in to change notification settings - Fork 239
feat: new key management service with multiple backends #2203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
39b3ea3
d9d73f6
02d5e49
ba5ae40
1e35cf3
b605d0d
70085ce
05043b0
eda0701
6bc2963
2409e45
1afa3ba
2a4f595
064fb32
5f960c9
f70f883
a1aa274
2fa433a
caa0d18
df2b5fa
47d93fe
cfa36cb
5d2926a
6963e16
e7b5ea5
f5a5f19
66d23bc
0505a01
d92dd4f
c42b537
be06d89
bc83a55
3d0404f
c23ca9f
270ef2a
eaa3acd
c2e2718
9c268c5
7b13bdb
324f8ad
e4efce0
90cca62
0e31465
9aaae00
cd40964
132246d
40bccfd
c4eb831
41defce
e6f74f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { AgentContext } from '@credo-ts/core' | ||
import { injectable } from 'tsyringe' | ||
|
||
import { AskarStoreRotateKeyOptions, AskarStoreExportOptions, AskarStoreImportOptions } from './AskarApiOptions' | ||
import { AskarStoreManager } from './AskarStoreManager' | ||
|
||
@injectable() | ||
export class AskarApi { | ||
public constructor(private agentContext: AgentContext, private askarStoreManager: AskarStoreManager) {} | ||
|
||
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<void> { | ||
await this.askarStoreManager.provisionStore(this.agentContext) | ||
} | ||
|
||
/** | ||
* @throws {AskarStoreNotFoundError} if the wallet does not exist | ||
* @throws {AskarStoreError} if another error occurs | ||
*/ | ||
public async openStore(): Promise<void> { | ||
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<void> { | ||
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<void> { | ||
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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,13 @@ | ||
import type { AskarModuleConfigOptions } from './AskarModuleConfig' | ||
import type { AgentContext, DependencyManager, Module } from '@credo-ts/core' | ||
|
||
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 { AskarMultiWalletDatabaseScheme, AskarModuleConfig } 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' | ||
|
||
export class AskarModule implements Module { | ||
public readonly config: AskarModuleConfig | ||
|
@@ -16,52 +16,68 @@ 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') | ||
} else { | ||
dependencyManager.registerContextScoped(InjectionSymbols.Wallet, AskarWallet) | ||
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.`) | ||
} | ||
|
||
// 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) | ||
kmsConfig.registerBackend(new AksarKeyManagementService()) | ||
} | ||
|
||
if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { | ||
throw new CredoError('There is an instance of StorageService already registered') | ||
} else { | ||
if (this.config.enableStorage) { | ||
if (dependencyManager.isRegistered(InjectionSymbols.StorageService)) { | ||
throw new CredoError( | ||
'Unable to register AskatStoreService. There is an instance of StorageService already registered' | ||
TimoGlastra marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
) | ||
} | ||
dependencyManager.registerSingleton(InjectionSymbols.StorageService, AskarStorageService) | ||
} | ||
|
||
dependencyManager.registerSingleton(AskarStoreManager) | ||
} | ||
|
||
public async initialize(agentContext: AgentContext): Promise<void> { | ||
// 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, metadata: Record<string, unknown>) { | ||
// TODO: I think we should also register the store here | ||
if (agentContext.contextCorrelationId === 'default') { | ||
const storeManager = agentContext.dependencyManager.resolve(AskarStoreManager) | ||
await storeManager.getInitializedStoreWithProfile(agentContext) | ||
return | ||
} else if (this.config.multiWalletDatabaseScheme === AskarMultiWalletDatabaseScheme.DatabasePerWallet) { | ||
agentContext.dependencyManager.registerInstance('AskarContextMetadata', metadata) | ||
const storeManager = agentContext.dependencyManager.resolve(AskarStoreManager) | ||
await storeManager.getInitializedStoreWithProfile(agentContext) | ||
} | ||
} | ||
|
||
public async onProvisionContext(agentContext: AgentContext) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added new module methods:
this will allow for lifecycle handling of specific contexts There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is really great! |
||
// We don't have any side effects to run | ||
if (agentContext.contextCorrelationId === 'default') return null | ||
if (this.config.multiWalletDatabaseScheme === AskarMultiWalletDatabaseScheme.ProfilePerWallet) return null | ||
|
||
// For new stores (so not profiles) we need to generate a wallet key | ||
return { | ||
walletKey: 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<void> { | ||
const storeManager = agentContext.dependencyManager.resolve(AskarStoreManager) | ||
|
||
await storeManager.closeContext(agentContext) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import type { Askar } from '@openwallet-foundation/askar-shared' | ||
import type { AskarWalletPostgresStorageConfig, AskarWalletSqliteStorageConfig } from './wallet' | ||
import type { Askar, KdfMethod } from '@openwallet-foundation/askar-shared' | ||
|
||
export enum AskarMultiWalletDatabaseScheme { | ||
/** | ||
|
@@ -12,7 +13,48 @@ export enum AskarMultiWalletDatabaseScheme { | |
ProfilePerWallet = 'ProfilePerWallet', | ||
} | ||
|
||
export interface AskarModuleConfigStoreOptions { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wallet config is now askar specific |
||
/** | ||
* 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}` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if we add the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it ok to do this in a follow up PR? It will require additional tests to make sure it works and don't want to expand the scope any further |
||
|
||
/** | ||
* The backend to use with backend specific configuraiton options. | ||
* | ||
* If not provided SQLite will be used by default | ||
*/ | ||
database?: AskarWalletSqliteStorageConfig | AskarWalletPostgresStorageConfig | ||
} | ||
|
||
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 | ||
} | ||
|
||
public get enableStorage() { | ||
return this.options.enableStorage | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Askar specific operations will now be part of the askar api. So there's no global rotateWalletKey anymore, as that's specific to Askars way of doing this.