What is 0penw0rld?
0penw0rld is an open-source, self-custody BCH toolkit — a set of standalone HTML apps that run entirely in the browser with zero backend. No accounts, no servers, no tracking.
Live: https://0penw0rld.com
The suite includes:
- 00 Wallet — Send/receive BCH, Ledger + WalletConnect v2 support
- 00 Chat — E2E encrypted messaging over OP_RETURN (the CCSH protocol described below)
- 00 Mesh — Nostr-based group chat with BCH identity
- 00 ID — On-chain identity registration via Nostr
- 00 Loan — CashToken lending protocol
All apps connect to user-configurable Fulcrum nodes via WebSocket. Users can plug their own endpoints (Fulcrum, Nostr relays, indexers) through a settings panel — everything is stored in localStorage.
The CCSH Protocol — E2E Encrypted Messaging on BCH
CCSH (Chat Cash) is a binary protocol for embedding end-to-end encrypted messages directly into BCH transactions using OP_RETURN outputs.
Design Goals
- True E2E encryption — only the recipient can decrypt
- Forward secrecy — ephemeral X25519 keys per message
- On-chain transport — messages are embedded in OP_RETURN (max 223 bytes)
- Multi-chunk support — large messages are split across multiple transactions
- No servers — the blockchain IS the transport layer
Cryptographic Stack
- X25519 ECDH — Key exchange (Curve25519)
- SHA-256 KDF — Derive AES key from shared secret
- AES-256-GCM — Authenticated symmetric encryption
- Ephemeral keys — Fresh X25519 keypair per message for forward secrecy
How It Works
- Alice generates a random X25519 keypair (separate from her BCH secp256k1 keys)
- Alice publishes her X25519 public key (32 bytes) as her “chat identity”
- To send a message to Bob:
- Generate an ephemeral X25519 keypair
- ECDH with Bob’s X25519 public key → shared secret
- SHA-256(shared_secret) → AES-256 key
- Encrypt plaintext with AES-256-GCM (random 12-byte nonce)
- Output:
ephemeral_pub(32) || nonce(12) || ciphertext+tag
- The encrypted blob is chunked into ≤162-byte pieces
- Each chunk is wrapped in a CCSH v1 packet (61-byte header)
- Each packet is embedded in an OP_RETURN output of a BCH transaction sent to Bob’s BCH address
- Bob’s scanner watches his address, detects CCSH magic bytes, reassembles and decrypts
Packet Format (CCSH v1)
Offset Size Field
─────────────────────────────────
0 4 MAGIC ("CCSH")
4 1 VERSION (0x01)
5 1 MSG_TYPE
6 1 FLAGS
7 16 MSG_ID (UUID4)
23 32 SENDER_PUB (X25519)
55 2 CHUNK_INDEX (uint16 BE)
57 2 CHUNK_TOTAL (uint16 BE)
59 2 CIPH_LEN (uint16 BE)
61 var CIPHERTEXT_CHUNK
─────────────────────────────────
Total header: 61 bytes
Max payload per TX: 223 - 61 = 162 bytes
Message Types
| Type | Value | Description |
|---|---|---|
| ENCRYPTED_CHUNK | 0x01 | Standard DM (X25519 encrypted) |
| ADDR_CHANGE | 0x02 | BCH address rotation notification |
| GROUP_MSG | 0x03 | Group message (AES-256-GCM with shared group key) |
| GROUP_INVITE | 0x04 | Group invitation (X25519 encrypted JSON) |
| GROUP_ADDR_CHANGE | 0x05 | Group key rotation |
Code — Packet Encoding/Decoding
# core/packet_v1.py
from __future__ import annotations
import uuid
from dataclasses import dataclass
MAGIC = b"CCSH"
VERSION = 0x01
MSG_TYPE_ENCRYPTED_CHUNK = 0x01
MSG_TYPE_ADDR_CHANGE = 0x02
MSG_TYPE_GROUP_MSG = 0x03
MSG_TYPE_GROUP_INVITE = 0x04
MSG_TYPE_GROUP_ADDR_CHANGE = 0x05
SENDER_PUB_LEN = 32 # X25519 raw pubkey
MSG_ID_LEN = 16 # UUID4 bytes
FIXED_HEADER_LEN = 4 + 1 + 1 + 1 + MSG_ID_LEN + SENDER_PUB_LEN + 2 + 2 + 2 # 61 bytes
@dataclass(frozen=True)
class PacketV1:
msg_id: bytes
sender_pub: bytes
chunk_index: int
chunk_total: int
ciphertext_chunk: bytes
msg_type: int = MSG_TYPE_ENCRYPTED_CHUNK
flags: int = 0
def pack_packet(pkt: PacketV1) -> bytes:
out = bytearray()
out += MAGIC
out += bytes([VERSION])
out += bytes([pkt.msg_type])
out += bytes([pkt.flags])
out += pkt.msg_id
out += pkt.sender_pub
out += int(pkt.chunk_index).to_bytes(2, "big")
out += int(pkt.chunk_total).to_bytes(2, "big")
out += int(len(pkt.ciphertext_chunk)).to_bytes(2, "big")
out += pkt.ciphertext_chunk
return bytes(out)
def unpack_packet(raw: bytes) -> PacketV1:
if len(raw) < FIXED_HEADER_LEN:
raise ValueError(f"raw too short: {len(raw)} < {FIXED_HEADER_LEN}")
if raw[0:4] != MAGIC:
raise ValueError("bad magic")
ver = raw[4]
if ver != VERSION:
raise ValueError(f"unsupported version: {ver}")
msg_type = raw[5]
flags = raw[6]
pos = 7
msg_id = raw[pos:pos + MSG_ID_LEN]; pos += MSG_ID_LEN
sender_pub = raw[pos:pos + SENDER_PUB_LEN]; pos += SENDER_PUB_LEN
chunk_index = int.from_bytes(raw[pos:pos + 2], "big"); pos += 2
chunk_total = int.from_bytes(raw[pos:pos + 2], "big"); pos += 2
clen = int.from_bytes(raw[pos:pos + 2], "big"); pos += 2
ciphertext_chunk = raw[pos:pos + clen]
return PacketV1(
msg_id=bytes(msg_id), sender_pub=bytes(sender_pub),
chunk_index=chunk_index, chunk_total=chunk_total,
ciphertext_chunk=bytes(ciphertext_chunk),
msg_type=msg_type, flags=flags,
)
Code — X25519 + AES-256-GCM Encryption
# core/crypto.py
import os, hashlib
from cryptography.hazmat.primitives.asymmetric import x25519
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def derive_key(shared_secret: bytes) -> bytes:
"""SHA-256 KDF: shared_secret -> 32-byte AES key"""
return hashlib.sha256(bytes(shared_secret)).digest()
def encrypt_for_pubkey(plaintext: bytes, recipient_pub: bytes) -> bytes:
"""
Encrypt for recipient X25519 pubkey with forward secrecy.
Output: eph_pub(32) || nonce(12) || ciphertext+tag
"""
recipient_pubkey = x25519.X25519PublicKey.from_public_bytes(recipient_pub)
eph_priv = x25519.X25519PrivateKey.generate()
eph_pub = eph_priv.public_key().public_bytes_raw()
shared = eph_priv.exchange(recipient_pubkey)
key = derive_key(shared)
aes = AESGCM(key)
nonce = os.urandom(12)
ciphertext = aes.encrypt(nonce, bytes(plaintext), None)
return eph_pub + nonce + ciphertext
def decrypt_with_privkey(data: bytes, recipient_priv) -> bytes:
"""Decrypt data produced by encrypt_for_pubkey()."""
eph_pub = data[:32]
nonce = data[32:44]
ciphertext = data[44:]
eph_pubkey = x25519.X25519PublicKey.from_public_bytes(eph_pub)
shared = recipient_priv.exchange(eph_pubkey)
key = derive_key(shared)
aes = AESGCM(key)
return aes.decrypt(nonce, ciphertext, None)
Code — OP_RETURN Builder
# core/opreturn.py
def build_op_return(payload: bytes) -> bytes:
"""Build OP_RETURN script (max 223 bytes payload)."""
if len(payload) > 223:
raise ValueError("OP_RETURN payload too large (>223)")
l = len(payload)
if l <= 75:
return b"\x6a" + bytes([l]) + payload
else:
return b"\x6a" + b"\x4c" + bytes([l]) + payload
def parse_op_return(script: bytes) -> bytes:
"""Parse OP_RETURN script back to payload."""
if len(script) < 2 or script[0] != 0x6a:
raise ValueError("Not OP_RETURN")
op = script[1]
if op <= 75:
ln, start = op, 2
elif op == 0x4c:
ln, start = script[2], 3
else:
raise ValueError(f"Unsupported push opcode: {op:#x}")
return script[start:start+ln]
Code — High-Level Protocol API
# core/protocol.py
from .packet_v1 import PacketV1, pack_packet, unpack_packet, new_msg_id
from . import chunks, crypto
def encode_message(plaintext, sender_priv_hex, sender_pub_hex,
recipient_pub_hex, max_chunk_size=162):
"""plaintext -> encrypt -> chunk -> CCSH v1 packets"""
recipient_pub = bytes.fromhex(recipient_pub_hex)
ct_bundle = crypto.encrypt_for_pubkey(plaintext.encode("utf-8"), recipient_pub)
blob_chunks = chunks.chunk_bytes(ct_bundle, max_chunk_size=max_chunk_size)
packets = []
msg_id = new_msg_id()
sender_pub = bytes.fromhex(sender_pub_hex)
total = len(blob_chunks)
for idx, part in enumerate(blob_chunks):
pkt = PacketV1(msg_id=msg_id, sender_pub=sender_pub,
chunk_index=idx, chunk_total=total,
ciphertext_chunk=part)
packets.append(pack_packet(pkt))
return packets
def decode_packets(packets, recipient_priv_hex):
"""CCSH v1 packets -> reassemble -> decrypt -> plaintext"""
from cryptography.hazmat.primitives.asymmetric import x25519
recipient_priv = x25519.X25519PrivateKey.from_private_bytes(
bytes.fromhex(recipient_priv_hex))
grouped = {}
for raw in packets:
p = unpack_packet(raw)
if p.msg_type != 0x01: continue
grouped.setdefault(p.msg_id, []).append(
(p.chunk_index, p.chunk_total, p.sender_pub, p.ciphertext_chunk))
decoded = []
for msg_id, parts in grouped.items():
parts_sorted = sorted(parts, key=lambda x: x[0])
sender_pub = parts_sorted[0][2]
blob = chunks.rebuild_bytes([c for (_, _, _, c) in parts_sorted])
plaintext_bytes = crypto.decrypt_with_privkey(blob, recipient_priv)
decoded.append({
"msg_id": msg_id.hex(),
"sender_pub_hex": sender_pub.hex(),
"plaintext": plaintext_bytes.decode("utf-8"),
})
return decoded
Transport Layer
Messages are transported as standard BCH transactions:
- Sender builds a TX with an OP_RETURN output containing the CCSH packet
- TX is sent to the recipient’s BCH address (dust amount ~546 sats)
- Recipient’s scanner monitors their address via Fulcrum (ElectrumX) WebSocket
- On new TX, scanner checks for CCSH magic bytes in OP_RETURN outputs
- Valid packets are buffered by msg_id until all chunks arrive, then decrypted
The scanner connects to Fulcrum nodes via WebSocket and subscribes to address notifications using the ElectrumX protocol (blockchain.address.subscribe).
WalletConnect v2 Support
Both 00 Wallet and 00 Chat support WalletConnect v2 using the wc2-bch-bcr standard:
- Namespace:
bch:bitcoincash - Methods:
bch_getAddresses,bch_signTransaction,bch_signMessage - Compatible wallets: Paytaca, Cashonize, Zapit
For 00 Chat, WalletConnect handles BCH transaction signing while X25519 encryption keys are generated and stored locally — the external wallet never touches the encryption layer.
Ledger Hardware Wallet Support
00 Wallet also supports Ledger hardware wallets via WebHID. The integration speaks raw APDU commands to the Bitcoin Cash app on the Ledger device — no Ledger Live or intermediary needed. This includes:
-
INS 0x40— Get public key (with on-device confirmation) -
INS 0x44/0x4A/0x48— Hash TX inputs, finalize outputs, sign (BIP143 sighash)
Full Ledger integration code is in the repo at landing/ledger.js.
Try It
Feedback, ideas, and contributions are welcome — the source code will be published soon.
Published : https://github.com/openworldcash/0penw0rld
Tags: #bch-native #e2e-encryption #op-return #walletconnect #open-source