spotlib

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Dec 21, 2025 License: MIT Imports: 38 Imported by: 1

README

GoDoc

spotlib

Spot connection library for Go. Enables secure, end-to-end encrypted communication between clients through the Spot network.

Features

  • End-to-end encrypted messaging using cryptographic identity cards
  • Automatic connection management with reconnection support
  • Request-response and fire-and-forget messaging patterns
  • net.PacketConn interface for UDP-like communication
  • Event-based status notifications
  • ID card caching with automatic updates

Installation

go get github.com/KarpelesLab/spotlib

Quick Start

Creating a Client
package main

import (
    "context"
    "log"
    "time"

    "github.com/KarpelesLab/spotlib"
)

func main() {
    // Create a new client with an ephemeral key
    client, err := spotlib.New()
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Wait for the client to come online
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := client.WaitOnline(ctx); err != nil {
        log.Fatal("failed to connect:", err)
    }

    log.Println("Connected! Client ID:", client.TargetId())
}
Using a Persistent Identity

The easiest way to maintain the same identity across restarts is to use NewDiskStore:

// Create a disk store that persists keys to ~/.config/spot/ (or equivalent)
store, err := spotlib.NewDiskStore()
if err != nil {
    log.Fatal(err)
}

// Create client with the stored keychain
client, err := spotlib.New(store.Keychain())

The disk store automatically:

  • Creates the storage directory if it doesn't exist
  • Generates a new ECDSA P-256 key if no keys exist
  • Loads existing keys on subsequent runs
  • Stores keys in PEM-encoded PKCS#8 format as id_<type>.key files

You can also specify a custom path:

store, err := spotlib.NewDiskStoreWithPath("/path/to/keys")

Alternatively, manage keys manually:

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"

    "github.com/KarpelesLab/spotlib"
)

// Generate or load your private key
privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)

// Create client with the key
client, err := spotlib.New(privateKey)

You can also pass a *cryptutil.Keychain for more advanced key management.

Sending Messages
Query (Request-Response)
// Send an encrypted query and wait for response
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

response, err := client.Query(ctx, "k.recipientID/endpoint", []byte("hello"))
if err != nil {
    log.Fatal(err)
}
log.Println("Response:", string(response))
Send (Fire-and-Forget)
// Send a one-way encrypted message
err := client.SendTo(ctx, "k.recipientID/endpoint", []byte("hello"))
Receiving Messages

Register handlers for incoming messages on specific endpoints:

import "github.com/KarpelesLab/spotproto"

client.SetHandler("myendpoint", func(msg *spotproto.Message) ([]byte, error) {
    log.Printf("Received from %s: %s", msg.Sender, string(msg.Body))

    // Return a response (or nil for no response)
    return []byte("acknowledged"), nil
})
Monitoring Connection Status
// Listen for status changes
go func() {
    for ev := range client.Events.On("status") {
        status := emitter.Arg[int](ev, 0)
        if status == 1 {
            log.Println("Online!")
        } else {
            log.Println("Offline")
        }
    }
}()

// Or wait for online status
client.Events.On("online") // triggered when going online
PacketConn Interface

For UDP-like communication patterns, use the net.PacketConn interface:

conn, err := client.ListenPacket("udp-endpoint")
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

// Send data
addr := spotlib.SpotAddr("k.recipientID/udp-endpoint")
conn.WriteTo([]byte("hello"), addr)

// Receive data
buf := make([]byte, 4096)
n, remoteAddr, err := conn.ReadFrom(buf)
if err != nil {
    log.Fatal(err)
}
log.Printf("Received %d bytes from %s", n, remoteAddr)
Blob Storage

Store and retrieve encrypted data that persists across sessions:

// Store data (encrypted, only readable by this client)
err := client.StoreBlob(ctx, "my-key", []byte("secret data"))

// Retrieve data
data, err := client.FetchBlob(ctx, "my-key")

// Delete data
err := client.StoreBlob(ctx, "my-key", nil)

Note: Blob storage is best-effort and has a size limit of ~49KB. Data may be purged after extended periods without access.

Group Membership
// Get members of a group
members, err := client.GetGroupMembers(ctx, groupKey)
for _, member := range members {
    log.Println("Member:", member)
}
Server Time
serverTime, err := client.GetTime(ctx)
log.Println("Server time:", serverTime)

