Skip to content
111 changes: 111 additions & 0 deletions docs/KeyManager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# KeyManager Architecture

## Overview

KeyManager supports multiple key source types through a provider abstraction pattern. This allows the system to support different key management solutions (raw private keys, GCP KMS, AWS KMS, etc.) without changing the core KeyManager logic.

## Architecture

```
KeyManager
├── IKeyProvider (interface)
│ ├── RawPrivateKeyProvider (implemented)
│ ├── GcpKmsProvider (future)
│ └── AwsKmsProvider (future)
└── Factory Pattern
└── createKeyProvider(config) → IKeyProvider
```

## Components

### 1. IKeyProvider Interface (`types.ts`)

Base interface that all key providers must implement:

```typescript
interface IKeyProvider {
getType(): KeyProviderType
initialize(): Promise<void>
getPeerId(): PeerId
getLibp2pPrivateKey(): any
getLibp2pPublicKey(): Uint8Array
getEthAddress(): string
getRawPrivateKeyBytes(): Uint8Array
cleanup?(): Promise<void>
}
```

### 2. KeyProviderType Enum

Defines supported key provider types:

- `RAW` - Raw private key from environment variable

### 3. RawPrivateKeyProvider (`providers/RawPrivateKeyProvider.ts`)

Implementation for raw private keys:

- Loads private key from config
- Derives libp2p keys and peerId
- Derives Ethereum address
- Provides raw private key bytes for EVM signer creation

### 4. Factory (`factory.ts`)

Creates and initializes the appropriate key provider:

```typescript
createKeyProvider(config: KeyProviderConfig): Promise<IKeyProvider>
```

### 5. KeyManager (`index.ts`)

Main class that:

- Wraps a key provider
- Manages EVM signer caching
- Provides unified API for key access

## Usage

### Current Usage (Raw Private Key)

```typescript
import { createKeyProvider } from './components/KeyManager/index.js'

const keyManager = new KeyManager(config)
```

## Adding New Key Providers

To add a new key provider (e.g., AWS KMS):

1. **Add to KeyProviderType**:

```typescript
export type KeyProviderType = 'raw' | 'gcp-kms' | 'aws' //new
```

2. **Create provider class**:

```typescript
export class AwsKmsProvider implements IKeyProvider {
// Implement all interface methods
}
```

3. **Update factory**:

```typescript
case 'aws'':
provider = new AwsKmsProvider(config)
break
```

## Benefits

1. **Extensibility**: Easy to add new key sources
2. **Testability**: Can mock key providers for testing
3. **Security**: Supports secure key management solutions (KMS)
4. **Separation of Concerns**: Key retrieval logic separated from key usage logic
94 changes: 94 additions & 0 deletions src/@types/KeyManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { PeerId } from '@libp2p/interface'
import { Wallet } from 'ethers'
import { EncryptMethod } from './fileObject.js'
import { Readable } from 'stream'
/**
* Key provider types supported by KeyManager
*/

export type KeyProviderType = 'raw' | 'gcp-kms' // 'aws-kms' in future
/**
* Base interface for key providers.
* Each key provider implementation must implement this interface.
* Initialization happens in the constructor.
*/
export interface IKeyProvider {
/**
* Get the type of this key provider
*/
getType(): string

/**
* Get the libp2p PeerId
*/
getPeerId(): PeerId

/**
* Get the libp2p private key
*/
getLibp2pPrivateKey(): any // libp2p PrivateKey type

/**
* Get the libp2p public key as Uint8Array
*/
getPublicKey(): Uint8Array

/**
* Get the Ethereum address derived from the private key
*/
getEthAddress(): string
/**
* Get the Ethereum Wallet derived from the private key
*/
getEthWallet(): Wallet

/**
* Get the raw private key bytes for EVM signer creation.
* This is used to create ethers Wallet instances.
*/
getRawPrivateKeyBytes(): Uint8Array

/**
* Encrypts data according to a given algorithm
* @param data data to encrypt
* @param algorithm encryption algorithm AES or ECIES
*/
encrypt(data: Uint8Array, algorithm: EncryptMethod): Promise<Buffer>

/**
* Decrypts data according to a given algorithm using node keys
* @param data data to decrypt
* @param algorithm decryption algorithm AES or ECIES
*/
decrypt(data: Uint8Array, algorithm: EncryptMethod): Promise<Buffer>
/**
* Encrypts a stream according to a given algorithm using node keys
* @param inputStream - Readable stream to encrypt
* @param algorithm - Encryption algorithm AES or ECIES
* @returns Readable stream with encrypted data
*/
encryptStream(inputStream: Readable, algorithm: EncryptMethod): Readable
/**
* Decrypts a stream according to a given algorithm using node keys
* @param inputStream - Readable stream to decrypt
* @param algorithm - Decryption algorithm AES or ECIES
* @returns Readable stream with decrypted data
*/
decryptStream(inputStream: Readable, algorithm: EncryptMethod): Readable
/**
* Decrypts using ethCrypto.decryptWithPrivateKey
* @param encryptedObject
* @returns Decrypted data
*/
ethCryptoDecryptWithPrivateKey(encryptedObject: any): Promise<any>
/**
* Signs message using ethers wallet.signMessage
* @param message - Message to sign
* @returns Signature
*/
signMessage(message: string): Promise<string>
/**
* Cleanup resources if needed (e.g., close connections)
*/
cleanup?(): Promise<void>
}
11 changes: 11 additions & 0 deletions src/@types/OceanNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RPCS } from './blockchain'
import { C2DClusterInfo, C2DDockerConfig } from './C2D/C2D'
import { FeeStrategy } from './Fees'
import { Schema } from '../components/database'
import { KeyProviderType } from './KeyManager'

