crypto

package
v0.1.4 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package crypto turns the bytes CtxHop stores into ciphertext nothing outside the user's machines can read.

Its failure mode is the opposite of the adapter's. There, a wrong guess corrupts an agent's data silently. Here, mistakes are loud: authentication simply fails. The one quiet failure is believing something was encrypted when it was not, which is why the tests assert directly that no recognisable plaintext survives.

Encryption is asymmetric because pushing and pulling are asymmetric acts. A push runs from the agent's SessionEnd hook, with nobody there to type a passphrase, so it must work from what is on disk alone. A pull is a person deciding to continue a session on this machine, so it can ask. Encrypting to a public key means the unattended half needs no secret at all: a stolen laptop can append to the user's storage and cannot read a word of it (spec §3.3).

Index

Constants

This section is empty.

Variables

View Source
var ErrCorrupt = errors.New("crypto: object failed authentication")

ErrCorrupt reports ciphertext that failed authentication, was truncated, or was presented at the wrong location.

View Source
var ErrDamagedKeyfile = errors.New("crypto: the storage keyfile is damaged; existing sessions can only be reached by restoring it")

ErrDamagedKeyfile reports a keyfile that is not a keyfile any more.

View Source
var ErrDeviceRevoked = errors.New("crypto: device is revoked or not enrolled")

ErrDeviceRevoked reports that a device no longer has a grant for the active key generation. Old data already copied to that device cannot be recalled, but it cannot unlock newly rotated data through this keyfile.

View Source
var ErrInvalidDeviceGrant = errors.New("crypto: device key grant is invalid")

ErrInvalidDeviceGrant reports a malformed or unauthenticated device grant.

View Source
var ErrInvalidDomainInviteProof = errors.New("crypto: invalid domain invite proof")

ErrInvalidDomainInviteProof reports an invitation proof that cannot be verified with the configured domain key.

View Source
var ErrManagedKeyfileRequired = errors.New("crypto: managed keyfile is required")

ErrManagedKeyfileRequired reports an operation that needs the managed device-authorized envelope rather than the legacy passphrase-only file.

View Source
var ErrPublicKeyMismatch = errors.New("crypto: the storage keyfile advertises a public key that does not belong to it; do not push to this storage")

ErrPublicKeyMismatch reports a keyfile whose public key is not the public half of the secret it wraps.

View Source
var ErrRecoveryKeyChecksum = errors.New("crypto: recovery key checksum does not match; check for a mistyped character")

ErrRecoveryKeyChecksum reports a recovery key that does not check out, which almost always means it was mistyped rather than that it is the wrong key.

View Source
var ErrUnsupportedVersion = errors.New("crypto: object format is newer than this version understands")

ErrUnsupportedVersion reports an object written by a newer release.

View Source
var ErrWrongPassphrase = errors.New("crypto: passphrase does not unlock this storage")

ErrWrongPassphrase reports that the passphrase did not open the envelope.

View Source
var ErrWrongRecoveryKey = errors.New("crypto: recovery key does not unlock this storage")

ErrWrongRecoveryKey reports that the recovery key did not open the envelope.

Functions

func Decrypt

func Decrypt(identity *ecdh.PrivateKey, path string, sealed []byte) ([]byte, error)

Decrypt opens an object stored at path.

Any modification to the ciphertext, the tag, the nonce, the ephemeral key, the header, or the path makes this fail. It never returns partial plaintext: a caller that acted on half a record would be acting on something nobody authenticated.

func DeviceID

func DeviceID(idKey []byte, localIdentity string) (string, error)

DeviceID derives the identifier this machine writes under.

func DomainFingerprint

func DomainFingerprint(namespace string, identityPublic []byte) (string, error)

DomainFingerprint derives a short, non-secret identifier for one configured storage namespace and its pinned public encryption identity. It is intended for human confirmation, not authorization: possession of a matching value does not grant access to the domain.

func DomainInviteProof

func DomainInviteProof(identifierKey, payload []byte) (string, error)

DomainInviteProof authenticates a canonical invitation payload with the domain's identifier key. The proof is portable, but the key never leaves the local configuration or the unlocked keyfile.

func Encrypt

func Encrypt(recipient *ecdh.PublicKey, path string, plaintext []byte) ([]byte, error)

Encrypt seals plaintext for storage at path, readable only by the holder of the matching private key.