Addressing

Spot uses the following address formats:

Prefix Description Example
k. Key-based address (encrypted) k.ABC123.../endpoint
@/ System endpoints @/time

Messages to k. addresses are automatically encrypted and signed. The recipient's public key is fetched and cached automatically.

Client Options

The New() function accepts various optional parameters:

client, err := spotlib.New(
    privateKey,                        // crypto.Signer for identity
    keychain,                          // *cryptutil.Keychain
    eventHub,                          // *emitter.Hub for custom event handling
    map[string]spotlib.MessageHandler{ // Initial handlers
        "endpoint": myHandler,
    },
    map[string]string{                 // Metadata for ID card
        "name": "my-client",
    },
)

Default Handlers

The client registers these handlers automatically:

Endpoint Description
ping Echo service for connectivity testing
version Returns library and Go runtime version
finger Returns the client's signed identity
check_update Triggers update check events
idcard_update Handles ID card update notifications

API Reference

Client Methods
Method Description
New(params...) Create a new client
Close() Gracefully shut down the client
WaitOnline(ctx) Block until connected
Query(ctx, target, body) Send request and wait for response
SendTo(ctx, target, payload) Send one-way message
SetHandler(endpoint, handler) Register message handler
ListenPacket(name) Get net.PacketConn interface
TargetId() Get client's address string
IDCard() Get client's identity card
ConnectionCount() Get (total, online) connection counts
GetIDCard(ctx, hash) Fetch remote identity card
StoreBlob(ctx, key, value) Store encrypted data
FetchBlob(ctx, key) Retrieve encrypted data
GetTime(ctx) Get server time
GetGroupMembers(ctx, key) List group members
Storage
Type/Function Description
ClientData Interface for providing client identity (requires Keychain() method)
NewDiskStore() Create disk store at default path (~/.config/spot/)
NewDiskStoreWithPath(path) Create disk store at custom path
(*diskStore).Keychain() Get keychain with loaded keys
(*diskStore).Path() Get storage directory path
(*diskStore).AddKey(key, type) Add and persist a new key

License

See LICENSE file for details.

Documentation

Overview

Package spotlib provides a client implementation for the Spot secure messaging protocol.

Spotlib enables secure, end-to-end encrypted communication between clients through the Spot network. It handles connection management, cryptographic identity, message routing, and provides both request-response and fire-and-forget messaging patterns.

Basic Usage

Create a new client with an optional private key for identity:

client, err := spotlib.New()
if err != nil {
    log.Fatal(err)
}
defer client.Close()

Wait for the client to come online:

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := client.WaitOnline(ctx); err != nil {
    log.Fatal(err)
}

Sending Messages

Send an encrypted query and wait for a response:

response, err := client.Query(ctx, "k.targetID/endpoint", []byte("payload"))

Send a one-way encrypted message:

err := client.SendTo(ctx, "k.targetID/endpoint", []byte("payload"))

Receiving Messages

Register a handler for incoming messages on an endpoint:

client.SetHandler("myendpoint", func(msg *spotproto.Message) ([]byte, error) {
    // Process message and return response
    return []byte("response"), nil
})

PacketConn Interface

For UDP-like communication, use ListenPacket to get a net.PacketConn:

conn, err := client.ListenPacket("udp-endpoint")
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

Identity and Addressing

Each client has a cryptographic identity represented by an IDCard. The client's address (TargetId) is derived from the SHA-256 hash of its public key and has the format "k.<base64url-encoded-hash>".

Messages to key-based addresses (starting with "k.") are automatically encrypted and signed. The recipient's public key is retrieved and cached automatically.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewDiskStore added in v0.2.20

func NewDiskStore() (*diskStore, error)

NewDiskStore creates a new disk-based store for client data. Data is stored in filepath.Join(os.UserConfigDir(), "spot"). If no keys exist, a new ECDSA P-256 key is generated automatically.

func NewDiskStoreWithPath added in v0.2.20

func NewDiskStoreWithPath(path string) (*diskStore, error)

NewDiskStoreWithPath creates a new disk-based store at the specified path. If path is empty, it defaults to filepath.Join(os.UserConfigDir(), "spot"). If no keys exist, a new ECDSA P-256 key is generated automatically.

func WithTimeout added in v0.2.3

