nimvault/crypto

Backend dispatch: route encryption and decryption to gpg or age.

Kept in its own module so neither backend has to know the other exists, and so the choice is made in exactly one place rather than at each call site.

Procs

proc blobExt(cfg: GpgConfig): string {....raises: [], tags: [], forbids: [].}
Blobs and the manifest carry the extension of the primitive that made them. Giving both backends the same name would make a vault that cannot be read look like a vault that is empty, which is the worst of the available failure modes: silent and reassuring.
proc blobPath(repo: string; cfg: GpgConfig; id: string): string {....raises: [],
    tags: [], forbids: [].}
Where a blob lives for the configured backend.
proc decryptFile(cfg: GpgConfig; inPath, outPath: string; verifySig = false) {.
    ...raises: [NimvaultError, OSError, IOError, ValueError], tags: [ReadEnvEffect,
    ReadIOEffect, ReadDirEffect, ExecIOEffect, RootEffect, TimeEffect,
    WriteIOEffect], forbids: [].}
age carries no signature, so verifySig has nothing to check per blob. That is not a weakening: the manifest records each blob's SHA-256 and the callers verify it before decrypting, with the manifest itself signed.
proc decryptProcess(cfg: GpgConfig; inPath, outPath: string): Process {.
    ...raises: [OSError, IOError, NimvaultError], tags: [ExecIOEffect,
    ReadEnvEffect, RootEffect, ReadDirEffect, ReadIOEffect], forbids: [].}
Spawn a decryption of inPath to outPath. Both backends decrypt as a subprocess with the same shape, which is what lets the batching in unseal and status stay backend-agnostic.
proc decryptToString(cfg: GpgConfig; inPath: string; verifySig = false): string {.
    ...raises: [NimvaultError, OSError, IOError, ValueError], tags: [ReadEnvEffect,
    ReadIOEffect, ReadDirEffect, ExecIOEffect, RootEffect, TimeEffect,
    WriteIOEffect], forbids: [].}
proc effectiveSigner(cfg: GpgConfig): string {....raises: [], tags: [], forbids: [].}
An empty signer means the config was built without going through initGpgConfig, which library callers and tests legitimately do. Treat it as the long-standing behaviour rather than as an error: a vault that worked before this dispatch existed must keep working.
proc encryptFile(cfg: GpgConfig; inPath, outPath: string) {.
    ...raises: [NimvaultError, OSError, IOError, ValueError], tags: [ExecIOEffect,
    ReadEnvEffect, RootEffect, ReadDirEffect, ReadIOEffect, TimeEffect,
    WriteIOEffect], forbids: [].}
proc encryptProcess(cfg: GpgConfig; inPath, outPath: string): Process {.
    ...raises: [OSError, IOError, NimvaultError], tags: [ExecIOEffect,
    ReadEnvEffect, RootEffect, ReadDirEffect, ReadIOEffect], forbids: [].}
proc findBlob(repo: string; cfg: GpgConfig; id: string): string {....raises: [],
    tags: [ReadDirEffect], forbids: [].}
Locate a blob whichever backend wrote it. Reading falls back to the other extension so a vault mid-migration still opens; writing always uses the configured one.
proc findManifest(repo: string; cfg: GpgConfig): string {.
    ...raises: [NimvaultError], tags: [ReadDirEffect], forbids: [].}
Locate an existing manifest whichever backend wrote it, and refuse to treat a manifest this configuration cannot read as an absent one.
proc manifestPath(repo: string; cfg: GpgConfig): string {....raises: [], tags: [],
    forbids: [].}
Path the configured backend would write. Use findManifest to read.
proc signaturesInBand(cfg: GpgConfig): bool {....raises: [], tags: [], forbids: [].}

Whether decrypting a blob also proves who wrote it.

gpg reports GOODSIG/BADSIG on its status stream while decrypting, so the per-blob check is free. age produces no such thing, and callers that insist on GOODSIG would reject every age blob. For those vaults the equivalent guarantee is assembled differently: the manifest is signed, and it records a digest of each blob that unseal and get verify before decrypting. Skipping the in-band check is therefore not a relaxation, it is the same property established one level up.

proc signManifest(cfg: GpgConfig; path: string) {.
    ...raises: [NimvaultError, OSError, IOError, ValueError], tags: [ReadEnvEffect,
    ReadIOEffect, ReadDirEffect, WriteDirEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect], forbids: [].}
gpg signs while encrypting, so it needs nothing here. age does not sign at all, which is why an age vault defaults to an ssh signer.
proc usesAge(cfg: GpgConfig): bool {....raises: [], tags: [], forbids: [].}
proc verifyManifest(cfg: GpgConfig; path: string; requireSig: bool) {.
    ...raises: [NimvaultError, OSError, IOError, ValueError], tags: [ReadDirEffect,
    ReadEnvEffect, ReadIOEffect, ExecIOEffect, RootEffect, WriteIOEffect,
    TimeEffect], forbids: [].}