Skip to main content

Overview

The encryption module provides AES-256-GCM encryption for secure vector storage. Keys are derived from Solana wallet public keys, ensuring only the wallet owner can decrypt their vectors.

Use Case

Encryption enables:
  • Private vector storage: Vectors remain confidential on Shadow Drive
  • Wallet-based access control: Only wallet owner can decrypt
  • Batch encryption: Efficient encryption of multiple vectors
  • Authenticated encryption: Detects tampering

AES-256-GCM

Industry-standard authenticated encryption

Wallet-Derived Keys

Keys derived from Solana public keys

Constants

const NONCE_SIZE: usize = 12; // GCM nonce size in bytes

Core Functions

encrypt_vectors

Encrypt a batch of vectors using AES-256-GCM.
pub fn encrypt_vectors(
    vectors: &[Vec<f32>],
    key: &[u8; 32]
) -> Result<Vec<u8>, SolVecError>
vectors
&[Vec<f32>]
required
Array of vectors to encrypt. All vectors must have the same dimension.
key
&[u8; 32]
required
32-byte encryption key. Should be derived from user’s Solana wallet using derive_key_from_pubkey.
Result
Result<Vec<u8>, SolVecError>
Returns encrypted bytes: nonce (12 bytes) + ciphertext. Returns SolVecError::EncryptionError on failure.
Format:
[nonce: 12 bytes][ciphertext: variable length]

Plaintext structure:
[num_vectors: 8 bytes][dimension: 8 bytes][vector_data: num_vectors * dimension * 4 bytes]
Example:
use solvec_core::encryption::encrypt_vectors;

let key = [42u8; 32]; // In production, use derive_key_from_pubkey
let vectors = vec![
    vec![1.0f32, 2.0, 3.0, 4.0],
    vec![5.0f32, 6.0, 7.0, 8.0],
];

let encrypted = encrypt_vectors(&vectors, &key)?;
println!("Encrypted {} bytes", encrypted.len());

// Upload encrypted to Shadow Drive
// shadow_drive.upload(encrypted)?;

decrypt_vectors

Decrypt vectors from AES-256-GCM ciphertext.
pub fn decrypt_vectors(
    encrypted: &[u8],
    key: &[u8; 32]
) -> Result<Vec<Vec<f32>>, SolVecError>
encrypted
&[u8]
required
Encrypted data from encrypt_vectors (nonce + ciphertext).
key
&[u8; 32]
required
Same 32-byte key used for encryption.
Result
Result<Vec<Vec<f32>>, SolVecError>
Returns decrypted vectors. Returns SolVecError::DecryptionError if:
  • Ciphertext is too short
  • Wrong key used
  • Data has been tampered with
  • Invalid plaintext format
Example:
use solvec_core::encryption::{encrypt_vectors, decrypt_vectors};

let key = [42u8; 32];
let vectors = vec![
    vec![1.0f32, 2.0, 3.0],
    vec![4.0f32, 5.0, 6.0],
];

// Encrypt
let encrypted = encrypt_vectors(&vectors, &key)?;

// Decrypt
let decrypted = decrypt_vectors(&encrypted, &key)?;

assert_eq!(vectors, decrypted);

derive_key_from_pubkey

Generate a deterministic key from a Solana wallet public key.
pub fn derive_key_from_pubkey(pubkey_bytes: &[u8; 32]) -> [u8; 32]
pubkey_bytes
&[u8; 32]
required
Solana wallet public key (32 bytes).
[u8; 32]
[u8; 32]
Derived 32-byte encryption key. Same public key always produces the same key.
Derivation:
key = SHA256("solvec-encryption-key-v1:" || pubkey_bytes)
Example:
use solvec_core::encryption::derive_key_from_pubkey;
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;

// Get user's wallet public key
let pubkey = Pubkey::from_str("7xj9F...abc")?;
let key = derive_key_from_pubkey(&pubkey.to_bytes());

// Use key for encryption
let encrypted = encrypt_vectors(&vectors, &key)?;

Complete Example: Full Pipeline

use solvec_core::encryption::*;
use solvec_core::hnsw::HNSWIndex;
use solvec_core::types::{DistanceMetric, Vector};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Step 1: Create index and add vectors
    let mut index = HNSWIndex::new(16, 200, DistanceMetric::Cosine);
    
    let vectors = vec![
        vec![0.9f32, 0.1, 0.0, 0.0],
        vec![0.8f32, 0.2, 0.1, 0.0],
        vec![0.0f32, 0.0, 0.9, 0.1],
    ];
    
    for (i, values) in vectors.iter().enumerate() {
        index.insert(Vector::new(format!("vec_{}", i), values.clone()))?;
    }
    
    // Step 2: Derive key from Solana wallet
    let wallet_pubkey = [1u8; 32]; // In production: get from Solana SDK
    let key = derive_key_from_pubkey(&wallet_pubkey);
    
    // Step 3: Encrypt vectors for Shadow Drive storage
    let encrypted = encrypt_vectors(&vectors, &key)?;
    println!("Encrypted {} vectors into {} bytes", vectors.len(), encrypted.len());
    
    // Step 4: Upload to Shadow Drive
    // shadow_drive_client.upload(encrypted)?;
    
    // Step 5: Later, download and decrypt
    // let downloaded = shadow_drive_client.download()?;
    let decrypted = decrypt_vectors(&encrypted, &key)?;
    
    // Step 6: Verify integrity
    assert_eq!(vectors.len(), decrypted.len());
    for (orig, dec) in vectors.iter().zip(decrypted.iter()) {
        for (a, b) in orig.iter().zip(dec.iter()) {
            assert!((a - b).abs() < 1e-6, "Decryption mismatch");
        }
    }
    
    println!("✅ Encryption roundtrip successful");
    
    Ok(())
}

