smsg

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 3, 2026 License: EUPL-1.2 Imports: 18 Imported by: 0

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

View Source
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

View Source
const (
	CompressionNone = ""     // No compression (default, backwards compatible)
	CompressionGzip = "gzip" // Gzip compression (stdlib, WASM compatible)
	CompressionZstd = "zstd" // Zstandard compression (faster, better ratio)
)

Compression types

View Source
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

View Source
const ABRSafetyFactor = 0.8

ABRSafetyFactor is the bandwidth multiplier for variant selection. Using 80% of available bandwidth prevents buffering on fluctuating networks.

View Source
const ABRVersion = "abr-v1"
View Source
const DefaultChunkSize = 1024 * 1024

Default chunk size for v3 chunked format (1MB)

View Source
const Magic = "SMSG"

Magic bytes for SMSG format

View Source
const Version = "1.0"

Version of the SMSG format

Variables

View Source
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

View Source
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 DeriveKey

func DeriveKey(password string) []byte

DeriveKey derives a 32-byte key from a password using SHA-256.

func DeriveStreamKey added in v0.2.0

func DeriveStreamKey(date, license, fingerprint string) []byte

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

func Encrypt(msg *Message, password string) ([]byte, error)

Encrypt encrypts a message with a password. Returns the encrypted SMSG container bytes.

func EncryptBase64

func EncryptBase64(msg *Message, password string) (string, error)

EncryptBase64 encrypts and returns base64-encoded result

func EncryptV2 added in v0.1.0

func EncryptV2(msg *Message, password string) ([]byte, error)

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

func EncryptV2WithManifest(msg *Message, password string, manifest *Manifest) ([]byte, error)

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

func EncryptWithHint(msg *Message, password, hint string) ([]byte, error)

EncryptWithHint encrypts with an optional password hint in the header

func EncryptWithManifest added in v0.1.0

func EncryptWithManifest(msg *Message, password string, manifest *Manifest) ([]byte, error)

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

func EncryptWithManifestBase64(msg *Message, password string, manifest *Manifest) (string, error)

EncryptWithManifestBase64 encrypts with manifest and returns base64

func GenerateCEK added in v0.2.0

func GenerateCEK() ([]byte, error)

GenerateCEK generates a random 32-byte Content Encryption Key

func GetCadenceWindowDuration added in v0.2.0

func GetCadenceWindowDuration(cadence Cadence) time.Duration

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

func GetRollingDatesAt(t time.Time) (current, next string)

GetRollingDatesAt returns today and tomorrow relative to a specific time

func GetRollingPeriods added in v0.2.0

func GetRollingPeriods(cadence Cadence, t time.Time) (current, next string)

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

func GetV3Payload(data []byte) ([]byte, error)

GetV3Payload extracts just the payload from a v3 file. Use with DecryptV3Chunk for individual chunk decryption.

func QuickDecrypt

func QuickDecrypt(encoded, password string) (string, error)

QuickDecrypt is a convenience function for simple message decryption

func QuickEncrypt

func QuickEncrypt(body, password string) (string, error)

QuickEncrypt is a convenience function for simple message encryption

func UnwrapCEK added in v0.2.0

func UnwrapCEK(wrappedB64 string, streamKey []byte) ([]byte, error)

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 Validate

func Validate(data []byte) error

Validate checks if data is a valid SMSG container (without decrypting)

func WrapCEK added in v0.2.0

func WrapCEK(cek, streamKey []byte) (string, error)

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 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 GetInfo

func GetInfo(data []byte) (*Header, error)

GetInfo extracts header info without decrypting

func GetInfoBase64

func GetInfoBase64(encoded string) (*Header, error)

GetInfoBase64 extracts header info from base64-encoded SMSG

func GetV3Header added in v0.2.0

func GetV3Header(data []byte) (*Header, error)

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

func GetV3HeaderFromPrefix(data []byte) (*Header, int, error)

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

func NewManifest(title string) *Manifest

NewManifest creates a new manifest with title

func (m *Manifest) AddLink(platform, url string) *Manifest

AddLink adds an artist link (platform -> URL)

func (*Manifest) AddTrack added in v0.1.0

func (m *Manifest) AddTrack(title string, start float64) *Manifest

AddTrack adds a track marker to the manifest

func (*Manifest) AddTrackFull added in v0.1.0

func (m *Manifest) AddTrackFull(title string, start, end float64, trackType string) *Manifest

AddTrackFull adds a track with all details

func (*Manifest) IsExpired added in v0.1.0

func (m *Manifest) IsExpired() bool

IsExpired checks if the license has expired

func (*Manifest) TimeRemaining added in v0.1.0

func (m *Manifest) TimeRemaining() int64

TimeRemaining returns seconds until expiration (0 if perpetual, negative if expired)

func (*Manifest) WithExpiration added in v0.1.0

func (m *Manifest) WithExpiration(expiresAt int64) *Manifest

WithExpiration sets the license expiration time

func (*Manifest) WithPreviewAccess added in v0.1.0

func (m *Manifest) WithPreviewAccess(seconds int) *Manifest

WithPreviewAccess sets up for preview (very short, e.g., 30 seconds)

func (*Manifest) WithRentalDuration added in v0.1.0

func (m *Manifest) WithRentalDuration(durationSeconds int64) *Manifest

WithRentalDuration sets expiration relative to issue time

func (*Manifest) WithStreamingAccess added in v0.1.0

func (m *Manifest) WithStreamingAccess(hours int) *Manifest

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

func Decrypt(data []byte, password string) (*Message, error)

Decrypt decrypts an SMSG container with a password Automatically handles both v1 (base64) and v2 (binary) formats

func DecryptBase64

func DecryptBase64(encoded, password string) (*Message, error)

DecryptBase64 decrypts a base64-encoded SMSG

func NewMessage

func NewMessage(body string) *Message

NewMessage creates a new message with the given body

func (*Message) AddAttachment

func (m *Message) AddAttachment(name, content, mimeType string) *Message

AddAttachment adds a file attachment (content is base64-encoded)

func (*Message) AddBinaryAttachment added in v0.1.0

func (m *Message) AddBinaryAttachment(name string, data []byte, mimeType string) *Message

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) SetMeta

func (m *Message) SetMeta(key, value string) *Message

SetMeta sets a metadata value

func (*Message) WithFrom

func (m *Message) WithFrom(from string) *Message

WithFrom sets the sender

func (*Message) WithReplyKey

func (m *Message) WithReplyKey(publicKeyB64 string) *Message

WithReplyKey sets the PKI public key for authenticated replies

func (*Message) WithReplyKeyInfo

func (m *Message) WithReplyKeyInfo(pki *PKIInfo) *Message

WithReplyKeyInfo sets full PKI information

func (*Message) WithSubject

func (m *Message) WithSubject(subject string) *Message

WithSubject sets the message subject

func (*Message) WithTimestamp

func (m *Message) WithTimestamp(ts int64) *Message

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.

func VariantFromSMSG added in v0.2.0

func VariantFromSMSG(name string, bandwidth, width, height int, smsgPath string) (*Variant, error)

VariantFromSMSG creates a Variant from an existing .smsg file. It reads the header to extract chunk count and file size.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL