devicekeys

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrKeychainUnreachable = errors.New("keychain backend unreachable")

ErrKeychainUnreachable wraps a keychain error that means the backend could not be reached (unsupported platform, or a missing Secret Service / D-Bus session), as opposed to a secret that is genuinely absent. Load paths return it so callers can distinguish "custody unreachable" from "key not found", and mint paths refuse to generate a divergent identity when they see it (P6-XP-04).

Functions

func Fingerprint

func Fingerprint(signingPublicKey, ageRecipient string) (string, error)

Fingerprint derives a full-strength (256-bit) device fingerprint that binds a device's Ed25519 signing public key to its age X25519 recipient. Both inputs are canonicalized by parse-then-re-encode so that cosmetically different encodings of the same keys (whitespace, non-canonical base64) yield the same fingerprint. The digest is SHA-256 over

"devstrap/device-fp/v1" || 0x00 || canonicalSigning || 0x00 || canonicalRecipient

encoded as unpadded, uppercase, standard-alphabet base32 (exactly 52 characters for 32 bytes) and returned as 13 dash-separated groups of 4.

This is deliberately a full fingerprint, never a short authentication string: the pairing channel is untrusted, and a truncated code would let an attacker brute-force a colliding key pair. Compare the whole value out-of-band.

func FingerprintEqual

func FingerprintEqual(a, b string) bool

FingerprintEqual reports whether two fingerprints are equal after normalization, using a constant-time comparison to avoid leaking how much of a mistyped value matched.

func IsKeychainUnavailable

func IsKeychainUnavailable(err error) bool

IsKeychainUnavailable reports whether a keychain error means the backend is missing/not-present (so an operation may tolerate it and use the file store) rather than a live-backend hard failure. It is the coarse predicate used by tolerant paths (e.g. delete); mint/store paths use the finer keychainUnreachable / keychainSecretMissing split so a divergent identity is never minted (P6-XP-04).

func NormalizeFingerprint

func NormalizeFingerprint(s string) string

NormalizeFingerprint strips grouping dashes and whitespace and uppercases the remainder so fingerprints copied with varied formatting compare equal.

func Sign

func Sign(privateKey, domain string, message []byte) (string, error)

func Verify

func Verify(publicKey, signature, domain string, message []byte) error

Types

type BackupEpoch

type BackupEpoch struct {
	Epoch int64
	KID   string
}

BackupEpoch names one held Workspace Content Key by its epoch and kid so a full backup can escrow it (P6-DATA-04). It mirrors the (epoch, kid) tuple the store records in workspace_keys.

type Custody

type Custody string

Custody names where a HybridStore keeps its secret material. It is a per-machine decision recorded once at init (P6-XP-04) and honored on every later run so a store never silently migrates custody backends:

  • CustodyKeychain: the OS keychain is authoritative. If it is unreachable, operations fail closed rather than falling back to the file store — that silent downgrade is exactly the split-custody wedge P6-XP-04 fixes.
  • CustodyFile: the 0600 file store is authoritative and the keychain is never consulted (headless/CI hosts, or DEVSTRAP_NO_KEYCHAIN=1).
  • CustodyUnset: no decision recorded yet (a pre-P6-XP-04 store). Legacy hybrid behavior applies: prefer the keychain, fall back to the file store when the backend is unavailable. The published-key mint guard still holds, so even this path can never mint a divergent identity.
const (
	CustodyUnset    Custody = ""
	CustodyKeychain Custody = "keychain"
	CustodyFile     Custody = "file"
)

type FileStore

type FileStore struct {
	Dir string
}

func NewFileStore

func NewFileStore(dir string) FileStore

func (FileStore) DeleteHubS3Credentials

func (s FileStore) DeleteHubS3Credentials(workspaceID string) error

DeleteHubS3Credentials removes the credential blob from the file fallback store; a missing file is not an error.

func (FileStore) Ensure