path is bound into both the key derivation and the additional authenticated data. Binding it means a shard only decrypts at the location it was written to: without that, an attacker - or a sync bug - could move one session's shard into another session and it would decrypt perfectly (spec §7.1).

func FormatRecoveryKey

func FormatRecoveryKey(raw []byte) string

FormatRecoveryKey renders a recovery key for a human to copy.

func KeyfilePath

func KeyfilePath() string

KeyfilePath is the object key the envelope is stored under.

func MarshalKeyfile

func MarshalKeyfile(k *Keyfile) ([]byte, error)

MarshalKeyfile renders the envelope for storage.

It refuses an envelope holding no wrapped key. This object is the one whose loss locks the user out of every session they have ever pushed, and the caller's next act is to write the result over the existing one, so a caller bug here is unrecoverable data loss. Refusing to write is always safe (BR-12).

func MigrateKeyfile

func MigrateKeyfile(k *Keyfile, passphrase, deviceID string, devicePublic *ecdh.PublicKey) error

MigrateKeyfile converts one v1 envelope to a managed v2 envelope. Existing passphrase and recovery wrapping are preserved byte-for-byte.

func NewDevicePrivateKey

func NewDevicePrivateKey() (*ecdh.PrivateKey, error)

NewDevicePrivateKey creates the long-lived local key used to unwrap this device's grants. It is unrelated to the content identity derived from an epoch data key.

func NewRecoveryKey

func NewRecoveryKey() ([]byte, string)

NewRecoveryKey generates a recovery key and its written form.

func OpenLocal

func OpenLocal(key []byte, label string, sealed []byte) ([]byte, error)

OpenLocal opens what SealLocal produced.

func ParseDevicePrivateKey

func ParseDevicePrivateKey(raw []byte) (*ecdh.PrivateKey, error)

ParseDevicePrivateKey parses the raw private key persisted in local secrets.

func ParseRecoveryKey

func ParseRecoveryKey(s string) ([]byte, error)

ParseRecoveryKey reads a recovery key back.

Input is normalised before anything else: case is ignored, separators are ignored, and the characters Crockford treats as aliases are folded. Somebody reading their own handwriting should not be told their key is wrong because they wrote a lowercase l for a 1.

func ProjectID

func ProjectID(idKey []byte, canonicalRemote string) (string, error)

ProjectID derives the identifier a project is stored under.

The remote path must not reveal what the user works on: an object listing alone would otherwise show how many projects someone has and what they are called, without the storage provider decrypting anything (PRD §8.3).

The input must be stable across machines, because every device has to derive the same identifier for the same project. That is why it is the normalised git remote and never a local path.

func RegisterManagedDevice

func RegisterManagedDevice(k *Keyfile, passphrase, deviceID string, devicePublic *ecdh.PublicKey) error

RegisterManagedDevice adds a new active member and grants it every retained epoch. It is idempotent for the same public key and refuses re-use of a revoked device ID.

func RotateManagedKeyfile

func RotateManagedKeyfile(k *Keyfile, currentPassphrase, nextPassphrase, removeDeviceID string) (string, error)

RotateManagedKeyfile generates a fresh content key and recovery key. If removeDeviceID is non-empty, that member is tombstoned and receives no grant for the new generation. A different passphrase is mandatory because a removed device may know the old one.

func SealLocal

func SealLocal(key []byte, label string, plaintext []byte) ([]byte, error)

SealLocal seals data that stays on this machine, under a key the machine also holds.

This is deliberately not the same thing as Encrypt. Objects bound for storage are sealed to a public key precisely so that an unattended push holds nothing that can read them back; a local file has the opposite requirement, since whatever reads it has no passphrase to offer either. Exposing the difference as two names keeps a caller from reaching for the wrong one.

What it protects is correspondingly narrower: a file copied on its own, or pasted into an issue, stays unreadable. It does nothing against someone who can already read the directory, because the key is in there too. Callers must describe it that way and no better.

func SessionID

func SessionID(idKey []byte, projectID, nativeID string) (string, error)

SessionID derives the identifier a session is stored under, scoped to its project so the same native id in two projects stays distinct.

func VerifyDomainInviteProof

func VerifyDomainInviteProof(identifierKey, payload []byte, proof string) error

VerifyDomainInviteProof verifies a proof without revealing whether the payload or the supplied key was the part that differed.

Types

type DataKey

type DataKey struct {
	// contains filtered or unexported fields
}

DataKey is the key everything else hangs off.

It is generated once, at random, and never changes. The passphrase and the recovery key each wrap it separately, which is what makes changing a passphrase a re-wrap rather than a re-encryption of everything ever uploaded (spec §3.1).

func NewDataKey

func NewDataKey() *DataKey

NewDataKey generates a fresh data key.

func (*DataKey) Close

func (d *DataKey) Close()

Close zeroes the key material and puts the key beyond use. Best effort, as ever in a garbage-collected language, but it shortens the window in which a heap dump contains the key.

Dropping the slice matters as much as zeroing it. Zeroing alone leaves a key of the right length holding thirty-two zero bytes, which derive would accept - and every closed key would then derive the same content key, one anybody can compute offline. A use-after-close would encrypt the user's sessions under a public constant and report success (spec §13.4).

func (*DataKey) IdentifierKey

func (d *DataKey) IdentifierKey() ([]byte, error)

IdentifierKey derives the key project, session and device identifiers are computed with.

Separate from the content key so that a compromise of one does not hand over the other, and so that neither purpose can be confused for the other.

func (*DataKey) IdentityPrivate

func (d *DataKey) IdentityPrivate() (*ecdh.PrivateKey, error)

IdentityPrivate derives the X25519 key objects are encrypted to.

Deriving it from the data key rather than generating it separately is what keeps the envelope unchanged: the passphrase and the recovery key still wrap one random secret, and everything else hangs off it (spec §3.1).

func (*DataKey) IdentityPublic

func (d *DataKey) IdentityPublic() (*ecdh.PublicKey, error)

IdentityPublic derives the public half, which is all a push needs.

This is the point of the whole hierarchy. A machine that only pushes holds no secret capable of reading anything: it encrypts to this value and cannot reverse it, so a stolen laptop yields the ability to append and nothing else (spec §3.3).

type KDFParams

type KDFParams struct {
	Name      string `json:"name"`
	Salt      []byte `json:"salt"`
	Time      uint32 `json:"time"`
	MemoryKiB uint32 `json:"memoryKiB"`
	Threads   uint8  `json:"threads"`
}

KDFParams are the Argon2id settings used to turn a passphrase into a key-encrypting key.

They are stored alongside the wrapped key rather than compiled in, so they can be raised later without invalidating anything: unlock with the old settings, re-wrap with the new ones (spec §4.1, §9).

func DefaultKDFParams

func DefaultKDFParams() KDFParams

DefaultKDFParams are tuned so unlocking is imperceptible while remaining expensive to attack: about 47ms on a current laptop.

type KeyEpoch

type KeyEpoch struct {
	Generation      uint64
	DataKey         *DataKey
	IdentityPrivate *ecdh.PrivateKey
	IdentityPublic  *ecdh.PublicKey
}

KeyEpoch is one unlocked content key and its derived object identity.

type KeyRing

type KeyRing struct {
	Generation    uint64
	IdentifierKey []byte
	Epochs        []KeyEpoch
	Members       []KeyfileMember
}

KeyRing is the readable key history for one authorized device. Epochs are sorted oldest to newest; the last epoch is always the active generation.

func (*KeyRing) ActiveDeviceIDs

func (r *KeyRing) ActiveDeviceIDs() map[string]struct{}

ActiveDeviceIDs returns the IDs which can receive the next generation.

func (*KeyRing) Close

func (r *KeyRing) Close()

Close releases every unlocked data key and clears the stable identifier key.

func (*KeyRing) Current

func (r *KeyRing) Current() *KeyEpoch

Current returns the active epoch.

func (*KeyRing) Identities

func (r *KeyRing) Identities() []*ecdh.PrivateKey

Identities returns current first, followed by historical identities.

type Keyfile

type Keyfile struct {
	Version int       `json:"version"`
	KDF     KDFParams `json:"kdf"`
	// IdentityPublic is stored in the clear because it is public. It is how a
	// second device learns which key to encrypt to, and unlocking verifies that
	// it really is the public half of the wrapped secret (spec §3.4).
	IdentityPublic       []byte `json:"identityPublic"`
	WrappedByPassphrase  []byte `json:"wrappedByPassphrase"`
	WrappedByRecoveryKey []byte `json:"wrappedByRecoveryKey"`
	// Generation and the fields below are present only in managed v2 files.
	// They are public metadata; epoch keys remain in encrypted wrappers.
	Generation uint64          `json:"generation,omitempty"`
	Members    []KeyfileMember `json:"members,omitempty"`
	Epochs     []KeyfileEpoch  `json:"epochs,omitempty"`
}

