Help us shape the future of Go! Take the 2026 Go Developer Survey

e2e

package
v0.50.0 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package e2e carries the server-side AWARENESS of client-side (E2E) encrypted folders — nothing more. The server never sees a password or a key and cannot decrypt anything; this package only lets the pipelines recognise the two artifacts the client leaves behind so they stop doing pointless (and potentially leaky) work:

  • the folder marker `.filex-e2e.json` at an encrypted folder's root (public salt + verify blob — hidden from listings, but readable via the preview endpoint so the client can unlock);
  • the `filexe2e` magic prefix every encrypted file starts with.

Design + threat model: docs/E2E-ENCRYPTION.md.

Index

Constants

View Source
const (
	LongNameSuffix = ".fxl"
	SidecarSuffix  = ".fxl.name"
)

LongNameSuffix and SidecarSuffix are the two spellings a shortened encrypted name takes (docs/E2E-ENCRYPTION.md → "Encrypted names"): `<43 base64url>.fxl` for the item, `<43 base64url>.fxl.name` for the sidecar that holds its full encrypted name.

View Source
const (

	// FileHeaderFixedLen is magic (8) + version (1) + header length (4).
	FileHeaderFixedLen = 13
	// FileHeaderMaxLen is the largest header a writer produces or a reader
	// accepts — so FileHeaderFixedLen+FileHeaderMaxLen bytes always hold it.
	FileHeaderMaxLen = 64 * 1024
)
View Source
const AdoptedByEnv = "env:" + EnvEscrowAdopt

AdoptedByEnv is what the record says when consent came from the env var. It names the mechanism, so a future second mechanism is legible in the data instead of being flattened into a bare "adopted".

View Source
const EnvEscrowAdopt = "FILEX_INSTALLATION_E2E_ESCROW_ADOPT"

EnvEscrowAdopt is the operator's separate, explicit consent to turn escrow on for an installation that already exists.

Why a second variable and not just "the key changed from empty, so they must have meant it": an escrow key arrives by being pasted into a compose file, a Helm values file or a .env, often copied from another deployment or from the quickstart. That is exactly the shape of an accident, and the accident is silent — the installation gains a second key holder and only new folders get it, so nothing looks wrong until somebody needs the key on an old folder and it does not work.

Why an env var and not a CLI subcommand or an admin button: the operator has to set EnvEscrowKey anyway, and that is an env var. A CLI step would mean shell access to the container plus database credentials — routine on a VPS, awkward on Kubernetes and impossible on several managed platforms filex is deployed to — and it would still leave two places that must agree. An admin button would put "give the operator a key to every folder from now on" behind an HTTP session; the person entitled to that decision is the person who can edit the environment, which is where the decision already lives. One extra line, in the file they are already editing, read once and then dead, is the smallest thing that is still a deliberate act.

View Source
const EnvEscrowKey = "FILEX_INSTALLATION_E2E_ESCROW_KEY"

EnvEscrowKey is the install-time escrow public key. Unset or empty means escrow is disabled for this installation.

View Source
const EscrowAlg = "RSA-OAEP-256"

EscrowAlg is the only escrow algorithm this version understands. It is written into every escrow slot so a future format change is legible rather than silent.

View Source
const EscrowKeyBits = 3072

EscrowKeyBits is the modulus size `filex e2e-escrow keygen` mints. 3072 buys a comfortable margin over 2048 while still generating in about a second, which matters because an operator runs it interactively.

View Source
const EscrowMinKeyBits = 2048

EscrowMinKeyBits rejects a public key too small to take seriously.

View Source
const FileExtension = ".fxe"

FileExtension is the stored-name suffix of a single encrypted file.

View Source
const InstallationFile = "installation.json"

InstallationFile is the operator-readable mirror inside the data dir.

View Source
const MarkerName = syspath.E2EKeyFile

MarkerName is the folder marker filename dropped by the client at the root of every encrypted folder. Spelled once, in syspath (E2EKeyFile), whose listing and keyless-write rules judge it by this very name.

View Source
const PinnedByFirstBoot = "first-boot"

PinnedByFirstBoot is what the record says when escrow was there from the installation's first boot. Adoption never overwrites it — see Installation.

View Source
const SettingInstallation = "installation.pinned"

SettingInstallation is the settings-table key holding the pinned record.

Variables

View Source
var (
	ErrPlaintextIntoEncrypted = errors.New("cannot move or copy a file into an encrypted folder: the server has no key, so the file would be stored unencrypted inside it. Upload it through the web UI with the folder unlocked instead")
	ErrEncryptedOutOfFolder   = errors.New("cannot move or copy an encrypted file out of its folder: it stays encrypted, and outside the folder nothing knows which password opens it")
	ErrAcrossEncrypted        = errors.New("cannot move or copy between two different encrypted folders: each folder has its own key, and the destination's key cannot open the source's files")
	ErrNestedEncrypted        = errors.New("cannot move an encrypted folder into another encrypted folder: encrypted folders cannot be nested")
)

ErrPlaintextIntoEncrypted / ErrEncryptedOutOfFolder / ErrAcrossEncrypted are the three ways to cross the boundary. They are separate errors because the operator-facing advice differs for each.

View Source
var ErrNoEscrowKey = errors.New("e2e: no escrow key configured")

ErrNoEscrowKey is returned when no escrow key is configured at all.

View Source
var FileMagicPrefix = []byte("filexfxe")

FileMagicPrefix is the 8-byte prefix of a single encrypted file (`.fxe`, docs/E2E-ENCRYPTION.md → "Single encrypted files"): a file encrypted on its own, outside any encrypted folder, carrying its own key slots.

View Source
var MagicPrefix = []byte("filexe2e")

MagicPrefix is the 8-byte prefix of every client-encrypted file.

Functions

func ConversionPending added in v0.48.0

func ConversionPending(markerBytes []byte) bool

ConversionPending reports whether a key file says an in-place conversion is under way: v3, `req` naming "conv", and a conv slot with `pending: true` (docs/E2E-ENCRYPTION.md → "Encrypting a folder you already have"). While it is, and only then, the server lets a write replace a plaintext file in the folder with its ciphertext without keeping the plaintext as a version.

func EscrowKeyID

func EscrowKeyID(spkiDER []byte) string

EscrowKeyID is the stable short name of an escrow key: the first 8 bytes of SHA-256 over the SPKI DER, hex-encoded. The browser computes the same value from the same bytes (escrowKeyId in e2ecrypto.ts).

func FileAt added in v0.48.0

func FileAt(ctx context.Context, lk NodeByPathLookup, storageID int64, rel string) bool

FileAt reports whether rel names a live, catalogued single encrypted file (`.fxe`): a FILE row whose name carries the extension. It is what the escrow and password endpoints act on when their subject is one file rather than a folder — the file holds its own key slots, so it is its own "root".

A directory named `something.fxe` is not one, and neither is a name the catalogue does not know: the endpoints answer 400 for both, as they do for a folder without a key file.

func FindRoot

func FindRoot(ctx context.Context, lk NodeByPathLookup, storageID int64, dir string) (string, bool)

FindRoot walks dir and its ancestors (deepest first, storage root last) and returns the relative path of the nearest directory carrying an encrypted-folder marker. ok=false when no ancestor is marked. The returned root is trimmed of slashes ("" = the storage root itself).

func GenerateEscrowKeyPair

func GenerateEscrowKeyPair(bits int) (spkiB64, pkcs8B64 string, err error)

GenerateEscrowKeyPair mints a fresh escrow keypair and returns both halves base64-encoded: SPKI for the server's config, PKCS#8 for the admin's safe.

⚠ The private half is returned to the caller and never written anywhere by filex. `filex e2e-escrow keygen` prints it once; that is the only copy that will ever exist.

func GuardTransfer

func GuardTransfer(ctx context.Context, lk NodeByPathLookup, srcStorageID int64, srcRels []string, dstStorageID int64, dstRel string) error

GuardTransfer reports whether a copy/move of srcRels (relative paths on srcStorageID) into dstRel (a directory on dstStorageID) crosses an encryption boundary.

A nil lookup, or a deployment with no encrypted folders at all, costs one cheap ancestor walk per item and always returns nil.

func HasEncryptedPrefix added in v0.48.0

func HasEncryptedPrefix(b []byte) bool

HasEncryptedPrefix reports whether b starts with either magic — a file in an encrypted folder or a single encrypted file. Every pipeline that would read content (thumbnails, content indexing, document editing) treats the two the same: ciphertext it cannot, and must not try to, render.

func HasMagicPrefix

func HasMagicPrefix(b []byte) bool

HasMagicPrefix reports whether b starts with the encrypted-FOLDER-file magic. HasEncryptedPrefix recognises every kind of encrypted file.

func IsRoot added in v0.50.0

func IsRoot(ctx context.Context, lk NodeByPathLookup, storageID int64, dir string) bool

IsRoot reports whether dir is itself an encrypted folder: it holds a live marker. The one test behind the listing badge (`e2e: true` on a folder row), the AI surface's `encrypted` folder and the transfer guard's "an encrypted folder may move, its marker travels with it".

func LooksEncryptedFile added in v0.48.0

func LooksEncryptedFile(name string) bool

LooksEncryptedFile reports whether a stored NAME is a single encrypted file's — the check that needs no read, for skipping work before anything is opened. The content sniff (HasEncryptedPrefix) is the authority.

func LooksEncryptedName added in v0.48.0

func LooksEncryptedName(name string) bool

LooksEncryptedName reports whether a stored name has the SHAPE of a name the browser encrypted: base64url with no dot and at least 23 characters, a shortened `<43>.fxl` item, its `.fxl.name` sidecar — or either of the first two followed by `.` and a folder's 22-character id (a folder's name, docs/E2E-ENCRYPTION.md → "Folder ids").

The server holds no key and cannot tell an encrypted name from a plaintext one that happens to look like it, so this is used for exactly one thing: REFUSING to invent a name next to one (the `-copy` suffix of a colliding copy or move). A name the server makes up inside a folder whose names are encrypted is a name nobody can decrypt, so the right answer there is a conflict the person resolves, not a silent new name.

func ReadFileHeader added in v0.48.0

func ReadFileHeader(b []byte) (map[string]json.RawMessage, bool)

ReadFileHeader returns the JSON fields of the `.fxe` header b starts with, or ok=false when b is not one (wrong magic or version, a length out of range, a header cut short, or not a JSON object).

func RetiredSecret added in v0.48.0

func RetiredSecret(old, now []byte) bool

RetiredSecret reports whether old holds the file key now holds under a password or recovery slot now no longer uses: a copy that opens the file's CURRENT contents with a secret that was changed. That, and only that, is what a password change leaves behind as a liability.

func SameFileKey added in v0.48.0

func SameFileKey(x, y []byte) bool

SameFileKey reports whether two `.fxe` headers wrap the same file key: the same wrapped `dek` bytes, which a writer never reproduces by chance (every wrap takes a fresh IV). Such a pair holds the same content under the same folder-master key — so the older one's password opens the newer one's body.

func UnderEncrypted

func UnderEncrypted(ctx context.Context, lk NodeByPathLookup, storageID int64, nodePath string) bool

UnderEncrypted reports whether nodePath (a FILE or dir path, relative) sits inside an encrypted folder subtree — i.e. any of its ancestor directories carries the marker. Used by the thumb pipeline and the content extractor to skip work on ciphertext.

Types

type Adoption

type Adoption struct {
	// Allowed is the consent itself. False means empty -> set is refused
	// exactly as it was before adoption existed.
	Allowed bool
	// By is written verbatim into the record as e2e_escrow_adopted_by.
	By string
}

Adoption is the operator's explicit consent to adopt escrow, and the record of how it was given.

func ResolveAdoption

func ResolveAdoption() Adoption

ResolveAdoption reads the operator's consent from the environment. Booleans follow the same rule as the rest of filex: "1" or "true".

type EscrowKey

