Three documents showed the remote layout with a plaintext snapshot ID as the metadata directory name, and docs/REPOSTRUCTURE.md blamed those IDs for the observable backup time. The store actually names each metadata directory with a one-way hash of the ID, so hostname and snapshot name are not visible; the backup time is, through the plaintext timestamp in the manifest, which is accepted behaviour. README, ARCHITECTURE.md, docs/DATAMODEL.md and docs/REPOSTRUCTURE.md now show the hashed layout, the derivation is documented once, and the privacy section lists what the unencrypted manifest exposes. Two code comments that claimed the timestamp was hidden are corrected. No behaviour change. Judgement call: docs/DATAMODEL.md was not named in the issue but had the same error. model: claude-opus-4-8 (implementation, review); claude-fable-5-1 (merge)
43 lines
1.7 KiB
Go
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[:])
|
|
}
|