Files
vaultik/internal/snapshot/remotekey.go
T
sneak 617a2c6966 Correct remote layout and privacy docs for hashed snapshot keys (closes #67)
The remote layout and threat model in three documents described plaintext
snapshot IDs as directory names and misattributed the observable backup
time to those IDs. In fact `RemoteSnapshotKey` names each metadata
directory (and the manifest `snapshot_id`) with a one-way double SHA-256
hash of the human ID, so hostname and snapshot name are not observable;
the backup time is, via the plaintext manifest timestamp, an accepted
design property (issue 81).

Document the derivation once in `docs/REPOSTRUCTURE.md` with a worked
example; README, ARCHITECTURE and DATAMODEL now show the hashed layout and
link to it. Rewrite the privacy section to state what the unencrypted
manifest really exposes. Fix two code comments that claimed the public
bytes hide the timestamp. Docs and comments only; no behaviour change.

Model: opus-4-8
2026-09-21 07:26:16 +00:00

43 lines
1.7 KiB
Go

package snapshot
import (
"crypto/sha256"
"encoding/hex"
)
// remoteKeyPrefix is mixed into the snapshot ID hash so the resulting
// hex digest is domain-separated from any other "double SHA256 of a
// string" identifier the user might also use. Keeping this stable is a
// hard compatibility requirement: changing it invalidates every
// existing snapshot's remote storage path.
const remoteKeyPrefix = "vaultik|"
// RemoteSnapshotKey returns the storage-side identifier for a snapshot
// given its human snapshot ID. It is hex(SHA256(SHA256(prefix + id))).
// The two SHA256 rounds match Bitcoin's "hash256" convention so the
// output looks like a 64-character hex blob with no exploitable
// structure visible to a remote observer.
//
// We use this in three places:
//
// - the "metadata/<remote-key>/..." subdirectory on the storage
// backend so a directory listing of the bucket / file:// dest
// doesn't reveal hostnames or configured snapshot names. (The
// backup time is not hidden: the manifest.json.zst inside that
// directory carries a plaintext RFC3339 timestamp.)
// - the `snapshot_id` field of the unencrypted manifest.json.zst
// for the same reason;
// - any code path that needs to translate a known local snapshot ID
// into the path it would occupy on remote storage.
//
// The human ID stays the user-visible handle everywhere else — local
// database joins, CLI arguments, summary lines, log fields — because
// it's never written to the public bytes once this function gates
// every storage-path construction.
func RemoteSnapshotKey(snapshotID string) string {
first := sha256.Sum256([]byte(remoteKeyPrefix + snapshotID))
second := sha256.Sum256(first[:])
return hex.EncodeToString(second[:])
}