type EscrowKey struct {
	// KID is the first 8 bytes of SHA-256(SPKI) in hex — the short name
	// that appears in every escrow slot and in the boot banner.
	KID string
	// SPKI is the DER public key, base64, exactly as the browser imports it.
	SPKI string
	Bits int
	// contains filtered or unexported fields
}

EscrowKey is a parsed, validated installation escrow public key.

func ParseEscrowPublicKey

func ParseEscrowPublicKey(s string) (*EscrowKey, error)

ParseEscrowPublicKey validates an operator-supplied escrow public key.

Accepts base64 SPKI DER with or without PEM armour and with any whitespace, because it arrives through an env var that has been through a shell, a .env file and probably a copy-paste.

func ResolveEscrow

func ResolveEscrow() (*EscrowKey, error)

ResolveEscrow reads and validates the install-time escrow key from the environment. A missing or empty value means escrow is off. A present but unparseable value is an error — silently running without escrow when the operator believes they configured it is the one outcome to avoid.

func (*EscrowKey) SealNonce

func (k *EscrowKey) SealNonce(nonce []byte) ([]byte, error)

SealNonce encrypts a challenge nonce to the escrow public key, so that only a caller actually holding the private half can echo it back.

This is what turns "someone claims they used the escrow key" into "someone demonstrably had the escrow key in their browser". It does NOT stop an operator from decrypting files offline with the same key and never telling anyone — see docs/E2E-ENCRYPTION.md.

type FileHeaderDiff added in v0.48.0

type FileHeaderDiff struct {
	// Valid: the new bytes carry a readable `.fxe` header.
	Valid bool
	// Compared: there was a readable previous header to compare with.
	Compared bool
	// Changes, sorted: "password" (salt, iter, verify, fmk_pw), "recovery_key"
	// (rk), "escrow" (esc), "content" (dek, nonce, size, chunk), "name" (the
	// sealed original name), "other" (anything else).
	Changes []string
	// SecretChanged: the password or the recovery slot changed — the event
	// after which a copy under the old secret is a liability.
	SecretChanged bool
}

FileHeaderDiff says what changed between two headers of the same `.fxe`.

func DiffFileHeaders added in v0.48.0

func DiffFileHeaders(before, after []byte) FileHeaderDiff

DiffFileHeaders compares the header before (the version an overwrite kept, or nil) with the header after (the file as it is now).

type Installation

type Installation struct {
	// E2EEscrowKID is the escrow key id, or "" when escrow is disabled.
	E2EEscrowKID string `json:"e2e_escrow_kid"`
	// E2EEscrowSPKI is the escrow public key, kept so the mirror file is
	// self-contained. It is public material; nothing here is a secret.
	E2EEscrowSPKI string `json:"e2e_escrow_spki,omitempty"`
	E2EEscrowAlg  string `json:"e2e_escrow_alg,omitempty"`
	// PinnedAt / PinnedBy describe the FIRST BOOT and are never rewritten,
	// not even by an adoption. "When was this installation created?" and
	// "when did it gain a second key?" are two questions, and answering the
	// second by overwriting the first would destroy the only record of the
	// installation's own age.
	PinnedAt string `json:"pinned_at,omitempty"`
	PinnedBy string `json:"pinned_by,omitempty"`
	// E2EEscrowAdoptedAt is set only when escrow was turned on AFTER the
	// first boot. Its presence is the whole answer to "was this first-boot
	// or adoption?", and its value is the boundary: folders created before
	// this instant carry no escrow slot and never will.
	E2EEscrowAdoptedAt string `json:"e2e_escrow_adopted_at,omitempty"`
	// E2EEscrowAdoptedBy records HOW consent was given (AdoptedByEnv today).
	E2EEscrowAdoptedBy string `json:"e2e_escrow_adopted_by,omitempty"`
	// E2EEscrowAdoptionNote is written alongside them so the mirror file
	// explains itself to a reader who has never seen this documentation.
	E2EEscrowAdoptionNote string `json:"e2e_escrow_adoption_note,omitempty"`
	// JustAdopted is true only on the boot that PERFORMED the adoption, so
	// that boot can say so once and loudly. It describes this process, not
	// the installation, so it is never written to the record — comparing
	// E2EEscrowAdoptedAt against the boot clock would have been close
	// enough almost always and wrong for a restart inside the same second.
	JustAdopted bool `json:"-"`
}

