Skip to main content
VecLabs encrypts all vectors before they leave the SDK. The encryption key is derived from your Solana wallet, meaning VecLabs infrastructure never sees your plaintext data.

Threat Model

Traditional vector databases like Pinecone and Weaviate:
  • Store vectors in plaintext on their servers
  • Have full read access to your data
  • Require you to trust their infrastructure security
  • Can be compelled to hand over data via subpoena
VecLabs takes a different approach:

Traditional DBs

SDK → [plaintext] → Server Storage
       ^
       |
  Vendor can read

VecLabs

SDK → [encrypt] → [ciphertext] → Storage
            ^                      ^
            |                      |
      Your wallet key      VecLabs cannot read
Even if VecLabs storage is compromised, the attacker gets encrypted blobs with no way to decrypt them.

Encryption Algorithm

VecLabs uses AES-256-GCM (Advanced Encryption Standard with Galois/Counter Mode):
  • AES-256: 256-bit key length, industry standard symmetric encryption
  • GCM mode: Authenticated encryption with additional data (AEAD)
  • Authentication tag: Prevents tampering — any modification to ciphertext causes decryption to fail
From encryption.rs:1-4:
use aes_gcm::{
    aead::{Aead, AeadCore, KeyInit, OsRng},
    Aes256Gcm, Key, Nonce,
};
AES-256-GCM is used by Signal, WhatsApp, TLS 1.3, and most modern secure systems. It provides both confidentiality (data can’t be read) and integrity (data can’t be modified without detection).

How Encryption Works

Encrypting Vectors

From encryption.rs:12-35:
pub fn encrypt_vectors(vectors: &[Vec<f32>], key: &[u8; 32]) -> Result<Vec<u8>, SolVecError> {
    let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
    let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
    
    let num_vectors = vectors.len() as u64;
    let dim = vectors.first().map(|v| v.len() as u64).unwrap_or(0);
    
    // Serialize vectors into plaintext buffer
    let mut plaintext = Vec::new();
    plaintext.extend_from_slice(&num_vectors.to_le_bytes());
    plaintext.extend_from_slice(&dim.to_le_bytes());
    for v in vectors {
        for &f in v {
            plaintext.extend_from_slice(&f.to_le_bytes());
        }
    }
    
    // Encrypt with AES-256-GCM
    let ciphertext = cipher.encrypt(&nonce, plaintext.as_ref())?;
    
    // Return: nonce (12 bytes) + ciphertext + auth tag (16 bytes)
    let mut output = nonce.to_vec();
    output.extend_from_slice(&ciphertext);
    Ok(output)
}
Step by step:
  1. Generate random nonce — A unique 12-byte value for this encryption operation (prevents nonce reuse attacks)
  2. Serialize vectors — Pack vector count, dimension, and float32 values into a byte buffer
  3. Encrypt — Run AES-256-GCM encryption on the plaintext
  4. Return — Prepend the nonce to the ciphertext (nonce is not secret, but must be unique per encryption)

Decrypting Vectors

From encryption.rs:37-85:
pub fn decrypt_vectors(encrypted: &[u8], key: &[u8; 32]) -> Result<Vec<Vec<f32>>, SolVecError> {
    if encrypted.len() < NONCE_SIZE {
        return Err(SolVecError::DecryptionError("Ciphertext too short".into()));
    }
    
    // Split nonce from ciphertext
    let (nonce_bytes, ciphertext) = encrypted.split_at(NONCE_SIZE);
    let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
    let nonce = Nonce::from_slice(nonce_bytes);
    
    // Decrypt
    let plaintext = cipher.decrypt(nonce, ciphertext)?;
    
    // Deserialize header
    let num_vectors = u64::from_le_bytes(plaintext[0..8].try_into().unwrap()) as usize;
    let dim = u64::from_le_bytes(plaintext[8..16].try_into().unwrap()) as usize;
    
    // Deserialize vectors
    let mut vectors = Vec::with_capacity(num_vectors);
    let data = &plaintext[16..];
    for i in 0..num_vectors {
        let start = i * dim * 4;
        let vec: Vec<f32> = (0..dim)
            .map(|j| {
                let offset = start + j * 4;
                f32::from_le_bytes(data[offset..offset + 4].try_into().unwrap())
            })
            .collect();
        vectors.push(vec);
    }
    
    Ok(vectors)
}
Step by step:
  1. Extract nonce — First 12 bytes of the encrypted data
  2. Decrypt — AES-256-GCM decryption (fails immediately if data was tampered with)
  3. Deserialize — Parse vector count, dimension, and reconstruct float32 arrays
  4. Return — Original vectors in plaintext