func (s FileStore) Ensure(deviceID string) (Identity, bool, error)

func (FileStore) EnsureSigning

func (s FileStore) EnsureSigning(deviceID string) (SigningIdentity, bool, error)

func (FileStore) Read

func (s FileStore) Read(deviceID string) (Identity, error)

func (FileStore) ReadHubS3Credentials

func (s FileStore) ReadHubS3Credentials(workspaceID string) (HubS3Credentials, error)

ReadHubS3Credentials loads the credential blob from the file fallback store.

func (FileStore) ReadSigning

func (s FileStore) ReadSigning(deviceID string) (SigningIdentity, error)

func (FileStore) ReadWCK

func (s FileStore) ReadWCK(workspaceID string, epoch int64, kid string) ([]byte, error)

ReadWCK loads a WCK from the file fallback store.

func (FileStore) WriteHubS3Credentials

func (s FileStore) WriteHubS3Credentials(workspaceID string, creds HubS3Credentials) error

WriteHubS3Credentials persists the credential blob to the file fallback store with mode 0600.

func (FileStore) WriteWCK

func (s FileStore) WriteWCK(workspaceID string, epoch int64, kid string, wck []byte) error

WriteWCK persists a WCK to the file fallback store as base64 with mode 0600.

type HubS3Credentials

type HubS3Credentials struct {
	AccessKeyID     string `json:"access_key_id"`
	SecretAccessKey string `json:"secret_access_key"`
}

HubS3Credentials is the hub S3/R2 credential pair kept in local custody (P6-HUB-02): one JSON blob per workspace under the device-identity keychain service, with a 0600 file fallback. The secret access key never enters SQLite, config.yaml, or logs — plaintext env remains only a CI/override fallback resolved by the CLI layer.

type HybridStore

type HybridStore struct {
	File    FileStore
	Secret  SecretBackend
	Custody Custody
}

func NewHybridStore

func NewHybridStore(dir string, secret SecretBackend) HybridStore

func (HybridStore) DeleteHubS3Credentials

func (s HybridStore) DeleteHubS3Credentials(ctx context.Context, workspaceID string) error

DeleteHubS3Credentials removes the stored pair from both custody backends (hub logout). Missing entries are not errors.

func (HybridStore) Ensure

func (s HybridStore) Ensure(ctx context.Context, deviceID, publishedRecipient string) (Identity, bool, error)

Ensure ensures a device age identity exists, minting one only when none is stored. publishedRecipient is the device's already-published age recipient ("" if none); when it is set and the keychain is unreachable, Ensure refuses to mint a divergent identity rather than wedge custody (P6-XP-04).

func (HybridStore) EnsureSigning

func (s HybridStore) EnsureSigning(ctx context.Context, deviceID, publishedPub string) (SigningIdentity, bool, error)

EnsureSigning ensures a device signing identity exists, minting one only when none is stored. publishedPub is the device's already-published signing public key ("" if none). When the keychain is unreachable, or when a key is already published but its private half is absent, EnsureSigning refuses to mint a divergent identity — the split-custody wedge P6-XP-04 fixes.

func (HybridStore) ExportForBackup

func (s HybridStore) ExportForBackup(ctx context.Context, dstDir, deviceID, workspaceID string, epochs []BackupEpoch) ([]string, error)

ExportForBackup reads every secret this device holds for the given device and workspace — the device age identity, the device signing identity, each held WCK epoch, and the hub S3 credentials when present — from the active custody backend and writes them into dstDir using the FileStore on-disk format with mode 0600 (P6-DATA-04). Because it reads through the recorded custody backend (via HybridStore.Read/ReadSigning/LoadWCK/LoadHubS3Credentials), it captures keychain-held material as portable escrow files for a full backup, not just the file-custody KeyDir. A basename already present in dstDir is left untouched, so it is safe to run over a directory the caller has pre-populated.

