{"version":3,"file":"KemKeyExchangeService.mjs","names":["KemKeyExchangeService","logger: Logger","metadata: KemKeyMetadata"],"sources":["../../src/services/KemKeyExchangeService.ts"],"sourcesContent":["/**\n * KEM Key Exchange Service\n *\n * Exchanges ML-KEM public keys between agents for quantum-safe vault encryption.\n *\n * Key Design:\n * - Uses existing X25519 channel to bootstrap ML-KEM key exchange\n * - Seals ML-KEM public keys in HPKE envelopes (RFC 9180)\n * - Stores peer's ML-KEM keys in connection metadata\n * - Uses signing protocol's provide-artifacts message (sealed-secret@1)\n *\n * Flow:\n * 1. Agent A generates ML-KEM keypair\n * 2. Agent A seals their ML-KEM public key using peer's X25519 key\n * 3. Agent A sends sealed key via provide-artifacts message\n * 4. Agent B unwraps the ML-KEM key using their X25519 private key\n * 5. Agent B stores Agent A's ML-KEM key in connection metadata\n * 6. Agent B can now encrypt vaults to Agent A's ML-KEM key\n */\n\nimport type { AgentContext, Logger } from '@credo-ts/core'\n\nimport { injectable, InjectionSymbols, inject } from '@credo-ts/core'\nimport { DidCommConnectionRepository } from '@credo-ts/didcomm'\n\nimport {\n  kemGenerateKeypair,\n  deriveKid,\n  toBase64Url,\n  fromBase64Url,\n} from '../crypto/wasm/VaultCrypto'\nimport { HPKEService } from './HPKEService'\nimport { KemKeypairRepository } from '../repository/KemKeypairRepository'\nimport { KemKeypairRecord } from '../repository/KemKeypairRecord'\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Types\n// ═══════════════════════════════════════════════════════════════════════════\n\n/**\n * ML-KEM keypair with derived key ID\n */\nexport interface KemKeypairWithKid {\n  /** Key identifier (derived from public key hash) */\n  kid: string\n  /** ML-KEM public key (1184 bytes for ML-KEM-768) */\n  publicKey: Uint8Array\n  /** ML-KEM secret key (2400 bytes for ML-KEM-768) */\n  secretKey: Uint8Array\n}\n\n/**\n * ML-KEM public key info (no secret key)\n */\nexport interface KemPublicKeyInfo {\n  /** Key identifier */\n  kid: string\n  /** ML-KEM public key */\n  publicKey: Uint8Array\n  /** Timestamp when the key was stored */\n  storedAt?: string\n}\n\n/**\n * Sealed artifact containing ML-KEM public key\n * Encrypted with HPKE to recipient's X25519 key\n */\nexport interface SealedKemKeyArtifact {\n  /** Artifact type identifier */\n  type: 'sealed-ml-kem-key@1'\n  /** HPKE envelope suite */\n  envelope: 'envelope-hpke@1'\n  /** Base64url-encoded ciphertext */\n  ciphertext: string\n  /** Base64url-encoded ephemeral public key */\n  ephemeralPublicKey: string\n  /** Key ID of the sealed ML-KEM key */\n  kid: string\n}\n\n/**\n * Connection metadata key for ML-KEM keys\n */\nexport const KEM_KEY_METADATA_KEY = 'vaults/kem-key'\n\n/**\n * Metadata stored in connection for peer's KEM key\n */\nexport interface KemKeyMetadata {\n  /** Key identifier */\n  kid: string\n  /** Base64url-encoded ML-KEM public key */\n  publicKey: string\n  /** When the key was stored */\n  storedAt: string\n  /** Optional: when the key exchange session was initiated */\n  exchangeSessionId?: string\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Service\n// ═══════════════════════════════════════════════════════════════════════════\n\n@injectable()\nexport class KemKeyExchangeService {\n  private logger: Logger\n  private connectionRepository: DidCommConnectionRepository\n  private hpkeService: HPKEService\n  private kemKeypairRepository: KemKeypairRepository\n\n  public constructor(\n    @inject(InjectionSymbols.Logger) logger: Logger,\n    connectionRepository: DidCommConnectionRepository,\n    hpkeService: HPKEService,\n    kemKeypairRepository: KemKeypairRepository\n  ) {\n    this.logger = logger\n    this.connectionRepository = connectionRepository\n    this.hpkeService = hpkeService\n    this.kemKeypairRepository = kemKeypairRepository\n  }\n\n  /**\n   * Generate a new ML-KEM-768 keypair\n   *\n   * @returns Keypair with derived key ID\n   */\n  public generateKemKeypair(): KemKeypairWithKid {\n    this.logger.debug('Generating ML-KEM-768 keypair')\n\n    const keypair = kemGenerateKeypair()\n    const kid = deriveKid(keypair.publicKey)\n\n    this.logger.debug(`Generated ML-KEM keypair with kid: ${kid.substring(0, 20)}...`)\n\n    return {\n      kid,\n      publicKey: keypair.publicKey,\n      secretKey: keypair.secretKey,\n    }\n  }\n\n  /**\n   * Create a sealed artifact containing our ML-KEM public key\n   *\n   * This creates an artifact suitable for sending via provide-artifacts message.\n   * The ML-KEM public key is sealed using HPKE (RFC 9180) to the recipient's P-256 public key.\n   *\n   * @param keypair - Our ML-KEM keypair\n   * @param recipientPublicKey - Recipient's P-256 public key for HPKE encryption\n   * @returns Sealed artifact ready for transmission\n   */\n  public async createKemKeyArtifact(\n    keypair: KemKeypairWithKid,\n    recipientPublicKey: Uint8Array\n  ): Promise<SealedKemKeyArtifact> {\n    this.logger.debug(`Creating sealed KEM key artifact for kid: ${keypair.kid.substring(0, 20)}...`)\n\n    // Prepare payload: { kid, publicKey }\n    const payload = JSON.stringify({\n      kid: keypair.kid,\n      publicKey: toBase64Url(keypair.publicKey),\n    })\n    const payloadBytes = new TextEncoder().encode(payload)\n\n    // Create AAD binding the artifact to the key ID\n    const aad = new TextEncoder().encode(`sealed-ml-kem-key:${keypair.kid}`)\n\n    // Encrypt using HPKE\n    const result = await this.hpkeService.encrypt(recipientPublicKey, payloadBytes, aad)\n\n    return {\n      type: 'sealed-ml-kem-key@1',\n      envelope: 'envelope-hpke@1',\n      ciphertext: toBase64Url(result.ciphertext),\n      ephemeralPublicKey: toBase64Url(result.ephemeralPublicKey),\n      kid: keypair.kid,\n    }\n  }\n\n  /**\n   * Unwrap a sealed KEM key artifact\n   *\n   * Decrypts the sealed artifact using HPKE to extract the peer's ML-KEM public key.\n   *\n   * @param artifact - Sealed artifact received from peer\n   * @param recipientSecretKey - Our P-256 private key to decrypt\n   * @returns Peer's ML-KEM public key info\n   */\n  public async unwrapKemKeyArtifact(\n    artifact: SealedKemKeyArtifact,\n    recipientSecretKey: Uint8Array\n  ): Promise<KemPublicKeyInfo> {\n    this.logger.debug(`Unwrapping KEM key artifact: ${artifact.kid.substring(0, 20)}...`)\n\n    const ciphertext = fromBase64Url(artifact.ciphertext)\n    const ephemeralPublicKey = fromBase64Url(artifact.ephemeralPublicKey)\n\n    // Create AAD (must match encryption)\n    const aad = new TextEncoder().encode(`sealed-ml-kem-key:${artifact.kid}`)\n\n    // Decrypt using HPKE\n    const result = await this.hpkeService.decrypt(recipientSecretKey, ciphertext, ephemeralPublicKey, aad)\n\n    const payload = JSON.parse(new TextDecoder().decode(result.plaintext)) as {\n      kid: string\n      publicKey: string\n    }\n\n    return {\n      kid: payload.kid,\n      publicKey: fromBase64Url(payload.publicKey),\n      storedAt: new Date().toISOString(),\n    }\n  }\n\n  /**\n   * Store peer's ML-KEM public key in connection metadata\n   *\n   * This allows retrieving the peer's KEM key later for vault encryption.\n   *\n   * @param agentContext - Agent context\n   * @param connectionId - Connection record ID\n   * @param keyInfo - Peer's ML-KEM public key info\n   */\n  public async storePeerKemKey(\n    agentContext: AgentContext,\n    connectionId: string,\n    keyInfo: KemPublicKeyInfo\n  ): Promise<void> {\n    this.logger.debug(`Storing peer KEM key for connection ${connectionId}: ${keyInfo.kid.substring(0, 20)}...`)\n\n    // Get connection record\n    const connection = await this.connectionRepository.getById(agentContext, connectionId)\n\n    // Store KEM key in metadata\n    const metadata: KemKeyMetadata = {\n      kid: keyInfo.kid,\n      publicKey: toBase64Url(keyInfo.publicKey),\n      storedAt: keyInfo.storedAt ?? new Date().toISOString(),\n    }\n\n    connection.metadata.set(KEM_KEY_METADATA_KEY, metadata)\n\n    // Save connection\n    await this.connectionRepository.update(agentContext, connection)\n\n    this.logger.info(`Stored peer KEM key for connection ${connectionId}`)\n  }\n\n  /**\n   * Retrieve peer's ML-KEM public key from connection metadata\n   *\n   * @param agentContext - Agent context\n   * @param connectionId - Connection record ID\n   * @returns Peer's KEM key info, or null if not found\n   */\n  public async getPeerKemKey(agentContext: AgentContext, connectionId: string): Promise<KemPublicKeyInfo | null> {\n    this.logger.debug(`Retrieving peer KEM key for connection ${connectionId}`)\n\n    // Get connection record\n    const connection = await this.connectionRepository.getById(agentContext, connectionId)\n\n    // Get KEM key from metadata\n    const metadata = connection.metadata.get(KEM_KEY_METADATA_KEY) as KemKeyMetadata | null\n\n    if (!metadata) {\n      this.logger.debug(`No peer KEM key found for connection ${connectionId}`)\n      return null\n    }\n\n    return {\n      kid: metadata.kid,\n      publicKey: fromBase64Url(metadata.publicKey),\n      storedAt: metadata.storedAt,\n    }\n  }\n\n  /**\n   * Delete peer's ML-KEM public key from connection metadata\n   *\n   * @param agentContext - Agent context\n   * @param connectionId - Connection record ID\n   */\n  public async deletePeerKemKey(agentContext: AgentContext, connectionId: string): Promise<void> {\n    this.logger.debug(`Deleting peer KEM key for connection ${connectionId}`)\n\n    // Get connection record\n    const connection = await this.connectionRepository.getById(agentContext, connectionId)\n\n    // Delete KEM key from metadata\n    connection.metadata.delete(KEM_KEY_METADATA_KEY)\n\n    // Save connection\n    await this.connectionRepository.update(agentContext, connection)\n\n    this.logger.info(`Deleted peer KEM key for connection ${connectionId}`)\n  }\n\n  /**\n   * Check if a connection has a peer KEM key\n   *\n   * @param agentContext - Agent context\n   * @param connectionId - Connection record ID\n   * @returns True if peer has KEM key stored\n   */\n  public async hasPeerKemKey(agentContext: AgentContext, connectionId: string): Promise<boolean> {\n    const keyInfo = await this.getPeerKemKey(agentContext, connectionId)\n    return keyInfo !== null\n  }\n\n  // ═══════════════════════════════════════════════════════════════════════════\n  // Local Keypair Storage\n  // ═══════════════════════════════════════════════════════════════════════════\n\n  /**\n   * Store a local KEM keypair (including secret key) for a connection\n   *\n   * Call this after generateKemKeypair() to persist the keypair so it can\n   * be retrieved later for vault decryption.\n   *\n   * @param agentContext - Agent context\n   * @param connectionId - Connection to associate the keypair with\n   * @param keypair - Full ML-KEM keypair\n   */\n  public async storeLocalKeypair(\n    agentContext: AgentContext,\n    connectionId: string,\n    keypair: KemKeypairWithKid\n  ): Promise<void> {\n    this.logger.debug(`Storing local KEM keypair for connection ${connectionId}: kid=${keypair.kid.substring(0, 20)}...`)\n\n    // Check for existing keypair on this connection\n    const existing = await this.kemKeypairRepository.findByConnectionId(agentContext, connectionId)\n    if (existing) {\n      this.logger.debug(`Updating existing keypair for connection ${connectionId}`)\n      existing.kid = keypair.kid\n      existing.publicKey = toBase64Url(keypair.publicKey)\n      existing.secretKey = toBase64Url(keypair.secretKey)\n      await this.kemKeypairRepository.update(agentContext, existing)\n      return\n    }\n\n    const record = new KemKeypairRecord({\n      connectionId,\n      kid: keypair.kid,\n      publicKey: toBase64Url(keypair.publicKey),\n      secretKey: toBase64Url(keypair.secretKey),\n    })\n\n    await this.kemKeypairRepository.save(agentContext, record)\n    this.logger.info(`Stored local KEM keypair for connection ${connectionId}`)\n  }\n\n  /**\n   * Get the local KEM keypair for a connection\n   *\n   * @param agentContext - Agent context\n   * @param connectionId - Connection record ID\n   * @returns Full keypair including secret key, or null if not found\n   */\n  public async getLocalKeypair(\n    agentContext: AgentContext,\n    connectionId: string\n  ): Promise<KemKeypairWithKid | null> {\n    const record = await this.kemKeypairRepository.findByConnectionId(agentContext, connectionId)\n    if (!record) {\n      return null\n    }\n    return {\n      kid: record.kid,\n      publicKey: fromBase64Url(record.publicKey),\n      secretKey: fromBase64Url(record.secretKey),\n    }\n  }\n\n  /**\n   * Find a local keypair by its key identifier (kid)\n   *\n   * This is the primary lookup used during vault decryption: extract recipient\n   * kids from vault header, then find which local keypair matches.\n   *\n   * @param agentContext - Agent context\n   * @param kid - Key identifier to search for\n   * @returns Keypair and associated connectionId, or null\n   */\n  public async findKeypairByKid(\n    agentContext: AgentContext,\n    kid: string\n  ): Promise<{ keypair: KemKeypairWithKid; connectionId: string } | null> {\n    const record = await this.kemKeypairRepository.findByKid(agentContext, kid)\n    if (!record) {\n      return null\n    }\n    return {\n      keypair: {\n        kid: record.kid,\n        publicKey: fromBase64Url(record.publicKey),\n        secretKey: fromBase64Url(record.secretKey),\n      },\n      connectionId: record.connectionId,\n    }\n  }\n\n  /**\n   * Find a local keypair matching any of the given recipient kids\n   *\n   * Used to find which local key can decrypt a vault when the vault header\n   * lists multiple possible recipients.\n   *\n   * @param agentContext - Agent context\n   * @param kids - Set of key identifiers from vault recipients\n   * @returns Matching keypair and connectionId, or null\n   */\n  public async findKeypairByRecipientKids(\n    agentContext: AgentContext,\n    kids: Set<string>\n  ): Promise<{ keypair: KemKeypairWithKid; connectionId: string } | null> {\n    for (const kid of kids) {\n      const result = await this.findKeypairByKid(agentContext, kid)\n      if (result) {\n        return result\n      }\n    }\n    return null\n  }\n\n  /**\n   * Check if a local keypair exists for a connection\n   *\n   * @param agentContext - Agent context\n   * @param connectionId - Connection record ID\n   * @returns True if local keypair exists\n   */\n  public async hasLocalKeypair(agentContext: AgentContext, connectionId: string): Promise<boolean> {\n    const record = await this.kemKeypairRepository.findByConnectionId(agentContext, connectionId)\n    return record !== null\n  }\n\n  /**\n   * Delete the local keypair for a connection\n   *\n   * @param agentContext - Agent context\n   * @param connectionId - Connection record ID\n   */\n  public async deleteLocalKeypair(agentContext: AgentContext, connectionId: string): Promise<void> {\n    await this.kemKeypairRepository.deleteByConnectionId(agentContext, connectionId)\n    this.logger.info(`Deleted local KEM keypair for connection ${connectionId}`)\n  }\n}\n"],"mappings":"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"}