πŸ” X1 Vault

Encrypted IPFS storage using wallet-derived keys

What is X1 Vault?

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.

How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ENCRYPTION FLOW β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ 1. Sign Message 2. Derive Key 3. Encrypt β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Wallet │───────▢│ SHA-256 │──────▢│ AES-256-GCMβ”‚ β”‚ β”‚ β”‚ signs: β”‚ β”‚ (hash) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ "IPFS_ENC.." β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β–Ό β–Ό β”‚ Encrypted β”‚ β”‚ β”‚ Signature (64 bytes) AES Key (32 bytes) β”‚ JSON β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ 4. Upload to IPFS β–Ό β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ { version, algorithm, wallet, derivationMsg, data } β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Derivation

  1. Your wallet signs the message: IPFS_ENCRYPTION_KEY_V1
  2. The signature is hashed with SHA-256 β†’ 32-byte AES key
  3. This key encrypts/decrypts all your files
  4. Same wallet = Same key = Access to all your files

File Format

{
  "version": 1,
  "algorithm": "AES-256-GCM",
  "wallet": "YourPublicKeyBase58...",
  "derivationMsg": "IPFS_ENCRYPTION_KEY_V1",
  "data": "base64(IV + ciphertext + authTag)"
}

The data field contains:

File Browser

The File Browser lets you explore your encrypted files with a familiar directory structure.

Features

Directory Organization

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

Directory Upload (CLI)

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...

API Reference

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

Upload Example (curl)

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"

Response

{"Name":"myfile.txt","Hash":"QmXxx...","Size":"1234"}

CLI Usage (For Bots & Agents)

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)

Node.js

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')

Python

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')

Browser Usage

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);

For AI Agents & Bots

X1 Vault is designed for automated, non-interactive use. Your agent reads the private key from disk β€” no wallet popup, no user interaction.

Quick Integration

# 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"

Why This Works for Agents

Multi-Agent Setup

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 "..."

Security Model

What's Protected

What's Visible

Trust Model

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.

Supported Wallets

FAQ

Can I decrypt on a different device?

Yes! As long as you have access to the same wallet (same private key), you can decrypt your files from any device.

What if I lose my wallet?

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.

Can someone else decrypt my files if they know the CID?

No. They can download the encrypted JSON, but without your wallet's signature, they cannot derive the decryption key.

Is the filename encrypted?

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).