Keyfile is the envelope holding the content-key material. In v1 both wrappings protect the same data key. In managed v2 they protect a bundle of retained epoch keys and the stable identifier key; per-device grants provide the unattended authorization boundary. A new device still needs only one wrapping plus its own enrollment, and no existing device has to be online.

func NewKeyfile

func NewKeyfile(passphrase string) (*Keyfile, string, error)

NewKeyfile generates a data key and wraps it under both a passphrase and a freshly generated recovery key.

The recovery key is returned in written form because this is the only moment it exists in a shape a person can keep. Nothing stores it, and it cannot be recovered afterwards.

func ParseKeyfile

func ParseKeyfile(data []byte) (*Keyfile, error)

ParseKeyfile reads an envelope from storage.

func (*Keyfile) ChangePassphrase

func (k *Keyfile) ChangePassphrase(current, next string) error

ChangePassphrase re-wraps the data key under a new passphrase.

The data key itself does not change, so every byte already uploaded stays readable. Re-deriving the content key from the passphrase instead would have meant re-encrypting the user's entire history to change a password.

func (*Keyfile) IdentityPublicKey

func (k *Keyfile) IdentityPublicKey() (*ecdh.PublicKey, error)

IdentityPublicKey parses the advertised public key, for a caller pinning it after a successful unlock.

func (*Keyfile) IsManaged

func (k *Keyfile) IsManaged() bool

IsManaged reports whether this is the device-authorized format.

func (*Keyfile) ResetPassphrase

func (k *Keyfile) ResetPassphrase(recoveryText, next string) error

ResetPassphrase sets a new passphrase using the recovery key, for the user who has forgotten the old one.

func (*Keyfile) UnlockKeyRingForDevice

func (k *Keyfile) UnlockKeyRingForDevice(deviceID string, private *ecdh.PrivateKey) (*KeyRing, error)

UnlockKeyRingForDevice opens the grants issued to one local device. It requires the active generation grant; historical grants alone are not enough to authorize a device after a rotation.

func (*Keyfile) UnlockKeyRingWithPassphrase

func (k *Keyfile) UnlockKeyRingWithPassphrase(passphrase string) (*KeyRing, error)

UnlockKeyRingWithPassphrase opens all epoch keys using the current passphrase.

func (*Keyfile) UnlockKeyRingWithRecoveryKey

func (k *Keyfile) UnlockKeyRingWithRecoveryKey(recoveryText string) (*KeyRing, error)

UnlockKeyRingWithRecoveryKey opens all epoch keys using the current recovery key.

func (*Keyfile) UnlockWithPassphrase

func (k *Keyfile) UnlockWithPassphrase(passphrase string) (*DataKey, error)

UnlockWithPassphrase opens the envelope.

func (*Keyfile) UnlockWithRecoveryKey

func (k *Keyfile) UnlockWithRecoveryKey(recoveryText string) (*DataKey, error)

UnlockWithRecoveryKey opens the envelope with the written recovery key.

func (*Keyfile) UpgradeKDF

func (k *Keyfile) UpgradeKDF(passphrase string) (bool, error)

UpgradeKDF re-wraps under stronger settings if the stored ones are weaker than today's defaults.

Reports whether anything changed, so a caller only writes the file back when it must. Existing ciphertext is untouched either way.

type KeyfileEpoch

type KeyfileEpoch struct {
	Generation     uint64         `json:"generation"`
	IdentityPublic []byte         `json:"identityPublic"`
	Grants         []KeyfileGrant `json:"grants"`
}

KeyfileEpoch describes a content-key generation and its per-device grants.

type KeyfileGrant

type KeyfileGrant struct {
	DeviceID string `json:"deviceId"`
	Wrapped  []byte `json:"wrapped"`
}

KeyfileGrant is one encrypted epoch key for one member.

type KeyfileMember

type KeyfileMember struct {
	DeviceID            string `json:"deviceId"`
	DevicePublic        []byte `json:"devicePublic"`
	RevokedAtGeneration uint64 `json:"revokedAtGeneration,omitempty"`
}

KeyfileMember is the public membership record for one local installation.

Jump to

Keyboard shortcuts

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