Documentation
¶
Overview ¶
Package omemo implements the OMEMO 2 protocol (XEP-0384) as a transport- and storage-independent orchestration layer on top of a Signal-protocol backend. It builds and parses OMEMO protocol objects, manages device, session, bundle and trust state, and decides when cryptographic operations happen - it never implements cryptography itself.
Index ¶
- Constants
- Variables
- func InitIdentity(ctx context.Context, store Store, jid string, deviceID DeviceID, ...) error
- type Bundle
- type Device
- type DeviceError
- type DeviceID
- type DeviceList
- type DeviceStore
- type EncryptedMessage
- type IdentityStore
- type KeyExchange
- type Manager
- func (m *Manager) Bundle(ctx context.Context) (Bundle, error)
- func (m *Manager) DecryptMessage(ctx context.Context, msg *EncryptedMessage) ([]byte, error)
- func (m *Manager) EncryptKeyTransport(ctx context.Context, jid string) (*EncryptedMessage, []DeviceError, error)
- func (m *Manager) EncryptMessage(ctx context.Context, jid string, plaintext []byte) (*EncryptedMessage, []DeviceError, error)
- func (m *Manager) GenerateOneTimePreKeys(ctx context.Context, n int) error
- func (m *Manager) LocalDevice() Device
- func (m *Manager) NeedsMorePreKeys(ctx context.Context) (bool, error)
- func (m *Manager) PublishBundle(ctx context.Context) error
- func (m *Manager) ResetSession(ctx context.Context, dev Device) error
- func (m *Manager) RotateSignedPreKey(ctx context.Context) error
- func (m *Manager) SyncDevices(ctx context.Context, jid string) error
- type Option
- type PreKey
- type PreKeyRecord
- type PreKeyStore
- type Protocol
- type RecipientKey
- type SessionStore
- type SignedPreKey
- type SignedPreKeyRecord
- type Store
- type Transport
- type TrustResolver
- type TrustState
- type TrustStore
Constants ¶
const DefaultPreKeyCount = 100
DefaultPreKeyCount is how many one-time prekeys GenerateOneTimePreKeys generates when asked for the recommended pool size.
const MinPreKeyCount = 25
MinPreKeyCount is OMEMO's minimum bundle prekey count; NeedsMorePreKeys reports true once the stored pool drops to or below this.
Variables ¶
var ErrBlockedDevice = errors.New("omemo: device is blocked")
ErrBlockedDevice is returned for a recipient device explicitly marked TrustUntrusted; such a device is always skipped regardless of a resolver.
var ErrNoRecipients = errors.New("omemo: no recipient device could be encrypted for")
ErrNoRecipients is returned by EncryptMessage when every recipient device failed, so no message could be produced at all.
var ErrOwnDeviceKeyMissing = errors.New("omemo: message has no key for this device")
ErrOwnDeviceKeyMissing is returned by DecryptMessage when an incoming message carries no RecipientKey for the local device.
var ErrPreKeyNotFound = errors.New("omemo: one-time prekey not found (already consumed or unknown)")
ErrPreKeyNotFound is returned by DecryptMessage (via Store.ConsumePreKey) when an incoming PreKeyMessage names a one-time prekey ID we don't have - almost always because the sender built the session from a stale cached bundle referencing an ID we already consumed and deleted (one-time prekeys are exactly that: usable once). The session that message would have started can never be recovered, same as ErrUnknownSession - callers should treat the two the same way (heal by pushing a fresh session).
var ErrUnknownSession = errors.New("omemo: no session for sender device")
ErrUnknownSession is returned by DecryptMessage when an incoming message carries no KeyExchange but no session for its sender device exists.
var ErrUntrustedDevice = errors.New("omemo: device is untrusted")
ErrUntrustedDevice is returned for a recipient device whose identity key has no trust decision (TrustUndecided) and no TrustResolver is configured, or whose resolver declined it.
Functions ¶
func InitIdentity ¶
func InitIdentity(ctx context.Context, store Store, jid string, deviceID DeviceID, protocol Protocol) error
InitIdentity generates a fresh identity key pair and device ID and stores them, along with an initial signed prekey and one-time prekey pool. It MUST be called exactly once for a new Store, before NewManager, with the same protocol NewManager will later be called with.
Types ¶
type Bundle ¶
type Bundle struct {
Device Device
// IdentityKey is the device's public identity key: 32-byte Ed25519 for
// ProtocolV2, 32-byte Curve25519 for ProtocolV1.
IdentityKey []byte
SignedPreKey SignedPreKey
// PreKeys is the pool a bundle publisher offers; a consumer establishing
// a session picks exactly one and the publisher MUST NOT reuse it.
PreKeys []PreKey
}
Bundle is a device's published X3DH key material, as fetched from or published to whatever transport (e.g. XMPP PEP) the application provides.
type DeviceError ¶
DeviceError reports a per-recipient-device failure during a best-effort multi-device operation such as EncryptMessage.
func (*DeviceError) Error ¶
func (e *DeviceError) Error() string
func (*DeviceError) Unwrap ¶
func (e *DeviceError) Unwrap() error
type DeviceID ¶
type DeviceID uint32
DeviceID identifies a single OMEMO device belonging to some JID.
type DeviceList ¶
DeviceList is the set of active device IDs known for a JID.
type DeviceStore ¶
type DeviceStore interface {
Devices(ctx context.Context, jid string) ([]DeviceID, error)
SetDevices(ctx context.Context, jid string, devices []DeviceID) error
RemoteIdentityKey(ctx context.Context, dev Device) (key []byte, ok bool, err error)
PutRemoteIdentityKey(ctx context.Context, dev Device, key []byte) error
}
DeviceStore persists known device lists (own and contacts') and the identity key last seen for each remote device.
type EncryptedMessage ¶
type EncryptedMessage struct {
Sender Device
Keys []RecipientKey
Payload []byte
// IV is the payload's initialization vector. It is only set for
// ProtocolV1 messages carrying a Payload: legacy OMEMO's payload cipher
// (unlike ProtocolV2's) uses an explicit, non-secret IV sent in the
// clear rather than one derived from key material inside the ratchet.
IV []byte
}
EncryptedMessage is a complete OMEMO envelope: one shared payload ciphertext (encrypted once) plus one wrapped key per recipient device.
Payload is nil for a key-transport message, which exists only to establish or refresh sessions and carries no message body.
type IdentityStore ¶
type IdentityStore interface {
IdentityKeyPair(ctx context.Context) ([]byte, error)
SetIdentityKeyPair(ctx context.Context, priv []byte) error
LocalDevice(ctx context.Context) (Device, error)
SetLocalDevice(ctx context.Context, dev Device) error
}
IdentityStore persists this device's own long-term identity.
The private key's format depends on the protocol a given Store instance is scoped to (a Manager is scoped to exactly one Protocol - see NewManager): a 64-byte Ed25519 seed+public key (crypto/ed25519.PrivateKey's format) for ProtocolV2, or a 32-byte raw Curve25519 scalar for ProtocolV1.
type KeyExchange ¶
type KeyExchange struct {
// IdentityKey is the sender's public identity key: Ed25519 for
// ProtocolV2, Curve25519 for ProtocolV1.
IdentityKey []byte
EphemeralKey []byte
SignedPreKeyID uint32
PreKeyID uint32
}
KeyExchange carries the X3DH parameters needed to establish a new session. It is present on a RecipientKey only for the first message sent to a device under a given session (an OMEMOKeyExchange in XEP-0384 terms).
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager is the high-level OMEMO protocol state machine. It owns device, session, bundle and trust state, and is the only type applications need to call: X3DH and the Double Ratchet are never invoked directly by callers.
A Manager is safe for concurrent use. Session state is guarded per remote device, so encrypting or decrypting for different devices proceeds independently.
func NewManager ¶
func NewManager(ctx context.Context, store Store, transport Transport, protocol Protocol, opts ...Option) (*Manager, error)
NewManager loads this device's identity from store and returns a ready Manager for protocol. Call InitIdentity first if this is a fresh Store.
protocol MUST match whatever protocol InitIdentity was called with for store - a Store is expected to be scoped to exactly one protocol (an account speaking both maintains two separate Store instances, one per protocol, each its own device identity and prekey pool).
func (*Manager) Bundle ¶
Bundle builds this device's currently published bundle from the identity and prekey state held in the Store, for publishing via PublishBundle.
func (*Manager) DecryptMessage ¶
DecryptMessage decrypts an incoming EncryptedMessage. If msg carries a KeyExchange for this device, a new session is established as the responding party; otherwise an existing session is required.
The returned plaintext is nil for a key-transport message.
func (*Manager) EncryptKeyTransport ¶
func (m *Manager) EncryptKeyTransport(ctx context.Context, jid string) (*EncryptedMessage, []DeviceError, error)
EncryptKeyTransport builds a key-transport message: it establishes or refreshes sessions with every known device of jid (and the local account's other devices), carrying no message body. Applications typically send this proactively to warm sessions ahead of an actual message, or to recover from a broken session.
func (*Manager) EncryptMessage ¶
func (m *Manager) EncryptMessage(ctx context.Context, jid string, plaintext []byte) (*EncryptedMessage, []DeviceError, error)
EncryptMessage encrypts plaintext for every known device of jid, plus every other known device of the local account (so the sender's own other clients, e.g. a phone, can decrypt messages sent from this device), creating sessions as needed. It is best-effort: a failure for one recipient device does not prevent encrypting for the others. The returned error is non-nil only if no recipient could be encrypted for at all; per-device failures are always reported in the returned slice.
func (*Manager) GenerateOneTimePreKeys ¶
GenerateOneTimePreKeys generates n new one-time prekeys and adds them to the pool.
func (*Manager) LocalDevice ¶
LocalDevice returns this Manager's own device identity.
func (*Manager) NeedsMorePreKeys ¶
NeedsMorePreKeys reports whether the one-time prekey pool has dropped to or below OMEMO's minimum bundle size and should be topped up (followed by PublishBundle - note the bundle itself only ever advertises the pool the Store holds; the caller's Transport.PublishBundle is expected to publish the full current pool, not just what Bundle returns).
func (*Manager) PublishBundle ¶
PublishBundle publishes this device's current bundle via Transport.
func (*Manager) ResetSession ¶ added in v0.1.2
ResetSession discards any session (good or broken) held with dev, so the next message to or from it forces a brand-new session via a fresh X3DH handshake against dev's currently-published bundle, instead of continuing to use a session that decrypt/encrypt calls have started failing against. A session going bad on our end (e.g. our own session storage was wiped) isn't something the far side can detect on its own - it keeps sending ordinary ratchet messages against a session we no longer recognize, which DecryptMessage can only ever fail with ErrUnknownSession, forever, until something forces a rebuild. Callers should follow this with EncryptKeyTransport to dev's JID, so the far side also picks up the new session instead of continuing to encrypt against the one just discarded.
func (*Manager) RotateSignedPreKey ¶
RotateSignedPreKey generates a new signed prekey, demoting the current one to stale for its rotation grace period. OMEMO recommends doing this every one to four weeks; the caller decides the schedule.
type Option ¶
type Option func(*Manager)
Option configures optional Manager behavior.
func WithTrustResolver ¶
func WithTrustResolver(r TrustResolver) Option
WithTrustResolver installs a callback consulted for recipient devices whose identity key has no trust decision yet. Without one, encrypting to such a device fails with ErrUntrustedDevice.
type PreKey ¶
PreKey is the public half of a single one-time prekey offered in a bundle. OMEMO demands a bundle carry at least 25, recommended around 100.
type PreKeyRecord ¶
PreKeyRecord is a one-time prekey together with its private half, as held by the device that published it, before it is consumed by a peer.
type PreKeyStore ¶
type PreKeyStore interface {
CurrentSignedPreKey(ctx context.Context) (SignedPreKeyRecord, error)
// StaleSignedPreKey returns the previous signed prekey, if one is still
// being kept around during its rotation grace period, and ok=false
// otherwise.
StaleSignedPreKey(ctx context.Context) (rec SignedPreKeyRecord, ok bool, err error)
// RotateSignedPreKey stores next as the current signed prekey, demoting
// the previous current one to stale.
RotateSignedPreKey(ctx context.Context, next SignedPreKeyRecord) error
PreKeyCount(ctx context.Context) (int, error)
// PreKeys lists the currently unconsumed one-time prekey pool, for
// publishing a bundle. It does not consume anything.
PreKeys(ctx context.Context) ([]PreKeyRecord, error)
// NextPreKeyID atomically allocates and returns the next unused one-time
// prekey ID. It MUST never repeat an ID, including ones already consumed
// and no longer present in the pool - the live count alone cannot be
// used to derive this, since consumed prekeys leave no trace behind.
NextPreKeyID(ctx context.Context) (uint32, error)
PutPreKeys(ctx context.Context, recs []PreKeyRecord) error
// ConsumePreKey atomically fetches and deletes a one-time prekey by ID.
// It MUST fail if the ID is unknown or was already consumed, since OMEMO
// forbids reusing a one-time prekey.
ConsumePreKey(ctx context.Context, id uint32) (PreKeyRecord, error)
}
PreKeyStore persists this device's own signed prekey (plus a stale one kept during its rotation grace period) and one-time prekey pool.
type Protocol ¶ added in v0.1.0
type Protocol int
Protocol distinguishes the two OMEMO wire protocols this library can speak. They share the Double Ratchet and X3DH DH-chain machinery but differ in identity key type, payload cipher, and wire format - see internal/signal for how each is dispatched.
type RecipientKey ¶
type RecipientKey struct {
Device DeviceID
Data []byte
// KeyExchange is set when this key was produced while establishing a new
// session, and must accompany Data so the recipient can complete X3DH.
KeyExchange *KeyExchange
}
RecipientKey is one recipient device's wrapped copy of a message's payload key, encrypted through that device's own Double Ratchet session.
type SessionStore ¶
type SessionStore interface {
Session(ctx context.Context, dev Device) (data []byte, ok bool, err error)
PutSession(ctx context.Context, dev Device, data []byte) error
DeleteSession(ctx context.Context, dev Device) error
}
SessionStore persists per-device Double Ratchet session state as an opaque blob (see internal/signal.Session.Marshal).
type SignedPreKey ¶
SignedPreKey is the public half of a bundle's signed prekey, rotated periodically (OMEMO recommends every one to four weeks).
type SignedPreKeyRecord ¶
SignedPreKeyRecord is a signed prekey together with its private half, as held by the device that published it.
type Store ¶
type Store interface {
IdentityStore
PreKeyStore
SessionStore
TrustStore
DeviceStore
}
Store aggregates all persistence this library needs. Implementations may back it with SQLite, Postgres, Badger, BoltDB, memory, or anything else.
type Transport ¶
type Transport interface {
FetchDeviceList(ctx context.Context, jid string) (DeviceList, error)
PublishDeviceList(ctx context.Context, list DeviceList) error
FetchBundle(ctx context.Context, dev Device) (Bundle, error)
PublishBundle(ctx context.Context, bundle Bundle) error
}
Transport is everything this library needs a concrete XMPP (or other) backend to provide network access for. It never sends or receives actual chat messages: an EncryptedMessage produced by Manager.EncryptMessage is the application's own responsibility to deliver via its normal message-sending path, and an incoming one is handed to Manager.DecryptMessage the same way. Transport exists only for the out-of-band bundle and device-list exchange (e.g. XMPP PEP) that OMEMO's own state machine initiates on its own schedule.
type TrustResolver ¶
TrustResolver is consulted for a recipient device in TrustUndecided state. Returning nil trusts the device for this call and persists that decision; returning an error skips the device for this call without persisting anything, leaving it TrustUndecided for future calls.
type TrustState ¶
type TrustState int
TrustState describes the trust decision associated with a remote device's identity key.
const ( // TrustUndecided means no trust decision has been made yet. Encrypting to // such a device is refused unless a TrustResolver is configured. TrustUndecided TrustState = iota // TrustTrusted means the identity key has been verified or accepted. TrustTrusted // TrustUntrusted means the identity key has been explicitly rejected // (e.g. it changed unexpectedly). Such a device is always skipped. TrustUntrusted )
func (TrustState) String ¶
func (s TrustState) String() string
type TrustStore ¶
type TrustStore interface {
Trust(ctx context.Context, identityKey []byte) (TrustState, error)
SetTrust(ctx context.Context, identityKey []byte, state TrustState) error
}
TrustStore persists trust decisions. Trust is bound to an identity key (its fingerprint) rather than a Device, since that key - not the device ID - is OMEMO's actual security anchor. identityKey bytes differ by protocol (see IdentityStore) but never collide across protocols since Ed25519 and Curve25519 keys occupy the same 32-byte space with effectively disjoint values, and callers additionally scope Store instances per protocol.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
signal
Package signal isolates every call into the xochimilco Signal-protocol backend (X3DH + Double Ratchet) behind a small, OMEMO-shaped seam.
|
Package signal isolates every call into the xochimilco Signal-protocol backend (X3DH + Double Ratchet) behind a small, OMEMO-shaped seam. |
|
Package memstore is a plain in-memory omemo.Store, useful for tests and as a reference for implementing the interface against a real database.
|
Package memstore is a plain in-memory omemo.Store, useful for tests and as a reference for implementing the interface against a real database. |