It returns the basenames written (sorted). A REQUIRED secret — the device age or signing identity, or any held WCK epoch — that cannot be read is a hard error naming the secret: a "full" backup must never silently omit key material. Hub S3 credentials are optional (a workspace may have no hub configured): a genuinely-absent entry is skipped, but any other read failure is fatal. No secret bytes are logged.

func (HybridStore) LoadHubS3Credentials

func (s HybridStore) LoadHubS3Credentials(ctx context.Context, workspaceID string) (HubS3Credentials, error)

LoadHubS3Credentials loads the stored hub S3 credential pair, or an error wrapping os.ErrNotExist when none is stored.

func (HybridStore) LoadWCK

func (s HybridStore) LoadWCK(ctx context.Context, workspaceID string, epoch int64, kid string) ([]byte, error)

LoadWCK loads the WCK for a workspace+epoch+kid, or an error wrapping os.ErrNotExist if this device does not hold it. The decorator calls this during Pull to obtain the key for an enc.v1 event's epoch.

func (HybridStore) Probe

func (s HybridStore) Probe(ctx context.Context) Custody

Probe classifies the current keychain reachability into a custody decision with a single read-only lookup, without side effects. It is used once at init to record the custody backend (P6-XP-04). A nil backend or an unreachable Secret Service resolves to file custody; a reachable backend (the probe account is expectedly absent) resolves to keychain custody.

func (HybridStore) Read

func (s HybridStore) Read(ctx context.Context, deviceID string) (Identity, error)

func (HybridStore) ReadSigning

func (s HybridStore) ReadSigning(ctx context.Context, deviceID string) (SigningIdentity, error)

func (HybridStore) StoreHubS3Credentials

func (s HybridStore) StoreHubS3Credentials(ctx context.Context, workspaceID string, creds HubS3Credentials) (string, error)

StoreHubS3Credentials persists the hub S3 credential pair (P6-HUB-02), reusing the keychain/file custody path used for device identities and WCKs: keychain preferred, 0600 file fallback only when the keychain is genuinely unavailable (DEVSTRAP_NO_KEYCHAIN or a missing Secret Service); a present-but-failing keychain fails closed (SECR-04/SECU-01). Returns where the credentials landed ("keychain" or "file") so the CLI can tell the user.

func (HybridStore) StoreWCK

func (s HybridStore) StoreWCK(ctx context.Context, workspaceID string, epoch int64, kid string, wck []byte) error

StoreWCK persists a 32-byte Workspace Content Key for the given workspace, epoch, and kid (P4-SEC-07 / P6-SEC-02), reusing the keychain/file custody path used for device identities. kid is the non-secret fingerprint hex(sha256(wck)); pass "" only for the legacy bare-epoch form. The keychain is preferred; a 0600 file fallback is used only when the keychain is genuinely unavailable (DEVSTRAP_NO_KEYCHAIN or a missing Secret Service), so headless/CI runs remain usable. A present-but-failing keychain fails closed (SECR-04/SECU-01).

func (HybridStore) WithCustody

func (s HybridStore) WithCustody(c Custody) HybridStore

WithCustody returns a copy of the store pinned to the recorded custody backend (P6-XP-04). Callers resolve the recorded decision (and any DEVSTRAP_NO_KEYCHAIN override) and stamp it here before minting or reading.

type Identity

type Identity struct {
	Private   string
	Recipient string
}

func NewIdentity

func NewIdentity() (Identity, error)

type SecretBackend

type SecretBackend interface {
	Store(ctx context.Context, service, account string, secret []byte) error
	Load(ctx context.Context, service, account string) ([]byte, error)
	Delete(ctx context.Context, service, account string) error
}

type SigningIdentity

type SigningIdentity struct {
	Private string
	Public  string
}

func NewSigningIdentity

func NewSigningIdentity() (SigningIdentity, error)

Jump to

Keyboard shortcuts

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