func WithTimeout(ctx context.Context, timeout time.Duration, cb func(context.Context))

WithTimeout makes it easy to call a method that requires a context with a specified timeout without having to worry about calling the cancel() method. Go typically suggests using defer, however if processing after a given method is called continues, there is a risk the cancel method will be called much later.

This method on the other hand performs the defer of cancel, which means that cancel will be called properly even in case of a panic.

Usage:

spotlib.WithTimeout(nil, 30*time.Second, func(ctx context.Context) {
   res, err = c.methodWithCtx(ctx)
}

if err := nil { ...

Types

type Client added in v0.0.3

type Client struct {
	Events *emitter.Hub // event hub for client events (online, offline, etc.)
	// contains filtered or unexported fields
}

Client holds information about a client, including its connections to the spot servers. It manages cryptographic identity, connection state, message handlers, and provides high-level methods for secure communication through the Spot protocol.

func New added in v0.0.3

func New(params ...any) (*Client, error)

New starts a new Client and establishes connection to the Spot system. If any key is passed, the first key will be used as the main signing key.

Parameters can include: - gobottle.PrivateKey or *gobottle.Keychain: keys to use for signing/encryption - *emitter.Hub: event hub to use instead of creating a new one - map[string]MessageHandler: initial message handlers to register - map[string]string: metadata to include in the client ID card

func (*Client) Close added in v0.1.1

func (c *Client) Close() error

Close gracefully shuts down the client and all its connections. This method is idempotent and safe to call multiple times.

func (*Client) ConnectionCount added in v0.0.4

func (c *Client) ConnectionCount() (uint32, uint32)

ConnectionCount returns the number of spot server connections, and the number of said connections which are online (ie. past the handshake step).

func (*Client) FetchBlob added in v0.2.9

func (c *Client) FetchBlob(ctx context.Context, key string) ([]byte, error)

FetchBlob fetches a blob previously stored with StoreBlob. The operation can be slow and is provided as best effort. The data will be decrypted and verified.

func (*Client) GetGroupMembers added in v0.1.3

func (c *Client) GetGroupMembers(ctx context.Context, groupKey []byte) ([]string, error)

GetGroupMembers retrieves a list of member IDs for the specified group key

func (*Client) GetIDCard added in v0.0.6

func (c *Client) GetIDCard(ctx context.Context, h []byte) (*gobottle.IDCard, error)

GetIDCard returns the ID card for the given hash It first checks the local cache, and if not found, fetches it from the server. Also automatically subscribes to updates for this ID card.

func (*Client) GetIDCardBin added in v0.0.6

func (c *Client) GetIDCardBin(ctx context.Context, h []byte) ([]byte, error)

GetIDCardBin returns the binary ID card for the given hash This also automatically subscribes the client to updates for this ID card

func (*Client) GetIDCardForRecipient added in v0.0.6

func (c *Client) GetIDCardForRecipient(ctx context.Context, rcv string) (*gobottle.IDCard, error)

GetIDCardForRecipient returns the ID Card of a given recipient, if any

func (*Client) GetTime added in v0.2.3

func (c *Client) GetTime(ctx context.Context) (time.Time, error)

GetTime queries the Spot server for its current time. This can be used for clock synchronization or to verify server connectivity.

func (*Client) IDCard added in v0.0.6

func (c *Client) IDCard() *gobottle.IDCard

IDCard returns the client's own identity card containing its public key and metadata

func (*Client) ListenPacket added in v0.1.4

func (c *Client) ListenPacket(name string) (net.PacketConn, error)

ListenPacket returns a net.PacketConn object that can be used to easily exchange encrypted packets with other peers without having to think about the underlying protocol details.

The name parameter defines the endpoint that will receive messages. Messages are automatically encrypted and signatures are verified.

func (*Client) Query added in v0.0.3

func (c *Client) Query(ctx context.Context, target string, body []byte) ([]byte, error)

Query sends a request & waits for the response. If the target is a key (starts with k.) the message will be encrypted & signed so only the recipient can open it.

This is a blocking call that returns the response body or an error. The context can be used to set a timeout or cancel the operation.

func (*Client) QueryTimeout added in v0.0.6

func (c *Client) QueryTimeout(timeout time.Duration, target string, body []byte) ([]byte, error)

QueryTimeout calls Query with the specified timeout duration as a convenience wrapper

func (*Client) SendTo added in v0.0.6

func (c *Client) SendTo(ctx context.Context, target string, payload []byte) error

SendTo encrypts and sends a payload to the given target

func (*Client) SendToWithFrom added in v0.1.4

func (c *Client) SendToWithFrom(ctx context.Context, target string, payload []byte, from string) error

SendToWithFrom encrypts and sends a payload to the given target, with the option to set the sender endpoint

func (*Client) SetHandler added in v0.0.6

func (c *Client) SetHandler(endpoint string, handler MessageHandler)

SetHandler registers a handler function for a specific endpoint If handler is nil, removes any existing handler for the endpoint

func (*Client) StoreBlob added in v0.2.9

func (c *Client) StoreBlob(ctx context.Context, key string, value []byte) error

StoreBlob stores the given value under the given key after encrypting it in a way that can only be retrieved by this client specifically, using the same private key. This can be useful to store some settings local to the node that may need to be re-obtained, however this method is to be considered best-effort and shouldn't be used for intensive storage activity. Note also that value will have a limit of slightly less than 49kB.

Data may also be purged after some time without access.

func (*Client) TargetId added in v0.0.4

func (c *Client) TargetId() string

TargetId returns the local client ID in the format 'k.<base64hash>' that can be used to transmit messages to this client

func (*Client) WaitOnline added in v0.2.1

func (c *Client) WaitOnline(ctx context.Context) error

WaitOnline waits for the client to establish at least one online connection Returns immediately if already online, otherwise blocks until online or context cancellation

type ClientData added in v0.2.20

type ClientData interface {
	// Keychain returns a keychain with at least one private key for client identity
	Keychain() *gobottle.Keychain
}

ClientData is an interface for providing client identity data including cryptographic keys. Implementations should return a keychain containing at least one private key for signing.

type InstantMessage added in v0.0.3

type InstantMessage struct {
	ID        uuid.UUID // Unique message identifier
	Flags     uint64    // Message flags for special handling
	Recipient string    // Target recipient identifier
	Sender    string    // Source sender identifier
	Body      []byte    // Actual message content
	Encrypted bool      // Whether the message was encrypted
	SignedBy  [][]byte  // Contains the public keys that signed the message when decoding
}

InstantMessage represents a message with metadata, content, and cryptographic information

func DecodeInstantMessage added in v0.0.3

func DecodeInstantMessage(buf []byte, res *gobottle.OpenResult, err error) (*InstantMessage, error)

DecodeInstantMessage extracts an InstantMessage from a cryptographic bottle It verifies the contents and populates metadata fields based on the bottle headers

func (*InstantMessage) Bottle added in v0.0.3

func (im *InstantMessage) Bottle() *gobottle.Bottle

Bottle converts the InstantMessage into a cryptographic bottle for secure transmission

func (*InstantMessage) Bytes added in v0.0.3

func (im *InstantMessage) Bytes() []byte

Bytes serializes the InstantMessage into a byte array for transmission

func (*InstantMessage) MarshalBinary added in v0.0.3

func (im *InstantMessage) MarshalBinary() ([]byte, error)

MarshalBinary implements the BinaryMarshaler interface for serialization

func (*InstantMessage) ReadFrom added in v0.0.3

func (im *InstantMessage) ReadFrom(r io.Reader) (int64, error)

ReadFrom implements the ReaderFrom interface for streaming deserialization

func (*InstantMessage) UnmarshalBinary added in v0.0.3

func (im *InstantMessage) UnmarshalBinary(r []byte) error

UnmarshalBinary implements the BinaryUnmarshaler interface for deserialization

type MessageHandler added in v0.0.6

type MessageHandler func(msg *spotproto.Message) ([]byte, error)

MessageHandler is a function type that processes incoming messages and optionally returns a response If an error is returned, it will be converted to an error message and sent back to the sender

type SpotAddr added in v0.1.4

type SpotAddr string

SpotAddr is a type implementing net.Addr that represents a spot address (typically, k.xxx/yyy) This allows using standard Go networking patterns with the Spot protocol

func (SpotAddr) Network added in v0.1.4

func (s SpotAddr) Network() string

Network returns the name of the network ("spot")

func (SpotAddr) String added in v0.1.4

func (s SpotAddr) String() string

String returns the address as a string

Jump to

Keyboard shortcuts

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