rotate: safety-spine scaffold + mandatory backup gate (NO rotation)
Design-phase foundation for v2 rotation. Changes NO credential at any service. - internal/rotate: Rotator interface + registry + PlanAll (zero drivers registered, so every credential plans as "(none)") - internal/rotate.Snapshot: the MANDATORY backup gate — seals the gopass prefix into an authenticated bundle, re-opens it, and confirms the entry count matches before returning; any failure blocks rotation - internal/sink.CountBundleRecords: read-only bundle completeness check - cmd: `incredigo rotate` runs the backup gate + prints the plan; it is dry-run only and refuses --execute (design phase) - tests: backup gate happy path, no-overwrite, tamper detection; race-clean Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -180,6 +180,43 @@ func copySecret(dst io.Writer, r io.Reader) error {
|
||||
}
|
||||
}
|
||||
|
||||
// CountBundleRecords walks a decrypted bundle stream and returns how many records
|
||||
// (entries) it contains, writing nothing. It is the completeness half of backup
|
||||
// verification: a bundle that the Sealer can Open is authentic; counting its
|
||||
// records confirms every expected entry is present. Secret bytes are read only to
|
||||
// skip past them (discarded).
|
||||
func CountBundleRecords(r io.Reader) (int, error) {
|
||||
var n int
|
||||
for {
|
||||
var pathLen uint16
|
||||
if err := binary.Read(r, binary.BigEndian, &pathLen); err != nil {
|
||||
if err == io.EOF {
|
||||
return n, nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
if pathLen == 0 {
|
||||
return n, nil // end-of-stream sentinel
|
||||
}
|
||||
if _, err := io.CopyN(io.Discard, r, int64(pathLen)); err != nil {
|
||||
return n, err
|
||||
}
|
||||
for { // skip the secret's chunks up to the zero-length marker
|
||||
var chunkLen uint32
|
||||
if err := binary.Read(r, binary.BigEndian, &chunkLen); err != nil {
|
||||
return n, err
|
||||
}
|
||||
if chunkLen == 0 {
|
||||
break
|
||||
}
|
||||
if _, err := io.CopyN(io.Discard, r, int64(chunkLen)); err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
func writePath(w io.Writer, path string) error {
|
||||
if len(path) > 0xffff {
|
||||
return fmt.Errorf("gopass path too long: %d bytes", len(path))
|
||||
|
||||
Reference in New Issue
Block a user