export interface OceanNodeDBConfig {
url: string | null
Expand All @@ -22,6 +23,16 @@ export interface OceanNodeKeys {
publicKey: any
privateKey: any
ethAddress: string
type?: KeyProviderType
// Raw private key config (when type is 'raw')
// GCP KMS config (when type is 'gcp-kms')
gcpKmsConfig?: {
projectId: string
location: string
keyRing: string
keyName: string
keyVersion?: string
}
}
/* eslint-disable no-unused-vars */
export enum dhtFilterMethod {
Expand Down
1 change: 1 addition & 0 deletions src/@types/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface Command {
command: string // command name
node?: string // if not present it means current node
authorization?: string
caller?: string | string[] // added by our node for rate limiting
}

export interface GetP2PPeerCommand extends Command {
Expand Down
73 changes: 60 additions & 13 deletions src/OceanNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { GENERIC_EMOJIS, LOG_LEVELS_STR } from './utils/logging/Logger.js'
import { BaseHandler } from './components/core/handler/handler.js'
import { C2DEngines } from './components/c2d/compute_engines.js'
import { Auth } from './components/Auth/index.js'
import { KeyManager } from './components/KeyManager/index.js'
import { BlockchainRegistry } from './components/BlockchainRegistry/index.js'
import { Blockchain } from './utils/blockchain.js'

export interface RequestLimiter {
requester: string | string[] // IP address or peer ID
Expand Down Expand Up @@ -41,8 +44,20 @@ export class OceanNode {
private db?: Database,
private node?: OceanP2P,
private provider?: OceanProvider,
private indexer?: OceanIndexer
private indexer?: OceanIndexer,
public keyManager?: KeyManager,
public blockchainRegistry?: BlockchainRegistry
) {
if (keyManager) {
this.keyManager = keyManager
} else {
this.keyManager = new KeyManager(config)
}
if (blockchainRegistry) {
this.blockchainRegistry = blockchainRegistry
} else {
this.blockchainRegistry = new BlockchainRegistry(this.keyManager, config)
}
this.coreHandlers = CoreHandlersRegistry.getInstance(this)
this.requestMap = new Map<string, RequestLimiter>()
this.config = config
Expand All @@ -55,7 +70,8 @@ export class OceanNode {
if (this.config) {
this.escrow = new Escrow(
this.config.supportedNetworks,
this.config.claimDurationTimeout
this.config.claimDurationTimeout,
this.blockchainRegistry
)
}
}
Expand All @@ -67,11 +83,28 @@ export class OceanNode {
node?: OceanP2P,
provider?: OceanProvider,
indexer?: OceanIndexer,
keyManager?: KeyManager,
blockchainRegistry?: BlockchainRegistry,
newInstance: boolean = false
): OceanNode {
if (!OceanNode.instance || newInstance) {
if (!keyManager || !blockchainRegistry) {
if (!config) {
throw new Error('KeyManager and BlockchainRegistry are required')
}
keyManager = new KeyManager(config)
blockchainRegistry = new BlockchainRegistry(keyManager, config)
}
// prepare compute engines
this.instance = new OceanNode(config, db, node, provider, indexer)
this.instance = new OceanNode(
config,
db,
node,
provider,
indexer,
keyManager,
blockchainRegistry
)
}
return this.instance
}
Expand All @@ -94,7 +127,12 @@ export class OceanNode {
OCEAN_NODE_LOGGER.error('C2DDatabase is mandatory for compute engines!')
return
}
this.c2dEngines = new C2DEngines(this.config, this.db.c2d, this.escrow)
this.c2dEngines = new C2DEngines(
this.config,
this.db.c2d,
this.escrow,
this.keyManager
)
await this.c2dEngines.startAllEngines()
}
}
Expand Down Expand Up @@ -123,14 +161,6 @@ export class OceanNode {
return this.coreHandlers
}

public setRemoteCaller(client: string | string[]) {
this.remoteCaller = client
}

public getRemoteCaller(): string | string[] {
return this.remoteCaller
}

public getRequestMapSize(): number {
return this.requestMap.size
}
Expand All @@ -143,12 +173,29 @@ export class OceanNode {
return this.auth
}

public getKeyManager(): KeyManager {
return this.keyManager
}

public getBlockchainRegistry(): BlockchainRegistry {
return this.blockchainRegistry
}

/**
* Get a Blockchain instance for the given chainId.
* Delegates to BlockchainRegistry.
*/
public getBlockchain(chainId: number): Blockchain | null {
return this.blockchainRegistry.getBlockchain(chainId)
}

public setConfig(config: OceanNodeConfig) {
this.config = config
if (this.config) {
this.escrow = new Escrow(
this.config.supportedNetworks,
this.config.claimDurationTimeout
this.config.claimDurationTimeout,
this.blockchainRegistry
)
}
}
Expand Down
Loading
Loading