PKE Encryption

X25519 public-key encryption for multi-recipient sharing.

Public Key Encryption (PKE) Guide

This guide covers the wallet-style public key encryption system for secure sharing without exchanging passphrases.

Overview

Unlike traditional passphrase-based encryption where secrets must be shared between parties, PKE uses asymmetric cryptography:

  • Public key address (`mm:...`) - Shareable identifier, like a crypto wallet address
  • Private key - Secret, stored encrypted on disk, never shared
  • Senders encrypt using only the recipient's public address
  • Only the private key holder can decrypt

Quick Start

Generate Your Identity

use matric_crypto::pke::{Keypair, save_private_key, save_public_key};

// Generate a new keypair
let keypair = Keypair::generate();

// Get your public address to share with others
let my_address = keypair.public.to_address();
println!("My address: {}", my_address);
// Example: mm:7Xq9KmPvR3nYhW2sT8uJcL4bN6aF5gD1eZ

// Save your keys
save_private_key(&keypair.private, "/path/to/private.key", "<PKE_PASSPHRASE>")?;
save_public_key(&keypair.public, "/path/to/public.key", Some("My Key"))?;

Share Your Address

Your address (`mm:...`) can be shared publicly. It's derived from your public key using BLAKE3 hashing and Base58Check encoding with a checksum to catch typos.

Address format: mm:<base58check-encoded-data>

Example: mm:7Xq9KmPvR3nYhW2sT8uJcL4bN6aF5gD1eZ

Encrypt for Recipients

To encrypt data, you only need the recipients' public keys (which can be derived from their addresses):

use matric_crypto::pke::{encrypt_pke, Keypair};

// Recipient public keys
let alice = Keypair::generate();
let bob = Keypair::generate();

// Encrypt for multiple recipients
let secret_data = b"Confidential information";
let encrypted = encrypt_pke(
    secret_data,
    &[alice.public.clone(), bob.public.clone()],
    Some("data.json".into())
)?;

// Save encrypted data to file
std::fs::write("encrypted.mmpke", &encrypted)?;

Decrypt with Your Private Key

use matric_crypto::pke::{decrypt_pke, load_private_key};

// Load your private key
let private_key = load_private_key("/path/to/private.key", "<PKE_PASSPHRASE>")?;

// Decrypt
let encrypted = std::fs::read("encrypted.mmpke")?;
let (plaintext, header) = decrypt_pke(&encrypted, &private_key)?;

println!("Decrypted {} bytes", plaintext.len());
println!("Original filename: {:?}", header.original_filename);

Address Format

Addresses use a Bitcoin-style format with built-in error detection:

┌─────────────────────────────────────────────────────────┐
│ mm:<version><hash><checksum>                            │
│                                                          │
│ - Prefix: "mm:" (Fortémi)                          │
│ - Version: 1 byte (0x01 for v1)                          │
│ - Hash: 20 bytes of BLAKE3(public_key)                   │
│ - Checksum: 4 bytes of BLAKE3(version || hash)           │
│ - Encoding: Base58 (Bitcoin alphabet)                    │
└─────────────────────────────────────────────────────────┘

Properties:

  • ~45 characters total length
  • No confusing characters (0, O, I, l excluded)
  • Checksum catches typos and copy-paste errors
  • Version byte allows future format upgrades

Address Validation

use matric_crypto::pke::Address;

// Parse and validate an address
let addr: Address = "mm:7Xq9KmPvR3nYhW2sT8uJcL4bN6aF5gD1eZ".parse()?;

// Verify checksum
assert!(addr.verify_checksum());

File Format (MMPKE01)

Encrypted files use the MMPKE01 format:

┌─────────────────────────────────────────────────────────────┐
│ Magic: "MMPKE01\
" (8 bytes)                                │
├─────────────────────────────────────────────────────────────┤
│ Header Length: u32 LE (4 bytes)                             │
├─────────────────────────────────────────────────────────────┤
│ Header (JSON):                                              │
│ {                                                           │
│   "version": 1,                                             │
│   "ephemeral_pubkey": "<base64>",                           │
│   "recipients": [                                           │
│     {                                                       │
│       "address": "mm:ABC123...",                            │
│       "encrypted_dek": "<base64>",                          │
│       "dek_nonce": "<base64>"                               │
│     }                                                       │
│   ],                                                        │
│   "data_nonce": "<base64>",                                 │
│   "original_filename": "<ORIGINAL_FILENAME>"                 │
│ }                                                           │
├─────────────────────────────────────────────────────────────┤
│ Encrypted Data (AES-256-GCM ciphertext + 16-byte tag)       │
└─────────────────────────────────────────────────────────────┘

