Documentation
¶
Overview ¶
Package thin provides a lightweight client API for the Katzenpost mixnet.
Overview ¶
The thin client package implements a client-daemon architecture where the thin client communicates with a separate client daemon process that handles the heavy cryptographic operations and mixnet protocol details. This design allows applications to integrate with Katzenpost without implementing the full complexity of the mixnet protocols.
Architecture ¶
The thin client connects to a client daemon via TCP or Unix domain sockets. The daemon handles:
- Sphinx packet creation and processing
- PKI document management and validation
- Mixnet routing and timing
- Cryptographic operations (encryption, decryption, signatures)
- Connection management to the mixnet
The thin client provides a simple API for:
- Sending and receiving messages
- Handling events and status updates
APIs ¶
This package provides two main APIs:
## Legacy API (deprecated for new projects)
The legacy API provides basic message sending functionality:
- SendMessage: Send a message with optional reply capability
- SendMessageWithoutReply: Send a fire-and-forget message
- BlockingSendMessage: Send a message and wait for reply
Note: ARQ (Automatic Repeat reQuest) is now used exclusively for the new Pigeonhole API.
## Pigeonhole Channel API
For more information about this API please see our API documentation, here: https://katzenpost.network/docs/client_integration/#pigeonhole-channel-api
The Pigeonhole protocol provides the following messages and their corresponding replies/events:
- NewKeypair
- EncryptRead
- EncryptWrite
- StartResendingEncryptedMessage
- CancelResendingEncryptedMessage
- StartResendingCopyCommand
- CancelResendingCopyCommand
- NextMessageBoxIndex
- CreateCourierEnvelopesFromPayload
- CreateCourierEnvelopesFromPayloads
- SetStreamBuffer
Configuration ¶
The thin client requires configuration specifying:
- Network and address of the client daemon
- Sphinx geometry parameters
- Pigeonhole geometry parameters
See the testdata/thinclient.toml file for an example configuration.
Index ¶
- Constants
- Variables
- func IsExpectedOutcome(err error) bool
- func ThinClientErrorToString(errorCode uint8) string
- type CancelResendingCopyCommand
- type CancelResendingCopyCommandReply
- type CancelResendingEncryptedMessage
- type CancelResendingEncryptedMessageReply
- type Config
- type ConnectionStatusEvent
- type CourierDescriptor
- type CreateCourierEnvelopesFromPayload
- type CreateCourierEnvelopesFromPayloadReply
- type CreateCourierEnvelopesFromPayloads
- type CreateCourierEnvelopesFromPayloadsReply
- type CreateCourierEnvelopesFromTombstoneRange
- type CreateCourierEnvelopesFromTombstoneRangeReply
- type CreateEnvelopesResult
- type DaemonDisconnectedEvent
- type DestinationPayload
- type EncryptRead
- type EncryptReadReply
- type EncryptWrite
- type EncryptWriteReply
- type Event
- type GetMessageBoxIndexCounter
- type GetMessageBoxIndexCounterReply
- type MessageIDGarbageCollected
- type MessageReplyEvent
- type MessageSentEvent
- type NewDocumentEvent
- type NewKeypair
- type NewKeypairReply
- type NewPKIDocumentEvent
- type NextMessageBoxIndex
- type NextMessageBoxIndexReply
- type Request
- type Response
- type SendChannelQuery
- type SendMessage
- type SessionToken
- type SessionTokenReply
- type ShutdownEvent
- type StartResendingCopyCommand
- type StartResendingCopyCommandReply
- type StartResendingEncryptedMessage
- type StartResendingEncryptedMessageReply
- type StartResendingResult
- type ThinClient
- func (t *ThinClient) BlockingSendMessage(ctx context.Context, payload []byte, destNode *[32]byte, destQueue []byte) ([]byte, error)
- func (t *ThinClient) CancelResendingCopyCommand(writeCapHash *[32]byte) error
- func (t *ThinClient) CancelResendingEncryptedMessage(envelopeHash *[32]byte) error
- func (t *ThinClient) Close() error
- func (t *ThinClient) CreateCourierEnvelopesFromMultiPayload(destinations []DestinationPayload, isStart bool, isLast bool, buffer []byte) (*CreateEnvelopesResult, error)
- func (t *ThinClient) CreateCourierEnvelopesFromPayload(payload []byte, destWriteCap *bacap.WriteCap, ...) (envelopes [][]byte, nextDestIndex *bacap.MessageBoxIndex, err error)
- func (t *ThinClient) CreateCourierEnvelopesFromTombstoneRange(destWriteCap *bacap.WriteCap, destStartIndex *bacap.MessageBoxIndex, ...) (envelopes [][]byte, nextBuffer []byte, nextDestIndex *bacap.MessageBoxIndex, ...)
- func (t *ThinClient) Dial() error
- func (t *ThinClient) Disconnect() error
- func (t *ThinClient) EncryptRead(readCap *bacap.ReadCap, messageBoxIndex *bacap.MessageBoxIndex) (messageCiphertext []byte, envelopeDescriptor []byte, envelopeHash *[32]byte, ...)
- func (t *ThinClient) EncryptWrite(plaintext []byte, writeCap *bacap.WriteCap, ...) (messageCiphertext []byte, envelopeDescriptor []byte, envelopeHash *[32]byte, ...)
- func (t *ThinClient) EventSink() chan Event
- func (t *ThinClient) GetAllCouriers() (couriers []CourierDescriptor, err error)
- func (t *ThinClient) GetConfig() *Config
- func (t *ThinClient) GetDistinctCouriers(n int) (couriers []CourierDescriptor, err error)
- func (t *ThinClient) GetLogger(prefix string) *logging.Logger
- func (t *ThinClient) GetMessageBoxIndexCounter(messageBoxIndex *bacap.MessageBoxIndex) (uint64, error)
- func (t *ThinClient) GetService(serviceName string) (*common.ServiceDescriptor, error)
- func (t *ThinClient) GetServices(capability string) ([]*common.ServiceDescriptor, error)
- func (t *ThinClient) IsConnected() bool
- func (t *ThinClient) NewKeypair(seed []byte) (writeCap *bacap.WriteCap, readCap *bacap.ReadCap, ...)
- func (t *ThinClient) NewMessageID() *[MessageIDLength]byte
- func (t *ThinClient) NewQueryID() *[QueryIDLength]byte
- func (t *ThinClient) NewSURBID() *[sConstants.SURBIDLength]byte
- func (t *ThinClient) NextMessageBoxIndex(messageBoxIndex *bacap.MessageBoxIndex) (nextMessageBoxIndex *bacap.MessageBoxIndex, err error)
- func (t *ThinClient) PKIDocument() *cpki.Document
- func (t *ThinClient) PKIDocumentForEpoch(epoch uint64) (*cpki.Document, error)
- func (t *ThinClient) SendMessage(surbID *[sConstants.SURBIDLength]byte, payload []byte, destNode *[32]byte, ...) error
- func (t *ThinClient) SendMessageWithoutReply(payload []byte, destNode *[32]byte, destQueue []byte) error
- func (t *ThinClient) Shutdown()
- func (t *ThinClient) StartResendingCopyCommand(writeCap *bacap.WriteCap) error
- func (t *ThinClient) StartResendingCopyCommandWithCourier(writeCap *bacap.WriteCap, courierIdentityHash *[32]byte, courierQueueID []byte) error
- func (t *ThinClient) StartResendingEncryptedMessage(readCap *bacap.ReadCap, writeCap *bacap.WriteCap, messageBoxIndex []byte, ...) (*StartResendingResult, error)
- func (t *ThinClient) StartResendingEncryptedMessageNoRetry(readCap *bacap.ReadCap, writeCap *bacap.WriteCap, messageBoxIndex []byte, ...) (*StartResendingResult, error)
- func (t *ThinClient) StartResendingEncryptedMessageReturnBoxExists(readCap *bacap.ReadCap, writeCap *bacap.WriteCap, messageBoxIndex []byte, ...) (*StartResendingResult, error)
- func (t *ThinClient) StopEventSink(ch chan Event)
- func (c *ThinClient) TombstoneRange(writeCap *bacap.WriteCap, start *bacap.MessageBoxIndex, maxCount uint32) (result *TombstoneRangeResult, err error)
- type ThinClose
- type ThinResponse
- type TombstoneEnvelope
- type TombstoneRangeResult
Constants ¶
const ( // MessageIDLength is the length of a message ID in bytes. MessageIDLength = 16 // QueryIDLength is the length of a query ID in bytes. QueryIDLength = 16 )
const ( // ThinClientSuccess indicates that the operation completed successfully // with no errors. This is the default success state. ThinClientSuccess uint8 = 0 // ThinClientErrorConnectionLost indicates that the connection to the daemon // was lost during the operation. The client should attempt to reconnect. ThinClientErrorConnectionLost uint8 = 1 // ThinClientErrorTimeout indicates that the operation timed out before // completion. This may occur during network operations or when waiting // for responses from the mixnet. ThinClientErrorTimeout uint8 = 2 // ThinClientErrorInvalidRequest indicates that the request format was // invalid or contained malformed data that could not be processed. ThinClientErrorInvalidRequest uint8 = 3 // ThinClientErrorInternalError indicates an internal error occurred within // the client daemon or thin client that prevented operation completion. ThinClientErrorInternalError uint8 = 4 // ThinClientErrorMaxRetries indicates that the maximum number of retry // attempts was exceeded for a reliable operation (such as ARQ). ThinClientErrorMaxRetries uint8 = 5 // ThinClientErrorInvalidChannel indicates that the specified channel ID // is invalid or malformed. ThinClientErrorInvalidChannel uint8 = 6 // ThinClientErrorChannelNotFound indicates that the specified channel // does not exist or has been garbage collected. ThinClientErrorChannelNotFound uint8 = 7 // ThinClientErrorPermissionDenied indicates that the operation was denied // due to insufficient permissions or capability restrictions. ThinClientErrorPermissionDenied uint8 = 8 // ThinClientErrorInvalidPayload indicates that the message payload was // invalid, too large, or otherwise could not be processed. ThinClientErrorInvalidPayload uint8 = 9 // or functionality is currently unavailable. ThinClientErrorServiceUnavailable uint8 = 10 // ThinClientErrorDuplicateCapability indicates that the provided capability // (read or write cap) has already been used and is considered a duplicate. ThinClientErrorDuplicateCapability uint8 = 11 // ThinClientErrorCourierCacheCorruption indicates that the courier's cache // has detected corruption. ThinClientErrorCourierCacheCorruption uint8 = 12 // ThinClientPropagationError indicates that the request could not be // propagated to replicas. ThinClientPropagationError uint8 = 13 // ThinClientErrorInvalidWriteCapability indicates that the provided write // capability is invalid. ThinClientErrorInvalidWriteCapability uint8 = 14 // ThinClientErrorInvalidReadCapability indicates that the provided read // capability is invalid. ThinClientErrorInvalidReadCapability uint8 = 15 // ThinClientErrorInvalidResumeWriteChannelRequest indicates that the provided // ResumeWriteChannel request is invalid. ThinClientErrorInvalidResumeWriteChannelRequest uint8 = 16 // ThinClientErrorInvalidResumeReadChannelRequest indicates that the provided // ResumeReadChannel request is invalid. ThinClientErrorInvalidResumeReadChannelRequest uint8 = 17 // ThinClientImpossibleHashError indicates that the provided hash is impossible // to compute, such as when the hash of a write capability is provided but // the write capability itself is not provided. ThinClientImpossibleHashError uint8 = 18 // ThinClientImpossibleNewWriteCapError indicates that the daemon was unable // to create a new write capability. ThinClientImpossibleNewWriteCapError uint8 = 19 // ThinClientImpossibleNewStatefulWriterError indicates that the daemon was unable // to create a new stateful writer. ThinClientImpossibleNewStatefulWriterError uint8 = 20 // ThinClientCapabilityAlreadyInUse indicates that the provided capability // is already in use. ThinClientCapabilityAlreadyInUse uint8 = 21 // ThinClientErrorMKEMDecryptionFailed indicates that MKEM decryption failed. // This occurs when the MKEM envelope cannot be decrypted with any of the replica keys. ThinClientErrorMKEMDecryptionFailed uint8 = 22 // ThinClientErrorBACAPDecryptionFailed indicates that BACAP decryption failed. // This occurs when the BACAP payload cannot be decrypted or signature verification fails. ThinClientErrorBACAPDecryptionFailed uint8 = 23 // ThinClientErrorStartResendingCancelled indicates that a StartResendingEncryptedMessage // operation was cancelled via CancelResendingEncryptedMessage before completion. ThinClientErrorStartResendingCancelled uint8 = 24 // ThinClientErrorInvalidTombstoneSig indicates that a replica claimed a box is // tombstoned but the signature verification failed. This means the tombstone is // forged or corrupted. ThinClientErrorInvalidTombstoneSig uint8 = 25 // ThinClientErrorCopyCommandFailed indicates that the courier reported // CopyStatusFailed for a StartResendingCopyCommand operation. This is // a thin-client-namespace signal: the daemon sets it when it observes a // terminal CopyStatusFailed reply from the courier, independent of any // replica error code. The ancillary ReplicaErrorCode / FailedEnvelopeIndex // fields on the reply (if present) provide diagnostic detail, but the // sentinel mapping is driven entirely by this code. ThinClientErrorCopyCommandFailed uint8 = 26 )
Thin client error codes provide standardized error reporting across the protocol. These codes are used in response messages to indicate the success or failure of operations, allowing applications to handle errors consistently.
Variables ¶
var ( // ErrBoxIDNotFound indicates that the requested box ID was not found on the replica. // This typically occurs when attempting to read from a non-existent mailbox. ErrBoxIDNotFound = errors.New("box ID not found") // ErrInvalidBoxID indicates that the box ID format is invalid. ErrInvalidBoxID = errors.New("invalid box ID") // ErrInvalidSignature indicates that the signature verification failed. ErrInvalidSignature = errors.New("invalid signature") // ErrDatabaseFailure indicates that the replica encountered a database error. ErrDatabaseFailure = errors.New("database failure") // ErrInvalidPayload indicates that the payload data is invalid. ErrInvalidPayload = errors.New("invalid payload") // ErrStorageFull indicates that the replica's storage capacity has been exceeded. ErrStorageFull = errors.New("storage full") // ErrReplicaInternalError indicates an internal error on the replica. ErrReplicaInternalError = errors.New("replica internal error") // ErrInvalidEpoch indicates that the epoch is invalid or expired. ErrInvalidEpoch = errors.New("invalid epoch") // ErrReplicationFailed indicates that replication to other replicas failed. ErrReplicationFailed = errors.New("replication failed") // ErrBoxAlreadyExists indicates that the box already contains data. // Pigeonhole writes are immutable - once a box has been written, it cannot be overwritten. ErrBoxAlreadyExists = errors.New("box already exists") // ErrInvalidEnvelope indicates that the courier envelope format is invalid. ErrInvalidEnvelope = errors.New("invalid envelope") // ErrCacheCorruption indicates that cache data corruption was detected. ErrCacheCorruption = errors.New("cache corruption") // ErrPropagationError indicates an error propagating the request to replicas. ErrPropagationError = errors.New("propagation error") // ErrInternalError indicates an internal client error. ErrInternalError = errors.New("internal error") // ErrMKEMDecryptionFailed indicates that MKEM decryption failed. // This occurs when the MKEM envelope cannot be decrypted with any of the replica keys. ErrMKEMDecryptionFailed = errors.New("MKEM decryption failed") // ErrBACAPDecryptionFailed indicates that BACAP decryption failed. // This occurs when the BACAP payload cannot be decrypted or signature verification fails. ErrBACAPDecryptionFailed = errors.New("BACAP decryption failed") // ErrStartResendingCancelled indicates that a StartResendingEncryptedMessage // operation was cancelled via CancelResendingEncryptedMessage before completion. ErrStartResendingCancelled = errors.New("start resending cancelled") // ErrTombstone indicates that the read operation found a tombstone. // The box was intentionally deleted by the writer. This is not a failure. ErrTombstone = errors.New("tombstone") // ErrInvalidTombstoneSignature indicates that a replica claimed a box is // tombstoned but the BACAP signature verification failed. ErrInvalidTombstoneSignature = errors.New("invalid tombstone signature") // ErrCopyCommandFailed indicates that the courier reported CopyStatusFailed // for a StartResendingCopyCommand operation. The ancillary ReplicaErrorCode // and FailedEnvelopeIndex fields on StartResendingCopyCommandReply carry // diagnostic detail when present. ErrCopyCommandFailed = errors.New("copy command failed") )
var ErrInvalidThinConfig = errors.New("thin: invalid config")
ErrInvalidThinConfig is returned by LoadFile when the thin-client TOML has structural problems: missing required sections, unknown keys, or a malformed Dial discriminator. Callers can match with errors.Is to distinguish config drift from transient IO errors.
Functions ¶
func IsExpectedOutcome ¶
IsExpectedOutcome returns true for error codes that represent completed operations rather than failures. These errors should not trigger retries.
func ThinClientErrorToString ¶
ThinClientErrorToString converts a thin client error code to a human-readable string. This function provides consistent error message formatting across the thin client protocol and is used for logging and error reporting.
Parameters:
- errorCode: The error code to convert
Returns:
- string: A human-readable description of the error
Types ¶
type CancelResendingCopyCommand ¶
type CancelResendingCopyCommand struct {
// QueryID is used for correlating this thin client request with the
// thin client response.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// WriteCapHash is the hash of the WriteCap used in StartResendingCopyCommand.
WriteCapHash *[32]byte `cbor:"write_cap_hash"`
}
CancelResendingCopyCommand requests the daemon to cancel resending a copy command.
type CancelResendingCopyCommandReply ¶
type CancelResendingCopyCommandReply struct {
// QueryID is used for correlating this reply with the CancelResendingCopyCommand request
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// ErrorCode indicates the reason for a failure to cancel the copy command if any.
// Otherwise it is set to zero for success.
ErrorCode uint8 `cbor:"error_code"`
}
CancelResendingCopyCommandReply is the reply to a CancelResendingCopyCommand request.
func (*CancelResendingCopyCommandReply) String ¶
func (e *CancelResendingCopyCommandReply) String() string
String returns a string representation of the CancelResendingCopyCommandReply.
type CancelResendingEncryptedMessage ¶
type CancelResendingEncryptedMessage struct {
// QueryID is used for correlating this thin client request with the
// thin client response.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// EnvelopeHash is the hash of the CourierEnvelope to cancel resending.
EnvelopeHash *[32]byte `cbor:"envelope_hash"`
}
CancelResendingEncryptedMessage requests the daemon to cancel resending an encrypted message.
type CancelResendingEncryptedMessageReply ¶
type CancelResendingEncryptedMessageReply struct {
// QueryID is used for correlating this reply with the CancelResendingEncryptedMessage request
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// ErrorCode indicates the reason for a failure to cancel resending the encrypted message if any.
// Otherwise it is set to zero for success.
ErrorCode uint8 `cbor:"error_code"`
}
CancelResendingEncryptedMessageReply is the reply to a CancelResendingEncryptedMessage request.
func (*CancelResendingEncryptedMessageReply) String ¶
func (e *CancelResendingEncryptedMessageReply) String() string
String returns a string representation of the CancelResendingEncryptedMessageReply.
type Config ¶
type Config struct {
// SphinxGeometry defines the Sphinx packet format parameters used by the
// client daemon. This must exactly match the daemon's configuration to
// ensure proper packet size validation and processing.
SphinxGeometry *geo.Geometry
// PigeonholeGeometry defines the Pigeonhole protocol parameters used for
// channel operations. This must match the daemon's configuration for
// proper payload size validation and channel operation compatibility.
PigeonholeGeometry *pigeonholeGeo.Geometry
// Dial is the subtable-discriminated dial-transport configuration.
// Exactly one of its inner subtables (Unix, Tcp, and in future Ssh /
// Pipe / Pigeonhole) must be populated.
Dial *transport.DialConfig
}
Config contains the configuration parameters for a ThinClient.
The configuration specifies how to connect to the client daemon and includes the cryptographic parameters that must match the daemon's configuration.
Configuration can be loaded from a TOML file using LoadFile() or created programmatically. The SphinxGeometry and PigeonholeGeometry parameters must exactly match those used by the client daemon.
Example TOML configuration:
[Dial.Tcp] Address = "localhost:64331" [SphinxGeometry] PacketLength = 3082 NrHops = 5 UserForwardPayloadLength = 2000 # ... other Sphinx parameters [PigeonholeGeometry] MaxPlaintextPayloadLength = 1553 # ... other Pigeonhole parameters
func FromConfig ¶
FromConfig creates a thin client Config from a client daemon config.Config.
This function extracts the relevant parameters from a full client daemon configuration and creates a thin client configuration that can connect to that daemon. The SphinxGeometry and PigeonholeGeometry are copied directly to ensure compatibility.
Parameters:
- cfg: The client daemon configuration
Returns:
- *Config: A thin client configuration compatible with the daemon
Panics:
- If cfg.SphinxGeometry is nil
- If cfg.PigeonholeGeometry is nil
func LoadFile ¶
LoadFile loads a thin client configuration from a TOML file.
The TOML file should contain the network connection parameters and cryptographic geometry specifications. See the package documentation for an example configuration format.
Parameters:
- filename: Path to the TOML configuration file
Returns:
- *Config: The loaded configuration
- error: Any error encountered reading or parsing the file
Example:
cfg, err := thin.LoadFile("thinclient.toml")
if err != nil {
log.Fatal("Failed to load config:", err)
}
type ConnectionStatusEvent ¶
type ConnectionStatusEvent struct {
// IsConnected indicates whether the daemon is connected to the mixnet.
// When true, full functionality is available. When false, only offline
// operations (channel preparation) will work.
IsConnected bool `cbor:"is_connected"`
// Err contains any error that caused a disconnection. This field is
// nil when IsConnected is true or when the disconnection was intentional.
Err error `cbor:"err"`
// InstanceToken is a random token generated by the daemon on startup.
// It uniquely identifies a daemon instance, allowing thin clients to
// detect whether a reconnect is to the same or a new daemon instance.
InstanceToken [16]byte `cbor:"instance_token"`
}
ConnectionStatusEvent is sent when the daemon's connection status changes.
This event indicates whether the client daemon is currently connected to the mixnet infrastructure. When IsConnected is false, the client operates in "offline mode" where channel preparation operations work but actual message transmission will fail.
Applications can use this event to:
- Display connection status to users
- Queue operations for later transmission when offline
- Implement retry logic for failed operations
func (*ConnectionStatusEvent) String ¶
func (e *ConnectionStatusEvent) String() string
String returns a string representation of the ConnectionStatusEvent.
type CourierDescriptor ¶
CourierDescriptor identifies a specific courier service for routing copy commands.
type CreateCourierEnvelopesFromPayload ¶
type CreateCourierEnvelopesFromPayload struct {
// QueryID is used for correlating this thin client request with the
// thin client response.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// Payload is the data to be written (max 10MB).
Payload []byte `cbor:"payload"`
// DestWriteCap is the write capability for the destination channel.
DestWriteCap *bacap.WriteCap `cbor:"dest_write_cap"`
// DestStartIndex is the starting index in the destination channel.
DestStartIndex *bacap.MessageBoxIndex `cbor:"dest_start_index"`
// IsStart indicates whether this is the first call in a multi-call sequence.
// When true, the first CopyStreamElement will have the IsStart flag set.
IsStart bool `cbor:"is_start"`
// IsLast indicates whether this is the last payload in the sequence.
// When true, the final CopyStreamElement will have IsFinal=true.
IsLast bool `cbor:"is_last"`
}
CreateCourierEnvelopesFromPayload creates multiple CourierEnvelopes from a payload of any size. The payload is automatically chunked and each chunk is wrapped in a CourierEnvelope. Each returned chunk is a serialized CopyStreamElement ready to be written to a box.
This method is stateless — no daemon state is kept between calls. Each call creates a fresh encoder, encodes all envelopes, flushes, and returns. The caller controls the copy stream boundaries via IsStart and IsLast flags.
Multiple calls can target the same destination stream by using NextDestIndex from the reply as the DestStartIndex for the next call.
type CreateCourierEnvelopesFromPayloadReply ¶
type CreateCourierEnvelopesFromPayloadReply struct {
// QueryID is used for correlating this reply with the CreateCourierEnvelopesFromPayload request
// that created it.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// Envelopes is a slice of serialized CopyStreamElements, one per chunk.
Envelopes [][]byte `cbor:"envelopes"`
// NextDestIndex is the next destination message box index after all boxes
// consumed by this call. Use this as DestStartIndex in subsequent calls
// to continue writing to the same destination stream.
NextDestIndex *bacap.MessageBoxIndex `cbor:"next_dest_index"`
// ErrorCode indicates the success or failure of the envelope creation.
// A value of ThinClientSuccess indicates successful creation.
ErrorCode uint8 `cbor:"error_code"`
}
CreateCourierEnvelopesFromPayloadReply is sent in response to a CreateCourierEnvelopesFromPayload request. It provides multiple serialized CopyStreamElements, one for each chunk of the payload.
func (*CreateCourierEnvelopesFromPayloadReply) String ¶
func (e *CreateCourierEnvelopesFromPayloadReply) String() string
String returns a string representation of the CreateCourierEnvelopesFromPayloadReply.
type CreateCourierEnvelopesFromPayloads ¶
type CreateCourierEnvelopesFromPayloads struct {
// QueryID is used for correlating this thin client request with the
// thin client response.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// Destinations is the list of payloads and their destination channels.
Destinations []DestinationPayload `cbor:"destinations"`
// IsStart indicates whether this is the first call in a multi-call sequence.
// When true, the first CopyStreamElement will have the IsStart flag set.
IsStart bool `cbor:"is_start"`
// IsLast indicates whether this is the last set of payloads in the sequence.
// When true, the final CopyStreamElement will have IsFinal=true.
IsLast bool `cbor:"is_last"`
// Buffer is the residual encoder buffer from a previous call.
// Pass nil on the first call.
Buffer []byte `cbor:"buffer"`
}
CreateCourierEnvelopesFromPayloads creates CourierEnvelopes from multiple payloads going to different destination channels. This is more space-efficient than calling CreateCourierEnvelopesFromPayload multiple times because envelopes from different destinations are packed together in the copy stream without wasting space.
This method is stateless — the Buffer field enables continuation across multiple calls without daemon-side state. Pass the Buffer from the previous call's reply to avoid wasting space in the last box of each call. On the first call, Buffer should be nil.
type CreateCourierEnvelopesFromPayloadsReply ¶
type CreateCourierEnvelopesFromPayloadsReply struct {
// QueryID is used for correlating this reply with the CreateCourierEnvelopesFromPayloads request
// that created it.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// Envelopes is a slice of serialized CopyStreamElements containing all the courier envelopes
// from all destinations packed efficiently together.
Envelopes [][]byte `cbor:"envelopes"`
// Buffer contains any data buffered by the encoder that hasn't been output yet.
// This can be persisted for crash recovery and restored via SetStreamBuffer.
Buffer []byte `cbor:"buffer"`
// NextDestIndices contains the next destination message box index for each
// destination, in the same order as the destinations in the request.
// Use these as StartIndex in subsequent calls to continue writing to the
// same destination streams.
NextDestIndices []*bacap.MessageBoxIndex `cbor:"next_dest_indices"`
// ErrorCode indicates the success or failure of the envelope creation.
// A value of ThinClientSuccess indicates successful creation.
ErrorCode uint8 `cbor:"error_code"`
}
CreateCourierEnvelopesFromPayloadsReply is sent in response to a CreateCourierEnvelopesFromPayloads request. It provides multiple serialized CopyStreamElements packed efficiently from multiple destination payloads.
func (*CreateCourierEnvelopesFromPayloadsReply) String ¶
func (e *CreateCourierEnvelopesFromPayloadsReply) String() string
String returns a string representation of the CreateCourierEnvelopesFromPayloadsReply.
type CreateCourierEnvelopesFromTombstoneRange ¶
type CreateCourierEnvelopesFromTombstoneRange struct {
// QueryID is used for correlating this thin client request with the
// thin client response.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// DestWriteCap is the write capability for the destination channel.
DestWriteCap *bacap.WriteCap `cbor:"dest_write_cap"`
// DestStartIndex is the starting index in the destination channel.
DestStartIndex *bacap.MessageBoxIndex `cbor:"dest_start_index"`
// MaxCount is the number of tombstones to create.
MaxCount uint32 `cbor:"max_count"`
// IsStart indicates whether this is the first call in a multi-call sequence.
// When true, the first CopyStreamElement will have the IsStart flag set.
IsStart bool `cbor:"is_start"`
// IsLast indicates whether this is the last call in the sequence.
// When true, the final CopyStreamElement will have IsFinal=true.
IsLast bool `cbor:"is_last"`
// Buffer is the residual encoder buffer from a previous call.
// Pass nil on the first call.
Buffer []byte `cbor:"buffer"`
}
CreateCourierEnvelopesFromTombstoneRange creates multiple CourierEnvelopes containing tombstones (empty payload with SignBox signatures) for a range of destination indices. Each returned chunk is a serialized CopyStreamElement ready to be written to a box.
This is the tombstone equivalent of CreateCourierEnvelopesFromPayload: instead of chunking and encrypting a payload, it creates one tombstone CourierEnvelope per index in the range [DestStartIndex, DestStartIndex + MaxCount).
The Buffer field enables stateless continuation: pass the Buffer from the previous call's reply to avoid wasting space in the last box of each call. On the first call, Buffer should be nil.
type CreateCourierEnvelopesFromTombstoneRangeReply ¶
type CreateCourierEnvelopesFromTombstoneRangeReply struct {
// QueryID is used for correlating this reply with the request.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// Envelopes is a slice of serialized CopyStreamElements.
Envelopes [][]byte `cbor:"envelopes"`
// Buffer is the residual encoder buffer to pass to the next call.
// Nil when IsLast was true in the request.
Buffer []byte `cbor:"buffer"`
// NextDestIndex is the next destination message box index after all
// tombstones created by this call.
NextDestIndex *bacap.MessageBoxIndex `cbor:"next_dest_index"`
// ErrorCode indicates the success or failure of the operation.
ErrorCode uint8 `cbor:"error_code"`
}
CreateCourierEnvelopesFromTombstoneRangeReply is sent in response to a CreateCourierEnvelopesFromTombstoneRange request. It provides serialized CopyStreamElements containing tombstone courier envelopes.
func (*CreateCourierEnvelopesFromTombstoneRangeReply) String ¶
func (e *CreateCourierEnvelopesFromTombstoneRangeReply) String() string
String returns a string representation of the CreateCourierEnvelopesFromTombstoneRangeReply.
type CreateEnvelopesResult ¶
type CreateEnvelopesResult struct {
// Envelopes contains the serialized CopyStreamElements ready to be written to boxes.
Envelopes [][]byte
// Buffer contains any data buffered by the encoder that hasn't been output yet.
// Pass this to the next call to avoid wasting space in the last box.
Buffer []byte
// NextDestIndices contains the next destination message box index for each
// destination, in the same order as the destinations in the request.
NextDestIndices []*bacap.MessageBoxIndex
}
CreateEnvelopesResult contains the result of creating courier envelopes, including the envelopes, buffer state for crash recovery, and next destination indices.
type DaemonDisconnectedEvent ¶
type DaemonDisconnectedEvent struct {
// IsGraceful is true if the daemon sent a ShutdownEvent before disconnecting.
// False means the connection was lost unexpectedly (crash, network issue).
IsGraceful bool
// Err contains the underlying error that caused the disconnect, if any.
Err error
}
DaemonDisconnectedEvent is emitted when the thin client loses its connection to the client daemon. The thin client will automatically attempt to reconnect.
func (*DaemonDisconnectedEvent) String ¶
func (e *DaemonDisconnectedEvent) String() string
String returns a string representation of the DaemonDisconnectedEvent.
type DestinationPayload ¶
type DestinationPayload struct {
// Payload is the data to be written to this destination.
Payload []byte `cbor:"payload"`
// WriteCap is the write capability for the destination channel.
WriteCap *bacap.WriteCap `cbor:"write_cap"`
// StartIndex is the starting index in the destination channel.
StartIndex *bacap.MessageBoxIndex `cbor:"start_index"`
}
DestinationPayload specifies a payload and its destination channel for multi-channel writes.
type EncryptRead ¶
type EncryptRead struct {
// QueryID is used for correlating this thin client request with the
// thin client response.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// ReadCap is the read capability that grants access to the channel.
ReadCap *bacap.ReadCap `cbor:"read_cap"`
// MessageBoxIndex specifies the starting read position for the channel.
MessageBoxIndex *bacap.MessageBoxIndex `cbor:"message_box_index"`
}
EncryptRead requests the encryption of a read operation for a given read capability.
type EncryptReadReply ¶
type EncryptReadReply struct {
// QueryID is used for correlating this reply with the EncryptRead request
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// MessageCiphertext is the encrypted message ciphertext that should be sent
// to the Courier service.
MessageCiphertext []byte `cbor:"message_ciphertext"`
// EnvelopeDescriptor contains the serialized EnvelopeDescriptor that
// contains the private key material needed to decrypt the envelope reply.
EnvelopeDescriptor []byte `cbor:"envelope_descriptor"`
// EnvelopeHash is the hash of the CourierEnvelope that was sent to the
// mixnet and is used to resume the read operation.
EnvelopeHash *[32]byte `cbor:"envelope_hash"`
// NextMessageBoxIndex is the next message box index to use for subsequent
// read operations. This is computed by the daemon using BACAP's NextIndex.
NextMessageBoxIndex *bacap.MessageBoxIndex `cbor:"next_message_box_index"`
// ErrorCode indicates the reason for a failure to encrypt the read if any.
// Otherwise it is set to zero for success.
ErrorCode uint8 `cbor:"error_code"`
}
EncryptReadReply is the reply to an EncryptRead request.
func (*EncryptReadReply) String ¶
func (e *EncryptReadReply) String() string
String returns a string representation of the EncryptReadReply.
type EncryptWrite ¶
type EncryptWrite struct {
// QueryID is used for correlating this thin client request with the
// thin client response.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// Plaintext is the plaintext message to be encrypted.
Plaintext []byte `cbor:"plaintext"`
// WriteCap is the write capability that grants access to the channel.
WriteCap *bacap.WriteCap `cbor:"write_cap"`
// MessageBoxIndex specifies the starting write position for the channel.
MessageBoxIndex *bacap.MessageBoxIndex `cbor:"message_box_index"`
}
EncryptWrite requests the encryption of a write operation for a given write capability.
type EncryptWriteReply ¶
type EncryptWriteReply struct {
// QueryID is used for correlating this reply with the EncryptWrite request
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// MessageCiphertext is the encrypted message ciphertext that should be sent
// to the Courier service.
MessageCiphertext []byte `cbor:"message_ciphertext"`
// EnvelopeDescriptor contains the serialized EnvelopeDescriptor that
// contains the private key material needed to decrypt the envelope reply.
EnvelopeDescriptor []byte `cbor:"envelope_descriptor"`
// EnvelopeHash is the hash of the CourierEnvelope that was sent to the
// mixnet and is used to resume the write operation.
EnvelopeHash *[32]byte `cbor:"envelope_hash"`
// NextMessageBoxIndex is the next message box index to use for subsequent
// write operations. This is computed by the daemon using BACAP's NextIndex.
NextMessageBoxIndex *bacap.MessageBoxIndex `cbor:"next_message_box_index"`
// ErrorCode indicates the reason for a failure to encrypt the write if any.
// Otherwise it is set to zero for success.
ErrorCode uint8 `cbor:"error_code"`
}
EncryptWriteReply is the reply to an EncryptWrite request.
func (*EncryptWriteReply) String ¶
func (e *EncryptWriteReply) String() string
String returns a string representation of the EncryptWriteReply.
type Event ¶
type Event interface {
// String returns a human-readable string representation of the Event.
String() string
}
Event is the generic interface for all events sent by the thin client.
Events are sent through channels returned by EventSink() and provide asynchronous notification of various client operations and status changes. Applications should use type assertions to handle specific event types.
Common event types include:
- ConnectionStatusEvent: Connection state changes
- MessageReplyEvent: Replies to sent messages
- NewDocumentEvent: PKI document updates
- Pigeonhole API replies (NewKeypairReply, StartResendingEncryptedMessageReply, etc.)
type GetMessageBoxIndexCounter ¶
type GetMessageBoxIndexCounter struct {
// QueryID is used for correlating this thin client request with the
// thin client response.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// MessageBoxIndex is the index whose counter should be returned.
MessageBoxIndex *bacap.MessageBoxIndex `cbor:"message_box_index"`
}
GetMessageBoxIndexCounter requests the daemon to return the BACAP Idx64 counter embedded in a MessageBoxIndex. This lets thin clients order and compare indexes without having to peek at the binary layout themselves (bacap.MessageBoxIndex.MarshalBinary puts Idx64 as the first 8 bytes in little-endian, but that's an implementation detail the thin client should not rely on). The reply type is GetMessageBoxIndexCounterReply.
type GetMessageBoxIndexCounterReply ¶
type GetMessageBoxIndexCounterReply struct {
// QueryID is used for correlating this reply with the GetMessageBoxIndexCounter request.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// Counter is the BACAP Idx64 value read out of the requested MessageBoxIndex.
Counter uint64 `cbor:"counter"`
// ErrorCode indicates the reason for a failure to read the counter if any.
// Otherwise it is set to zero for success.
ErrorCode uint8 `cbor:"error_code"`
}
GetMessageBoxIndexCounterReply is the reply to a GetMessageBoxIndexCounter request.
func (*GetMessageBoxIndexCounterReply) String ¶
func (e *GetMessageBoxIndexCounterReply) String() string
String returns a string representation of the GetMessageBoxIndexCounterReply.
type MessageIDGarbageCollected ¶
type MessageIDGarbageCollected struct {
// MessageID is the local unique identifier for the message.
MessageID *[MessageIDLength]byte `cbor:"message_id"`
}
MessageIDGarbageCollected is the event used to signal when a given message ID has been garbage collected.
func (*MessageIDGarbageCollected) String ¶
func (e *MessageIDGarbageCollected) String() string
String returns a string representation of a MessageIDGarbageCollected.
type MessageReplyEvent ¶
type MessageReplyEvent struct {
// MessageID is the unique identifier for the request associated with the
// reply.
MessageID *[MessageIDLength]byte `cbor:"message_id"`
// SURBID must be a unique identity for each request.
// This field should be nil if WithSURB is false.
SURBID *[constants.SURBIDLength]byte `cbor:"surbid"`
// Payload is the reply payload if any.
Payload []byte `cbor:"payload"`
// ReplyIndex is the index of the reply that was actually used when processing
// this message. This is particularly relevant for pigeonhole channel reads.
ReplyIndex *uint8 `cbor:"reply_index,omitempty"`
// ErrorCode indicates the success or failure of the message operation.
// A value of ThinClientSuccess (0) indicates no error occurred.
// Non-zero values indicate specific error conditions.
ErrorCode uint8 `cbor:"error_code"`
}
MessageReplyEvent is the event sent when a new message is received.
func (*MessageReplyEvent) String ¶
func (e *MessageReplyEvent) String() string
String returns a string representation of the MessageReplyEvent.
type MessageSentEvent ¶
type MessageSentEvent struct {
// MessageID is the local unique identifier for the message, generated
// when the message was enqueued.
MessageID *[MessageIDLength]byte `cbor:"message_id"`
// SURBID must be a unique identity for each request.
// This field should be nil if WithSURB is false.
SURBID *[constants.SURBIDLength]byte `cbor:"surbid"`
// SentAt contains the time the message was sent.
SentAt time.Time `cbor:"sent_at"`
// ReplyETA is the expected round trip time to receive a response.
ReplyETA time.Duration `cbor:"reply_eta"`
// Err is the error message if any error was encountered when sending the message.
// Empty string indicates no error occurred.
Err string `cbor:"err,omitempty"`
}
MessageSentEvent is the event sent when a message has been fully transmitted.
func (*MessageSentEvent) String ¶
func (e *MessageSentEvent) String() string
String returns a string representation of a MessageSentEvent.
type NewDocumentEvent ¶
NewDocumentEvent is the new document event, signaling that we have received a new document from the PKI.
func (*NewDocumentEvent) String ¶
func (e *NewDocumentEvent) String() string
String returns a string representation of a NewDocumentEvent.
type NewKeypair ¶
type NewKeypair struct {
// QueryID is used for correlating this thin client request with the
// thin client response.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// Seed is the 32 byte seed used to derive the keypair.
Seed []byte `cbor:"seed"`
}
NewKeypair requests the creation of a new keypair for use with the Pigeonhole protocol. The reply type, is NewKeypairReply.
type NewKeypairReply ¶
type NewKeypairReply struct {
// QueryID is used for correlating this reply with the NewKeypair request
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// WriteCap is the write capability that should be stored for channel
WriteCap *bacap.WriteCap `cbor:"write_cap"`
// ReadCap is the read capability that can be shared with others to allow
// them to read messages from this channel.
ReadCap *bacap.ReadCap `cbor:"read_cap"`
// FirstMessageIndex is the first message index that should be used when
// writing messages to the channel.
FirstMessageIndex *bacap.MessageBoxIndex `cbor:"first_message_index"`
// ErrorCode indicates the reason for a failure to create a new keypair if any.
// Otherwise it is set to zero for success.
ErrorCode uint8 `cbor:"error_code"`
}
NewKeypairReply is the reply to a NewKeypair request.
func (*NewKeypairReply) String ¶
func (e *NewKeypairReply) String() string
String returns a string representation of the NewKeypairReply.
type NewPKIDocumentEvent ¶
type NewPKIDocumentEvent struct {
Payload []byte `cbor:"payload"`
}
NewPKIDocumentEvent is the unix domain socket protocol message used by the daemon to tell the thin client about new PKI document events. The payload field contains a CBOR encoded PKI document, stripped of signatures.
func (*NewPKIDocumentEvent) String ¶
func (e *NewPKIDocumentEvent) String() string
String returns a string representation of a NewDocumentEvent.
type NextMessageBoxIndex ¶
type NextMessageBoxIndex struct {
// QueryID is used for correlating this thin client request with the
// thin client response.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// MessageBoxIndex is the current index to increment.
MessageBoxIndex *bacap.MessageBoxIndex `cbor:"message_box_index"`
}
NextMessageBoxIndex requests the daemon to increment a MessageBoxIndex. This is used when sending multiple messages to different mailboxes using the same WriteCap. The reply type is NextMessageBoxIndexReply.
type NextMessageBoxIndexReply ¶
type NextMessageBoxIndexReply struct {
// QueryID is used for correlating this reply with the NextMessageBoxIndex request
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// NextMessageBoxIndex is the incremented message box index.
NextMessageBoxIndex *bacap.MessageBoxIndex `cbor:"next_message_box_index"`
// ErrorCode indicates the reason for a failure to increment the index if any.
// Otherwise it is set to zero for success.
ErrorCode uint8 `cbor:"error_code"`
}
NextMessageBoxIndexReply is the reply to a NextMessageBoxIndex request.
func (*NextMessageBoxIndexReply) String ¶
func (e *NextMessageBoxIndexReply) String() string
String returns a string representation of the NextMessageBoxIndexReply.
type Request ¶
type Request struct {
SessionToken *SessionToken `cbor:"session_token"`
// ThinClose is used to indicate that the thin client is disconnecting
// from the daemon.
ThinClose *ThinClose `cbor:"thin_close"`
// SendMessage is used to send a message through the mix network.
// Note that this is part of the legacy API and should not be used for newer
// works using the Pigeonhole protocol.
SendMessage *SendMessage `cbor:"send_message"`
// NewKeypair is used to create a new keypair for use with the Pigeonhole protocol.
NewKeypair *NewKeypair `cbor:"new_keypair"`
// EncryptRead is used to encrypt a read operation for a given read capability.
EncryptRead *EncryptRead `cbor:"encrypt_read"`
// EncryptWrite is used to encrypt a write operation for a given write capability.
EncryptWrite *EncryptWrite `cbor:"encrypt_write"`
// StartResendingEncryptedMessage is used to start resending an encrypted message.
StartResendingEncryptedMessage *StartResendingEncryptedMessage `cbor:"start_resending_encrypted_message"`
// CancelResendingEncryptedMessage is used to cancel resending an encrypted message.
CancelResendingEncryptedMessage *CancelResendingEncryptedMessage `cbor:"cancel_resending_encrypted_message"`
// StartResendingCopyCommand is used to send a copy command with ARQ.
StartResendingCopyCommand *StartResendingCopyCommand `cbor:"start_resending_copy_command"`
// CancelResendingCopyCommand is used to cancel resending a copy command.
CancelResendingCopyCommand *CancelResendingCopyCommand `cbor:"cancel_resending_copy_command"`
// NextMessageBoxIndex is used to increment a MessageBoxIndex.
NextMessageBoxIndex *NextMessageBoxIndex `cbor:"next_message_box_index"`
// GetMessageBoxIndexCounter reads the Idx64 counter out of a MessageBoxIndex.
GetMessageBoxIndexCounter *GetMessageBoxIndexCounter `cbor:"get_message_box_index_counter"`
// CreateCourierEnvelopesFromPayload is used to create multiple CourierEnvelopes from a payload of any size.
CreateCourierEnvelopesFromPayload *CreateCourierEnvelopesFromPayload `cbor:"create_courier_envelopes_from_payload"`
// CreateCourierEnvelopesFromPayloads is used to create CourierEnvelopes from multiple payloads
// going to different destination channels. This is more space-efficient than calling
// CreateCourierEnvelopesFromPayload multiple times.
CreateCourierEnvelopesFromPayloads *CreateCourierEnvelopesFromPayloads `cbor:"create_courier_envelopes_from_multi_payload"`
// CreateCourierEnvelopesFromTombstoneRange is used to create tombstone CourierEnvelopes
// for a range of destination indices, encoded as copy stream elements.
CreateCourierEnvelopesFromTombstoneRange *CreateCourierEnvelopesFromTombstoneRange `cbor:"create_courier_envelopes_from_tombstone_range"`
}
Request is the thin client's request message to the client daemon. It can result in one or more Response messages being sent back to the thin client.
type Response ¶
type Response struct {
SessionTokenReply *SessionTokenReply `cbor:"session_token_reply"`
// ShutdownEvent is sent when the client daemon is shutting down.
ShutdownEvent *ShutdownEvent `cbor:"shutdown_event"`
// ConnectionStatusEvent is sent when the client daemon's connection status changes.
ConnectionStatusEvent *ConnectionStatusEvent `cbor:"connection_status_event"`
// NewPKIDocumentEvent is sent when the client daemon receives a new PKI document.
NewPKIDocumentEvent *NewPKIDocumentEvent `cbor:"new_pki_document_event"`
// MessageSentEvent is sent when the client daemon successfully sends a message.
MessageSentEvent *MessageSentEvent `cbor:"message_sent_event"`
// MessageReplyEvent is sent when the client daemon receives a reply to a message.
MessageReplyEvent *MessageReplyEvent `cbor:"message_reply_event"`
// MessageIDGarbageCollected is sent when the client daemon garbage collects a message ID.
MessageIDGarbageCollected *MessageIDGarbageCollected `cbor:"message_id_garbage_collected"`
// NewKeypairReply is sent when the client daemon successfully creates a new keypair.
NewKeypairReply *NewKeypairReply `cbor:"new_keypair_reply"`
// EncryptReadReply is sent when the client daemon successfully encrypts a read operation.
EncryptReadReply *EncryptReadReply `cbor:"encrypt_read_reply"`
// EncryptWriteReply is sent when the client daemon successfully encrypts a write operation.
EncryptWriteReply *EncryptWriteReply `cbor:"encrypt_write_reply"`
// StartResendingEncryptedMessageReply is sent when the client daemon successfully starts resending an encrypted message.
StartResendingEncryptedMessageReply *StartResendingEncryptedMessageReply `cbor:"start_resending_encrypted_message_reply"`
// CancelResendingEncryptedMessageReply is sent when the client daemon successfully cancels resending an encrypted message.
CancelResendingEncryptedMessageReply *CancelResendingEncryptedMessageReply `cbor:"cancel_resending_encrypted_message_reply"`
// StartResendingCopyCommandReply is sent when the client daemon successfully sends a copy command with ARQ.
StartResendingCopyCommandReply *StartResendingCopyCommandReply `cbor:"start_resending_copy_command_reply"`
// CancelResendingCopyCommandReply is sent when the client daemon successfully cancels resending a copy command.
CancelResendingCopyCommandReply *CancelResendingCopyCommandReply `cbor:"cancel_resending_copy_command_reply"`
// NextMessageBoxIndexReply is sent when the client daemon successfully increments a MessageBoxIndex.
NextMessageBoxIndexReply *NextMessageBoxIndexReply `cbor:"next_message_box_index_reply"`
// GetMessageBoxIndexCounterReply is sent in response to a
// GetMessageBoxIndexCounter request and carries the BACAP Idx64 value.
GetMessageBoxIndexCounterReply *GetMessageBoxIndexCounterReply `cbor:"get_message_box_index_counter_reply"`
// CreateCourierEnvelopesFromPayloadReply is sent when the client daemon successfully creates courier envelopes from a payload.
CreateCourierEnvelopesFromPayloadReply *CreateCourierEnvelopesFromPayloadReply `cbor:"create_courier_envelopes_from_payload_reply"`
// CreateCourierEnvelopesFromPayloadsReply is sent when the client daemon successfully creates courier envelopes from multiple payloads.
CreateCourierEnvelopesFromPayloadsReply *CreateCourierEnvelopesFromPayloadsReply `cbor:"create_courier_envelopes_from_multi_payload_reply"`
// CreateCourierEnvelopesFromTombstoneRangeReply is sent when the client daemon successfully
// creates tombstone courier envelopes for a range of destination indices.
CreateCourierEnvelopesFromTombstoneRangeReply *CreateCourierEnvelopesFromTombstoneRangeReply `cbor:"create_courier_envelopes_from_tombstone_range_reply"`
}
Response is the client daemon's response message to the thin client.
type SendChannelQuery ¶
type SendChannelQuery struct {
// MessageID is the unique identifier for the request associated with the
// query reply.
MessageID *[MessageIDLength]byte `cbor:"message_id"`
// ChannelID is optional and only used for sending channel messages.
// For non-channel messages, this field should be nil.
ChannelID *uint16 `cbor:"channel_id,omitempty"`
// DestinationIdHash is 32 byte hash of the destination Service's
// identity public key.
DestinationIdHash *[hash.HashSize]byte `cbor:"destination_id_hash"`
// RecipientQueueID is the queue identity which will receive the message.
// This queue ID is meant to be the queue ID of the Pigeonhole protocol Courier service.
RecipientQueueID []byte `cbor:"recipient_queue_id"`
// Payload is the Pigeonole protocol ciphertext payload which will be encapsulated in the Sphinx payload.
Payload []byte `cbor:"payload"`
}
SendChannelQuery is used to send a Pigeonhole protocol ciphertext query payload through the mix network.
type SendMessage ¶
type SendMessage struct {
// ID is the unique identifier with respect to the Payload.
// This is only used by the ARQ.
ID *[MessageIDLength]byte `cbor:"id"`
// WithSURB indicates if the message should be sent with a SURB
// in the Sphinx payload.
WithSURB bool `cbor:"with_surb"`
// SURBID must be a unique identity for each request.
// This field should be nil if WithSURB is false.
SURBID *[constants.SURBIDLength]byte `cbor:"surbid"`
// DestinationIdHash is 32 byte hash of the destination Provider's
// identity public key.
DestinationIdHash *[hash.HashSize]byte `cbor:"destination_id_hash"`
// RecipientQueueID is the queue identity which will receive the message.
RecipientQueueID []byte `cbor:"recipient_queue_id"`
// Payload is the actual Sphinx packet.
Payload []byte `cbor:"payload"`
}
SendMessage is used to send a message through the mix network it is part of the legacy API and should not be used for newer works using the Pigeonhole protocol.
type SessionToken ¶
type SessionToken struct {
ClientInstanceToken [16]byte `cbor:"client_instance_token"`
}
SessionToken is sent by the thin client as its first request after the handshake.
type SessionTokenReply ¶
SessionTokenReply is the daemon's response to a SessionToken request.
func (*SessionTokenReply) String ¶
func (r *SessionTokenReply) String() string
type ShutdownEvent ¶
type ShutdownEvent struct{}
ShutdownEvent is sent when the client daemon is shutting down.
This event indicates that the daemon is terminating and the thin client connection will be lost. Applications should handle this event by performing cleanup and potentially attempting to reconnect.
func (*ShutdownEvent) String ¶
func (e *ShutdownEvent) String() string
String returns a string representation of the ShutdownEvent.
type StartResendingCopyCommand ¶
type StartResendingCopyCommand struct {
// QueryID is used for correlating this thin client request with the
// thin client response.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// WriteCap is the write capability for the temporary channel that contains
// the data to be copied. The courier will derive a ReadCap from this
// to read the data.
WriteCap *bacap.WriteCap `cbor:"write_cap"`
// CourierIdentityHash is optional. If set, the daemon will send the copy command
// to this specific courier instead of selecting a random one.
// This enables nested copy commands with different couriers per layer.
CourierIdentityHash *[32]byte `cbor:"courier_identity_hash,omitempty"`
// CourierQueueID is optional. Must be set if CourierIdentityHash is set.
// This is the recipient queue ID for the specified courier.
CourierQueueID []byte `cbor:"courier_queue_id,omitempty"`
}
StartResendingCopyCommand requests the daemon to send a copy command to a courier with ARQ (automatic repeat request) for reliable delivery. The copy command instructs the courier to read data from a temporary channel and write it to the destination channel.
type StartResendingCopyCommandReply ¶
type StartResendingCopyCommandReply struct {
// QueryID is used for correlating this reply with the StartResendingCopyCommand request
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// ErrorCode indicates the reason for a failure to execute the copy command if any.
// Otherwise it is set to zero for success.
ErrorCode uint8 `cbor:"error_code"`
// ReplicaErrorCode is the pigeonhole replica ErrorCode that caused
// the Copy command to abort on the courier. Meaningful only when
// ErrorCode indicates a Copy failure and the courier identified a
// specific replica-side reason (e.g. ReplicaErrorBoxAlreadyExists).
ReplicaErrorCode uint8 `cbor:"replica_error_code,omitempty"`
// FailedEnvelopeIndex is the 1-based sequential position in the
// copy stream of the envelope whose write triggered the abort.
// 0 if not applicable. Not a BACAP message index.
FailedEnvelopeIndex uint64 `cbor:"failed_envelope_index,omitempty"`
}
StartResendingCopyCommandReply is the reply to a StartResendingCopyCommand request.
func (*StartResendingCopyCommandReply) String ¶
func (e *StartResendingCopyCommandReply) String() string
String returns a string representation of the StartResendingCopyCommandReply.
type StartResendingEncryptedMessage ¶
type StartResendingEncryptedMessage struct {
// QueryID is used for correlating this thin client request with the
// thin client response.
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// ReadCap is the read capability that grants access to the channel.
ReadCap *bacap.ReadCap `cbor:"read_cap"`
// WriteCap is the write capability that grants access to the channel.
WriteCap *bacap.WriteCap `cbor:"write_cap"`
// MessageBoxIndex is the current message box index being operated on.
MessageBoxIndex []byte `cbor:"message_box_index"`
// ReplyIndex is the index of the reply that was actually used when processing.
// This field is optional - if nil, the daemon will use the default reply index.
ReplyIndex *uint8 `cbor:"reply_index,omitempty"`
// EnvelopeDescriptor contains the serialized EnvelopeDescriptor that
// contains the private key material needed to decrypt the envelope reply.
EnvelopeDescriptor []byte `cbor:"envelope_descriptor"`
// MessageCiphertext is the encrypted message ciphertext that should be sent
MessageCiphertext []byte `cbor:"message_ciphertext"`
// EnvelopeHash is the hash of the CourierEnvelope that was sent to the
// mixnet and is used to resume the read operation.
EnvelopeHash *[32]byte `cbor:"envelope_hash"`
// NoRetryOnBoxIDNotFound controls whether BoxIDNotFound errors on reads
// trigger automatic retries. By default (false), reads will retry up to
// 10 times to handle replication lag. Set to true to get immediate
// BoxIDNotFound error without retries.
NoRetryOnBoxIDNotFound bool `cbor:"no_retry_on_box_id_not_found,omitempty"`
// NoIdempotentBoxAlreadyExists controls whether BoxAlreadyExists errors on writes
// are treated as idempotent success. By default (false), BoxAlreadyExists is treated
// as success (the write already happened). Set to true to return BoxAlreadyExists
// as an error instead.
NoIdempotentBoxAlreadyExists bool `cbor:"no_idempotent_box_already_exists,omitempty"`
}
StartResendingEncryptedMessage requests the daemon to start resending an encrypted message.
type StartResendingEncryptedMessageReply ¶
type StartResendingEncryptedMessageReply struct {
// QueryID is used for correlating this reply with the StartResendingEncryptedMessage request
QueryID *[QueryIDLength]byte `cbor:"query_id"`
// Plaintext is the plaintext message that was read from the channel.
Plaintext []byte `cbor:"plaintext"`
// ErrorCode indicates the reason for a failure to start resending the encrypted message if any.
// Otherwise it is set to zero for success.
ErrorCode uint8 `cbor:"error_code"`
// CourierIdentityHash is the 32-byte hash of the identity key of the courier that was
// selected to handle this message. Callers can watch PKI document updates for this
// courier disappearing from consensus and cancel+re-encrypt if it does.
CourierIdentityHash *[32]byte `cbor:"courier_identity_hash"`
// CourierQueueID is the queue ID of the courier that was selected.
CourierQueueID []byte `cbor:"courier_queue_id"`
}
StartResendingEncryptedMessageReply is the reply to a StartResendingEncryptedMessage request.
func (*StartResendingEncryptedMessageReply) String ¶
func (e *StartResendingEncryptedMessageReply) String() string
String returns a string representation of the StartResendingEncryptedMessageReply.
type StartResendingResult ¶
type StartResendingResult struct {
// Plaintext is the decrypted message for read operations, or empty for writes.
Plaintext []byte
// CourierIdentityHash is the 32-byte hash of the identity key of the courier that
// handled this message. Callers can watch PKI document updates for this courier
// disappearing from consensus and cancel+re-encrypt if needed.
CourierIdentityHash *[32]byte
// CourierQueueID is the queue ID of the courier that handled this message.
CourierQueueID []byte
}
StartResendingResult is returned by StartResendingEncryptedMessage and its variants.
type ThinClient ¶
ThinClient handles communication between mixnet applications and the client daemon.
The ThinClient implements a lightweight client architecture where cryptographic operations, PKI management, and mixnet protocol handling are delegated to a separate client daemon process. This design allows applications to integrate with Katzenpost without implementing the full complexity of the mixnet protocols.
Key responsibilities of ThinClient:
- Maintain connection to the client daemon via TCP or Unix sockets
- Provide high-level APIs for message sending and channel operations
- Handle event distribution to application code
- Manage PKI document caching and epoch transitions
- Coordinate request/response correlation for blocking operations
The ThinClient is safe for concurrent use by multiple goroutines.
Lifecycle:
- Create with NewThinClient()
- Connect with Dial()
- Use messaging/channel APIs
- Clean up with Close()
Example:
cfg, _ := thin.LoadFile("config.toml")
logging := &config.Logging{Level: "INFO"}
client := thin.NewThinClient(cfg, logging)
defer client.Close()
err := client.Dial()
if err != nil {
log.Fatal(err)
}
// Use client for messaging...
func NewThinClient ¶
func NewThinClient(cfg *Config, logging *config.Logging) *ThinClient
NewThinClient creates a new ThinClient instance.
This function initializes a new thin client with the provided configuration and logging settings. The client is created in a disconnected state; call Dial() to establish connection to the client daemon.
The client will validate that required geometry parameters are present and set up internal channels and workers for event handling.
Parameters:
- cfg: Configuration specifying daemon connection and crypto parameters
- logging: Logging configuration for the client
Returns:
- *ThinClient: A new thin client instance ready for connection
Panics:
- If cfg.SphinxGeometry is nil
- If cfg.PigeonholeGeometry is nil
- If logging configuration is invalid
Example:
cfg, _ := thin.LoadFile("config.toml")
logging := &config.Logging{
Level: "INFO",
File: "", // stdout
}
client := thin.NewThinClient(cfg, logging)
func (*ThinClient) BlockingSendMessage ¶
func (t *ThinClient) BlockingSendMessage(ctx context.Context, payload []byte, destNode *[32]byte, destQueue []byte) ([]byte, error)
BlockingSendMessage sends a message and blocks until a reply is received.
This method provides a synchronous request-response pattern by automatically generating a SURB ID, sending the message, and waiting for the reply. It blocks until either a reply is received or the context times out.
This is convenient for simple request-response interactions but lacks the advanced features of the Pigeonhole Channel API such as message ordering, channel persistence, and offline operation support.
Requirements:
- The daemon must be connected to the mixnet (IsConnected() == true)
- The destination service must be available in the current PKI document
- A context with appropriate timeout should be provided
Parameters:
- ctx: Context for cancellation and timeout control (recommended: 30s timeout)
- payload: Message data to send
- destNode: Hash of the destination service's identity key
- destQueue: Queue ID of the destination service
Returns:
- []byte: Reply payload from the destination service
- error: Any error encountered during sending or while waiting for reply
Example:
// Send message to echo service and wait for reply
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
echoService, err := client.GetService("echo")
if err != nil {
return err
}
destNode := hash.Sum256(echoService.MixDescriptor.IdentityKey)
reply, err := client.BlockingSendMessage(ctx, []byte("Hello"),
&destNode, echoService.RecipientQueueID)
if err != nil {
return err
}
fmt.Printf("Echo reply: %s\n", reply)
func (*ThinClient) CancelResendingCopyCommand ¶
func (t *ThinClient) CancelResendingCopyCommand(writeCapHash *[32]byte) error
CancelResendingCopyCommand cancels ARQ resending for a copy command.
This method stops the automatic repeat request (ARQ) for a previously started copy command. This is useful when:
- A reply has been received through another channel
- The operation should be aborted
- The copy command is no longer needed
Parameters:
- writeCapHash: Hash of the serialized WriteCap to cancel
Returns:
- error: Any error encountered during cancellation
Example:
err := client.CancelResendingCopyCommand(writeCapHash)
if err != nil {
log.Printf("Failed to cancel copy command: %v", err)
}
func (*ThinClient) CancelResendingEncryptedMessage ¶
func (t *ThinClient) CancelResendingEncryptedMessage(envelopeHash *[32]byte) error
CancelResendingEncryptedMessage cancels ARQ resending for an encrypted message.
This method stops the automatic repeat request (ARQ) for a previously started encrypted message transmission. This is useful when:
- A reply has been received through another channel
- The operation should be aborted
- The message is no longer needed
Parameters:
- envelopeHash: Hash of the courier envelope to cancel
Returns:
- error: Any error encountered during cancellation
Example:
err := client.CancelResendingEncryptedMessage(envHash)
if err != nil {
log.Printf("Failed to cancel resending: %v", err)
}
func (*ThinClient) Close ¶
func (t *ThinClient) Close() error
Close gracefully shuts down the thin client and closes the daemon connection.
This method performs a clean shutdown by:
- Sending a close notification to the daemon
- Closing the network connection
- Stopping all background workers
After calling Close(), the ThinClient instance should not be used further. Any ongoing operations will be interrupted and may return errors.
Returns:
- error: Any error encountered during shutdown
func (*ThinClient) CreateCourierEnvelopesFromMultiPayload ¶
func (t *ThinClient) CreateCourierEnvelopesFromMultiPayload(destinations []DestinationPayload, isStart bool, isLast bool, buffer []byte) (*CreateEnvelopesResult, error)
CreateCourierEnvelopesFromMultiPayload creates CourierEnvelopes from multiple payloads going to different destination channels. This is more space-efficient than calling CreateCourierEnvelopesFromPayload multiple times because all envelopes from all destinations are packed together in the same encoder without wasting space.
This method is stateless — the buffer parameter enables continuation across multiple calls without daemon-side state. Pass the Buffer from the previous call's result to avoid wasting space in the last box of each call. On the first call, buffer should be nil.
Parameters:
- destinations: Slice of DestinationPayload specifying payloads and their destination channels
- isStart: Whether this is the first call (sets IsStart flag on first element)
- isLast: Set to true on the final call to flush the encoder
- buffer: Residual encoder buffer from previous call (nil on first call)
Returns:
- *CreateEnvelopesResult: Contains envelopes, buffer state, and NextDestIndices
- error: Any error encountered
func (*ThinClient) CreateCourierEnvelopesFromPayload ¶
func (t *ThinClient) CreateCourierEnvelopesFromPayload(payload []byte, destWriteCap *bacap.WriteCap, destStartIndex *bacap.MessageBoxIndex, isStart bool, isLast bool) (envelopes [][]byte, nextDestIndex *bacap.MessageBoxIndex, err error)
CreateCourierEnvelopesFromPayload creates multiple CourierEnvelopes from a payload of any size.
This method is stateless — no daemon state is kept between calls. Each call creates a fresh encoder, encodes all envelopes, flushes, and returns. The payload is limited to 10MB to prevent accidental memory exhaustion.
Each returned chunk is a serialized CopyStreamElement ready to be written to a box. The caller controls the copy stream boundaries via isStart and isLast flags.
The returned chunks must be written to a temporary copy stream channel using EncryptWrite + StartResendingEncryptedMessage. After the stream is complete, send a Copy command to the courier with the write capability for the temp stream.
Parameters:
- payload: The data to be written (max 10MB)
- destWriteCap: Write capability for the destination channel
- destStartIndex: Starting index in the destination channel
- isStart: Whether this is the first call (sets IsStart flag on first element)
- isLast: Whether this is the last call (sets IsFinal flag on last element)
Returns:
- [][]byte: Slice of CopyStreamElements ready to write to the copy stream
- *bacap.MessageBoxIndex: Next destination index (use as destStartIndex in next call)
- error: Any error encountered during envelope creation
func (*ThinClient) CreateCourierEnvelopesFromTombstoneRange ¶
func (t *ThinClient) CreateCourierEnvelopesFromTombstoneRange( destWriteCap *bacap.WriteCap, destStartIndex *bacap.MessageBoxIndex, maxCount uint32, isStart bool, isLast bool, buffer []byte, ) (envelopes [][]byte, nextBuffer []byte, nextDestIndex *bacap.MessageBoxIndex, err error)
CreateCourierEnvelopesFromTombstoneRange creates tombstone CourierEnvelopes for a range of destination indices, encoded as copy stream elements ready to be written to a temporary copy stream channel.
This combines the tombstone creation logic (SignBox with empty payload) with the courier envelope wrapping and copy stream encoding of CreateCourierEnvelopesFromPayload.
The buffer parameter enables stateless continuation across multiple calls without wasting space in the last box. Pass nil on the first call, then pass the returned nextBuffer to the next call.
Parameters:
- destWriteCap: Write capability for the destination channel
- destStartIndex: Starting index in the destination channel
- maxCount: Number of tombstones to create
- isStart: Whether this is the first call (sets IsStart flag on first element)
- isLast: Whether this is the last call (sets IsFinal flag on last element)
- buffer: Residual encoder buffer from previous call (nil on first call)
Returns:
- [][]byte: Slice of CopyStreamElements ready to write to the copy stream
- []byte: Residual buffer for next call (nil when isLast=true)
- *bacap.MessageBoxIndex: Next destination index
- error: Any error encountered
func (*ThinClient) Dial ¶
func (t *ThinClient) Dial() error
Dial establishes a connection to the client daemon and initializes the client.
This method performs the complete connection handshake with the client daemon:
- Establishes network connection (TCP or Unix socket)
- Receives initial connection status from daemon
- Receives initial PKI document
- Starts background workers for event handling
The client supports both online and offline modes. In offline mode (when the daemon is not connected to the mixnet), channel preparation operations will work but actual message transmission will fail.
After successful connection, the client will automatically handle:
- PKI document updates
- Connection status changes
- Event distribution to application code
Returns:
- error: Any error encountered during connection or handshake
Example:
client := thin.NewThinClient(cfg, logging)
err := client.Dial()
if err != nil {
log.Fatal("Failed to connect to daemon:", err)
}
defer client.Close()
if !client.IsConnected() {
log.Println("Daemon is offline - limited functionality available")
}
func (*ThinClient) Disconnect ¶
func (t *ThinClient) Disconnect() error
Disconnect closes the connection without sending ThinClose. The daemon preserves all state for this client's app ID, allowing the client to reconnect and resume with the same session token.
func (*ThinClient) EncryptRead ¶
func (t *ThinClient) EncryptRead(readCap *bacap.ReadCap, messageBoxIndex *bacap.MessageBoxIndex) (messageCiphertext []byte, envelopeDescriptor []byte, envelopeHash *[32]byte, nextMessageBoxIndex *bacap.MessageBoxIndex, err error)
EncryptRead encrypts a read operation for a given read capability.
This method prepares an encrypted read request that can be sent to the courier service to retrieve a message from a pigeonhole box. The returned ciphertext should be sent via StartResendingEncryptedMessage.
Parameters:
- readCap: Read capability that grants access to the channel
- messageBoxIndex: Starting read position for the channel
Returns:
- []byte: Encrypted message ciphertext to send to courier
- []byte: Envelope descriptor for decrypting the reply
- *[32]byte: Hash of the courier envelope
- *bacap.MessageBoxIndex: Next message box index for subsequent reads
- error: Any error encountered during encryption
Example:
ciphertext, envDesc, envHash, nextIndex, err := client.EncryptRead(
readCap, messageBoxIndex)
if err != nil {
log.Fatal("Failed to encrypt read:", err)
}
// Send ciphertext via StartResendingEncryptedMessage
func (*ThinClient) EncryptWrite ¶
func (t *ThinClient) EncryptWrite(plaintext []byte, writeCap *bacap.WriteCap, messageBoxIndex *bacap.MessageBoxIndex) (messageCiphertext []byte, envelopeDescriptor []byte, envelopeHash *[32]byte, nextMessageBoxIndex *bacap.MessageBoxIndex, err error)
EncryptWrite encrypts a write operation for a given write capability.
This method prepares an encrypted write request that can be sent to the courier service to store a message in a pigeonhole box. The returned ciphertext should be sent via StartResendingEncryptedMessage.
Parameters:
- plaintext: The plaintext message to encrypt
- writeCap: Write capability that grants access to the channel
- messageBoxIndex: Starting write position for the channel
Returns:
- []byte: Encrypted message ciphertext to send to courier
- []byte: Envelope descriptor for decrypting the reply
- *[32]byte: Hash of the courier envelope
- *bacap.MessageBoxIndex: Next message box index for subsequent writes
- error: Any error encountered during encryption
Example:
plaintext := []byte("Hello, Bob!")
ciphertext, envDesc, envHash, nextIndex, err := client.EncryptWrite(
plaintext, writeCap, messageBoxIndex)
if err != nil {
log.Fatal("Failed to encrypt write:", err)
}
// Send ciphertext via StartResendingEncryptedMessage
func (*ThinClient) EventSink ¶
func (t *ThinClient) EventSink() chan Event
EventSink returns a buffered channel that receives all events from the thin client.
This method creates a new event channel that will receive copies of all events generated by the thin client, including:
- Connection status changes
- PKI document updates
- Message sent confirmations
- Message replies
- Channel operation results
- Error notifications
The returned channel is buffered with capacity 1. Events are never silently dropped: the fan-out worker blocks until the subscriber accepts each event, matching the "no loss" contract the Rust and Python thin clients uphold. Consequently an application that stops consuming from its sink will stall the entire fan-out (including events destined for other subscribers); applications must drain promptly or call StopEventSink() to release their subscription.
Important: Always call StopEventSink() when done with the channel to prevent resource leaks and ensure proper cleanup.
Note: The event sink channel is NOT closed when the client shuts down. Consumers should also select on HaltCh() to detect shutdown, or they can check for a ShutdownEvent in the event stream.
Returns:
- chan Event: A buffered channel that will receive all client events
Example:
eventSink := client.EventSink()
defer client.StopEventSink(eventSink)
for {
select {
case event := <-eventSink:
switch e := event.(type) {
case *MessageReplyEvent:
fmt.Printf("Received reply: %s\n", e.Payload)
case *ConnectionStatusEvent:
fmt.Printf("Connection status: %v\n", e.IsConnected)
case *NewDocumentEvent:
fmt.Printf("New PKI document for epoch %d\n", e.Document.Epoch)
}
case <-client.HaltCh():
return // Client is shutting down
}
}
func (*ThinClient) GetAllCouriers ¶
func (t *ThinClient) GetAllCouriers() (couriers []CourierDescriptor, err error)
GetAllCouriers returns all available courier services from the current PKI document. Use this to select specific couriers for nested copy commands.
func (*ThinClient) GetConfig ¶
func (t *ThinClient) GetConfig() *Config
GetConfig returns the client's configuration.
Returns:
- *Config: The configuration used to create this client
func (*ThinClient) GetDistinctCouriers ¶
func (t *ThinClient) GetDistinctCouriers(n int) (couriers []CourierDescriptor, err error)
GetDistinctCouriers returns N distinct random couriers. Returns an error if fewer than N couriers are available.
func (*ThinClient) GetLogger ¶
func (t *ThinClient) GetLogger(prefix string) *logging.Logger
GetLogger returns a logger instance with the specified prefix.
This allows applications to create loggers that integrate with the thin client's logging system and maintain consistent log formatting.
Parameters:
- prefix: String prefix for log messages from this logger
Returns:
- *logging.Logger: A logger instance with the specified prefix
func (*ThinClient) GetMessageBoxIndexCounter ¶
func (t *ThinClient) GetMessageBoxIndexCounter(messageBoxIndex *bacap.MessageBoxIndex) (uint64, error)
GetMessageBoxIndexCounter returns the BACAP Idx64 counter embedded in a MessageBoxIndex. Callers can use this to order or compare two indexes without having to know bacap.MessageBoxIndex's binary layout.
Parameters:
- messageBoxIndex: the index to inspect
Returns:
- uint64: the Idx64 counter
- error: any error encountered
func (*ThinClient) GetService ¶
func (t *ThinClient) GetService(serviceName string) (*common.ServiceDescriptor, error)
GetService returns a randomly selected service matching the specified capability.
This method is a convenience wrapper around GetServices() that randomly selects one service from all available services with the given capability. This provides automatic load balancing across available service instances.
Parameters:
- serviceName: The name of the service capability to find
Returns:
- *common.ServiceDescriptor: A randomly selected service descriptor
- error: Error if no services with the capability are found
Example:
// Get a random courier service for load balancing
courier, err := client.GetService("courier")
if err != nil {
log.Fatal("No courier service available:", err)
}
fmt.Printf("Using courier: %s\n", courier.Name)
func (*ThinClient) GetServices ¶
func (t *ThinClient) GetServices(capability string) ([]*common.ServiceDescriptor, error)
GetServices returns all services matching the specified capability name.
This method searches the current PKI document for services that provide the specified capability. Services in Katzenpost are identified by their capability names (e.g., "echo", "courier", "keyserver").
Parameters:
- capability: The name of the service capability to search for
Returns:
- []*common.ServiceDescriptor: Slice of all matching service descriptors
- error: Error if no services with the capability are found
Example:
// Find all courier services
couriers, err := client.GetServices("courier")
if err != nil {
log.Fatal("No courier services available:", err)
}
fmt.Printf("Found %d courier services\n", len(couriers))
func (*ThinClient) IsConnected ¶
func (t *ThinClient) IsConnected() bool
IsConnected returns true if the client daemon is connected to the mixnet.
This indicates whether the daemon has an active connection to the mixnet infrastructure. When false, the client is in "offline mode" where channel operations (prepare operations) will work but actual message transmission will fail.
Returns:
- bool: true if daemon is connected to mixnet, false otherwise
func (*ThinClient) NewKeypair ¶
func (t *ThinClient) NewKeypair(seed []byte) (writeCap *bacap.WriteCap, readCap *bacap.ReadCap, firstMessageIndex *bacap.MessageBoxIndex, err error)
NewKeypair creates a new keypair for use with the Pigeonhole protocol.
This method generates a WriteCap and ReadCap from the provided seed using the BACAP (Blinding-and-Capability) protocol. The WriteCap should be stored securely for writing messages, while the ReadCap can be shared with others to allow them to read messages.
Parameters:
- seed: 32-byte seed used to derive the keypair
Returns:
- *bacap.WriteCap: Write capability for sending messages
- *bacap.ReadCap: Read capability that can be shared with recipients
- *bacap.MessageBoxIndex: First message index to use when writing
- error: Any error encountered during keypair creation
Example:
seed := make([]byte, 32)
_, err := rand.Reader.Read(seed)
if err != nil {
log.Fatal(err)
}
writeCap, readCap, firstIndex, err := client.NewKeypair(seed)
if err != nil {
log.Fatal("Failed to create keypair:", err)
}
// Share readCap with Bob so he can read messages
// Store writeCap for sending messages
func (*ThinClient) NewMessageID ¶
func (t *ThinClient) NewMessageID() *[MessageIDLength]byte
NewMessageID generates a new cryptographically random message identifier.
Message IDs are used to correlate requests with responses in both legacy and channel APIs. Each message should have a unique ID to prevent confusion and enable proper event correlation.
Returns:
- *[MessageIDLength]byte: A new random message ID
Panics:
- If the random number generator fails
func (*ThinClient) NewQueryID ¶
func (t *ThinClient) NewQueryID() *[QueryIDLength]byte
NewQueryID generates a new cryptographically random query identifier.
Query IDs are used in the channel API to correlate channel operation requests with their responses. Each query should have a unique ID.
Returns:
- *[QueryIDLength]byte: A new random query ID
Panics:
- If the random number generator fails
func (*ThinClient) NewSURBID ¶
func (t *ThinClient) NewSURBID() *[sConstants.SURBIDLength]byte
NewSURBID generates a new Single Use Reply Block identifier.
SURB IDs are used in the legacy API to correlate reply messages with their original requests. Each SURB should have a unique ID.
Returns:
- *[sConstants.SURBIDLength]byte: A new random SURB ID
func (*ThinClient) NextMessageBoxIndex ¶
func (t *ThinClient) NextMessageBoxIndex(messageBoxIndex *bacap.MessageBoxIndex) (nextMessageBoxIndex *bacap.MessageBoxIndex, err error)
NextMessageBoxIndex increments a MessageBoxIndex using the BACAP NextIndex method.
This method is used when sending multiple messages to different mailboxes using the same WriteCap or ReadCap. It properly advances the cryptographic state by:
- Incrementing the Idx64 counter
- Deriving new encryption and blinding keys using HKDF
- Updating the HKDF state for the next iteration
The client daemon handles the cryptographic operations using our BACAP library documented here: https://pkg.go.dev/github.com/katzenpost/hpqc/bacap
Parameters:
- messageBoxIndex: Current message box index to increment
Returns:
- *bacap.MessageBoxIndex: The next message box index
- error: Any error encountered during increment
Example:
nextIndex, err := client.NextMessageBoxIndex(currentIndex)
if err != nil {
log.Fatal("Failed to increment index:", err)
}
// Use nextIndex for the next message
func (*ThinClient) PKIDocument ¶
func (t *ThinClient) PKIDocument() *cpki.Document
PKIDocument returns the thin client's current PKI document.
The PKI document contains the current network topology, service information, and cryptographic parameters for the current epoch. This document is automatically updated when the client daemon receives new PKI information.
Returns:
- *cpki.Document: The current PKI document, or nil if none available
Thread-safe: This method can be called concurrently from multiple goroutines.
func (*ThinClient) PKIDocumentForEpoch ¶
func (t *ThinClient) PKIDocumentForEpoch(epoch uint64) (*cpki.Document, error)
PKIDocumentForEpoch returns the PKI document for a specific epoch from cache.
This method provides access to PKI documents from previous epochs that are cached by the client. This is important for maintaining consistency during epoch transitions where different participants might be using PKI documents from different epochs, which can lead to different envelope hashes and communication failures.
The client automatically caches the last 5 epochs of PKI documents. If the requested epoch is not in cache, the current document is returned as a fallback.
Parameters:
- epoch: The epoch number for which to retrieve the PKI document
Returns:
- *cpki.Document: The PKI document for the specified epoch
- error: Error if no document is available for the epoch
Example:
// Get document for a specific epoch during channel operations
doc, err := client.PKIDocumentForEpoch(12345)
if err != nil {
log.Printf("No PKI document for epoch %d: %v", 12345, err)
return
}
// Use doc for epoch-specific operations...
func (*ThinClient) SendMessage ¶
func (t *ThinClient) SendMessage(surbID *[sConstants.SURBIDLength]byte, payload []byte, destNode *[32]byte, destQueue []byte) error
SendMessage sends a message with reply capability using the legacy API.
This method sends a message with a Single Use Reply Block (SURB) that allows the destination to send a reply. The method is asynchronous - it only blocks until the daemon receives the send request, not until the message is actually transmitted or a reply is received.
To receive replies, applications must monitor events from EventSink() and look for MessageReplyEvent instances with matching SURB IDs.
Requirements:
- The daemon must be connected to the mixnet (IsConnected() == true)
- The destination service must be available in the current PKI document
- A unique SURB ID must be provided for reply correlation
Parameters:
- surbID: Unique identifier for correlating replies (use NewSURBID())
- payload: Message data to send
- destNode: Hash of the destination service's identity key
- destQueue: Queue ID of the destination service
Returns:
- error: Any error encountered during message preparation or sending
Example:
// Create event sink to receive replies
eventSink := client.EventSink()
defer client.StopEventSink(eventSink)
// Send message with reply capability
surbID := client.NewSURBID()
echoService, _ := client.GetService("echo")
destNode := hash.Sum256(echoService.MixDescriptor.IdentityKey)
err := client.SendMessage(surbID, []byte("Hello"), &destNode, echoService.RecipientQueueID)
// Wait for reply in event loop
for event := range eventSink {
if reply, ok := event.(*MessageReplyEvent); ok {
if bytes.Equal(reply.SURBID[:], surbID[:]) {
fmt.Printf("Reply: %s\n", reply.Payload)
break
}
}
}
func (*ThinClient) SendMessageWithoutReply ¶
func (t *ThinClient) SendMessageWithoutReply(payload []byte, destNode *[32]byte, destQueue []byte) error
SendMessageWithoutReply sends a fire-and-forget message using the legacy API.
This method sends a message without any reply capability. The message is encapsulated in a Sphinx packet and sent through the mixnet, but no response can be received. This is suitable for notifications or one-way communication.
Requirements:
- The daemon must be connected to the mixnet (IsConnected() == true)
- The destination service must be available in the current PKI document
Parameters:
- payload: Message data to send
- destNode: Hash of the destination service's identity key
- destQueue: Queue ID of the destination service
Returns:
- error: Any error encountered during message preparation or sending
Example:
// Find an echo service
echoService, err := client.GetService("echo")
if err != nil {
return err
}
// Send a fire-and-forget message
destNode := hash.Sum256(echoService.MixDescriptor.IdentityKey)
destQueue := echoService.RecipientQueueID
err = client.SendMessageWithoutReply([]byte("Hello"), &destNode, destQueue)
func (*ThinClient) Shutdown ¶
func (t *ThinClient) Shutdown()
Shutdown cleanly shuts down the ThinClient instance.
This method stops all background workers and closes the connection to the client daemon. It is equivalent to calling Halt() and is provided for compatibility. For proper cleanup, prefer using Close().
func (*ThinClient) StartResendingCopyCommand ¶
func (t *ThinClient) StartResendingCopyCommand(writeCap *bacap.WriteCap) error
StartResendingCopyCommand sends a copy command via ARQ and blocks until completion.
This method BLOCKS until a reply is received. It uses the ARQ (Automatic Repeat reQuest) mechanism to reliably send copy commands to the courier, automatically retrying if the reply is not received in time.
The copy command instructs the courier to read from a temporary copy stream channel and write the parsed envelopes to their destination channels. The courier:
- Derives a ReadCap from the WriteCap
- Reads boxes from the temporary channel
- Parses boxes into CourierEnvelopes
- Sends each envelope to intermediate replicas for replication
- Writes tombstones to clean up the temporary channel
Parameters:
- writeCap: Write capability for the temporary copy stream channel
Returns:
- error: Any error encountered during the operation
Example:
err := client.StartResendingCopyCommand(tempWriteCap)
if err != nil {
log.Fatal("Copy command failed:", err)
}
func (*ThinClient) StartResendingCopyCommandWithCourier ¶
func (t *ThinClient) StartResendingCopyCommandWithCourier( writeCap *bacap.WriteCap, courierIdentityHash *[32]byte, courierQueueID []byte, ) error
StartResendingCopyCommandWithCourier sends a copy command to a specific courier.
This method is like StartResendingCopyCommand but allows specifying which courier should process the copy command. This is useful for nested copy commands where different couriers should handle different layers for improved privacy.
Parameters:
- writeCap: Write capability for the temporary copy stream channel
- courierIdentityHash: Hash of the courier's identity key
- courierQueueID: Queue ID for the courier service
Returns:
- error: Any error encountered during the operation
func (*ThinClient) StartResendingEncryptedMessage ¶
func (t *ThinClient) StartResendingEncryptedMessage(readCap *bacap.ReadCap, writeCap *bacap.WriteCap, messageBoxIndex []byte, replyIndex *uint8, envelopeDescriptor []byte, messageCiphertext []byte, envelopeHash *[32]byte) (*StartResendingResult, error)
StartResendingEncryptedMessage sends an encrypted message via ARQ and blocks until completion.
This method BLOCKS until a reply is received. CancelResendingEncryptedMessage is only useful when called from another goroutine to interrupt this blocking call.
The message will be resent periodically until either:
- A reply is received from the courier (this method returns)
- The message is cancelled via CancelResendingEncryptedMessage (from another goroutine)
- The client is shut down
This is used for both read and write operations in the new Pigeonhole API.
The daemon implements a finite state machine (FSM) for handling the stop-and-wait ARQ protocol:
- For default write operations (writeCap != nil, readCap == nil, noIdempotentBoxAlreadyExists == false): The method waits for an ACK from the courier and returns immediately. The ACK confirms the courier received the envelope and will dispatch it to both shard replicas. This requires only a single round-trip through the mixnet.
- For BoxAlreadyExists-aware writes (noIdempotentBoxAlreadyExists == true): The method waits for an ACK, then sends a second SURB to retrieve the replica's error code. This requires two round-trips through the mixnet.
- For read operations (readCap != nil, writeCap == nil): The method waits for an ACK from the courier, then the daemon automatically sends a new SURB to request the payload, and this method waits for the payload. The daemon performs all decryption (MKEM envelope + BACAP payload) and returns the fully decrypted plaintext.
Parameters:
- readCap: Read capability (can be nil for write operations, required for reads)
- writeCap: Write capability (can be nil for read operations, required for writes)
- messageBoxIndex: Current message box index being operated on (required for reads)
- replyIndex: Index of the reply to use (typically 0 or 1)
- envelopeDescriptor: Serialized envelope descriptor for MKEM decryption
- messageCiphertext: MKEM-encrypted message to send (from EncryptRead or EncryptWrite)
- envelopeHash: Hash of the courier envelope
Returns:
- *StartResendingResult: Contains Plaintext (decrypted message for reads, empty for writes), CourierIdentityHash (hash of the courier that handled this message), and CourierQueueID (queue ID of that courier).
- error: Any error encountered during the operation. Specific errors can be checked using errors.Is():
- ErrBoxIDNotFound: The requested box ID was not found on the replica
- ErrInvalidBoxID: The box ID format is invalid
- ErrInvalidSignature: Signature verification failed
- ErrDatabaseFailure: Replica database error
- ErrInvalidPayload: Invalid payload data
- ErrStorageFull: Replica storage capacity exceeded
- ErrReplicaInternalError: Internal replica error
- ErrInvalidEpoch: Invalid or expired epoch
- ErrReplicationFailed: Replication to other replicas failed
- ErrMKEMDecryptionFailed: MKEM envelope decryption failed (outer layer)
- ErrBACAPDecryptionFailed: BACAP payload decryption failed (inner layer)
- ErrStartResendingCancelled: Operation was cancelled via CancelResendingEncryptedMessage
Example:
result, err := client.StartResendingEncryptedMessage(
readCap, nil, nextIndex, &replyIdx, envDesc, ciphertext, envHash)
if err != nil {
if errors.Is(err, thin.ErrBoxIDNotFound) {
log.Println("Box not found - may be empty or expired")
} else {
log.Fatal("Failed to start resending:", err)
}
}
fmt.Printf("Received: %s\n", result.Plaintext)
func (*ThinClient) StartResendingEncryptedMessageNoRetry ¶
func (t *ThinClient) StartResendingEncryptedMessageNoRetry(readCap *bacap.ReadCap, writeCap *bacap.WriteCap, messageBoxIndex []byte, replyIndex *uint8, envelopeDescriptor []byte, messageCiphertext []byte, envelopeHash *[32]byte) (*StartResendingResult, error)
StartResendingEncryptedMessageNoRetry is like StartResendingEncryptedMessage but disables automatic retries on BoxIDNotFound errors. Use this when you want immediate error feedback rather than waiting for potential replication lag to resolve. The CancelResendingEncryptedMessage method can cancel operations started with either method.
func (*ThinClient) StartResendingEncryptedMessageReturnBoxExists ¶
func (t *ThinClient) StartResendingEncryptedMessageReturnBoxExists(readCap *bacap.ReadCap, writeCap *bacap.WriteCap, messageBoxIndex []byte, replyIndex *uint8, envelopeDescriptor []byte, messageCiphertext []byte, envelopeHash *[32]byte) (*StartResendingResult, error)
StartResendingEncryptedMessageReturnBoxExists is like StartResendingEncryptedMessage but returns BoxAlreadyExists errors instead of treating them as idempotent success. Use this when you want to detect whether a write was actually performed or if the box already existed. The CancelResendingEncryptedMessage method can cancel operations started with this method.
func (*ThinClient) StopEventSink ¶
func (t *ThinClient) StopEventSink(ch chan Event)
StopEventSink stops sending events to the specified channel and cleans up resources.
This method removes the channel from the event distribution system and should be called when the application is done processing events from a channel returned by EventSink(). Failure to call this method may result in resource leaks and continued event processing overhead.
Parameters:
- ch: The event channel returned by EventSink() to stop
Example:
eventSink := client.EventSink() defer client.StopEventSink(eventSink) // Ensure cleanup // Process events...
func (*ThinClient) TombstoneRange ¶
func (c *ThinClient) TombstoneRange( writeCap *bacap.WriteCap, start *bacap.MessageBoxIndex, maxCount uint32, ) (result *TombstoneRangeResult, err error)
TombstoneRange creates tombstones for a range of pigeonhole boxes. Tombstones are created by calling EncryptWrite with an empty plaintext. The daemon detects this and signs empty payloads instead of encrypting, which the replica recognizes as deletion requests.
To tombstone a single box, use maxCount=1.
type ThinClose ¶
type ThinClose struct {
}
ThinClose is used to indicate that the thin client is disconnecting from the daemon.
type ThinResponse ¶
type ThinResponse struct {
// SURBID is a unique identifier for this response that should precisely
// match the application's chosen SURBID of the sent message. This allows
// the application to correlate responses with requests when using the
// legacy SendMessage API.
SURBID *[sConstants.SURBIDLength]byte
// ID is the unique identifier for the corresponding sent message.
// This is used for message correlation in the legacy API.
ID *[MessageIDLength]byte
// Payload contains the decrypted response data from the destination service.
// The format and content depend on the service being contacted.
Payload []byte
}
ThinResponse encapsulates a message response from the mixnet that is passed to the client application. This is part of the legacy API.
ThinResponse contains the decrypted reply payload along with identifiers that allow the application to correlate responses with the original requests.
type TombstoneEnvelope ¶
type TombstoneEnvelope struct {
MessageCiphertext []byte
EnvelopeDescriptor []byte
EnvelopeHash *[32]byte
BoxIndex *bacap.MessageBoxIndex
}
type TombstoneRangeResult ¶
type TombstoneRangeResult struct {
Envelopes []*TombstoneEnvelope
Next *bacap.MessageBoxIndex
}