Private Uploads
By default, all content uploaded to Arweave is public — anyone with the hash can read it. Private uploads let you store encrypted content permanently on Arweave where only authorized wallets can decrypt it.
How it works
Your content
│
▼
Derive encryption key: sha256(walletPrivateKey + "inkd-private-v1")
│
▼
AES-256-GCM encrypt with derived key
│
▼
Upload ciphertext to Arweave ← publicly stored, unreadable without key
│
▼
on-chain: version.contentHash = ar:// of encrypted blob
version.isPrivate = trueWhat's on Arweave: only encrypted bytes. The plaintext never leaves your device.
What's on-chain: the Arweave hash of the encrypted content and an isPrivate flag — no plaintext, no key.
Who can decrypt: only the wallet that uploaded — the key is deterministically derived from the wallet private key.
Security model
| Threat | Protection |
|---|---|
| Arweave node operator | Sees only ciphertext |
| Inkd API server | Receives encrypted blob, never the key |
| Third-party observer | AES-256-GCM ciphertext only |
| Unauthorized wallet | Key derived from owner's private key — unguessable without it |
| Key loss | Content permanently inaccessible (no recovery) |
Encryption: AES-256-GCM. Key derivation: sha256(walletPrivateKey + "inkd-private-v1").
Telegram Bot
The bot asks before every upload:
🔓 Public 🔒 Private
✖️ CancelTap 🔒 Private — the bot encrypts using your bot-managed wallet key before upload. One tap, no extra steps.
For existing private projects, the bot shows a 🔓 View Content button in the project detail view. Tapping it decrypts the content on the fly using your wallet key and sends it back to you in the chat.
CLI
# Push a private version (encrypts before upload)
inkd version push \
--id 10 \
--file ./secret-model.bin \
--tag v1.0.0 \
--privateOutput:
🔒 Private upload — content will be encrypted before Arweave storage
Encrypting and uploading ./secret-model.bin (14.2 KB)...
✓ Version v1.0.0 pushed! 🔒 Private
Content (encrypted): ar://QmAbc123…
Access manifest: ar://QmDef456…
TX: 0xabc…
Decrypt with: inkd version decrypt --id 10 --index 0 --out ./decryptedSDK
import { ProjectsClient } from "@inkd/sdk";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import fs from "fs";
const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const wallet = createWalletClient({ account, chain: base, transport: http() });
const client = new ProjectsClient({
wallet,
privateKey: "0xYOUR_PRIVATE_KEY", // required for private uploads
});
// Upload and register a private version
const result = await client.pushPrivateVersion(projectId, {
content: fs.readFileSync("./secret-model.bin"),
tag: "v1.0.0",
contentType: "application/octet-stream",
});
console.log(result.txHash); // on-chain TX
console.log(result.contentHash); // ar:// of encrypted content
console.log(result.metadataHash); // ar:// of access manifest
console.log(result.isPrivate); // trueDecrypt a private version
// Fetch and decrypt — only works if your wallet is in the access manifest
const plaintext = await client.decryptVersion(
result.contentHash, // ar:// of encrypted content
result.metadataHash // ar:// of access manifest
);
fs.writeFileSync("./decrypted.bin", plaintext);Grant access to another wallet
// Add collaborator — they can now decrypt this project's versions
await client.addCollaborator(projectId, {
address: "0xCOLLABORATOR_ADDRESS",
compressedPublicKey: "02abc...", // secp256k1 compressed pubkey
manifestArweaveHash: result.metadataHash,
});Important notes
- Key loss = permanent data loss. There is no recovery mechanism. If you lose access to your wallet, encrypted content is gone forever.
--privaterequires--filein the CLI. Pre-uploaded hashes cannot be retroactively encrypted.- Public and private versions can coexist in the same project — each version is independently public or private.
- Collaborator management is per-project. Adding a collaborator uploads a new access manifest with their wrapped key.