Inspecting Encrypted Files

use matric_crypto::pke::get_pke_recipients;

let encrypted = std::fs::read("encrypted.mmpke")?;
let recipients = get_pke_recipients(&encrypted)?;

println!("This file can be decrypted by:");
for addr in recipients {
    println!("  {}", addr);
}

Cryptographic Details

Algorithms Used

ComponentAlgorithmPurpose
Key ExchangeX25519 (Curve25519)ECDH shared secret derivation
Key DerivationHKDF-SHA256Derive encryption keys from shared secrets
Symmetric EncryptionAES-256-GCMAuthenticated encryption
Address HashingBLAKE3Fast, secure address derivation

Encryption Flow

ENCRYPTION (sender → recipients)

1. Generate ephemeral X25519 keypair
2. Generate random DEK (Data Encryption Key, 32 bytes)
3. For each recipient:
   a. ECDH: ephemeral_private + recipient_public → shared_secret
   b. HKDF: shared_secret → KEK (Key Encryption Key)
   c. AES-GCM: encrypt DEK with KEK
4. AES-GCM: encrypt plaintext with DEK
5. Serialize MMPKE01 format with ephemeral public key

Decryption Flow

DECRYPTION (recipient)

1. Parse MMPKE01 header
2. Find recipient block matching our address
3. ECDH: my_private + ephemeral_public → shared_secret
4. HKDF: shared_secret → KEK
5. AES-GCM: decrypt encrypted_dek with KEK → DEK
6. AES-GCM: decrypt ciphertext with DEK → plaintext

Security Properties

Forward Secrecy

Each encryption operation generates a fresh ephemeral keypair. Even if a recipient's long-term private key is compromised later, past encrypted messages remain secure because the ephemeral keys are not stored.

Multi-Recipient Efficiency

The data is encrypted only once (with the DEK). Adding more recipients only adds small KEK-wrapped DEK blocks to the header, not re-encryption of the full payload.

Key Protection

Private keys are encrypted at rest using:

  • Argon2id key derivation (memory-hard, GPU-resistant)
  • AES-256-GCM encryption
  • MMPKEKEY format with secure salt and nonce generation

Tamper Detection

AES-256-GCM is an AEAD (Authenticated Encryption with Associated Data) cipher. Any modification to the ciphertext or header will be detected during decryption.

Key Management

Generating Keys

use matric_crypto::pke::Keypair;

// Generate new keypair
let keypair = Keypair::generate();

// Or from an existing private key
let keypair = Keypair::from_private(existing_private_key);

Saving Keys

use matric_crypto::pke::{save_private_key, save_public_key};

// Private key (encrypted with passphrase)
save_private_key(&keypair.private, "~/.matric/private.key", "<PKE_PASSPHRASE>")?;

// Public key (plaintext, shareable)
save_public_key(&keypair.public, "~/.matric/public.key", Some("Work Key"))?;

Loading Keys

use matric_crypto::pke::{load_private_key, load_public_key};

let private = load_private_key("~/.matric/private.key", "<PKE_PASSPHRASE>")?;
let public = load_public_key("~/.matric/public.key")?;

Key Backup

Critical: Back up your private key securely. If lost, you cannot decrypt any data encrypted for your address.

Recommended backup strategies: 1. Encrypted USB drive - Store the encrypted private key file 2. Paper backup - Print the passphrase and store securely 3. Password manager - Store the passphrase with key file location

Error Handling

Common Errors

ErrorCauseSolution
`InvalidAddress`Typo or corrupted addressVerify checksum, re-copy address
`Decryption failed`Wrong key or corrupted dataUse correct private key
`No recipient block`Not an intended recipientCheck you have the right file
`PassphraseTooShort`Passphrase < 12 charactersUse a longer passphrase

Checking Decryptability

use matric_crypto::pke::can_decrypt_pke;

if can_decrypt_pke(&encrypted, &my_private_key) {
    println!("I can decrypt this file");
} else {
    println!("I am not a recipient");
}

Best Practices

1. Use strong passphrases for private key files (12+ characters) 2. Verify addresses before encrypting sensitive data 3. Back up private keys securely 4. Rotate keys periodically for long-term security 5. Use unique addresses for different contexts (work, personal)