Installation is the pinned record. Fields are add-only: a newer filex reading an older record must see zero values, never a parse error.

func PinInstallation

func PinInstallation(ctx context.Context, store SettingsStore, dataDir string, escrow *EscrowKey, now string, adopt Adoption) (Installation, error)

PinInstallation records the install-time settings on first boot, verifies them on every boot after that, and performs the one change that is allowed: adopting escrow on an installation that already exists.

adopt carries the operator's explicit consent (see Adoption). It is consulted for exactly one transition, escrow off -> escrow on, and is ignored everywhere else — a first boot does not need consent, and no consent makes a key SWAP or a key REMOVAL safe.

Returns the effective record. An *InstallationMismatchError means the caller must abort — see server.New.

func (Installation) EscrowAdopted

func (i Installation) EscrowAdopted() bool

EscrowAdopted reports whether escrow was adopted rather than present from the first boot.

type InstallationMismatchError

type InstallationMismatchError struct {
	Setting  string
	Pinned   string
	Supplied string
	DataDir  string
	// Adoptable is true for the one transition that CAN be performed —
	// escrow off -> escrow on — when the operator has not yet said they
	// mean it. The message then explains adoption and its permanent limit
	// instead of only listing the two ways to back out.
	Adoptable bool
}

InstallationMismatchError is returned when the environment disagrees with what this installation was initialised with. Its message is the whole point: it must tell an operator what changed, why filex will not simply do as it is told, and what their actual options are.

func (*InstallationMismatchError) Error

func (e *InstallationMismatchError) Error() string

type KeyFileDiff added in v0.48.0

type KeyFileDiff struct {
	// Valid: the new bytes are a JSON object with a version and a salt — a
	// key file, not a stray upload under its name. Nothing is deleted
	// otherwise.
	Valid bool
	// Changes, sorted: "password", "recovery_key", "escrow", "level",
	// "rekey", "conversion", "created" and "other".
	Changes []string
	// KeySlotChanged: the password or the recovery slot changed, so every
	// earlier version wraps the folder key under a secret that should no
	// longer open it.
	KeySlotChanged bool
}

KeyFileDiff says what a rewrite changed.

func DiffKeyFiles added in v0.48.0

func DiffKeyFiles(before, after []byte) KeyFileDiff

DiffKeyFiles compares the key file before a rewrite with the one after it. before may be nil (not known — no version was kept).

type NodeByPathLookup

type NodeByPathLookup interface {
	GetNodeByPath(ctx context.Context, storageID int64, pathHash string) (*model.Node, error)
}

NodeByPathLookup is the narrow store surface the ancestor walk needs. db.Store satisfies it.

type SettingsStore

type SettingsStore interface {
	GetSetting(ctx context.Context, key string) (string, error)
	UpsertSetting(ctx context.Context, key, value string) error
}

SettingsStore is the narrow store surface the pin needs. db.Store satisfies it.

type TransferGuardError

type TransferGuardError struct {
	Source string
	Dest   string
	Reason error
}

TransferGuardError names the item that was refused alongside the reason, so the message a user sees says which file, not just "no".

func (*TransferGuardError) Error

func (e *TransferGuardError) Error() string

func (*TransferGuardError) Unwrap

func (e *TransferGuardError) Unwrap() error

Directories

Path Synopsis
Package fxewatch is the server's own record of a rewritten single encrypted file (`.fxe`): an audit row for every rewrite of one, and — when the rewrite changed its password or recovery slot — the deletion of the older versions that still open the file with the old secret.
Package fxewatch is the server's own record of a rewritten single encrypted file (`.fxe`): an audit row for every rewrite of one, and — when the rewrite changed its password or recovery slot — the deletion of the older versions that still open the file with the old secret.

Jump to

Keyboard shortcuts

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