Integration Test Example

From the VecLabs full pipeline integration test:
// Encrypt vectors for Shadow Drive
let key = [0u8; 32];
let raw_vectors: Vec<Vec<f32>> = test_vectors.iter().map(|(_, v)| v.clone()).collect();
let encrypted = encrypt_vectors(&raw_vectors, &key).unwrap();
let decrypted = decrypt_vectors(&encrypted, &key).unwrap();

assert_eq!(raw_vectors.len(), decrypted.len());
for (orig, dec) in raw_vectors.iter().zip(decrypted.iter()) {
    for (a, b) in orig.iter().zip(dec.iter()) {
        assert!(
            (a - b).abs() < 1e-6,
            "Decrypted values must match original"
        );
    }
}

println!("✅ Step 4: Encryption/decryption roundtrip passed");
println!("   Encrypted size: {} bytes → Shadow Drive", encrypted.len());

Security Properties

Authenticated Encryption

AES-256-GCM provides both confidentiality and authenticity

Random Nonces

Each encryption uses a unique random nonce (prevents replay attacks)

Key Derivation

Deterministic keys from Solana wallets

Tamper Detection

Decryption fails if ciphertext is modified

Error Handling

EncryptionError

Thrown when encryption fails:
let result = encrypt_vectors(&vectors, &key);
match result {
    Ok(encrypted) => println!("Encrypted {} bytes", encrypted.len()),
    Err(SolVecError::EncryptionError(msg)) => eprintln!("Encryption failed: {}", msg),
    Err(e) => eprintln!("Other error: {}", e),
}

DecryptionError

Thrown when decryption fails:
let result = decrypt_vectors(&encrypted, &wrong_key);
match result {
    Ok(vectors) => println!("Decrypted {} vectors", vectors.len()),
    Err(SolVecError::DecryptionError(msg)) => eprintln!("Decryption failed: {}", msg),
    Err(e) => eprintln!("Other error: {}", e),
}
Common causes:
  • Wrong key: Key doesn’t match the one used for encryption
  • Corrupted data: Ciphertext has been modified or corrupted
  • Truncated data: Incomplete encrypted data
  • Invalid format: Plaintext doesn’t match expected format

Advanced Usage

Multiple Vector Batches

let batch1 = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
let batch2 = vec![vec![5.0, 6.0], vec![7.0, 8.0]];

let encrypted1 = encrypt_vectors(&batch1, &key)?;
let encrypted2 = encrypt_vectors(&batch2, &key)?;

// Store with different keys in Shadow Drive
// shadow_drive.upload("batch1", encrypted1)?;
// shadow_drive.upload("batch2", encrypted2)?;

Verification Without Decryption

// Verify ciphertext is valid without full decryption
let is_valid = decrypt_vectors(&encrypted, &key).is_ok();
if is_valid {
    println!("✅ Ciphertext is valid");
} else {
    println!("❌ Ciphertext is corrupted or wrong key");
}

Different Keys Per User

let user1_pubkey = [1u8; 32];
let user2_pubkey = [2u8; 32];

let key1 = derive_key_from_pubkey(&user1_pubkey);
let key2 = derive_key_from_pubkey(&user2_pubkey);

// Each user's vectors encrypted with their own key
let encrypted1 = encrypt_vectors(&user1_vectors, &key1)?;
let encrypted2 = encrypt_vectors(&user2_vectors, &key2)?;

// user2 cannot decrypt user1's data
assert!(decrypt_vectors(&encrypted1, &key2).is_err());

Performance Characteristics

  • Encryption: O(N * D) where N = number of vectors, D = dimension
  • Decryption: O(N * D)
  • Key derivation: O(1) (single SHA-256 hash)
  • Overhead: 12 bytes (nonce) + 16 bytes (GCM tag) + 16 bytes (header)

Best Practices

  1. Always use derive_key_from_pubkey in production
  2. Never reuse keys across different applications (version string prevents this)
  3. Store encrypted data on Shadow Drive, not raw vectors
  4. Verify decryption success before using vectors
  5. Handle decryption errors gracefully (user might not have access)

Integration with VecLabs Pipeline

┌─────────────┐
│ User Wallet │ → derive_key_from_pubkey → [32-byte key]
└─────────────┘

┌─────────────┐
│   Vectors   │ → encrypt_vectors → [encrypted blob]
└─────────────┘

┌─────────────┐
│ Shadow Drive│ ← upload encrypted data
└─────────────┘

┌─────────────┐
│   Download  │ → decrypt_vectors → [original vectors]
└─────────────┘

┌─────────────┐
│ HNSW Index  │ ← load decrypted vectors
└─────────────┘

See Also

Build docs developers (and LLMs) love