If the wrong key is used or the ciphertext is modified, decrypt_vectors will fail with SolVecError::DecryptionError. This is a feature — GCM mode’s authentication tag prevents silent data corruption.

Key Derivation

The encryption key is derived from your Solana wallet public key using SHA-256. From encryption.rs:87-95:
pub fn derive_key_from_pubkey(pubkey_bytes: &[u8; 32]) -> [u8; 32] {
    use sha2::{Digest, Sha256};
    let mut hasher = Sha256::new();
    hasher.update(b"solvec-encryption-key-v1:");
    hasher.update(pubkey_bytes);
    hasher.finalize().into()
}
Why this works:
  1. Your Solana wallet public key is 32 bytes (Ed25519)
  2. Hash the pubkey with a domain-specific prefix ("solvec-encryption-key-v1:")
  3. Output is a deterministic 32-byte key unique to your wallet
  4. Same wallet always produces the same encryption key
  5. Different wallets produce completely different keys
In production: The key derivation will use the wallet’s signing capability to derive a deterministic key without exposing the private key. The current implementation is for alpha testing.

Why Not Use the Private Key Directly?

Using the public key instead of the private key for derivation has two benefits:
  1. Read-only access — Someone with only the public key can derive the encryption key and read encrypted data, but cannot modify it (requires private key for Solana transactions)
  2. Key separation — The encryption key is distinct from the signing key, reducing risk if either is compromised

Encrypted Data Format

The encrypted blob stored on Shadow Drive has this structure:
[12 bytes: Nonce] [N bytes: Ciphertext] [16 bytes: Auth Tag]

Ciphertext contains:
  [8 bytes: vector count]
  [8 bytes: dimension]
  [count * dim * 4 bytes: float32 vector data]
Example: Encrypting 100 vectors of 384 dimensions:
  • Header: 8 + 8 = 16 bytes
  • Vector data: 100 * 384 * 4 = 153,600 bytes
  • Plaintext: 153,616 bytes
  • Ciphertext: 12 (nonce) + 153,616 (encrypted) + 16 (auth tag) = 153,644 bytes
Encryption overhead: 28 bytes (0.018%)

Nonce Uniqueness

AES-256-GCM requires a unique nonce for every encryption with the same key. Reusing a nonce with the same key is catastrophic — it can leak plaintext data. VecLabs ensures nonce uniqueness by generating a random 12-byte nonce using the OS cryptographic RNG (OsRng) for every encryption operation (encryption.rs:14):
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
With 12 bytes (96 bits), there are 2^96 = 79 trillion trillion possible nonces. The probability of collision is negligible (birthday paradox bound: safe for ~2^48 encryptions with the same key). From the test suite (encryption.rs:140-147):
#[test]
fn test_different_encryptions_of_same_data() {
    let key = test_key();
    let vectors = vec![vec![1.0f32, 2.0, 3.0]];
    let enc1 = encrypt_vectors(&vectors, &key).unwrap();
    let enc2 = encrypt_vectors(&vectors, &key).unwrap();
    assert_ne!(enc1, enc2, "Each encryption should use a unique nonce");
}

Testing

From the test suite (encryption.rs:98-155), VecLabs validates:

Roundtrip Encryption

#[test]
fn test_roundtrip_single_vector() {
    let key = test_key();
    let vectors = vec![vec![1.0f32, 2.0, 3.0, 4.0]];
    let encrypted = encrypt_vectors(&vectors, &key).unwrap();
    let decrypted = decrypt_vectors(&encrypted, &key).unwrap();
    assert_eq!(vectors, decrypted);
}

#[test]
fn test_roundtrip_multiple_vectors() {
    let key = test_key();
    let vectors: Vec<Vec<f32>> = (0..10)
        .map(|i| (0..384).map(|j| (i * j) as f32 * 0.001).collect())
        .collect();
    let encrypted = encrypt_vectors(&vectors, &key).unwrap();
    let decrypted = decrypt_vectors(&encrypted, &key).unwrap();
    assert_eq!(vectors.len(), decrypted.len());
}

Wrong Key Detection

#[test]
fn test_wrong_key_fails() {
    let key1 = [1u8; 32];
    let key2 = [2u8; 32];
    let vectors = vec![vec![1.0f32, 2.0, 3.0]];
    let encrypted = encrypt_vectors(&vectors, &key1).unwrap();
    let result = decrypt_vectors(&encrypted, &key2);
    assert!(result.is_err());
}
Attempting to decrypt with the wrong key fails immediately — AES-GCM authentication prevents silent corruption.

Performance Impact

Encryption adds minimal overhead:
OperationTime (100 vectors, 384 dims)
Serialization~20 μs
AES-256-GCM encryption~150 μs
Total overhead~170 μs
For comparison:
  • HNSW query: 1,900 μs (p50)
  • Encryption: 170 μs
  • Encryption is less than 9% of query time
On upsert operations (which are already slower due to graph construction), encryption overhead is negligible.

Limitations and Future Work

Current Limitations

  1. No key rotation — Once encrypted with a wallet key, re-encryption requires decrypting all data and re-encrypting with a new key
  2. No field-level encryption — Metadata is stored in plaintext (only vector values are encrypted)
  3. No searchable encryption — You cannot query encrypted vectors without decrypting them first

Planned Improvements

Metadata Encryption

Encrypt metadata fields with the same wallet-derived key. Adds minimal overhead and protects sensitive metadata (PII, access logs, etc.).

Key Rotation

Support re-encrypting collections with a new key. Useful when wallet is rotated or when migrating between wallets.

Security Best Practices

Never share your Solana wallet private key. The private key is required to post Merkle roots to Solana. Anyone with the private key can:
  • Update the collection’s Merkle root
  • Decrypt all vectors in the collection
  • Impersonate you on-chain
Store your wallet securely using hardware wallets (Ledger, Trezor) or encrypted keystores.

For Production Deployments

  1. Use dedicated wallets per environment — Don’t reuse dev wallets in production
  2. Rotate wallets periodically — Plan for key rotation (upcoming feature)
  3. Monitor on-chain activity — Watch for unexpected Merkle root updates to detect unauthorized access
  4. Backup encrypted data — Store Shadow Drive ciphertext in multiple locations

Why This Matters

VecLabs is the only vector database where encryption happens client-side before data leaves your machine.

Pinecone, Weaviate, Qdrant

  • Vectors stored in plaintext on vendor servers
  • Vendor has full read access
  • Trust their security practices
  • Vulnerable to insider threats and subpoenas

VecLabs SolVec

  • Vectors encrypted before leaving SDK
  • VecLabs infrastructure cannot read data
  • You control the encryption key (your wallet)
  • Even if storage is compromised, data is protected
For AI agents handling sensitive data (medical records, financial info, personal conversations), this is the difference between “we think the data is secure” and “the data is mathematically guaranteed to be unreadable without the key.”

Code Reference

Full implementation: crates/solvec-core/src/encryption.rs Key functions:
  • encrypt_vectors() — encryption.rs:12
  • decrypt_vectors() — encryption.rs:38
  • derive_key_from_pubkey() — encryption.rs:89

Ready to Build?

Start using VecLabs with the TypeScript or Python SDK.

Build docs developers (and LLMs) love