Skip to content

Issue #3: Build Cryptographic Utilities with Secure Key Management #18

@wheval

Description

@wheval

Build Cryptographic Utilities with Secure Key Management

Description:
Implement cryptographic utilities for key generation, signing, verification, and secure storage. This is a security-critical component that must follow best practices for non-custodial wallet architecture.

Context:
The wallet needs to generate keypairs, sign transactions, and securely store private keys encrypted with user passwords. Critical principle: Secret keys must NEVER be transmitted or stored on any server - they should only exist encrypted on the user's device. This package must be auditable and well-tested.

Security Architecture:
This implementation follows a non-custodial approach where:

  1. Secret keys are generated locally on the user's device
  2. Keys are encrypted with user password before storage
  3. Signing happens locally (keys never leave the device)
  4. Only signed transactions (not keys) are transmitted

For browser extensions, use chrome.storage.local for encrypted key storage. For mobile apps, use platform-specific secure storage (iOS Keychain, Android Shared Preferences with encryption).

Requirements:

  • Implement keypair generation using @stellar/stellar-sdk (Ed25519)
  • Add BIP39 mnemonic generation and validation (12 words)
  • Derive keypair from mnemonic (support account index)
  • Implement PBKDF2 key derivation from passwords (100k iterations, salt)
  • Add AES-256-GCM encryption/decryption for secret keys
  • Implement local transaction signing (keys never transmitted)
  • Add signature verification functions
  • Password strength validator (using zxcvbn)
  • Secure random IV generation for each encryption
  • Zero out sensitive data from memory after use
  • Unit tests for all crypto functions (100% coverage required)
  • Security documentation explaining all algorithms and threat model

Implementation Guide:

import * as StellarSdk from '@stellar/stellar-sdk';
import * as bip39 from 'bip39';
import { hkdf } from '@noble/hashes/hkdf';
import { sha256 } from '@noble/hashes/sha256';
import { pbkdf2 } from '@noble/hashes/pbkdf2';
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
import { randomBytes } from '@noble/hashes/utils';

// 1. Generate mnemonic and keypair
export function generateMnemonic(): string {
  return bip39.generateMnemonic(128); // 12 words
}

export function deriveKeypairFromMnemonic(
  mnemonic: string,
  accountIndex: number = 0
): StellarSdk.Keypair {
  // Validate mnemonic
  if (!bip39.validateMnemonic(mnemonic)) {
    throw new Error('Invalid mnemonic');
  }
  
  // Generate seed from mnemonic
  const seed = bip39.mnemonicToSeedSync(mnemonic);
  
  // Derive key using HKDF
  const path = `m/44'/148'/${accountIndex}'`;
  const derivedKey = hkdf(sha256, seed, undefined, path, 32);
  
  return StellarSdk.Keypair.fromRawEd25519Seed(Buffer.from(derivedKey));
}

// 2. Encrypt secret key with password (NEVER store unencrypted!)
export async function encryptSecretKey(
  secretKey: string,
  password: string
): Promise<{ ciphertext: string; salt: string; nonce: string }> {
  // Generate random salt
  const salt = randomBytes(32);
  
  // Derive encryption key from password using PBKDF2
  const derivedKey = pbkdf2(sha256, password, salt, {
    c: 100_000, // 100k iterations
    dkLen: 32
  });
  
  // Generate random nonce
  const nonce = randomBytes(24);
  
  // Encrypt using XChaCha20-Poly1305
  const cipher = xchacha20poly1305(derivedKey, nonce);
  const secretBytes = Buffer.from(secretKey, 'utf-8');
  const ciphertext = cipher.encrypt(secretBytes);
  
  return {
    ciphertext: Buffer.from(ciphertext).toString('base64'),
    salt: Buffer.from(salt).toString('base64'),
    nonce: Buffer.from(nonce).toString('base64')
  };
}

// 3. Decrypt secret key
export async function decryptSecretKey(
  encrypted: { ciphertext: string; salt: string; nonce: string },
  password: string
): Promise<string> {
  const salt = Buffer.from(encrypted.salt, 'base64');
  const nonce = Buffer.from(encrypted.nonce, 'base64');
  const ciphertext = Buffer.from(encrypted.ciphertext, 'base64');
  
  // Derive key from password
  const derivedKey = pbkdf2(sha256, password, salt, {
    c: 100_000,
    dkLen: 32
  });
  
  // Decrypt
  const cipher = xchacha20poly1305(derivedKey, nonce);
  const decrypted = cipher.decrypt(ciphertext);
  
  return Buffer.from(decrypted).toString('utf-8');
}

