Skip to content

Consolidate secret encryption and decryption into a shared utility #93

@phertyameen

Description

@phertyameen

Summary

The ephemeral account secret key is encrypted when an account is created and decrypted when a claim is redeemed to sign the sweep transaction. These two operations currently live in two completely separate files using two completely different implementations. When Issue #83 upgrades encryption from base64 to AES-256-GCM, the decryption side will not be updated automatically, meaning the claim flow will silently fail trying to decrypt an AES-encrypted secret using base64 decoding. This issue consolidates both sides into a single shared utility so they can never drift apart again.

Background: Why this happened

AccountsService owns the encryptSecret() method because that's where accounts are created. ClaimRedemptionProvider owns a separate decryptSecret() method because that's where secrets are needed for sweep signing. Neither file knows about the other's implementation. Currently both use base64, so they happen to be compatible, but that's coincidence, not design. Once real encryption lands (Issue #83), this hidden coupling becomes a runtime breakage.

Understanding the secret lifecycle

The ephemeral account keypair is generated at account creation time. The secret key is encrypted and stored in the secretKeyEncrypted column of the accounts table. It stays there until a claim is redeemed, at which point ClaimRedemptionProvider reads it from the account record, decrypts it, and passes it to SweepsService so the sweep transaction can be signed on behalf of the ephemeral account. The secret is never returned in any API response, it only lives in the database and briefly in memory during a sweep.

What a shared utility looks like

A shared encryption utility should live somewhere that both AccountsService and ClaimRedemptionProvider can import it without creating a circular dependency. The src/common/ directory is the right place for cross-module utilities in this project, look at how other shared code is structured there. The utility should expose both encrypt(plaintext: string): string and decrypt(ciphertext: string): string, reading its key configuration from environment config.

Important constraints

The encryption key must come from environment config: it must not be hardcoded
The encrypted format must be self-contained: the output of encrypt() must carry everything decrypt() needs to reverse it (IV, auth tag, ciphertext), without relying on any external state
Both methods must be the only implementations in the codebase: remove the inline methods from both AccountsService and ClaimRedemptionProvider once the utility exists
The utility must be covered by unit tests that verify encrypt → decrypt round-trips correctly and that a wrong key fails decryption, not just that the methods exist

Where to look

src/modules/accounts/accounts.service.ts: current encryptSecret() implementation (base64 placeholder)
src/modules/claims/providers/claim-redemption.provider.ts: current decryptSecret() implementation (also base64 placeholder)
src/common/: where the shared utility should live

Issue #83: describes the AES-256-GCM approach this utility should implement. This issue and Issue #83 should be treated as a pair: the utility created here is the implementation that Issue 83 described

Acceptance Criteria

  • A shared encryption utility exists in src/common/ with both encrypt() and decrypt() methods
  • The utility uses AES-256-GCM with a randomly generated IV per encryption call
  • Both AccountsService and ClaimRedemptionProvider use the shared utility, their inline methods are removed
  • The encryption key is sourced from environment config in one place, not duplicated
  • Unit tests verify: successful round-trip, wrong key fails, tampered ciphertext fails
  • .env.example documents the key format and how to generate one if not already done by Issue Replace base64 secret encoding with AES-256-GCM encryption in AccountsService #83

Metadata

Metadata

Assignees

No one assigned

    Labels

    criticalVery important for the project to function.nestjsBackend frameworksecuritytypescriptPrograming language

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions