# RFC-001: Open Source DRM for Independent Artists **Status**: Proposed **Author**: [Snider](https://github.com/Snider/) **Created**: 2026-01-10 **License**: EUPL-1.2 --- **Revision History** | Date | Status | Notes | |------|--------|-------| | 2026-01-12 | Proposed | **Chunked streaming**: v3 now supports optional ChunkSize for independently decryptable chunks - enables seek, HTTP Range, and decrypt-while-downloading. | | 2026-01-12 | Proposed | **v3 Streaming**: LTHN rolling keys with configurable cadence (daily/12h/6h/1h). CEK wrapping for zero-trust streaming. WASM v1.3.0 with decryptV3(). | | 2026-01-10 | Proposed | Technical review passed. Fixed section numbering (7.x, 8.x, 9.x, 11.x). Updated WASM size to 5.9MB. Implementation verified complete for stated scope. | --- ## Abstract This RFC describes an open-source Digital Rights Management (DRM) system designed for independent artists to distribute encrypted media directly to fans without platform intermediaries. The system uses ChaCha20-Poly1305 authenticated encryption with a "password-as-license" model, enabling zero-trust distribution where the encryption key serves as both the license and the decryption mechanism. ## 1. Motivation ### 1.1 The Problem Traditional music distribution forces artists into platforms that: - Take 30-70% of revenue (Spotify, Apple Music, Bandcamp) - Control the relationship between artist and fan - Require ongoing subscription for access - Can delist content unilaterally Existing DRM systems (Widevine, FairPlay) require: - Platform integration and licensing fees - Centralized key servers - Proprietary implementations - Trust in third parties ### 1.2 The Solution A DRM system where: - **The password IS the license** - no key servers, no escrow - **Artists keep 100%** - sell direct, any payment processor - **Host anywhere** - CDN, IPFS, S3, personal server - **Browser or native** - same encryption, same content - **Open source** - auditable, forkable, community-owned ## 2. Design Philosophy ### 2.1 "Honest DRM" Traditional DRM operates on a flawed premise: that sufficiently complex technology can prevent copying. History proves otherwise—every DRM system has been broken. The result is systems that: - Punish paying customers with restrictions - Get cracked within days/weeks anyway - Require massive infrastructure (key servers, license servers) - Create single points of failure This system embraces a different philosophy: **DRM for honest people**. The goal isn't to stop determined pirates (impossible). The goal is: 1. Make the legitimate path easy and pleasant 2. Make casual sharing slightly inconvenient 3. Create a social/economic deterrent (sharing = giving away money) 4. Remove all friction for paying customers ### 2.2 Password-as-License The password IS the license. This is not a limitation—it's the core innovation. ``` Traditional DRM: Purchase → License Server → Device Registration → Key Exchange → Playback (5 steps, 3 network calls, 2 points of failure) dapp.fm: Purchase → Password → Playback (2 steps, 0 network calls, 0 points of failure) ``` Benefits: - **No accounts** - No email harvesting, no password resets, no data breaches - **No servers** - Artist can disappear; content still works forever - **No revocation anxiety** - You bought it, you own it - **Transferable** - Give your password to a friend (like lending a CD) - **Archival** - Works in 50 years if you have the password ### 2.3 Encryption as Access Control We use military-grade encryption (ChaCha20-Poly1305) not because we need military-grade security, but because: 1. It's fast (important for real-time media) 2. It's auditable (open standard, RFC 8439) 3. It's already implemented everywhere (Go stdlib, browser crypto) 4. It provides authenticity (Poly1305 MAC prevents tampering) The threat model isn't nation-states—it's casual piracy. The encryption just needs to be "not worth the effort to crack for a $10 album." ## 3. Architecture ### 3.1 System Components ``` ┌─────────────────────────────────────────────────────────────┐ │ DISTRIBUTION LAYER │ │ CDN / IPFS / S3 / GitHub / Personal Server │ │ (Encrypted .smsg files - safe to host anywhere) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ PLAYBACK LAYER │ │ ┌─────────────────┐ ┌─────────────────────────────┐ │ │ │ Browser Demo │ │ Native Desktop App │ │ │ │ (WASM) │ │ (Wails + Go) │ │ │ │ │ │ │ │ │ │ ┌───────────┐ │ │ ┌───────────────────────┐ │ │ │ │ │ stmf.wasm │ │ │ │ Go SMSG Library │ │ │ │ │ │ │ │ │ │ (pkg/smsg) │ │ │ │ │ │ ChaCha20 │ │ │ │ │ │ │ │ │ │ Poly1305 │ │ │ │ ChaCha20-Poly1305 │ │ │ │ │ └───────────┘ │ │ └───────────────────────┘ │ │ │ └─────────────────┘ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ LICENSE LAYER │ │ Password = License Key = Decryption Key │ │ (Sold via Gumroad, Stripe, PayPal, Crypto, etc.) │ └─────────────────────────────────────────────────────────────┘ ``` ### 3.2 SMSG Container Format See: `examples/formats/smsg-format.md` Key properties: - **Magic number**: "SMSG" (0x534D5347) - **Algorithm**: ChaCha20-Poly1305 (authenticated encryption) - **Format**: v1 (JSON+base64) or v2 (binary, 25% smaller) - **Compression**: zstd (default), gzip, or none - **Manifest**: Unencrypted metadata (title, artist, license, expiry, links) - **Payload**: Encrypted media with attachments #### Format Versions | Format | Payload Structure | Size | Speed | Use Case | |--------|------------------|------|-------|----------| | **v1** | JSON with base64-encoded attachments | +33% overhead | Baseline | Legacy | | **v2** | Binary header + raw attachments + zstd | ~Original size | 3-10x faster | Download-to-own | | **v3** | CEK + wrapped keys + rolling LTHN | ~Original size | 3-10x faster | **Streaming** | | **v3+chunked** | v3 with independently decryptable chunks | ~Original size | Seekable | **Chunked streaming** | v2 is recommended for download-to-own (perpetual license). v3 is recommended for streaming (time-limited access). v3 with chunking is recommended for large files requiring seek capability or decrypt-while-downloading. ### 3.3 Key Derivation (v1/v2) ``` License Key (password) │ ▼ SHA-256 Hash │ ▼ 32-byte Symmetric Key │ ▼ ChaCha20-Poly1305 Decryption ``` Simple, auditable, no key escrow. **Note on password hashing**: SHA-256 is used for simplicity and speed. For high-value content, artists may choose to use stronger KDFs (Argon2, scrypt) in custom implementations. The format supports algorithm negotiation via the header. ### 3.4 Streaming Key Derivation (v3) v3 format uses **LTHN rolling keys** for zero-trust streaming. The platform controls key refresh cadence. ``` ┌──────────────────────────────────────────────────────────────────┐ │ v3 STREAMING KEY FLOW │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ SERVER (encryption time): │ │ ───────────────────────── │ │ 1. Generate random CEK (Content Encryption Key) │ │ 2. Encrypt content with CEK (one-time) │ │ 3. For current period AND next period: │ │ streamKey = SHA256(LTHN(period:license:fingerprint)) │ │ wrappedKey = ChaCha(CEK, streamKey) │ │ 4. Store wrapped keys in header (CEK never transmitted) │ │ │ │ CLIENT (decryption time): │ │ ──────────────────────── │ │ 1. Derive streamKey = SHA256(LTHN(period:license:fingerprint)) │ │ 2. Try to unwrap CEK from current period key │ │ 3. If fails, try next period key │ │ 4. Decrypt content with unwrapped CEK │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` #### LTHN Hash Function LTHN is rainbow-table resistant because the salt is derived from the input itself: ``` LTHN(input) = SHA256(input + reverse_leet(input)) where reverse_leet swaps: o↔0, l↔1, e↔3, a↔4, s↔z, t↔7 Example: LTHN("2026-01-12:license:fp") = SHA256("2026-01-12:license:fp" + "pf:3zn3ci1:21-10-6202") ``` You cannot compute the hash without knowing the original input. #### Cadence Options The platform chooses the key refresh rate. Faster cadence = tighter access control. | Cadence | Period Format | Rolling Window | Use Case | |---------|---------------|----------------|----------| | `daily` | `2026-01-12` | 24-48 hours | Standard streaming | | `12h` | `2026-01-12-AM/PM` | 12-24 hours | Premium content | | `6h` | `2026-01-12-00/06/12/18` | 6-12 hours | High-value content | | `1h` | `2026-01-12-15` | 1-2 hours | Live events | The rolling window ensures smooth key transitions. At any time, both the current period key AND the next period key are valid. #### Zero-Trust Properties - **Server never stores keys** - Derived on-demand from LTHN - **Keys auto-expire** - No revocation mechanism needed - **Sharing keys is pointless** - They expire within the cadence window - **Fingerprint binds to device** - Different device = different key - **License ties to user** - Different user = different key ### 3.5 Chunked Streaming (v3 with ChunkSize) When `StreamParams.ChunkSize > 0`, v3 format splits content into independently decryptable chunks, enabling: - **Decrypt-while-downloading** - Play media as chunks arrive - **HTTP Range requests** - Fetch specific chunks by byte offset - **Seekable playback** - Jump to any position without decrypting previous chunks ``` ┌──────────────────────────────────────────────────────────────────┐ │ V3 CHUNKED FORMAT │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ Header (cleartext): │ │ format: "v3" │ │ chunked: { │ │ chunkSize: 1048576, // 1MB default │ │ totalChunks: N, │ │ totalSize: X, // unencrypted total │ │ index: [ // for HTTP Range / seeking │ │ { offset: 0, size: Y }, │ │ { offset: Y, size: Z }, │ │ ... │ │ ] │ │ } │ │ wrappedKeys: [...] // same as non-chunked v3 │ │ │ │ Payload: │ │ [chunk 0: nonce + encrypted + tag] │ │ [chunk 1: nonce + encrypted + tag] │ │ ... │ │ [chunk N: nonce + encrypted + tag] │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` **Key insight**: Each chunk is encrypted with the same CEK but gets its own random nonce, making chunks independently decryptable. The chunk index in the header enables: 1. **Seeking**: Calculate which chunk contains byte offset X, fetch just that chunk 2. **Range requests**: Use HTTP Range headers to fetch specific encrypted chunks 3. **Streaming**: Decrypt chunk 0 for metadata, then stream chunks 1-N as they arrive **Usage example**: ```go params := &StreamParams{ License: "user-license", Fingerprint: "device-fp", ChunkSize: 1024 * 1024, // 1MB chunks } // Encrypt with chunking encrypted, _ := EncryptV3(msg, params, manifest) // For streaming playback: header, _ := GetV3Header(encrypted) cek, _ := UnwrapCEKFromHeader(header, params) payload, _ := GetV3Payload(encrypted) for i := 0; i < header.Chunked.TotalChunks; i++ { chunk, _ := DecryptV3Chunk(payload, cek, i, header.Chunked) player.Write(chunk) // Stream to audio/video player } ``` ### 3.6 Supported Content Types SMSG is content-agnostic. Any file can be an attachment: | Type | MIME | Use Case | |------|------|----------| | Audio | audio/mpeg, audio/flac, audio/wav | Music, podcasts | | Video | video/mp4, video/webm | Music videos, films | | Images | image/png, image/jpeg | Album art, photos | | Documents | application/pdf | Liner notes, lyrics | | Archives | application/zip | Multi-file releases | | Any | application/octet-stream | Anything else | Multiple attachments per SMSG are supported (e.g., album + cover art + PDF booklet). ## 4. Demo Page Architecture **Live Demo**: https://demo.dapp.fm ### 4.1 Components ``` demo/ ├── index.html # Single-page application ├── stmf.wasm # Go WASM decryption module (~5.9MB) ├── wasm_exec.js # Go WASM runtime ├── demo-track.smsg # Sample encrypted content (v2/zstd) └── profile-avatar.jpg # Artist avatar ``` ### 4.2 UI Modes The demo has three modes, accessible via tabs: | Mode | Purpose | Default | |------|---------|---------| | **Profile** | Artist landing page with auto-playing content | Yes | | **Fan** | Upload and decrypt purchased .smsg files | No | | **Artist** | Re-key content, create new packages | No | ### 4.3 Profile Mode (Default) ``` ┌─────────────────────────────────────────────────────────────┐ │ dapp.fm [Profile] [Fan] [Artist] │ ├─────────────────────────────────────────────────────────────┤ │ Zero-Trust DRM ⚠️ Demo pre-seeded with keys │ ├─────────────────────────────────────────────────────────────┤ │ [No Middlemen] [No Fees] [Host Anywhere] [Browser/Native] │ ├─────────────────┬───────────────────────────────────────────┤ │ SIDEBAR │ MAIN CONTENT │ │ ┌───────────┐ │ ┌─────────────────────────────────────┐ │ │ │ Avatar │ │ │ 🛒 Buy This Track on Beatport │ │ │ │ │ │ │ 95%-100%* goes to the artist │ │ │ │ Artist │ │ ├─────────────────────────────────────┤ │ │ │ Name │ │ │ │ │ │ │ │ │ │ VIDEO PLAYER │ │ │ │ Links: │ │ │ (auto-starts at 1:08) │ │ │ │ Beatport │ │ │ with native controls │ │ │ │ Spotify │ │ │ │ │ │ │ YouTube │ │ ├─────────────────────────────────────┤ │ │ │ etc. │ │ │ About the Artist │ │ │ └───────────┘ │ │ (Bio text) │ │ │ │ └─────────────────────────────────────┘ │ ├─────────────────┴───────────────────────────────────────────┤ │ GitHub · EUPL-1.2 · Viva La OpenSource 💜 │ └─────────────────────────────────────────────────────────────┘ ``` ### 4.4 Decryption Flow ``` User clicks "Play Demo Track" │ ▼ fetch(demo-track.smsg) │ ▼ Convert to base64 ◄─── CRITICAL: Must handle binary vs text format │ See: examples/failures/001-double-base64-encoding.md ▼ BorgSMSG.getInfo(base64) │ ▼ Display manifest (title, artist, license) │ ▼ BorgSMSG.decryptStream(base64, password) │ ▼ Create Blob from Uint8Array │ ▼ URL.createObjectURL(blob) │ ▼