// 4. Sign transaction locally (key never leaves device)
export function signTransaction(
  transaction: StellarSdk.Transaction,
  secretKey: string
): StellarSdk.Transaction {
  const keypair = StellarSdk.Keypair.fromSecret(secretKey);
  transaction.sign(keypair);
  return transaction;
}

// 5. Verify signature
export function verifySignature(
  message: Buffer,
  signature: Buffer,
  publicKey: string
): boolean {
  const keypair = StellarSdk.Keypair.fromPublicKey(publicKey);
  return keypair.verify(message, signature);
}

// 6. Password strength validation
import zxcvbn from 'zxcvbn';

export function validatePasswordStrength(password: string): {
  score: number; // 0-4
  feedback: string[];
  isValid: boolean;
} {
  const result = zxcvbn(password);
  return {
    score: result.score,
    feedback: [
      ...(result.feedback.warning ? [result.feedback.warning] : []),
      ...result.feedback.suggestions
    ],
    isValid: result.score >= 3 // Require "strong" or better
  };
}

Usage:

import { Crypto } from '@ancore/crypto';

// Onboarding: Generate new wallet
const mnemonic = Crypto.generateMnemonic();
const keypair = Crypto.deriveKeypairFromMnemonic(mnemonic, 0);

// Validate password
const strength = Crypto.validatePasswordStrength('myPassword123!');
if (!strength.isValid) {
  console.error('Password too weak:', strength.feedback);
}

// Encrypt and store secret key
const encrypted = await Crypto.encryptSecretKey(
  keypair.secret(),
  'userPassword'
);
// Store encrypted in chrome.storage.local

// Later: Sign transaction
const decrypted = await Crypto.decryptSecretKey(encrypted, 'userPassword');
const signedTx = Crypto.signTransaction(transaction, decrypted);
// Submit signedTx to network

Key Security Principles:

  1. Never transmit secret keys - Only send signed transactions
  2. Always encrypt before storage - Use user password + PBKDF2 + AES-GCM
  3. Use secure random - For salts, nonces, and IVs
  4. Zero out sensitive data - Clear keys from memory after use
  5. Validate mnemonics - Check BIP39 checksum before deriving keys

Files to Create:

  • packages/crypto/src/mnemonic.ts (BIP39 generation and derivation)
  • packages/crypto/src/encryption.ts (PBKDF2 + XChaCha20-Poly1305)
  • packages/crypto/src/signing.ts (Transaction signing and verification)
  • packages/crypto/src/password.ts (Strength validation with zxcvbn)
  • packages/crypto/src/types.ts (EncryptedKey type)
  • packages/crypto/src/__tests__/mnemonic.test.ts
  • packages/crypto/src/__tests__/encryption.test.ts
  • packages/crypto/src/__tests__/signing.test.ts
  • packages/crypto/src/__tests__/password.test.ts
  • packages/crypto/SECURITY.md (Threat model and algorithm choices)
  • packages/crypto/README.md (Usage examples and security warnings)

Dependencies:

  • @stellar/stellar-sdk (^12.0.0) - Ed25519 keypairs and signing
  • @noble/hashes (^1.3.0) - PBKDF2, SHA256, HKDF
  • @noble/ciphers (^0.4.0) - XChaCha20-Poly1305 encryption
  • bip39 (^3.1.0) - BIP39 mnemonic generation
  • zxcvbn (^4.6.0) - Password strength estimation

Note: Use @noble/* packages instead of WebCrypto for better compatibility and auditable code.

Success Criteria:

  • All cryptographic operations use industry-standard algorithms (PBKDF2, XChaCha20-Poly1305, Ed25519)
  • Secret keys NEVER logged, transmitted, or stored unencrypted
  • Each encryption uses unique random salt and nonce
  • Password must be "strong" (zxcvbn score ≥ 3)
  • All edge cases tested (wrong password, corrupted data, invalid mnemonic)
  • Sensitive data zeroed from memory after use

Definition of Done:

  • All requirements implemented with security-first approach
  • 100% test coverage for all crypto functions
  • Security checklist completed (no key leaks, proper randomness)
  • No secrets or real keys in tests or logs (use test fixtures)
  • SECURITY.md explains algorithms, threat model, and custody approach
  • Code follows non-custodial architecture (keys never leave device)

Additional Resources:

Labels: crypto, security, critical, needs-review
Complexity: 200 points (High)
Estimated Effort: 4-5 days
Priority: Critical


Questions or need help? Join our Telegram community

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions