Manual Decryption Guide
This guide explains how to manually decrypt your vault data using standard cryptographic tools. We provide this for transparency and to ensure you always have access to your data — even if our service is unavailable.
Data Sovereignty: Your data belongs to you. With your passphrase and the exported encrypted bundle, you can always recover your data independently using open-source tools.
Prerequisites
You will need:
- Node.js (version 16 or later)
- Your master passphrase (the one you use to access your vault)
- Your exported encrypted bundle (download from the vault page)
Important: Only the master passphrase can decrypt exported bundles. Read-only and write codes cannot decrypt exported bundles because the export contains only the main passphrase's wrapped DEK, not the wrapped DEKs for shared codes.
Encryption Overview
Your vault uses a two-layer encryption scheme with optional digital signatures for write access:
- Key Derivation: Your passphrase is converted to a Key Encryption Key (KEK) using Argon2id, a memory-hard password hashing algorithm designed to resist brute-force attacks.
- Key Wrapping: A random Data Encryption Key (DEK) is encrypted ("wrapped") with the KEK using XChaCha20-Poly1305.
- Data Encryption: Your vault data is encrypted with the DEK using XChaCha20-Poly1305, an authenticated encryption algorithm.
- Write Authorization (optional): An Ed25519 signing keypair is stored inside the vault. The private key is wrapped with your KEK. Write operations must be signed with this key to be accepted by the server.
Technical Details
Algorithm Compatibility: Vaults created after February 2026 use XChaCha20-Poly1305. Older vaults may use XSalsa20-Poly1305. The scripts below automatically detect and handle both algorithms — you don't need to know which one your vault uses.
Decryption Steps
Here is a complete Node.js script that decrypts your vault:
// decrypt-vault.js
// Usage: node decrypt-vault.js <encrypted-bundle.json> <passphrase>
const sodium = require('libsodium-wrappers-sumo');
const fs = require('fs');
// Decrypt using XChaCha20-Poly1305 (current algorithm)
function decryptXChaCha20(ciphertext, nonce, key) {
return sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
null, // nsec (unused)
ciphertext,
null, // additional data
nonce,
key
);
}
// Decrypt using XSalsa20-Poly1305 (legacy algorithm, pre-2026)
function decryptXSalsa20(ciphertext, nonce, key) {
return sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
}
// Try decryption with fallback for legacy vaults
function decryptWithFallback(ciphertext, nonce, key, algorithm) {
// If algorithm is specified, use it directly
if (algorithm === 'xchacha20-poly1305') {
return decryptXChaCha20(ciphertext, nonce, key);
}
if (algorithm === 'xsalsa20-poly1305') {
return decryptXSalsa20(ciphertext, nonce, key);
}
// No algorithm specified - try XChaCha20 first, fall back to XSalsa20
try {
return decryptXChaCha20(ciphertext, nonce, key);
} catch (e) {
return decryptXSalsa20(ciphertext, nonce, key);
}
}
async function decryptVault(bundlePath, passphrase) {
// Initialize libsodium
await sodium.ready;
// Read the encrypted bundle
const bundle = JSON.parse(fs.readFileSync(bundlePath, 'utf8'));
const { data, encryption } = bundle;
// Check encryption algorithm (newer exports include this)
const algorithm = encryption?.algorithm || null;
console.log('Encryption algorithm:', algorithm || '(not specified, will auto-detect)');
// Decode base64 values to Uint8Array
// Use ORIGINAL variant (standard +/= with padding) for compatibility
const B64 = sodium.base64_variants.ORIGINAL;
const kdfSalt = sodium.from_base64(data.kdfSalt, B64);
const wrappedKey = sodium.from_base64(data.wrappedKey, B64);
const wrappedKeyNonce = sodium.from_base64(data.wrappedKeyNonce, B64);
const ciphertext = sodium.from_base64(data.ciphertext, B64);
const nonce = sodium.from_base64(data.nonce, B64);
console.log('\nStep 1: Deriving KEK from passphrase...');
console.log(' (This may take a few seconds due to Argon2id)');
// Step 1: Derive Key Encryption Key (KEK) from passphrase
const kek = sodium.crypto_pwhash(
32, // key size
passphrase,
kdfSalt,
encryption.kdf.opslimit,
encryption.kdf.memlimit,
sodium.crypto_pwhash_ALG_ARGON2ID13
);
console.log('Step 2: Unwrapping DEK...');
// Step 2: Unwrap the Data Encryption Key (DEK)
let dek;
try {
dek = decryptWithFallback(wrappedKey, wrappedKeyNonce, kek, algorithm);
} catch (e) {
console.error('ERROR: Failed to unwrap DEK.');
console.error('This usually means the passphrase is incorrect.');
process.exit(1);
}
console.log('Step 3: Decrypting vault data...');
// Step 3: Decrypt the vault data
let plaintext;
try {
plaintext = decryptWithFallback(ciphertext, nonce, dek, algorithm);
} catch (e) {
console.error('ERROR: Failed to decrypt vault data.');
process.exit(1);
}
// Convert to string
const jsonString = sodium.to_string(plaintext);
const vaultData = JSON.parse(jsonString);
console.log('\nSuccess! Vault decrypted.');
console.log('\n--- Decrypted Vault Data ---\n');
console.log(JSON.stringify(vaultData, null, 2));
// Optionally save to file
const outputPath = bundlePath.replace('.json', '-decrypted.json');
fs.writeFileSync(outputPath, JSON.stringify(vaultData, null, 2));
console.log('\nSaved to:', outputPath);
}
// Get command line arguments
const args = process.argv.slice(2);
if (args.length !== 2) {
console.log('Usage: node decrypt-vault.js <encrypted-bundle.json> <passphrase>');
console.log('');
console.log('Example:');
console.log(' node decrypt-vault.js vault-encrypted-2024-01-15.json \\');
console.log(' "sunset-harbor-crystal-wisdom-river-falcon-7284"');
process.exit(1);
}
decryptVault(args[0], args[1]).catch(console.error);Running the Script
- Create a new directory and save the script as
decrypt-vault.js - Install libsodium:
npm install libsodium-wrappers-sumo
- Place your exported encrypted bundle (JSON file) in the same directory
- Run the script:
node decrypt-vault.js vault-encrypted-2024-01-15.json "your-passphrase-here"
Security Note: The decrypted output contains all your vault data in plain text. Handle it carefully — store it securely or delete it after use.
Alternative: Using Python
If you prefer Python, you can use the pynacl library:
# decrypt-vault.py
# pip install pynacl
import json
import sys
import base64
from nacl.pwhash import argon2id
from nacl.secret import SecretBox
from nacl import bindings
def decrypt_xchacha20(ciphertext, nonce, key):
"""Decrypt using XChaCha20-Poly1305 (current algorithm)"""
return bindings.crypto_aead_xchacha20poly1305_ietf_decrypt(
ciphertext, None, nonce, key
)
def decrypt_xsalsa20(ciphertext, nonce, key):
"""Decrypt using XSalsa20-Poly1305 (legacy algorithm)"""
box = SecretBox(key)
return box.decrypt(ciphertext, nonce)
def decrypt_with_fallback(ciphertext, nonce, key, algorithm):
"""Try decryption with fallback for legacy vaults"""
if algorithm == 'xchacha20-poly1305':
return decrypt_xchacha20(ciphertext, nonce, key)
if algorithm == 'xsalsa20-poly1305':
return decrypt_xsalsa20(ciphertext, nonce, key)
# No algorithm specified - try XChaCha20 first, fall back to XSalsa20
try:
return decrypt_xchacha20(ciphertext, nonce, key)
except Exception:
return decrypt_xsalsa20(ciphertext, nonce, key)
def decrypt_vault(bundle_path, passphrase):
# Read the encrypted bundle
with open(bundle_path, 'r') as f:
bundle = json.load(f)
data = bundle['data']
encryption = bundle['encryption']
# Check encryption algorithm (newer exports include this)
algorithm = encryption.get('algorithm')
print(f'Encryption algorithm: {algorithm or "(not specified, will auto-detect)"}')
# Decode base64 values
kdf_salt = base64.b64decode(data['kdfSalt'])
wrapped_key = base64.b64decode(data['wrappedKey'])
wrapped_key_nonce = base64.b64decode(data['wrappedKeyNonce'])
ciphertext = base64.b64decode(data['ciphertext'])
nonce = base64.b64decode(data['nonce'])
print('\nStep 1: Deriving KEK from passphrase...')
# Derive KEK using Argon2id
kek = argon2id.kdf(
32, # key size
passphrase.encode('utf-8'),
kdf_salt,
opslimit=encryption['kdf']['opslimit'],
memlimit=encryption['kdf']['memlimit']
)
print('Step 2: Unwrapping DEK...')
# Unwrap DEK
dek = decrypt_with_fallback(wrapped_key, wrapped_key_nonce, kek, algorithm)
print('Step 3: Decrypting vault data...')
# Decrypt vault data
plaintext = decrypt_with_fallback(ciphertext, nonce, dek, algorithm)
vault_data = json.loads(plaintext.decode('utf-8'))
print('\nSuccess! Vault decrypted.')
print(json.dumps(vault_data, indent=2))
# Save to file
output_path = bundle_path.replace('.json', '-decrypted.json')
with open(output_path, 'w') as f:
json.dump(vault_data, f, indent=2)
print(f'\nSaved to: {output_path}')
if __name__ == '__main__':
if len(sys.argv) != 3:
print('Usage: python decrypt-vault.py <bundle.json> <passphrase>')
sys.exit(1)
decrypt_vault(sys.argv[1], sys.argv[2])Understanding the Decrypted Data
The decrypted vault contains your data along with some internal fields used by the application. Here's what you'll see:
The signing key fields are used to cryptographically authorize write operations. They don't affect your ability to read the data — they're only needed if you want to make changes through our service. For data recovery purposes, you can ignore these fields and focus on the profiles array which contains your actual information.
Note: The signingPrivateKey is itself encrypted (wrapped with your master KEK). Even in the decrypted output, it remains protected — an additional layer of security for write capabilities.
Troubleshooting
"Failed to unwrap DEK"
This error means the passphrase is incorrect. Double-check that you're using the exact passphrase, including hyphens and the number at the end.
Out of memory errors
Argon2id uses 256 MB of memory by design. If your system runs out of memory, try closing other applications or using a machine with more RAM.
Slow decryption
The key derivation step (Argon2id) is intentionally slow — this is a security feature that makes brute-force attacks impractical. It typically takes 1-3 seconds.