From 0929d77e902c3611b907abba2d77950089230582 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 24 Mar 2026 09:56:57 -0700 Subject: [PATCH] Add locked files and directory-only entries. Locked files (--lock): repo-authoritative entries. Checkpoint skips them (preserves repo version). Status reports "drifted" instead of "modified". Restore always overwrites if hash differs, no prompt. Use case: system-managed files the OS overwrites. Directory-only entries (--dir): track directory itself without recursing. Restore ensures directory exists with correct permissions. Use case: directories that must exist but contents are managed elsewhere. Add refactored to use AddOptions struct (Encrypt, Lock, DirOnly) instead of variadic bools. Proto: ManifestEntry gains locked field. convert.go updated. 7 new tests. ARCHITECTURE.md and README.md updated. Co-Authored-By: Claude Opus 4.6 (1M context) --- ARCHITECTURE.md | 21 ++++ README.md | 2 + cmd/sgard/add.go | 16 ++- garden/encrypt_e2e_test.go | 2 +- garden/encrypt_fido2_test.go | 2 +- garden/encrypt_test.go | 12 +- garden/garden.go | 104 +++++++++++----- garden/locked_test.go | 229 +++++++++++++++++++++++++++++++++++ garden/mirror.go | 2 +- manifest/manifest.go | 1 + proto/sgard/v1/sgard.proto | 1 + server/convert.go | 2 + sgardpb/sgard.pb.go | 13 +- 13 files changed, 363 insertions(+), 44 deletions(-) create mode 100644 garden/locked_test.go diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d095fbd..14ab507 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -687,6 +687,27 @@ on disk is newer or the times match, sgard prompts for confirmation. **Timestamp comparison truncates to seconds** for cross-platform filesystem compatibility. +**Locked files (`--lock`).** A locked entry is repo-authoritative — the +on-disk copy is treated as potentially corrupted by the system, not as +a user edit. Semantics: +- **`add --lock`** — tracks the file normally, marks it as locked +- **`checkpoint`** — skips locked files entirely (preserves the repo version) +- **`status`** — reports locked files with changed hashes as `drifted` + (distinct from `modified`, which implies a user edit) +- **`restore`** — always restores locked files if the hash differs, + regardless of timestamp, without prompting. Skips if hash matches. +- **`add`** (without `--lock`) — can be used to explicitly update a locked + file in the repo when the on-disk version is intentionally new + +Use case: system-managed files like `~/.config/user-dirs.dirs` that get +overwritten by the OS but should be kept at a known-good state. + +**Directory-only entries (`--dir`).** `add --dir ` tracks the +directory itself as a structural entry without recursing into its +contents. On restore, sgard ensures the directory exists with the +correct permissions. Use case: directories that must exist for other +software to function, but whose contents are managed elsewhere. + **Remote config resolution:** `--remote` flag > `SGARD_REMOTE` env > `/remote` file. diff --git a/README.md b/README.md index 70ccc9e..cf4067d 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ sgard restore --repo /mnt/usb/dotfiles |---|---| | `init` | Create a new repository | | `add ...` | Track files, directories (recursed), or symlinks | +| `add --lock ...` | Track as locked (repo-authoritative, auto-restores on drift) | +| `add --dir ` | Track directory itself without recursing into contents | | `remove ...` | Stop tracking files | | `checkpoint [-m msg]` | Re-hash tracked files and update the manifest | | `restore [path...] [-f]` | Restore files to their original locations | diff --git a/cmd/sgard/add.go b/cmd/sgard/add.go index 4ec8e05..41c2adc 100644 --- a/cmd/sgard/add.go +++ b/cmd/sgard/add.go @@ -10,7 +10,11 @@ import ( "github.com/spf13/cobra" ) -var encryptFlag bool +var ( + encryptFlag bool + lockFlag bool + dirOnlyFlag bool +) var addCmd = &cobra.Command{ Use: "add ...", @@ -31,7 +35,13 @@ var addCmd = &cobra.Command{ } } - if err := g.Add(args, encryptFlag); err != nil { + opts := garden.AddOptions{ + Encrypt: encryptFlag, + Lock: lockFlag, + DirOnly: dirOnlyFlag, + } + + if err := g.Add(args, opts); err != nil { return err } @@ -51,5 +61,7 @@ func promptPassphrase() (string, error) { func init() { addCmd.Flags().BoolVar(&encryptFlag, "encrypt", false, "encrypt file contents before storing") + addCmd.Flags().BoolVar(&lockFlag, "lock", false, "mark as locked (repo-authoritative, restore always overwrites)") + addCmd.Flags().BoolVar(&dirOnlyFlag, "dir", false, "track directory itself without recursing into contents") rootCmd.AddCommand(addCmd) } diff --git a/garden/encrypt_e2e_test.go b/garden/encrypt_e2e_test.go index 56c0e4f..95f4d2a 100644 --- a/garden/encrypt_e2e_test.go +++ b/garden/encrypt_e2e_test.go @@ -45,7 +45,7 @@ func TestEncryptionE2E(t *testing.T) { } // Encrypted files. - if err := g.Add([]string{sshConfig, awsCreds}, true); err != nil { + if err := g.Add([]string{sshConfig, awsCreds}, AddOptions{Encrypt: true}); err != nil { t.Fatalf("Add encrypted: %v", err) } // Plaintext file. diff --git a/garden/encrypt_fido2_test.go b/garden/encrypt_fido2_test.go index a6e22e3..a97a93d 100644 --- a/garden/encrypt_fido2_test.go +++ b/garden/encrypt_fido2_test.go @@ -234,7 +234,7 @@ func TestEncryptedRoundTripWithFIDO2(t *testing.T) { t.Fatalf("writing: %v", err) } - if err := g.Add([]string{secretFile}, true); err != nil { + if err := g.Add([]string{secretFile}, AddOptions{Encrypt: true}); err != nil { t.Fatalf("Add: %v", err) } diff --git a/garden/encrypt_test.go b/garden/encrypt_test.go index f603341..9cca0c2 100644 --- a/garden/encrypt_test.go +++ b/garden/encrypt_test.go @@ -128,7 +128,7 @@ func TestAddEncrypted(t *testing.T) { t.Fatalf("writing secret file: %v", err) } - if err := g.Add([]string{secretFile}, true); err != nil { + if err := g.Add([]string{secretFile}, AddOptions{Encrypt: true}); err != nil { t.Fatalf("Add encrypted: %v", err) } @@ -205,7 +205,7 @@ func TestEncryptedRestoreRoundTrip(t *testing.T) { t.Fatalf("writing: %v", err) } - if err := g.Add([]string{secretFile}, true); err != nil { + if err := g.Add([]string{secretFile}, AddOptions{Encrypt: true}); err != nil { t.Fatalf("Add: %v", err) } @@ -243,7 +243,7 @@ func TestEncryptedCheckpoint(t *testing.T) { t.Fatalf("writing: %v", err) } - if err := g.Add([]string{secretFile}, true); err != nil { + if err := g.Add([]string{secretFile}, AddOptions{Encrypt: true}); err != nil { t.Fatalf("Add: %v", err) } @@ -285,7 +285,7 @@ func TestEncryptedStatus(t *testing.T) { t.Fatalf("writing: %v", err) } - if err := g.Add([]string{secretFile}, true); err != nil { + if err := g.Add([]string{secretFile}, AddOptions{Encrypt: true}); err != nil { t.Fatalf("Add: %v", err) } @@ -330,7 +330,7 @@ func TestEncryptedDiff(t *testing.T) { t.Fatalf("writing: %v", err) } - if err := g.Add([]string{secretFile}, true); err != nil { + if err := g.Add([]string{secretFile}, AddOptions{Encrypt: true}); err != nil { t.Fatalf("Add: %v", err) } @@ -372,7 +372,7 @@ func TestAddEncryptedRequiresDEK(t *testing.T) { t.Fatalf("writing: %v", err) } - err = g.Add([]string{testFile}, true) + err = g.Add([]string{testFile}, AddOptions{Encrypt: true}) if err == nil { t.Fatal("Add --encrypt without DEK should fail") } diff --git a/garden/garden.go b/garden/garden.go index aed01f1..b7ae25b 100644 --- a/garden/garden.go +++ b/garden/garden.go @@ -138,11 +138,10 @@ func (g *Garden) DeleteBlob(hash string) error { return g.store.Delete(hash) } -// addEntry adds a single file or symlink to the manifest. The abs path must -// already be resolved and info must come from os.Lstat. If skipDup is true, -// already-tracked paths are silently skipped instead of returning an error. -// If encrypt is true, the file blob is encrypted before storing. -func (g *Garden) addEntry(abs string, info os.FileInfo, now time.Time, skipDup, encrypt bool) error { +// addEntry adds a single file or symlink to the manifest. If skipDup is true, +// already-tracked paths are silently skipped. If encrypt is true, the file +// blob is encrypted before storing. If lock is true, the entry is marked locked. +func (g *Garden) addEntry(abs string, info os.FileInfo, now time.Time, skipDup, encrypt, lock bool) error { tilded := toTildePath(abs) if g.findEntry(tilded) != nil { @@ -155,6 +154,7 @@ func (g *Garden) addEntry(abs string, info os.FileInfo, now time.Time, skipDup, entry := manifest.Entry{ Path: tilded, Mode: fmt.Sprintf("%04o", info.Mode().Perm()), + Locked: lock, Updated: now, } @@ -198,15 +198,23 @@ func (g *Garden) addEntry(abs string, info os.FileInfo, now time.Time, skipDup, return nil } +// AddOptions controls the behavior of Add. +type AddOptions struct { + Encrypt bool // encrypt file blobs before storing + Lock bool // mark entries as locked (repo-authoritative) + DirOnly bool // for directories: track the directory itself, don't recurse +} + // Add tracks new files, directories, or symlinks. Each path is resolved // to an absolute path, inspected for its type, and added to the manifest. // Regular files are hashed and stored in the blob store. Directories are -// recursively walked and all leaf files and symlinks are added individually. -// If encrypt is true, file blobs are encrypted before storing (requires -// the DEK to be unlocked). -func (g *Garden) Add(paths []string, encrypt ...bool) error { - enc := len(encrypt) > 0 && encrypt[0] - if enc && g.dek == nil { +// recursively walked unless opts.DirOnly is set. +func (g *Garden) Add(paths []string, opts ...AddOptions) error { + var o AddOptions + if len(opts) > 0 { + o = opts[0] + } + if o.Encrypt && g.dek == nil { return fmt.Errorf("DEK not unlocked; run sgard encrypt init or unlock first") } @@ -224,24 +232,40 @@ func (g *Garden) Add(paths []string, encrypt ...bool) error { } if info.IsDir() { - err := filepath.WalkDir(abs, func(path string, d os.DirEntry, err error) error { + if o.DirOnly { + // Track the directory itself as a structural entry. + tilded := toTildePath(abs) + if g.findEntry(tilded) != nil { + return fmt.Errorf("already tracking %s", tilded) + } + entry := manifest.Entry{ + Path: tilded, + Type: "directory", + Mode: fmt.Sprintf("%04o", info.Mode().Perm()), + Locked: o.Lock, + Updated: now, + } + g.manifest.Files = append(g.manifest.Files, entry) + } else { + err := filepath.WalkDir(abs, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + fi, err := os.Lstat(path) + if err != nil { + return fmt.Errorf("stat %s: %w", path, err) + } + return g.addEntry(path, fi, now, true, o.Encrypt, o.Lock) + }) if err != nil { - return err + return fmt.Errorf("walking directory %s: %w", abs, err) } - if d.IsDir() { - return nil - } - fi, err := os.Lstat(path) - if err != nil { - return fmt.Errorf("stat %s: %w", path, err) - } - return g.addEntry(path, fi, now, true, enc) - }) - if err != nil { - return fmt.Errorf("walking directory %s: %w", abs, err) } } else { - if err := g.addEntry(abs, info, now, false, enc); err != nil { + if err := g.addEntry(abs, info, now, false, o.Encrypt, o.Lock); err != nil { return err } } @@ -284,6 +308,11 @@ func (g *Garden) Checkpoint(message string) error { entry.Mode = fmt.Sprintf("%04o", info.Mode().Perm()) + // Locked entries are repo-authoritative — checkpoint skips them. + if entry.Locked { + continue + } + switch entry.Type { case "file": data, err := os.ReadFile(abs) @@ -379,7 +408,11 @@ func (g *Garden) Status() ([]FileStatus, error) { compareHash = entry.PlaintextHash } if hash != compareHash { - results = append(results, FileStatus{Path: entry.Path, State: "modified"}) + if entry.Locked { + results = append(results, FileStatus{Path: entry.Path, State: "drifted"}) + } else { + results = append(results, FileStatus{Path: entry.Path, State: "modified"}) + } } else { results = append(results, FileStatus{Path: entry.Path, State: "ok"}) } @@ -425,12 +458,21 @@ func (g *Garden) Restore(paths []string, force bool, confirm func(path string) b return fmt.Errorf("expanding path %s: %w", entry.Path, err) } - // Check if the file exists and whether we need confirmation. - if !force { + // Locked entries always restore if content differs — no prompt. + if entry.Locked && entry.Type == "file" { + if currentHash, err := HashFile(abs); err == nil { + compareHash := entry.Hash + if entry.Encrypted && entry.PlaintextHash != "" { + compareHash = entry.PlaintextHash + } + if currentHash == compareHash { + continue // already matches, skip + } + } + // File is missing or hash differs — proceed to restore. + } else if !force { + // Normal entries: check timestamp for confirmation. if info, err := os.Lstat(abs); err == nil { - // File exists. If on-disk mtime >= manifest updated, ask. - // Truncate to seconds because filesystem mtime granularity - // varies across platforms. diskTime := info.ModTime().Truncate(time.Second) entryTime := entry.Updated.Truncate(time.Second) if !diskTime.Before(entryTime) { diff --git a/garden/locked_test.go b/garden/locked_test.go new file mode 100644 index 0000000..4cf7536 --- /dev/null +++ b/garden/locked_test.go @@ -0,0 +1,229 @@ +package garden + +import ( + "os" + "path/filepath" + "testing" +) + +func TestAddLocked(t *testing.T) { + root := t.TempDir() + repoDir := filepath.Join(root, "repo") + + g, err := Init(repoDir) + if err != nil { + t.Fatalf("Init: %v", err) + } + + testFile := filepath.Join(root, "testfile") + if err := os.WriteFile(testFile, []byte("locked content\n"), 0o644); err != nil { + t.Fatalf("writing: %v", err) + } + + if err := g.Add([]string{testFile}, AddOptions{Lock: true}); err != nil { + t.Fatalf("Add: %v", err) + } + + if !g.manifest.Files[0].Locked { + t.Error("entry should be locked") + } +} + +func TestCheckpointSkipsLocked(t *testing.T) { + root := t.TempDir() + repoDir := filepath.Join(root, "repo") + + g, err := Init(repoDir) + if err != nil { + t.Fatalf("Init: %v", err) + } + + testFile := filepath.Join(root, "testfile") + if err := os.WriteFile(testFile, []byte("original"), 0o644); err != nil { + t.Fatalf("writing: %v", err) + } + + if err := g.Add([]string{testFile}, AddOptions{Lock: true}); err != nil { + t.Fatalf("Add: %v", err) + } + + origHash := g.manifest.Files[0].Hash + + // Modify the file — checkpoint should NOT update the hash. + if err := os.WriteFile(testFile, []byte("system overwrote this"), 0o644); err != nil { + t.Fatalf("modifying: %v", err) + } + + if err := g.Checkpoint(""); err != nil { + t.Fatalf("Checkpoint: %v", err) + } + + if g.manifest.Files[0].Hash != origHash { + t.Error("checkpoint should skip locked files — hash should not change") + } +} + +func TestStatusReportsDrifted(t *testing.T) { + root := t.TempDir() + repoDir := filepath.Join(root, "repo") + + g, err := Init(repoDir) + if err != nil { + t.Fatalf("Init: %v", err) + } + + testFile := filepath.Join(root, "testfile") + if err := os.WriteFile(testFile, []byte("original"), 0o644); err != nil { + t.Fatalf("writing: %v", err) + } + + if err := g.Add([]string{testFile}, AddOptions{Lock: true}); err != nil { + t.Fatalf("Add: %v", err) + } + + // Modify — status should report "drifted" not "modified". + if err := os.WriteFile(testFile, []byte("system changed this"), 0o644); err != nil { + t.Fatalf("modifying: %v", err) + } + + statuses, err := g.Status() + if err != nil { + t.Fatalf("Status: %v", err) + } + if len(statuses) != 1 || statuses[0].State != "drifted" { + t.Errorf("expected drifted, got %v", statuses) + } +} + +func TestRestoreAlwaysRestoresLocked(t *testing.T) { + root := t.TempDir() + repoDir := filepath.Join(root, "repo") + + g, err := Init(repoDir) + if err != nil { + t.Fatalf("Init: %v", err) + } + + testFile := filepath.Join(root, "testfile") + if err := os.WriteFile(testFile, []byte("correct content"), 0o644); err != nil { + t.Fatalf("writing: %v", err) + } + + if err := g.Add([]string{testFile}, AddOptions{Lock: true}); err != nil { + t.Fatalf("Add: %v", err) + } + + // System overwrites the file. + if err := os.WriteFile(testFile, []byte("system garbage"), 0o644); err != nil { + t.Fatalf("overwriting: %v", err) + } + + // Restore without --force — locked files should still be restored. + if err := g.Restore(nil, false, nil); err != nil { + t.Fatalf("Restore: %v", err) + } + + got, err := os.ReadFile(testFile) + if err != nil { + t.Fatalf("reading: %v", err) + } + if string(got) != "correct content" { + t.Errorf("content = %q, want %q", got, "correct content") + } +} + +func TestRestoreSkipsLockedWhenHashMatches(t *testing.T) { + root := t.TempDir() + repoDir := filepath.Join(root, "repo") + + g, err := Init(repoDir) + if err != nil { + t.Fatalf("Init: %v", err) + } + + testFile := filepath.Join(root, "testfile") + if err := os.WriteFile(testFile, []byte("content"), 0o644); err != nil { + t.Fatalf("writing: %v", err) + } + + if err := g.Add([]string{testFile}, AddOptions{Lock: true}); err != nil { + t.Fatalf("Add: %v", err) + } + + // File is unchanged — restore should skip it (no unnecessary writes). + if err := g.Restore(nil, false, nil); err != nil { + t.Fatalf("Restore: %v", err) + } + + // If we got here without error, it means it didn't try to overwrite + // an identical file, which is correct. +} + +func TestAddDirOnly(t *testing.T) { + root := t.TempDir() + repoDir := filepath.Join(root, "repo") + + g, err := Init(repoDir) + if err != nil { + t.Fatalf("Init: %v", err) + } + + // Create a directory with a file inside. + testDir := filepath.Join(root, "testdir") + if err := os.MkdirAll(testDir, 0o755); err != nil { + t.Fatalf("creating dir: %v", err) + } + if err := os.WriteFile(filepath.Join(testDir, "file"), []byte("data"), 0o644); err != nil { + t.Fatalf("writing: %v", err) + } + + // Add with --dir — should NOT recurse. + if err := g.Add([]string{testDir}, AddOptions{DirOnly: true}); err != nil { + t.Fatalf("Add: %v", err) + } + + if len(g.manifest.Files) != 1 { + t.Fatalf("expected 1 entry (directory), got %d", len(g.manifest.Files)) + } + if g.manifest.Files[0].Type != "directory" { + t.Errorf("type = %s, want directory", g.manifest.Files[0].Type) + } + if g.manifest.Files[0].Hash != "" { + t.Error("directory entry should have no hash") + } +} + +func TestDirOnlyRestoreCreatesDirectory(t *testing.T) { + root := t.TempDir() + repoDir := filepath.Join(root, "repo") + + g, err := Init(repoDir) + if err != nil { + t.Fatalf("Init: %v", err) + } + + testDir := filepath.Join(root, "testdir") + if err := os.MkdirAll(testDir, 0o755); err != nil { + t.Fatalf("creating dir: %v", err) + } + + if err := g.Add([]string{testDir}, AddOptions{DirOnly: true}); err != nil { + t.Fatalf("Add: %v", err) + } + + // Remove directory. + _ = os.RemoveAll(testDir) + + // Restore should recreate it. + if err := g.Restore(nil, true, nil); err != nil { + t.Fatalf("Restore: %v", err) + } + + info, err := os.Stat(testDir) + if err != nil { + t.Fatalf("directory not restored: %v", err) + } + if !info.IsDir() { + t.Error("restored path should be a directory") + } +} diff --git a/garden/mirror.go b/garden/mirror.go index 979c910..ac0ff25 100644 --- a/garden/mirror.go +++ b/garden/mirror.go @@ -37,7 +37,7 @@ func (g *Garden) MirrorUp(paths []string) error { if lstatErr != nil { return fmt.Errorf("stat %s: %w", path, lstatErr) } - return g.addEntry(path, fi, now, true, false) + return g.addEntry(path, fi, now, true, false, false) }) if err != nil { return fmt.Errorf("walking directory %s: %w", abs, err) diff --git a/manifest/manifest.go b/manifest/manifest.go index aabbf0a..8a1a414 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -15,6 +15,7 @@ type Entry struct { Hash string `yaml:"hash,omitempty"` PlaintextHash string `yaml:"plaintext_hash,omitempty"` Encrypted bool `yaml:"encrypted,omitempty"` + Locked bool `yaml:"locked,omitempty"` Type string `yaml:"type"` Mode string `yaml:"mode,omitempty"` Target string `yaml:"target,omitempty"` diff --git a/proto/sgard/v1/sgard.proto b/proto/sgard/v1/sgard.proto index b0adde0..c8a9093 100644 --- a/proto/sgard/v1/sgard.proto +++ b/proto/sgard/v1/sgard.proto @@ -16,6 +16,7 @@ message ManifestEntry { google.protobuf.Timestamp updated = 6; string plaintext_hash = 7; // SHA-256 of plaintext (encrypted entries only) bool encrypted = 8; + bool locked = 9; // repo-authoritative; restore always overwrites } // KekSlot describes a single KEK source for unwrapping the DEK. diff --git a/server/convert.go b/server/convert.go index 3c95470..187ae17 100644 --- a/server/convert.go +++ b/server/convert.go @@ -56,6 +56,7 @@ func EntryToProto(e manifest.Entry) *sgardpb.ManifestEntry { Updated: timestamppb.New(e.Updated), PlaintextHash: e.PlaintextHash, Encrypted: e.Encrypted, + Locked: e.Locked, } } @@ -70,6 +71,7 @@ func ProtoToEntry(p *sgardpb.ManifestEntry) manifest.Entry { Updated: p.GetUpdated().AsTime(), PlaintextHash: p.GetPlaintextHash(), Encrypted: p.GetEncrypted(), + Locked: p.GetLocked(), } } diff --git a/sgardpb/sgard.pb.go b/sgardpb/sgard.pb.go index b684497..5f42af6 100644 --- a/sgardpb/sgard.pb.go +++ b/sgardpb/sgard.pb.go @@ -85,6 +85,7 @@ type ManifestEntry struct { Updated *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=updated,proto3" json:"updated,omitempty"` PlaintextHash string `protobuf:"bytes,7,opt,name=plaintext_hash,json=plaintextHash,proto3" json:"plaintext_hash,omitempty"` // SHA-256 of plaintext (encrypted entries only) Encrypted bool `protobuf:"varint,8,opt,name=encrypted,proto3" json:"encrypted,omitempty"` + Locked bool `protobuf:"varint,9,opt,name=locked,proto3" json:"locked,omitempty"` // repo-authoritative; restore always overwrites unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -175,6 +176,13 @@ func (x *ManifestEntry) GetEncrypted() bool { return false } +func (x *ManifestEntry) GetLocked() bool { + if x != nil { + return x.Locked + } + return false +} + // KekSlot describes a single KEK source for unwrapping the DEK. type KekSlot struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1071,7 +1079,7 @@ var File_sgard_v1_sgard_proto protoreflect.FileDescriptor const file_sgard_v1_sgard_proto_rawDesc = "" + "\n" + - "\x14sgard/v1/sgard.proto\x12\bsgard.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xf2\x01\n" + + "\x14sgard/v1/sgard.proto\x12\bsgard.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x8a\x02\n" + "\rManifestEntry\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + "\x04hash\x18\x02 \x01(\tR\x04hash\x12\x12\n" + @@ -1080,7 +1088,8 @@ const file_sgard_v1_sgard_proto_rawDesc = "" + "\x06target\x18\x05 \x01(\tR\x06target\x124\n" + "\aupdated\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\aupdated\x12%\n" + "\x0eplaintext_hash\x18\a \x01(\tR\rplaintextHash\x12\x1c\n" + - "\tencrypted\x18\b \x01(\bR\tencrypted\"\xe4\x01\n" + + "\tencrypted\x18\b \x01(\bR\tencrypted\x12\x16\n" + + "\x06locked\x18\t \x01(\bR\x06locked\"\xe4\x01\n" + "\aKekSlot\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x1f\n" + "\vargon2_time\x18\x02 \x01(\x05R\n" +