Documentation
¶
Overview ¶
Package client is the UI-independent Propagare client core.
Frontends should depend on this package (or generated bindings around it), never on node transports or cryptographic primitives directly. The Core owns replication, signed receipts, storage audits, fallback, fetch, and capability-based deletion.
The direct node transport supports CA-PKI-free, identity-pinned hybrid TLS, but remains a direct reference/bootstrap transport. A production frontend must use an audited onion or mix provider before claiming metadata anonymity.
Index ¶
- Constants
- Variables
- func OpenDirectItem(privateHPKEKey []byte, item protocol.StoredItem) ([]byte, error)
- func RandomCapability() (string, error)
- func RandomDeleteToken() ([]byte, error)
- type ClientStorageUsage
- type ClientStore
- type Config
- type Core
- func (c *Core) Audit(ctx context.Context, delivery Delivery) map[string]error
- func (c *Core) AuditAndRepair(ctx context.Context, delivery Delivery) (Delivery, map[string]error, error)
- func (c *Core) ClientStorageUsage() (ClientStorageUsage, bool)
- func (c *Core) Close() error
- func (c *Core) Delete(ctx context.Context, delivery Delivery) map[string]error
- func (c *Core) DeleteItemEverywhere(ctx context.Context, itemID string, deleteToken []byte) map[string]error
- func (c *Core) Fetch(ctx context.Context, routeTags []string) ([]protocol.StoredItem, error)
- func (c *Core) LoadDelivery(ctx context.Context, itemID string, now time.Time) (Delivery, error)
- func (c *Core) PendingDeliveries(ctx context.Context, afterItemID string, limit int, now time.Time) ([]Delivery, error)
- func (c *Core) PruneClientStorage(ctx context.Context, targetBytes int64, now time.Time) (PruneReport, error)
- func (c *Core) Reputation() []NodeScore
- func (c *Core) SendDirect(ctx context.Context, recipientHPKEPublicKey []byte, routeTag string, ...) (Delivery, error)
- func (c *Core) SetClientStorageLimit(ctx context.Context, maxBytes int64, now time.Time) (PruneReport, error)
- func (c *Core) StoreOpaque(ctx context.Context, routeTag string, encryptedPayload, deleteToken []byte) (Delivery, error)
- func (c *Core) StoreReplicated(ctx context.Context, item protocol.StoredItem, deleteToken []byte) (Delivery, error)
- type Delivery
- type DirectoryBootstrap
- type DiskClientStoreConfig
- type EncryptedDiskStore
- func (store *EncryptedDiskStore) Close() error
- func (store *EncryptedDiskStore) Delete(ctx context.Context, id string) error
- func (store *EncryptedDiskStore) Get(ctx context.Context, id string) (LocalRecord, error)
- func (store *EncryptedDiskStore) ListIDs(ctx context.Context, kind, afterID string, limit int) ([]string, error)
- func (store *EncryptedDiskStore) PruneTo(ctx context.Context, targetBytes int64, now time.Time) (PruneReport, error)
- func (store *EncryptedDiskStore) Put(ctx context.Context, record LocalRecord, now time.Time) (PruneReport, error)
- func (store *EncryptedDiskStore) SetLimit(ctx context.Context, maxBytes int64, now time.Time) (PruneReport, error)
- func (store *EncryptedDiskStore) Usage() ClientStorageUsage
- type EncryptedSyncReplayStore
- type HTTPNode
- func ConnectDirectoryRecords(ctx context.Context, directory VerifiedDirectory, limit int, ...) ([]*HTTPNode, error)
- func ConnectDirectoryRecordsForDevelopment(ctx context.Context, directory VerifiedDirectory, limit int, ...) ([]*HTTPNode, error)
- func ConnectPinnedHTTPNode(ctx context.Context, baseURL string, identity protocol.NodePublicIdentity, ...) (*HTTPNode, error)
- func DiscoverHTTPNode(ctx context.Context, baseURL string, httpClient *http.Client) (*HTTPNode, error)
- func DiscoverHTTPNodeForDevelopment(ctx context.Context, baseURL string, httpClient *http.Client) (*HTTPNode, error)
- func (n *HTTPNode) BaseURL() string
- func (n *HTTPNode) Delete(ctx context.Context, request protocol.DeleteRequest) (protocol.DeleteReceipt, error)
- func (n *HTTPNode) Fetch(ctx context.Context, routeTags []string) ([]protocol.StoredItem, error)
- func (n *HTTPNode) Identity() protocol.NodePublicIdentity
- func (n *HTTPNode) Parameters(ctx context.Context) (protocol.NodeParameters, error)
- func (n *HTTPNode) Prove(ctx context.Context, request protocol.ProofRequest) (protocol.StorageProof, error)
- func (n *HTTPNode) Store(ctx context.Context, item protocol.StoredItem) (protocol.StorageReceipt, error)
- type LocalRecord
- type NodeScore
- type PrunePolicy
- type PruneReport
- type Reputation
- func (r *Reputation) Allowed(nodeID string, now time.Time) bool
- func (r *Reputation) Exclude(nodeID string, duration time.Duration, reason error)
- func (r *Reputation) Failure(nodeID string, err error)
- func (r *Reputation) Snapshot() []NodeScore
- func (r *Reputation) Success(nodeID string, proof bool)
- type VerifiedDirectory
Constants ¶
const ( ClientStoreVersion = 1 DefaultClientStorageBytes = int64(10 * 1024 * 1024 * 1024) MaxClientStorageBytes = DefaultClientStorageBytes MinClientStorageBytes = int64(64 * 1024) MaxLocalRecordPayloadBytes = 4 * 1024 * 1024 MaxClientStoreRecords = 262_144 MaxClientStoreListPage = 1024 )
const ( LocalKindDelivery = "delivery" LocalKindInbox = "inbox" LocalKindOutbox = "outbox" LocalKindFileCache = "file_cache" LocalKindReplay = "replay" LocalKindGroupState = "group_state" LocalKindDeviceEvent = "device_event" LocalKindReputation = "reputation" )
const MaxClientNodes = 64
const (
MaxPendingDeliveryPage = 256
)
Variables ¶
var ( ErrInvalidDeliveryState = errors.New("invalid delivery state") ErrNoAllowedNodes = errors.New("no non-excluded node is available") )
var ( ErrDeliveryExpired = errors.New("persisted delivery has expired") ErrDeliveryStateChanged = errors.New("persisted delivery state changed during the operation") )
var ( ErrUntrustedNodeTransport = errors.New("node transport is not authenticated") ErrNodeTransport = errors.New("node transport failed") ErrNodeHTTPStatus = errors.New("node returned an HTTP error") ErrInvalidNodeResponse = errors.New("node returned an invalid response") ErrNodeResponseTooLarge = errors.New("node response exceeds size limit") )
var ( ErrClientStoreClosed = errors.New("client store is closed") ErrClientStorageFull = errors.New("client storage limit reached with no safely prunable records") ErrLocalRecordExists = errors.New("local client record already exists") ErrLocalRecordNotFound = errors.New("local client record not found") )
var ErrSyncEventReplay = errors.New("device-sync event was already accepted")
Functions ¶
func OpenDirectItem ¶
func OpenDirectItem(privateHPKEKey []byte, item protocol.StoredItem) ([]byte, error)
func RandomCapability ¶
func RandomDeleteToken ¶
Types ¶
type ClientStorageUsage ¶
type ClientStore ¶
type ClientStore interface {
Put(ctx context.Context, record LocalRecord, now time.Time) (PruneReport, error)
Get(ctx context.Context, id string) (LocalRecord, error)
ListIDs(ctx context.Context, kind, afterID string, limit int) ([]string, error)
Delete(ctx context.Context, id string) error
PruneTo(ctx context.Context, targetBytes int64, now time.Time) (PruneReport, error)
SetLimit(ctx context.Context, maxBytes int64, now time.Time) (PruneReport, error)
Usage() ClientStorageUsage
Close() error
}
ClientStore is the framework-independent persistence boundary used by Core. Put must not return success until the record is durably committed: Core uses that return as the barrier before an external storage side effect. ListIDs must return at most limit IDs, strictly increasing and unique, all greater than afterID and belonging to kind. Core revalidates these requirements so a custom implementation fails closed. Production key material must be loaded from an OS/hardware-backed SecretVault and supplied to the encrypted implementation; it is never written beside the database.
type Config ¶
type Config struct {
Nodes []*HTTPNode
Replicas int
WriteQuorum int
Reputation *Reputation
Store ClientStore
}
type Core ¶
type Core struct {
// contains filtered or unexported fields
}
func NewEphemeralForDevelopment ¶
NewEphemeralForDevelopment explicitly opts out of durable repair and delete capability recovery. It is intended only for tests and private development.
func (*Core) AuditAndRepair ¶
func (c *Core) AuditAndRepair(ctx context.Context, delivery Delivery) (Delivery, map[string]error, error)
AuditAndRepair verifies each acknowledged replica and rewrites the same ciphertext to replacement nodes when any replica no longer proves storage. Valid old receipts are retained and merged with new repair receipts. The operation is serialized per item and the merged state is persisted before any repair Store request is sent.
func (*Core) ClientStorageUsage ¶
func (c *Core) ClientStorageUsage() (ClientStorageUsage, bool)
func (*Core) DeleteItemEverywhere ¶
func (*Core) LoadDelivery ¶
LoadDelivery restores authenticated repair/deletion state after a restart. The encrypted local record and every hybrid node receipt are verified again before the delete capability is returned to the caller.
func (*Core) PendingDeliveries ¶
func (c *Core) PendingDeliveries(ctx context.Context, afterItemID string, limit int, now time.Time) ([]Delivery, error)
PendingDeliveries returns one bounded, stable page of persisted delivery state. Passing the final ItemID from the previous page resumes after it, so a restarted client can discover repair and deletion capabilities without already knowing every item identifier.
func (*Core) PruneClientStorage ¶
func (*Core) Reputation ¶
func (*Core) SendDirect ¶
func (c *Core) SendDirect(ctx context.Context, recipientHPKEPublicKey []byte, routeTag string, plaintext []byte) (Delivery, error)
SendDirect is a development/bootstrap envelope. It does not provide a message ratchet, forward secrecy, post-compromise security, or metadata anonymity. Production messaging must use message.StrictPipeline with audited providers. Every stored item lives for exactly the fixed protocol retention window; earlier removal requires the delete capability.
func (*Core) SetClientStorageLimit ¶
func (*Core) StoreOpaque ¶
func (c *Core) StoreOpaque(ctx context.Context, routeTag string, encryptedPayload, deleteToken []byte) (Delivery, error)
StoreOpaque stores data that is already end-to-end encrypted, for example a fixed-size encrypted file chunk. The node never receives the delete token. The item expires exactly after the fixed protocol retention window.
func (*Core) StoreReplicated ¶
type Delivery ¶
type Delivery struct {
Item protocol.StoredItem `json:"item"`
DeleteToken []byte `json:"delete_token"`
Receipts []protocol.StorageReceipt `json:"receipts"`
FailedNodes map[string]string `json:"failed_nodes,omitempty"`
}
type DirectoryBootstrap ¶
type DiskClientStoreConfig ¶
type EncryptedDiskStore ¶
type EncryptedDiskStore struct {
// contains filtered or unexported fields
}
EncryptedDiskStore uses AES-256-GCM with a fresh random nonce per atomic record write. File names are hashes of record IDs; all record metadata and payload bytes are encrypted at rest.
func NewEncryptedDiskStore ¶
func NewEncryptedDiskStore(config DiskClientStoreConfig, now time.Time) (*EncryptedDiskStore, error)
func (*EncryptedDiskStore) Close ¶
func (store *EncryptedDiskStore) Close() error
func (*EncryptedDiskStore) Delete ¶
func (store *EncryptedDiskStore) Delete(ctx context.Context, id string) error
func (*EncryptedDiskStore) Get ¶
func (store *EncryptedDiskStore) Get(ctx context.Context, id string) (LocalRecord, error)
func (*EncryptedDiskStore) ListIDs ¶
func (store *EncryptedDiskStore) ListIDs(ctx context.Context, kind, afterID string, limit int) ([]string, error)
ListIDs provides bounded, stable pagination without decrypting every record. Metadata used for this index exists only in memory after authenticated store startup; record filenames remain hashed on disk.
func (*EncryptedDiskStore) PruneTo ¶
func (store *EncryptedDiskStore) PruneTo(ctx context.Context, targetBytes int64, now time.Time) (PruneReport, error)
func (*EncryptedDiskStore) Put ¶
func (store *EncryptedDiskStore) Put(ctx context.Context, record LocalRecord, now time.Time) (PruneReport, error)
func (*EncryptedDiskStore) SetLimit ¶
func (store *EncryptedDiskStore) SetLimit(ctx context.Context, maxBytes int64, now time.Time) (PruneReport, error)
func (*EncryptedDiskStore) Usage ¶
func (store *EncryptedDiskStore) Usage() ClientStorageUsage
type EncryptedSyncReplayStore ¶
type EncryptedSyncReplayStore struct {
// contains filtered or unexported fields
}
EncryptedSyncReplayStore implements account.SyncReplayStore with the same authenticated, process-locked disk store used by Core. It does not own the underlying store; the caller remains responsible for closing it.
func NewEncryptedSyncReplayStore ¶
func NewEncryptedSyncReplayStore(store *EncryptedDiskStore) (*EncryptedSyncReplayStore, error)
func (*EncryptedSyncReplayStore) Accept ¶
func (replays *EncryptedSyncReplayStore) Accept(ctx context.Context, accountID, eventID string, expiresAt time.Time) error
Accept durably reserves one account/event pair. Concurrent callers and a process restarted after the atomic file publication can observe at most one successful acceptance.
type HTTPNode ¶
type HTTPNode struct {
// contains filtered or unexported fields
}
HTTPNode deliberately has no exported mutable fields. A production-capable instance can only be created by ConnectPinnedHTTPNode. Discovery returns an informational descriptor, while the explicitly named development constructor permits operational plain HTTP only to literal private or loopback addresses.
func ConnectDirectoryRecords ¶
func ConnectDirectoryRecords(ctx context.Context, directory VerifiedDirectory, limit int, httpClient *http.Client) ([]*HTTPNode, error)
ConnectDirectoryRecords verifies that each contacted endpoint presents the exact hybrid identity admitted by the directory. Limit prevents a directory response from making the client dial an unbounded number of nodes. This production path only permits identity-pinned HTTPS, even when the directory policy admits private IP addresses.
func ConnectDirectoryRecordsForDevelopment ¶
func ConnectDirectoryRecordsForDevelopment(ctx context.Context, directory VerifiedDirectory, limit int, httpClient *http.Client) ([]*HTTPNode, error)
ConnectDirectoryRecordsForDevelopment explicitly opts into plain HTTP for verified records whose endpoint is a literal loopback or private IP. It must not be used by production clients.
func ConnectPinnedHTTPNode ¶
func ConnectPinnedHTTPNode(ctx context.Context, baseURL string, identity protocol.NodePublicIdentity, httpClient *http.Client) (*HTTPNode, error)
ConnectPinnedHTTPNode establishes a CA-PKI-free TLS 1.3 channel whose server key must match the hybrid-signed Node identity supplied by a pinned seed or verified directory record.
func DiscoverHTTPNode ¶
func DiscoverHTTPNodeForDevelopment ¶
func DiscoverHTTPNodeForDevelopment(ctx context.Context, baseURL string, httpClient *http.Client) (*HTTPNode, error)
DiscoverHTTPNodeForDevelopment permits plain HTTP only for literal loopback or private addresses. Production directory callers use ConnectPinnedHTTPNode so route capabilities and delete tokens are encrypted without relying on a public certificate authority.
func (*HTTPNode) Delete ¶
func (n *HTTPNode) Delete(ctx context.Context, request protocol.DeleteRequest) (protocol.DeleteReceipt, error)
func (*HTTPNode) Identity ¶
func (n *HTTPNode) Identity() protocol.NodePublicIdentity
func (*HTTPNode) Parameters ¶
func (*HTTPNode) Prove ¶
func (n *HTTPNode) Prove(ctx context.Context, request protocol.ProofRequest) (protocol.StorageProof, error)
func (*HTTPNode) Store ¶
func (n *HTTPNode) Store(ctx context.Context, item protocol.StoredItem) (protocol.StorageReceipt, error)
type LocalRecord ¶
type LocalRecord struct {
Version uint8 `json:"version"`
ID string `json:"id"`
Kind string `json:"kind"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
PrunePolicy PrunePolicy `json:"prune_policy"`
Payload []byte `json:"payload"`
}
LocalRecord is encrypted as one authenticated unit. PrunePolicy must be explicit: caches and user-selected history use PruneOldest, while pending protocol state uses PruneNever or PruneAfterExpiry.
type NodeScore ¶
type NodeScore struct {
NodeID string `json:"node_id"`
SuccessfulStores uint64 `json:"successful_stores"`
SuccessfulProofs uint64 `json:"successful_proofs"`
Failures uint64 `json:"failures"`
ConsecutiveFails uint32 `json:"consecutive_failures"`
ExcludedUntil time.Time `json:"excluded_until"`
LastFailure string `json:"last_failure,omitempty"`
}
type PrunePolicy ¶
type PrunePolicy string
const ( // PruneNever protects protocol-critical state until explicitly deleted. An // expired record may still be removed because it can no longer be used. PruneNever PrunePolicy = "never" // PruneOldest makes cache/history data immediately eligible for oldest-first // eviction when the configured storage limit is reached. PruneOldest PrunePolicy = "oldest" // PruneAfterExpiry protects the record until ExpiresAt. PruneAfterExpiry PrunePolicy = "after_expiry" )
type PruneReport ¶
type Reputation ¶
type Reputation struct {
// contains filtered or unexported fields
}
func NewReputation ¶
func NewReputation() *Reputation
func (*Reputation) Exclude ¶
func (r *Reputation) Exclude(nodeID string, duration time.Duration, reason error)
func (*Reputation) Failure ¶
func (r *Reputation) Failure(nodeID string, err error)
func (*Reputation) Snapshot ¶
func (r *Reputation) Snapshot() []NodeScore
func (*Reputation) Success ¶
func (r *Reputation) Success(nodeID string, proof bool)
type VerifiedDirectory ¶
type VerifiedDirectory struct {
// contains filtered or unexported fields
}
VerifiedDirectory can only be populated by FetchNodeDirectory outside this package. Its records and trust policy are kept private so callers cannot substitute unsigned records between verification and connection.
func FetchNodeDirectory ¶
func FetchNodeDirectory(ctx context.Context, config DirectoryBootstrap, httpClient *http.Client, now time.Time) (VerifiedDirectory, error)
FetchNodeDirectory reconciles signed snapshots from multiple pinned seeds. The returned records are the union of all successfully verified views; one seed cannot add an unapproved node, although any directory can still omit a node from its own response.
func (VerifiedDirectory) Records ¶
func (directory VerifiedDirectory) Records() []nodedir.Record
Records returns defensive copies for display and node selection UIs. Mutating them cannot affect the records later consumed by ConnectDirectoryRecords.