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
- Variables
- func EscrowKeyID(spkiDER []byte) string
- func FindRoot(ctx context.Context, lk NodeByPathLookup, storageID int64, dir string) (string, bool)
- func GenerateEscrowKeyPair(bits int) (spkiB64, pkcs8B64 string, err error)
- func GuardTransfer(ctx context.Context, lk NodeByPathLookup, srcStorageID int64, srcRels []string, ...) error
- func HasMagicPrefix(b []byte) bool
- func UnderEncrypted(ctx context.Context, lk NodeByPathLookup, storageID int64, nodePath string) bool
- type Adoption
- type EscrowKey
- type Installation
- type InstallationMismatchError
- type NodeByPathLookup
- type SettingsStore
- type TransferGuardError
Constants ¶
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".
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.
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.
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.
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.
const EscrowMinKeyBits = 2048
EscrowMinKeyBits rejects a public key too small to take seriously.
const InstallationFile = "installation.json"
InstallationFile is the operator-readable mirror inside the data dir.
const MarkerName = ".filex-e2e.json"
MarkerName is the folder marker filename dropped by the client at the root of every encrypted folder.
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.
const SettingInstallation = "installation.pinned"
SettingInstallation is the settings-table key holding the pinned record.
Variables ¶
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.
var ErrNoEscrowKey = errors.New("e2e: no escrow key configured")
ErrNoEscrowKey is returned when no escrow key is configured at all.
var MagicPrefix = []byte("filexe2e")
MagicPrefix is the 8-byte prefix of every client-encrypted file.
Functions ¶
func EscrowKeyID ¶
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 FindRoot ¶
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 ¶
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 HasMagicPrefix ¶
HasMagicPrefix reports whether b starts with the encrypted-file magic.
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 ¶
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 ¶
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 ¶
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 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 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 ¶
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