Documentation
¶
Overview ¶
Package smsg - Adaptive Bitrate Streaming (ABR) support
ABR enables multi-bitrate streaming with automatic quality switching based on network conditions. Similar to HLS/DASH but with ChaCha20-Poly1305 encryption.
Architecture:
- Master manifest (.json) lists available quality variants
- Each variant is a standard v3 chunked .smsg file
- Same password decrypts all variants (CEK unwrapped once)
- Player switches variants at chunk boundaries based on bandwidth
Package smsg implements Secure Message encryption using password-based ChaCha20-Poly1305. SMSG (Secure Message) enables encrypted message exchange where the recipient decrypts using a pre-shared password. Useful for secure support replies, confidential documents, and any scenario requiring password-protected content.
Format versions:
- v1: JSON with base64-encoded attachments (legacy)
- v2: Binary format with zstd compression (current)
- v3: Streaming with LTHN rolling keys (planned)
Encryption note: Nonces are embedded in ciphertext, not transmitted separately. See smsg.go header comment for details.
Index ¶
- Constants
- Variables
- func DecryptV3(data []byte, params *StreamParams) (*Message, *Header, error)
- func DecryptV3Chunk(payload []byte, cek []byte, chunkIndex int, chunked *ChunkedInfo) ([]byte, error)
- func DeriveKey(password string) []byte
- func DeriveStreamKey(date, license, fingerprint string) []byte
- func Encrypt(msg *Message, password string) ([]byte, error)
- func EncryptBase64(msg *Message, password string) (string, error)
- func EncryptV2(msg *Message, password string) ([]byte, error)
- func EncryptV2WithManifest(msg *Message, password string, manifest *Manifest) ([]byte, error)
- func EncryptV2WithOptions(msg *Message, password string, manifest *Manifest, compression string) ([]byte, error)
- func EncryptV3(msg *Message, params *StreamParams, manifest *Manifest) ([]byte, error)
- func EncryptWithHint(msg *Message, password, hint string) ([]byte, error)
- func EncryptWithManifest(msg *Message, password string, manifest *Manifest) ([]byte, error)
- func EncryptWithManifestBase64(msg *Message, password string, manifest *Manifest) (string, error)
- func GenerateCEK() ([]byte, error)
- func GetCadenceWindowDuration(cadence Cadence) time.Duration
- func GetRollingDates() (current, next string)
- func GetRollingDatesAt(t time.Time) (current, next string)
- func GetRollingPeriods(cadence Cadence, t time.Time) (current, next string)
- func GetV3Payload(data []byte) ([]byte, error)
- func QuickDecrypt(encoded, password string) (string, error)
- func QuickEncrypt(body, password string) (string, error)
- func UnwrapCEK(wrappedB64 string, streamKey []byte) ([]byte, error)
- func UnwrapCEKFromHeader(header *Header, params *StreamParams) ([]byte, error)
- func Validate(data []byte) error
- func WrapCEK(cek, streamKey []byte) (string, error)
- func WriteABRManifest(manifest *ABRManifest, path string) error
- type ABRBandwidthEstimator
- type ABRManifest
- type Attachment
- type Cadence
- type ChunkInfo
- type ChunkedInfo
- type Header
- type Manifest
- func (m *Manifest) AddLink(platform, url string) *Manifest
- func (m *Manifest) AddTrack(title string, start float64) *Manifest
- func (m *Manifest) AddTrackFull(title string, start, end float64, trackType string) *Manifest
- func (m *Manifest) IsExpired() bool
- func (m *Manifest) TimeRemaining() int64
- func (m *Manifest) WithExpiration(expiresAt int64) *Manifest
- func (m *Manifest) WithPreviewAccess(seconds int) *Manifest
- func (m *Manifest) WithRentalDuration(durationSeconds int64) *Manifest
- func (m *Manifest) WithStreamingAccess(hours int) *Manifest
- type Message
- func (m *Message) AddAttachment(name, content, mimeType string) *Message
- func (m *Message) AddBinaryAttachment(name string, data []byte, mimeType string) *Message
- func (m *Message) GetAttachment(name string) *Attachment
- func (m *Message) SetMeta(key, value string) *Message
- func (m *Message) WithFrom(from string) *Message
- func (m *Message) WithReplyKey(publicKeyB64 string) *Message
- func (m *Message) WithReplyKeyInfo(pki *PKIInfo) *Message
- func (m *Message) WithSubject(subject string) *Message
- func (m *Message) WithTimestamp(ts int64) *Message
- type PKIInfo
- type StreamParams
- type Track
- type Variant
- type WrappedKey
Constants ¶
const ( FormatV1 = "" // Original format: JSON with base64-encoded attachments FormatV2 = "v2" // Binary format: JSON header + raw binary attachments FormatV3 = "v3" // Streaming format: CEK wrapped with rolling LTHN keys, optional chunking )
Format versions
const ( CompressionNone = "" // No compression (default, backwards compatible) CompressionGzip = "gzip" // Gzip compression (stdlib, WASM compatible) CompressionZstd = "zstd" // Zstandard compression (faster, better ratio) )
Compression types
const ( // KeyMethodDirect uses password directly (v1/v2 behavior) KeyMethodDirect = "" // KeyMethodLTHNRolling uses LTHN hash with rolling date windows // Key = SHA256(LTHN(date:license:fingerprint)) // Valid keys: current period and next period (rolling window) KeyMethodLTHNRolling = "lthn-rolling" )
Key derivation methods for v3 streaming
const ABRSafetyFactor = 0.8
ABRSafetyFactor is the bandwidth multiplier for variant selection. Using 80% of available bandwidth prevents buffering on fluctuating networks.
const ABRVersion = "abr-v1"
const DefaultChunkSize = 1024 * 1024
Default chunk size for v3 chunked format (1MB)
const Magic = "SMSG"
Magic bytes for SMSG format
const Version = "1.0"
Version of the SMSG format
Variables ¶
var ( ErrInvalidMagic = errors.New("invalid SMSG magic") ErrInvalidPayload = errors.New("invalid SMSG payload") ErrDecryptionFailed = errors.New("decryption failed (wrong password?)") ErrPasswordRequired = errors.New("password is required") ErrEmptyMessage = errors.New("message cannot be empty") ErrStreamKeyExpired = errors.New("stream key expired (outside rolling window)") ErrNoValidKey = errors.New("no valid wrapped key found for current date") ErrLicenseRequired = errors.New("license is required for stream decryption") )
Errors
var ABRPresets = []struct { Name string Width int Height int Bitrate string // For ffmpeg BPS int // Bits per second }{ {"1080p", 1920, 1080, "5M", 5000000}, {"720p", 1280, 720, "2.5M", 2500000}, {"480p", 854, 480, "1M", 1000000}, {"360p", 640, 360, "500K", 500000}, }
Standard ABR quality presets
Functions ¶
func DecryptV3 ¶ added in v0.2.0
func DecryptV3(data []byte, params *StreamParams) (*Message, *Header, error)
DecryptV3 decrypts a v3 streaming message using rolling keys. It tries today's key first, then tomorrow's key. Automatically handles both chunked and non-chunked v3 formats.
func DecryptV3Chunk ¶ added in v0.2.0
func DecryptV3Chunk(payload []byte, cek []byte, chunkIndex int, chunked *ChunkedInfo) ([]byte, error)
DecryptV3Chunk decrypts a single chunk by index. This enables streaming playback and seeking without decrypting the entire file.
Usage for streaming:
header, _ := GetV3Header(data)
cek, _ := UnwrapCEKFromHeader(header, params)
payload, _ := GetV3Payload(data)
for i := 0; i < header.Chunked.TotalChunks; i++ {
chunk, _ := DecryptV3Chunk(payload, cek, i, header.Chunked)
player.Write(chunk)
}
func DeriveStreamKey ¶ added in v0.2.0
DeriveStreamKey derives a 32-byte ChaCha key from date, license, and fingerprint. Uses LTHN hash which is rainbow-table resistant (salt derived from input itself).
The derived key is: SHA256(LTHN("YYYY-MM-DD:license:fingerprint"))
func Encrypt ¶
Encrypt encrypts a message with a password. Returns the encrypted SMSG container bytes.
func EncryptBase64 ¶
EncryptBase64 encrypts and returns base64-encoded result
func EncryptV2 ¶ added in v0.1.0
EncryptV2 encrypts a message using v2 binary format (smaller file size) Attachments are stored as raw binary instead of base64-encoded JSON Uses zstd compression by default (faster than gzip, better ratio)
func EncryptV2WithManifest ¶ added in v0.1.0
EncryptV2WithManifest encrypts with v2 binary format and public manifest Uses zstd compression by default (faster than gzip, better ratio)
func EncryptV2WithOptions ¶ added in v0.1.0
func EncryptV2WithOptions(msg *Message, password string, manifest *Manifest, compression string) ([]byte, error)
EncryptV2WithOptions encrypts with full control over format options
func EncryptV3 ¶ added in v0.2.0
func EncryptV3(msg *Message, params *StreamParams, manifest *Manifest) ([]byte, error)
EncryptV3 encrypts a message using v3 streaming format with rolling keys. The content is encrypted with a random CEK, which is then wrapped with stream keys for today and tomorrow.
When params.ChunkSize > 0, content is split into independently decryptable chunks, enabling decrypt-while-downloading and seeking.
func EncryptWithHint ¶
EncryptWithHint encrypts with an optional password hint in the header
func EncryptWithManifest ¶ added in v0.1.0
EncryptWithManifest encrypts with public manifest metadata in the clear text header The manifest is visible without decryption, enabling content discovery and indexing
func EncryptWithManifestBase64 ¶ added in v0.1.0
EncryptWithManifestBase64 encrypts with manifest and returns base64
func GenerateCEK ¶ added in v0.2.0
GenerateCEK generates a random 32-byte Content Encryption Key
func GetCadenceWindowDuration ¶ added in v0.2.0
GetCadenceWindowDuration returns the duration of one period for a cadence
func GetRollingDates ¶ added in v0.2.0
func GetRollingDates() (current, next string)
GetRollingDates returns today and tomorrow's date strings in YYYY-MM-DD format This is the default daily cadence.
func GetRollingDatesAt ¶ added in v0.2.0
GetRollingDatesAt returns today and tomorrow relative to a specific time
func GetRollingPeriods ¶ added in v0.2.0
GetRollingPeriods returns the current and next period strings based on cadence. The period string format varies by cadence:
- daily: "2006-01-02"
- 12h: "2006-01-02-AM" or "2006-01-02-PM"
- 6h: "2006-01-02-00", "2006-01-02-06", "2006-01-02-12", "2006-01-02-18"
- 1h: "2006-01-02-15" (hour in 24h format)
func GetV3Payload ¶ added in v0.2.0
GetV3Payload extracts just the payload from a v3 file. Use with DecryptV3Chunk for individual chunk decryption.
func QuickDecrypt ¶
QuickDecrypt is a convenience function for simple message decryption
func QuickEncrypt ¶
QuickEncrypt is a convenience function for simple message encryption
func UnwrapCEK ¶ added in v0.2.0
UnwrapCEK unwraps a Content Encryption Key using a stream key Takes base64-encoded wrapped key, returns raw CEK bytes
func UnwrapCEKFromHeader ¶ added in v0.2.0
func UnwrapCEKFromHeader(header *Header, params *StreamParams) ([]byte, error)
UnwrapCEKFromHeader unwraps the CEK from a v3 header using stream params. Returns the CEK for use with DecryptV3Chunk.
func WrapCEK ¶ added in v0.2.0
WrapCEK wraps a Content Encryption Key with a stream key Returns base64-encoded wrapped key (includes nonce)
func WriteABRManifest ¶ added in v0.2.0
func WriteABRManifest(manifest *ABRManifest, path string) error
WriteABRManifest writes the ABR manifest to a JSON file.
Types ¶
type ABRBandwidthEstimator ¶ added in v0.2.0
type ABRBandwidthEstimator struct {
// contains filtered or unexported fields
}
ABRBandwidthEstimator tracks download speeds for adaptive quality selection.
func NewABRBandwidthEstimator ¶ added in v0.2.0
func NewABRBandwidthEstimator(maxSamples int) *ABRBandwidthEstimator
NewABRBandwidthEstimator creates a new bandwidth estimator.
func (*ABRBandwidthEstimator) Estimate ¶ added in v0.2.0
func (e *ABRBandwidthEstimator) Estimate() int
Estimate returns the estimated bandwidth in bits per second. Uses average of recent samples, or 1 Mbps default if no samples.
func (*ABRBandwidthEstimator) RecordSample ¶ added in v0.2.0
func (e *ABRBandwidthEstimator) RecordSample(bytes int, durationMs int)
RecordSample records a bandwidth sample from a download. bytes is the number of bytes downloaded, durationMs is the time in milliseconds.
type ABRManifest ¶ added in v0.2.0
type ABRManifest struct {
Version string `json:"version"` // "abr-v1"
Title string `json:"title"` // Content title
Duration int `json:"duration"` // Total duration in seconds
Variants []Variant `json:"variants"` // Quality variants (sorted by bandwidth, ascending)
DefaultIdx int `json:"defaultIdx"` // Default variant index (typically 720p)
Password string `json:"-"` // Shared password for all variants (not serialized)
}
ABRManifest represents a multi-bitrate variant playlist for adaptive streaming. Similar to HLS master playlist but with encrypted SMSG variants.
func NewABRManifest ¶ added in v0.2.0
func NewABRManifest(title string) *ABRManifest
NewABRManifest creates a new ABR manifest with the given title.
func ParseABRManifest ¶ added in v0.2.0
func ParseABRManifest(data []byte) (*ABRManifest, error)
ParseABRManifest parses an ABR manifest from JSON bytes.
func ReadABRManifest ¶ added in v0.2.0
func ReadABRManifest(path string) (*ABRManifest, error)
ReadABRManifest reads an ABR manifest from a JSON file.
func (*ABRManifest) AddVariant ¶ added in v0.2.0
func (m *ABRManifest) AddVariant(v Variant)
AddVariant adds a quality variant to the manifest. Variants are automatically sorted by bandwidth (ascending) after adding.
func (*ABRManifest) GetVariant ¶ added in v0.2.0
func (m *ABRManifest) GetVariant(idx int) *Variant
GetVariant returns the variant at the given index, or nil if out of range.
func (*ABRManifest) SelectVariant ¶ added in v0.2.0
func (m *ABRManifest) SelectVariant(bandwidthBPS int) int
SelectVariant selects the best variant for the given bandwidth (bits per second). Returns the index of the highest quality variant that fits within the bandwidth.
type Attachment ¶
type Attachment struct {
Name string `json:"name"`
Content string `json:"content,omitempty"` // base64-encoded (v1) or empty (v2, populated on decrypt)
MimeType string `json:"mime,omitempty"`
Size int `json:"size,omitempty"` // binary size in bytes
}
Attachment represents a file attached to the message
type Cadence ¶ added in v0.2.0
type Cadence string
Cadence defines how often stream keys rotate
const ( // CadenceDaily rotates keys every 24 hours (default) // Date format: "2006-01-02" CadenceDaily Cadence = "daily" // CadenceHalfDay rotates keys every 12 hours // Date format: "2006-01-02-AM" or "2006-01-02-PM" CadenceHalfDay Cadence = "12h" // CadenceQuarter rotates keys every 6 hours // Date format: "2006-01-02-00", "2006-01-02-06", "2006-01-02-12", "2006-01-02-18" CadenceQuarter Cadence = "6h" // CadenceHourly rotates keys every hour // Date format: "2006-01-02-15" (24-hour format) CadenceHourly Cadence = "1h" )
type ChunkInfo ¶ added in v0.2.0
type ChunkInfo struct {
Offset int `json:"offset"` // byte offset in payload
Size int `json:"size"` // encrypted chunk size (includes nonce + tag)
}
ChunkInfo describes a single chunk in v3 chunked format
type ChunkedInfo ¶ added in v0.2.0
type ChunkedInfo struct {
ChunkSize int `json:"chunkSize"` // size of each chunk before encryption
TotalChunks int `json:"totalChunks"` // number of chunks
TotalSize int64 `json:"totalSize"` // total unencrypted size
Index []ChunkInfo `json:"index"` // chunk locations for seeking
}
ChunkedInfo contains chunking metadata for v3 streaming When present, enables decrypt-while-downloading and seeking
type Header ¶
type Header struct {
Version string `json:"version"`
Algorithm string `json:"algorithm"`
Format string `json:"format,omitempty"` // v2 for binary, v3 for streaming, empty for v1 (base64)
Compression string `json:"compression,omitempty"` // gzip, zstd, or empty for none
Hint string `json:"hint,omitempty"` // optional password hint
Manifest *Manifest `json:"manifest,omitempty"` // public metadata for discovery
// V3 streaming fields
KeyMethod string `json:"keyMethod,omitempty"` // lthn-rolling for v3
Cadence Cadence `json:"cadence,omitempty"` // key rotation frequency (daily, 12h, 6h, 1h)
WrappedKeys []WrappedKey `json:"wrappedKeys,omitempty"` // CEK wrapped with rolling keys
// V3 chunked streaming (optional - enables decrypt-while-downloading)
Chunked *ChunkedInfo `json:"chunked,omitempty"` // chunk index for seeking/range requests
}
Header represents the SMSG container header
func GetInfoBase64 ¶
GetInfoBase64 extracts header info from base64-encoded SMSG
func GetV3Header ¶ added in v0.2.0
GetV3Header extracts the header from a v3 file without decrypting. Useful for getting chunk index for Range requests.
func GetV3HeaderFromPrefix ¶ added in v0.2.0
GetV3HeaderFromPrefix parses the v3 header from just the file prefix. This enables streaming: parse header as soon as first few KB arrive. Returns header and payload offset (where encrypted chunks start).
File format:
- Bytes 0-3: Magic "SMSG"
- Bytes 4-5: Version (2-byte little endian)
- Bytes 6-8: Header length (3-byte big endian)
- Bytes 9+: Header JSON
- Payload starts at offset 9 + headerLen
type Manifest ¶ added in v0.1.0
type Manifest struct {
// Content identification
Title string `json:"title,omitempty"`
Artist string `json:"artist,omitempty"`
Album string `json:"album,omitempty"`
Genre string `json:"genre,omitempty"`
Year int `json:"year,omitempty"`
// Release info
ReleaseType string `json:"release_type,omitempty"` // single, album, ep, mix
Duration int `json:"duration,omitempty"` // total duration in seconds
Format string `json:"format,omitempty"` // dapp.fm/v1, etc.
// License expiration (for streaming/rental models)
ExpiresAt int64 `json:"expires_at,omitempty"` // Unix timestamp when license expires (0 = never)
IssuedAt int64 `json:"issued_at,omitempty"` // Unix timestamp when license was issued
LicenseType string `json:"license_type,omitempty"` // perpetual, rental, stream, preview
// Track list (like CD master)
Tracks []Track `json:"tracks,omitempty"`
// Artist links - direct to artist, skip the middlemen
Links map[string]string `json:"links,omitempty"` // platform -> URL (bandcamp, soundcloud, website, etc.)
// Custom metadata
Tags []string `json:"tags,omitempty"`
Extra map[string]string `json:"extra,omitempty"`
}
Manifest contains public metadata visible without decryption This enables content discovery, indexing, and preview
func NewManifest ¶ added in v0.1.0
NewManifest creates a new manifest with title
func (*Manifest) AddTrackFull ¶ added in v0.1.0
AddTrackFull adds a track with all details
func (*Manifest) TimeRemaining ¶ added in v0.1.0
TimeRemaining returns seconds until expiration (0 if perpetual, negative if expired)
func (*Manifest) WithExpiration ¶ added in v0.1.0
WithExpiration sets the license expiration time
func (*Manifest) WithPreviewAccess ¶ added in v0.1.0
WithPreviewAccess sets up for preview (very short, e.g., 30 seconds)
func (*Manifest) WithRentalDuration ¶ added in v0.1.0
WithRentalDuration sets expiration relative to issue time
func (*Manifest) WithStreamingAccess ¶ added in v0.1.0
WithStreamingAccess sets up for streaming (short expiration, e.g., 24 hours)
type Message ¶
type Message struct {
// Core message content
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
// Optional attachments
Attachments []Attachment `json:"attachments,omitempty"`
// PKI for authenticated replies
ReplyKey *PKIInfo `json:"reply_key,omitempty"`
// Metadata
From string `json:"from,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
}
Message represents the decrypted message content
func Decrypt ¶
Decrypt decrypts an SMSG container with a password Automatically handles both v1 (base64) and v2 (binary) formats
func DecryptBase64 ¶
DecryptBase64 decrypts a base64-encoded SMSG
func NewMessage ¶
NewMessage creates a new message with the given body
func (*Message) AddAttachment ¶
AddAttachment adds a file attachment (content is base64-encoded)
func (*Message) AddBinaryAttachment ¶ added in v0.1.0
AddBinaryAttachment adds a raw binary attachment (for v2 format) The content will be base64-encoded for API compatibility
func (*Message) GetAttachment ¶
func (m *Message) GetAttachment(name string) *Attachment
GetAttachment finds an attachment by name
func (*Message) WithReplyKey ¶
WithReplyKey sets the PKI public key for authenticated replies
func (*Message) WithReplyKeyInfo ¶
WithReplyKeyInfo sets full PKI information
func (*Message) WithSubject ¶
WithSubject sets the message subject
func (*Message) WithTimestamp ¶
WithTimestamp sets the timestamp
type PKIInfo ¶
type PKIInfo struct {
PublicKey string `json:"public_key"` // base64-encoded X25519 public key
KeyID string `json:"key_id,omitempty"` // optional key identifier
Algorithm string `json:"algorithm,omitempty"` // e.g., "x25519"
Fingerprint string `json:"fingerprint,omitempty"` // SHA256 fingerprint of public key
}
PKIInfo contains public key information for authenticated replies
type StreamParams ¶ added in v0.2.0
type StreamParams struct {
License string // User's license identifier
Fingerprint string // Device/session fingerprint
Cadence Cadence // Key rotation cadence (default: daily)
ChunkSize int // Optional: chunk size for decrypt-while-downloading (0 = no chunking)
}
StreamParams contains the parameters needed for stream key derivation
type Track ¶ added in v0.1.0
type Track struct {
Title string `json:"title"`
Start float64 `json:"start"` // start time in seconds
End float64 `json:"end,omitempty"` // end time in seconds (0 = until next track)
Type string `json:"type,omitempty"` // intro, verse, chorus, drop, outro, etc.
TrackNum int `json:"track_num,omitempty"` // track number for multi-track releases
}
Track represents a track marker in a release (like CD chapters)
type Variant ¶ added in v0.2.0
type Variant struct {
Name string `json:"name"` // Human-readable name: "1080p", "720p", etc.
Bandwidth int `json:"bandwidth"` // Required bandwidth in bits per second
Width int `json:"width"` // Video width in pixels
Height int `json:"height"` // Video height in pixels
Codecs string `json:"codecs"` // Codec string: "avc1.640028,mp4a.40.2"
URL string `json:"url"` // Relative path to .smsg file
ChunkCount int `json:"chunkCount"` // Number of chunks (for progress calculation)
FileSize int64 `json:"fileSize"` // File size in bytes
}
Variant represents a single quality level in an ABR stream. Each variant is a standard v3 chunked .smsg file.
type WrappedKey ¶ added in v0.2.0
type WrappedKey struct {
Date string `json:"date"` // ISO date "YYYY-MM-DD" for key derivation
Wrapped string `json:"wrapped"` // base64([nonce][ChaCha(CEK, streamKey)])
}
WrappedKey represents a CEK (Content Encryption Key) wrapped with a time-bound stream key. The stream key is derived from LTHN(date:license:fingerprint) and is never transmitted. Only the wrapped CEK (which includes its own nonce) is stored in the header.