Encrypted IPFS storage using wallet-derived keys
A decentralized encrypted storage system where your Solana/X1 wallet IS your encryption key. Files are encrypted client-side before upload β the server never sees your data.
IPFS_ENCRYPTION_KEY_V1{
"version": 1,
"algorithm": "AES-256-GCM",
"wallet": "YourPublicKeyBase58...",
"derivationMsg": "IPFS_ENCRYPTION_KEY_V1",
"data": "base64(IV + ciphertext + authTag)"
}
The data field contains:
The File Browser lets you explore your encrypted files with a familiar directory structure.
Files are organized into directories based on their filename paths. Upload with paths like docs/README.md or src/main.py to create folder structure.
# Files with paths create directories:
docs/SOUL.md β π docs/
docs/README.md β βββ SOUL.md
layers/action.py β βββ README.md
layers/memory.py β π layers/
config.json β βββ action.py
β βββ memory.py
β π config.json
Upload entire directories with preserved structure:
# Upload a directory (encrypts all files)
python3 upload-dir.py ~/my-project/
# Upload without encryption (public files)
python3 upload-dir.py --plain ~/my-project/
# Output:
# Uploading: my-project/src/main.py
# Uploading: my-project/docs/README.md
# ...
# Directory CID: QmXxx...
# Browse at: https://vault.x1.xyz/ipfs/browse.html?cid=QmXxx...
| Endpoint | Method | Description |
|---|---|---|
/api/v0/add?pin=true |
POST | Upload file (multipart form, set X-Pubkey and X-Filename headers) |
/index/files?pubkey=X |
GET | List all files for a wallet |
/index/file/{cid} |
GET | Get file metadata |
/index/file/{cid} |
DELETE | Remove from index (set X-Pubkey header) |
/api/v0/ls?arg={cid} |
POST | List directory contents (for IPFS directory CIDs) |
/api/v0/cat?arg={cid} |
POST | Get raw file content |
curl -X POST "https://vault.x1.xyz/ipfs/api/v0/add?pin=true" \
-H "X-Pubkey: YourWalletPublicKey" \
-H "X-Filename: myfile.txt" \
-F "file=@encrypted.json"
{"Name":"myfile.txt","Hash":"QmXxx...","Size":"1234"}
Private key is loaded from disk β no interactive wallet needed. Perfect for automated scripts, AI agents, and server-side applications.
Key file format (either works):
# .env format
SOLANA_PRIVATE_KEY=YourBase58PrivateKey...
# Or raw base58 key (no prefix)
npm install @solana/web3.js tweetnacl bs58 form-data
#!/usr/bin/env node
const fs = require('fs');
const crypto = require('crypto');
const { Keypair } = require('@solana/web3.js');
const nacl = require('tweetnacl');
const bs58 = require('bs58');
const VAULT_URL = 'https://vault.x1.xyz/ipfs';
const DERIVATION_MSG = 'IPFS_ENCRYPTION_KEY_V1';
async function upload(keyPath, filename, data) {
// Load key
const keyFile = fs.readFileSync(keyPath, 'utf8');
const match = keyFile.match(/SOLANA_PRIVATE_KEY=([^\s\n]+)/);
const secretKey = bs58.decode(match ? match[1] : keyFile.trim());
const keypair = Keypair.fromSecretKey(secretKey);
const publicKey = keypair.publicKey.toBase58();
// Derive AES key
const sig = nacl.sign.detached(Buffer.from(DERIVATION_MSG), keypair.secretKey);
const aesKey = crypto.createHash('sha256').update(sig).digest();
// Encrypt
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv);
const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
const combined = Buffer.concat([iv, encrypted, cipher.getAuthTag()]);
// Build payload
const payload = JSON.stringify({
version: 1, algorithm: 'AES-256-GCM', wallet: publicKey,
derivationMsg: DERIVATION_MSG, data: combined.toString('base64')
});
// Upload
const FormData = require('form-data');
const form = new FormData();
form.append('file', Buffer.from(payload), { filename: 'encrypted.json' });
const resp = await fetch(`${VAULT_URL}/api/v0/add?pin=true`, {
method: 'POST',
headers: { 'X-Pubkey': publicKey, 'X-Filename': filename, ...form.getHeaders() },
body: form
});
return JSON.parse((await resp.text()).split('\n')[0]);
}
// Usage: upload('~/.env', 'file.txt', 'Hello world')
pip install pynacl base58 cryptography requests
import os, json, base64, hashlib
from nacl.signing import SigningKey
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base58, requests
VAULT_URL = 'https://vault.x1.xyz/ipfs'
DERIVATION_MSG = b'IPFS_ENCRYPTION_KEY_V1'
def upload(key_path, filename, data):
# Load key
with open(os.path.expanduser(key_path)) as f:
content = f.read()
import re
match = re.search(r'SOLANA_PRIVATE_KEY=([^\s\n]+)', content)
secret = base58.b58decode(match.group(1) if match else content.strip())
signing_key = SigningKey(secret[:32])
public_key = base58.b58encode(bytes(signing_key.verify_key)).decode()
# Derive AES key
signature = signing_key.sign(DERIVATION_MSG).signature
aes_key = hashlib.sha256(signature).digest()
# Encrypt
iv = os.urandom(12)
aesgcm = AESGCM(aes_key)
ciphertext = aesgcm.encrypt(iv, data.encode(), None)
combined = base64.b64encode(iv + ciphertext).decode()
# Build payload
payload = json.dumps({
'version': 1, 'algorithm': 'AES-256-GCM', 'wallet': public_key,
'derivationMsg': 'IPFS_ENCRYPTION_KEY_V1', 'data': combined
})
# Upload
resp = requests.post(
f'{VAULT_URL}/api/v0/add?pin=true',
headers={'X-Pubkey': public_key, 'X-Filename': filename},
files={'file': ('encrypted.json', payload)}
)
return resp.json()
# Usage: upload('~/.env', 'file.txt', 'Hello world')
class VaultClient {
constructor(baseUrl = 'https://vault.x1.xyz/ipfs') {
this.baseUrl = baseUrl;
this.publicKey = null;
this.derivedKey = null;
}
async connect() {
const provider = window.x1_wallet || window.phantom?.solana || window.solana;
if (!provider) throw new Error('No wallet');
await provider.connect();
this.publicKey = provider.publicKey.toString();
const sig = await provider.signMessage(
new TextEncoder().encode('IPFS_ENCRYPTION_KEY_V1'), 'utf8'
);
const sigBytes = sig instanceof Uint8Array ? sig : new Uint8Array(sig.signature);
const hash = await crypto.subtle.digest('SHA-256', sigBytes);
this.derivedKey = await crypto.subtle.importKey(
'raw', hash, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']
);
}
async upload(content, filename) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const data = new TextEncoder().encode(content);
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, this.derivedKey, data);
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
const payload = JSON.stringify({
version: 1, algorithm: 'AES-256-GCM', wallet: this.publicKey,
derivationMsg: 'IPFS_ENCRYPTION_KEY_V1',
data: btoa(String.fromCharCode(...combined))
});
const form = new FormData();
form.append('file', new Blob([payload]));
const resp = await fetch(`${this.baseUrl}/api/v0/add?pin=true`, {
method: 'POST',
headers: { 'X-Pubkey': this.publicKey, 'X-Filename': filename },
body: form
});
return JSON.parse((await resp.text()).split('\n')[0]);
}
async decrypt(cid) {
const resp = await fetch(`https://ipfs.io/ipfs/${cid}`);
const json = await resp.json();
if (json.wallet !== this.publicKey) throw new Error('Wrong wallet');
const combined = Uint8Array.from(atob(json.data), c => c.charCodeAt(0));
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: combined.slice(0, 12) },
this.derivedKey, combined.slice(12)
);
return new TextDecoder().decode(decrypted);
}
}
// Usage:
// const vault = new VaultClient();
// await vault.connect();
// const result = await vault.upload('Hello', 'hello.txt');
// console.log(result.Hash);
X1 Vault is designed for automated, non-interactive use. Your agent reads the private key from disk β no wallet popup, no user interaction.
# Store your agent's wallet key
echo "SOLANA_PRIVATE_KEY=YourBase58Key..." > ~/.agent-wallet.env
# Upload encrypted file
node ipfs-upload.js ~/.agent-wallet.env memory.md "$(cat memory.md)"
# Or with Python
python3 upload.py ~/.agent-wallet.env notes.txt "Agent notes here"
Each agent gets its own wallet. Files are isolated by wallet β Agent A cannot decrypt Agent B's files.
# Agent A's files
python3 upload.py /keys/agent-a.env data.json "..."
# Agent B's files (separate wallet, separate encryption)
python3 upload.py /keys/agent-b.env data.json "..."
The server only stores ciphertext. It cannot:
However, the server can delete files or refuse to serve them. For maximum resilience, pin your CIDs to multiple IPFS nodes.
signMessage()Yes! As long as you have access to the same wallet (same private key), you can decrypt your files from any device.
If you lose access to your wallet's private key, your encrypted files are permanently inaccessible. There is no recovery mechanism β this is by design.
No. They can download the encrypted JSON, but without your wallet's signature, they cannot derive the decryption key.
No. Filenames are stored in plaintext in the index. For sensitive files, use generic names or leave the filename empty (the system will use the CID).