nimvault/manifest

Search:
Group by:

Vault manifest operations: entry types, load/save, ID generation.

Types

EntryKind = enum
  ekFile = "file",          ## Regular file entry
  ekDir = "dir"              ## Directory entry (for future use)
                             ## hash = SHA-256 of ciphertext blob; contentHash = SHA-256 of plaintext (optional, v4+).
VaultEntry = tuple[id, path, hash: string, kind: EntryKind, contentHash: string]

Consts

EntryDirName = "e"
Per-entry records. Two machines that add different files write different paths under here, so git can merge them. The combined manifest cannot.
EntryHeader = "# vault-entry-v7"
EnvelopeHeader = "# vault-envelope"
Set when this vault has per-file data keys. check uses it to refuse a vault whose blobs still match but whose key file is gone.
SealKeyHeader = "# vault-seal-key"
Comment header, so every older reader skips it as a comment.

Procs

proc dropEntryRecord(repo: string; cfg: GpgConfig; id: string) {.
    ...raises: [OSError], tags: [ReadDirEffect, WriteDirEffect], forbids: [].}
Remove one split record this command just deleted. Other stems stay.
proc entryDir(repo: string): string {....raises: [], tags: [], forbids: [].}
proc entryFile(repo: string; cfg: GpgConfig; id: string): string {....raises: [],
    tags: [], forbids: [].}
proc expandHome(p: string): string {....raises: [],
                                     tags: [ReadEnvEffect, ReadIOEffect],
                                     forbids: [].}
Expand ~ to $HOME in path strings.
proc findEntryFile(repo: string; cfg: GpgConfig; id: string): string {.
    ...raises: [], tags: [ReadDirEffect], forbids: [].}
proc genId(): string {....raises: [], tags: [], forbids: [].}
16-char random hex via cryptographic randomness.
proc hasSplitEntries(repo: string): bool {....raises: [OSError],
    tags: [ReadDirEffect], forbids: [].}
proc isPathSafe(cfg: GpgConfig; manifestPath: string): bool {....raises: [],
    tags: [ReadEnvEffect, ReadIOEffect], forbids: [].}
Validate that a manifest path resolves within expected boundaries. Returns false for directory traversal attempts (e.g. ../../etc/passwd).
proc loadManifest(repo: string; verifySig = false; cfg = GpgConfig()): seq[
    VaultEntry] {....raises: [OSError, Exception, IOError, ValueError,
                           NimvaultError], tags: [ReadDirEffect, ReadEnvEffect,
    ReadIOEffect, ExecIOEffect, RootEffect, WriteIOEffect, TimeEffect],
                  forbids: [].}
Entries only; see loadManifestMeta when the seal key matters.
proc loadManifestMeta(repo: string; verifySig = false; cfg = GpgConfig()): tuple[
    entries: seq[VaultEntry], sealKey: string, envelope: bool] {.
    ...raises: [OSError, Exception, IOError, ValueError, NimvaultError], tags: [
    ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect], forbids: [].}

Decrypt and parse the vault manifest, with the seal key it was written under (empty for v4 and earlier, which did not record one). Returns empty seq if no manifest exists. Supports v1–v5 (v4 adds plaintext contentHash for fast status; v5 adds the # vault-seal-key header).

cfg is optional so the many call sites that only read a gpg vault stay unchanged. findManifest still refuses to report a vault sealed by the other backend as empty, so omitting it fails loudly rather than quietly.

proc loadSplitEntries(repo: string; cfg: GpgConfig; verifySig = false): tuple[
    entries: seq[VaultEntry], deks: DekTable] {.
    ...raises: [OSError, Exception, IOError, ValueError, NimvaultError], tags: [
    ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect], forbids: [].}
Union of every per-entry record this machine can open.
proc resolvePath(cfg: GpgConfig; path: string): string {....raises: [],
    tags: [ReadEnvEffect, ReadIOEffect], forbids: [].}
Resolve a manifest path to an absolute filesystem path. When cfg.root is set, paths are relative to root. Otherwise, ~/... paths are expanded via expandHome.
proc saveEntryRecord(repo: string; cfg: GpgConfig; e: VaultEntry; dek: string) {....raises: [
    RegexError, OSError, IOError, NimvaultError, OSError, IOError, ValueError,
    Exception], tags: [WriteDirEffect, ReadDirEffect, ReadEnvEffect,
                       ReadIOEffect, WriteIOEffect, ExecIOEffect, RootEffect,
                       TimeEffect], forbids: [].}
Write one signed encrypted record. A different id is a different path, which is why two adds merge.
proc saveManifest(repo: string; entries: seq[VaultEntry]; cfg: GpgConfig;
                  sealKey = ""; envelope = false) {....raises: [OSError, IOError,
    Exception, ValueError, NimvaultError, RegexError, NimvaultError, OSError,
    IOError, ValueError], tags: [ReadDirEffect, WriteDirEffect, ReadEnvEffect,
                                 ReadIOEffect, ExecIOEffect, RootEffect,
                                 WriteIOEffect, TimeEffect], forbids: [].}
Serialize entries (v4: blob hash, kind, plaintext content hash) and encrypt. A non-empty sealKey records what the blobs are encrypted to (v5), which is what lets a later seal skip unchanged files without going stale across a recipient change.
proc saveManifestKeep(repo: string; entries: seq[VaultEntry]; cfg: GpgConfig;
                      envelope = false) {....raises: [OSError, Exception, IOError,
    ValueError, NimvaultError, RegexError], tags: [ReadDirEffect, ReadEnvEffect,
    ReadIOEffect, ExecIOEffect, RootEffect, WriteIOEffect, TimeEffect,
    WriteDirEffect], forbids: [].}
Rewrite the entry list without dropping the seal key or the envelope mark. add/rm/mv/addDir used to call saveManifest with the defaults and so forced the next seal to re-encrypt every blob.
proc saveSplitEntries(repo: string; cfg: GpgConfig; entries: seq[VaultEntry];
                      deks: DekTable; force = false) {....raises: [OSError,
    IOError, RegexError, NimvaultError, ValueError, Exception], tags: [
    WriteDirEffect, ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect,
    RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
Write the records this identity can open. Unchanged stems stay as they are on disk so a sibling add is a new path, not a rewrite. force is for rotate, which must rewrap even when the fields match. Unread stems stay; dropEntryRecord is the only remover.
proc sealKey(cfg: GpgConfig): string {....raises: [], tags: [], forbids: [].}

Fingerprint of everything that decides what a blob is encrypted to.

seal may only skip a file whose plaintext is unchanged; that is sound for content, and wrong the moment the recipient, backend or signer moves, because then unchanged plaintext still owes a fresh blob under the new key. Recording this alongside the entries turns a key rotation into a full re-seal instead of a vault half-sealed to a key that is gone.

proc splitRecordIds(repo: string): HashSet[string] {....raises: [OSError],
    tags: [ReadDirEffect], forbids: [].}
Ids that have a per-entry file, including those this identity cannot open. A wrap-subset view must not treat the rest as gone.
proc storePath(cfg: GpgConfig; absPath: string; repo: string): string {.
    ...raises: [Exception], tags: [RootEffect, ReadEnvEffect, ReadIOEffect],
    forbids: [].}
Convert an absolute path to the stored manifest format. When cfg.root is set, stores relative to root. Otherwise, stores with ~/ prefix if under HOME.
proc vaultDir(repo: string): string {....raises: [], tags: [], forbids: [].}
Path to the .vault directory within a repo.