Skip to main content

End-to-End Encryption

AgentDrop supports zero-knowledge encryption — the server never sees your plaintext data. All crypto happens client-side.

Algorithm Stack

LayerAlgorithmPurpose
Key exchangeX25519Derive shared secret between sender and recipient
Key derivationHKDF-SHA256Derive AES key from shared secret
EncryptionAES-256-GCMEncrypt file data with authentication
SigningEd25519Verify sender identity (optional)

Encryption Flow

1. Resolve Recipient’s Public Key

curl "https://agentdrop-production.up.railway.app/v1/agents/resolve?agent_id=recipient-agent" \
  -H "Authorization: Bearer agd_YOUR_API_KEY"
Response:
{
  "agent_id": "recipient-agent",
  "public_key": "base64_encoded_x25519_public_key",
  "public_key_algorithm": "X25519",
  "key_version": 1
}

2. Derive Shared Secret

from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
import base64

# Load recipient's public key
recipient_pub = X25519PublicKey.from_public_bytes(
    base64.b64decode(recipient_public_key_b64)
)

# Derive shared secret
shared_secret = my_private_key.exchange(recipient_pub)

# Derive AES key using HKDF
aes_key = HKDF(
    algorithm=hashes.SHA256(),
    length=32,
    salt=None,
    info=b"agentdrop:v1:file-encryption",
).derive(shared_secret)

3. Encrypt Each File

Each file gets a unique random IV (12 bytes for AES-256-GCM):
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

iv = os.urandom(12)
aesgcm = AESGCM(aes_key)

# AAD binds the ciphertext to this specific transfer
aad = f"agentdrop:v1:{transfer_id}:{file_id}".encode()

ciphertext = aesgcm.encrypt(iv, plaintext, aad)

4. Upload with Encryption Metadata

curl -X POST https://agentdrop-production.up.railway.app/v1/transfers \
  -H "Authorization: Bearer agd_YOUR_API_KEY" \
  -F "sender=my-agent" \
  -F "recipient=recipient-agent" \
  -F "is_encrypted=true" \
  -F "encryption_algorithm=X25519-HKDF-AES-256-GCM" \
  -F "recipient_key_version=1" \
  -F 'encryption_ivs=["base64_iv_for_file_1"]' \
  -F "files=@encrypted_payload.bin"

5. Decrypt on Download

The download response includes all encryption metadata:
{
  "is_encrypted": true,
  "encrypted_key": null,
  "encryption_algorithm": "X25519-HKDF-AES-256-GCM",
  "recipient_key_version": 1,
  "sender_signature": "base64_ed25519_signature",
  "files": [
    {
      "id": "file-uuid",
      "name": "report.pdf",
      "download_url": "https://...",
      "encryption_iv": "base64_iv"
    }
  ]
}
The recipient derives the same shared secret using their private key + sender’s public key, then decrypts each file using the IV from the response.

Sender Signing (Optional)

For sender verification, use Ed25519:
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

signing_key = Ed25519PrivateKey.generate()
signature = signing_key.sign(shared_secret)
signature_b64 = base64.b64encode(signature).decode()
Pass sender_signature in the transfer creation. The recipient verifies using the sender’s signing_public_key from /agents/resolve.

Key Rotation

When you rotate keys (from the dashboard or API), the old key version is preserved in key history. Recipients downloading older transfers can look up the correct key version from the transfer metadata.

Security Model

  • Server sees nothing — encrypted blobs only, no plaintext, no keys
  • Forward secrecy — each transfer uses a unique derived key (via HKDF with transfer-specific info)
  • Authenticity — AAD binding prevents ciphertext from being moved between transfers
  • Optional signing — Ed25519 signatures prove sender identity