The Python SDK supports encrypting files before they’re uploaded to the 0G network, so storage nodes never see your plaintext. Two schemes are available, both wire-compatible with the official TypeScript SDK and the Go storage client:
VersionSchemeKey materialUse case
v1AES-256-CTR with a caller-supplied 32-byte keySymmetric keyYou manage the key out-of-band (e.g. KMS, password-derived)
v2ECIES on secp256k1 + AES-256-CTRRecipient’s public key for encrypt, private key for decryptEncrypt for an arbitrary recipient without pre-sharing a secret
A small encryption header (17 bytes for v1, 50 bytes for v2) is prepended to the ciphertext and stored alongside it. The download path detects the header automatically and decrypts when you supply the matching key.

v1 — symmetric encryption

You generate a 32-byte key (out-of-band) and pass it on both ends.
import os
from core.indexer import Indexer
from core.file import ZgFile

indexer = Indexer(INDEXER_RPC)
file    = ZgFile.from_file_path("./secret.txt")
key     = os.urandom(32)   # 32-byte AES-256 key

# Encrypted upload
result, err = indexer.upload(
    file,
    BLOCKCHAIN_RPC,
    account,
    upload_opts = {
        "tags": b"\x00",
        "finalityRequired": True,
        "account": account,
        "encryption": {"type": "aes256", "key": key},
    },
)
file.close()
print(f"Encrypted root: {result['rootHash']}")

# Encrypted download — pass symmetric_key to auto-decrypt in place
err = indexer.download(
    result["rootHash"],
    "./secret.txt",
    symmetric_key = key,
)
If you download without symmetric_key, the raw ciphertext (including the 17-byte header) lands on disk and you can decrypt later via the primitives — see Low-level primitives below.

v2 — ECIES (public-key recipient)

ECIES lets anyone with the recipient’s secp256k1 public key encrypt a file that only the holder of the matching private key can decrypt. The SDK derives a fresh ephemeral keypair per encryption and embeds the ephemeral public key in the header, so no key needs to be pre-shared.
recipient_pub = "0x031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"
# 33-byte compressed secp256k1 pubkey (hex or bytes both accepted)

result, err = indexer.upload(
    file,
    BLOCKCHAIN_RPC,
    account,
    upload_opts = {
        "tags": b"\x00",
        "finalityRequired": True,
        "account": account,
        "encryption": {"type": "ecies", "recipient_pub_key": recipient_pub},
    },
)

# Recipient decrypts with their private key
err = indexer.download(
    result["rootHash"],
    "./secret.txt",
    private_key = PRIVATE_KEY_HEX,
)
recipient_pub_key accepts:
  • 33-byte compressed secp256k1 public key (bytes)
  • 65-byte uncompressed (with 0x04 prefix)
  • 64-byte raw (no prefix)
  • Hex string of any of the above (with or without 0x prefix)

Detect encrypted files before downloading

Indexer.peek_header(root_hash) fetches just enough bytes from the first segment to identify whether a stored file is encrypted and which scheme it uses. Useful for rendering a “this file is encrypted, supply a key” prompt before committing to a full download.
header, err = indexer.peek_header(root_hash)
if err is not None:
    raise err

if header is None:
    print("Plaintext file — download directly")
elif header.version == 1:
    print("AES-256 — supply symmetric_key= to download")
elif header.version == 2:
    print("ECIES — supply private_key= to download")

Typed configuration

EncryptionConfig and DecryptionConfig give you a typed alternative to the raw upload_opts dict and integrate with AppConfig:
from config import EncryptionConfig, DecryptionConfig, get_config, ConfigOverrides

cfg = get_config(
    ConfigOverrides(
        encryption = EncryptionConfig(type="aes256", key=os.urandom(32)),
    )
)

# Render back into the dict shape `Uploader` expects
result, err = indexer.upload(file, BLOCKCHAIN_RPC, account, upload_opts={
    "tags": b"\x00",
    "finalityRequired": True,
    "account": account,
    "encryption": cfg.encryption.to_upload_opt(),
})

Low-level primitives

For custom flows (encrypting in memory, decrypting a file you already have on disk, etc.) the primitives are exported under core.encryption:
from core.encryption import (
    new_symmetric_header,    # v1 factory
    new_ecies_header,        # v2 factory
    crypt_at,                # AES-256-CTR (with byte-offset support)
    decrypt_file,            # parse header + decrypt body
    parse_encryption_header,
    resolve_decryption_key,
    derive_ecies_encrypt_key,
    derive_ecies_decrypt_key,
)
from core.decryption import try_decrypt, try_decrypt_fragments
Most callers won’t need these — indexer.upload and indexer.download with the key arguments above cover the common cases.

Encrypt in memory

from core.encryption import new_symmetric_header, crypt_at

header = new_symmetric_header()
key    = os.urandom(32)
plaintext = b"sensitive payload"
body      = crypt_at(key, header.nonce, 0, plaintext)
encrypted = header.to_bytes() + body

Best-effort decrypt

try_decrypt is non-throwing — it returns the raw bytes unchanged if decryption fails for any reason (file not encrypted, wrong key type, off-curve ephemeral pubkey, etc.) so callers can fall back to the ciphertext:
from core.decryption import try_decrypt

with open("./downloaded.bin", "rb") as f:
    raw = f.read()

result = try_decrypt(raw, symmetric_key=key)        # v1
# or
result = try_decrypt(raw, private_key=PRIVATE_KEY)  # v2

if result.decrypted:
    plaintext = result.bytes
else:
    print("Not encrypted, or wrong key — returned raw bytes")

Wire format

For interop with the Go and TypeScript clients:
v1:  [0x01][nonce:16]                       — 17 bytes header
v2:  [0x02][ephemeral_pub:33][nonce:16]     — 50 bytes header

cipher:    AES-256-CTR
counter:   nonce + big-endian add of floor(offset / 16)
ECIES:     secp256k1 ECDH → raw 32-byte shared X
KDF:       HKDF-SHA256, empty salt, info = b"0g-storage-client/ecies/v1/aes-256"
output:    32-byte AES key
The Python SDK’s encryption module is a direct port of src.ts/common/encryption.ts from @0gfoundation/0g-ts-sdk and uses the same TS reference vectors for its test suite.
AES-CTR has no authentication tag — it does not detect tampering. If you need authenticated encryption on top, encrypt with the SDK then sign the resulting ciphertext separately, or wrap the plaintext with an HMAC before uploading.

Next steps

Storage SDK basics

Upload, download, and merkle helpers without encryption.

Error handling

Exception hierarchy and retry semantics for transient errors.