# RFC-010: WASM Decryption API
**Status**: Draft
**Author**: [Snider](https://github.com/Snider/)
**Created**: 2026-01-13
**License**: EUPL-1.2
**Depends On**: RFC-002, RFC-007, RFC-009
---
## Abstract
This RFC specifies the WebAssembly (WASM) API for browser-based decryption of SMSG content and STMF form encryption. The API is exposed through two JavaScript namespaces: `BorgSMSG` for content decryption and `BorgSTMF` for form encryption.
## 1. Overview
The WASM module provides:
- SMSG decryption (v1, v2, v3, chunked, ABR)
- SMSG encryption
- STMF form encryption/decryption
- Metadata extraction without decryption
## 2. Module Loading
### 2.1 Files Required
```
stmf.wasm (~5.9MB) Compiled Go WASM module
wasm_exec.js (~20KB) Go WASM runtime
```
### 2.2 Initialization
```html
```
### 2.3 Ready Event
```javascript
document.addEventListener('borgstmf:ready', (event) => {
console.log('WASM ready, version:', event.detail.version);
});
```
## 3. BorgSMSG Namespace
### 3.1 Version
```javascript
BorgSMSG.version // "1.6.0"
BorgSMSG.ready // true when loaded
```
### 3.2 Metadata Functions
#### getInfo(base64) → Promise
Get manifest without decryption.
```javascript
const info = await BorgSMSG.getInfo(base64Content);
// info.version, info.algorithm, info.format
// info.manifest.title, info.manifest.artist
// info.isV3Streaming, info.isChunked
// info.wrappedKeys (for v3)
```
#### getInfoBinary(uint8Array) → Promise
Binary input variant (no base64 decode needed).
```javascript
const bytes = new Uint8Array(await response.arrayBuffer());
const info = await BorgSMSG.getInfoBinary(bytes);
```
### 3.3 Decryption Functions
#### decrypt(base64, password) → Promise
Full decryption (v1 format, base64 attachments).
```javascript
const msg = await BorgSMSG.decrypt(base64Content, password);
// msg.body, msg.subject, msg.from
// msg.attachments[0].name, .content (base64), .mime
```
#### decryptStream(base64, password) → Promise
Streaming decryption (v2 format, binary attachments).
```javascript
const msg = await BorgSMSG.decryptStream(base64Content, password);
// msg.attachments[0].data (Uint8Array)
// msg.attachments[0].mime
```
#### decryptBinary(uint8Array, password) → Promise
Binary input, binary output.
```javascript
const bytes = new Uint8Array(await fetch(url).then(r => r.arrayBuffer()));
const msg = await BorgSMSG.decryptBinary(bytes, password);
```
#### quickDecrypt(base64, password) → Promise
Returns body text only (fast path).
```javascript
const body = await BorgSMSG.quickDecrypt(base64Content, password);
```
### 3.4 V3 Streaming Functions
#### decryptV3(base64, params) → Promise
Decrypt v3 streaming content with LTHN rolling keys.
```javascript
const msg = await BorgSMSG.decryptV3(base64Content, {
license: "user-license-key",
fingerprint: "device-fingerprint" // optional
});
```
#### getV3ChunkInfo(base64) → Promise
Get chunk index for seeking without full decrypt.
```javascript
const chunkInfo = await BorgSMSG.getV3ChunkInfo(base64Content);
// chunkInfo.chunkSize (default 1MB)
// chunkInfo.totalChunks
// chunkInfo.totalSize
// chunkInfo.index[i].offset, .size
```
#### unwrapV3CEK(base64, params) → Promise
Unwrap CEK for manual chunk decryption. Returns base64 CEK.
```javascript
const cekBase64 = await BorgSMSG.unwrapV3CEK(base64Content, {
license: "license",
fingerprint: "fp"
});
```
#### decryptV3Chunk(base64, cekBase64, chunkIndex) → Promise
Decrypt single chunk by index.
```javascript
const chunk = await BorgSMSG.decryptV3Chunk(base64Content, cekBase64, 5);
```
#### parseV3Header(uint8Array) → Promise
Parse header from partial data (for streaming).
```javascript
const header = await BorgSMSG.parseV3Header(bytes);
// header.format, header.keyMethod, header.cadence
// header.payloadOffset (where chunks start)
// header.wrappedKeys, header.chunked, header.manifest
```
#### unwrapCEKFromHeader(wrappedKeys, params, cadence) → Promise
Unwrap CEK from parsed header.
```javascript
const cek = await BorgSMSG.unwrapCEKFromHeader(
header.wrappedKeys,
{license: "lic", fingerprint: "fp"},
"daily"
);
```
#### decryptChunkDirect(chunkBytes, cek) → Promise
Low-level chunk decryption with pre-unwrapped CEK.
```javascript
const plaintext = await BorgSMSG.decryptChunkDirect(chunkBytes, cek);
```
### 3.5 Encryption Functions
#### encrypt(message, password, hint?) → Promise
Encrypt message (v1 format). Returns base64.
```javascript
const encrypted = await BorgSMSG.encrypt({
body: "Hello",
attachments: [{
name: "file.txt",
content: btoa("data"),
mime: "text/plain"
}]
}, password, "optional hint");
```
#### encryptWithManifest(message, password, manifest) → Promise
Encrypt with manifest (v2 format). Returns base64.
```javascript
const encrypted = await BorgSMSG.encryptWithManifest(message, password, {
title: "My Track",
artist: "Artist Name",
licenseType: "perpetual"
});
```
### 3.6 ABR Functions
#### parseABRManifest(jsonString) → Promise
Parse HLS-style ABR manifest.
```javascript
const manifest = await BorgSMSG.parseABRManifest(manifestJson);
// manifest.version, manifest.title, manifest.duration
// manifest.variants[i].name, .bandwidth, .url
// manifest.defaultIdx
```
#### selectVariant(manifest, bandwidthBps) → Promise
Select best variant for bandwidth (returns index).
```javascript
const idx = await BorgSMSG.selectVariant(manifest, measuredBandwidth);
// Uses 80% safety threshold
```
## 4. BorgSTMF Namespace
### 4.1 Key Generation
```javascript
const keypair = await BorgSTMF.generateKeyPair();
// keypair.publicKey (base64 X25519)
// keypair.privateKey (base64 X25519) - KEEP SECRET
```
### 4.2 Encryption
```javascript
// Encrypt JSON string
const encrypted = await BorgSTMF.encrypt(
JSON.stringify(formData),
serverPublicKeyBase64
);
// Encrypt with metadata
const encrypted = await BorgSTMF.encryptFields(
{email: "user@example.com", password: "secret"},
serverPublicKeyBase64,
{timestamp: Date.now().toString()} // optional metadata
);
```
## 5. Type Definitions
### 5.1 ManifestInfo
```typescript
interface ManifestInfo {
version: string;
algorithm: string;
format?: string;
compression?: string;
hint?: string;
keyMethod?: string; // "LTHN" for v3
cadence?: string; // "daily", "12h", "6h", "1h"
wrappedKeys?: WrappedKey[];
isV3Streaming: boolean;
chunked?: ChunkInfo;
isChunked: boolean;
manifest?: Manifest;
}
```
### 5.2 Message / StreamMessage
```typescript
interface Message {
from?: string;
to?: string;
subject?: string;
body: string;
timestamp?: number;
attachments: Attachment[];
replyKey?: KeyInfo;
meta?: Record;
}
interface Attachment {
name: string;
mime: string;
size: number;
content?: string; // base64 (v1)
data?: Uint8Array; // binary (v2/v3)
}
```
### 5.3 ChunkInfo
```typescript
interface ChunkInfo {
chunkSize: number; // default 1048576 (1MB)
totalChunks: number;
totalSize: number;
index: ChunkEntry[];
}
interface ChunkEntry {
offset: number;
size: number;
}
```
### 5.4 Manifest
```typescript
interface Manifest {
title: string;
artist?: string;
album?: string;
genre?: string;
year?: number;
releaseType?: string; // "single", "album", "ep", "mix"
duration?: number; // seconds
format?: string;
expiresAt?: number; // Unix timestamp
issuedAt?: number; // Unix timestamp
licenseType?: string; // "perpetual", "rental", "stream", "preview"
tracks?: Track[];
tags?: string[];
links?: Record;
extra?: Record;
}
```
## 6. Error Handling
### 6.1 Pattern
All functions throw on error:
```javascript
try {
const msg = await BorgSMSG.decrypt(content, password);
} catch (e) {
console.error(e.message);
}
```
### 6.2 Common Errors
| Error | Cause |
|-------|-------|
| `decrypt requires 2 arguments` | Wrong argument count |
| `decryption failed: {reason}` | Wrong password or corrupted |
| `invalid format` | Not a valid SMSG file |
| `unsupported version` | Unknown format version |
| `key expired` | v3 rolling key outside window |
| `invalid base64: {reason}` | Base64 decode failed |
| `chunk out of range` | Invalid chunk index |
## 7. Performance
### 7.1 Binary vs Base64
- Binary functions (`*Binary`, `decryptStream`) are ~30% faster
- Avoid double base64 encoding
### 7.2 Large Files (>50MB)
Use chunked streaming:
```javascript
// Efficient: Cache CEK, stream chunks
const header = await BorgSMSG.parseV3Header(bytes);
const cek = await BorgSMSG.unwrapCEKFromHeader(header.wrappedKeys, params);
for (let i = 0; i < header.chunked.totalChunks; i++) {
const chunk = await BorgSMSG.decryptChunkDirect(payload, cek);
player.write(chunk);
// chunk is GC'd after each iteration
}
```
### 7.3 Typical Execution Times
| Operation | Size | Time |
|-----------|------|------|
| getInfo | any | ~50-100ms |
| decrypt (small) | <1MB | ~200-500ms |
| decrypt (large) | 100MB | 2-5s |
| decryptV3Chunk | 1MB | ~200-400ms |
| generateKeyPair | - | ~50-200ms |
## 8. Browser Compatibility
| Browser | Support |
|---------|---------|
| Chrome 57+ | Full |
| Firefox 52+ | Full |
| Safari 11+ | Full |
| Edge 16+ | Full |
| IE | Not supported |
Requirements:
- WebAssembly support
- Async/await (ES2017)
- Uint8Array
## 9. Memory Management
- WASM module: ~5.9MB static
- Per-operation: Peak ~2-3x file size during decryption
- Go GC reclaims after Promise resolution
- Keys never leave WASM memory
## 10. Implementation Reference
- Source: `pkg/wasm/stmf/main.go` (1758 lines)
- Build: `GOOS=js GOARCH=wasm go build -o stmf.wasm ./pkg/wasm/stmf/`
## 11. Security Considerations
1. **Password handling**: Clear from memory after use
2. **Memory isolation**: WASM sandbox prevents JS access
3. **Constant-time crypto**: Go crypto uses safe operations
4. **Key protection**: Keys never exposed to JavaScript
## 12. Future Work
- [ ] WebWorker support for background decryption
- [ ] Streaming API with ReadableStream
- [ ] Smaller WASM size via TinyGo
- [ ] Native Web Crypto fallback for simple operations