From 6e938646dfb25cca035c00f4237f2d4ca4f2730e Mon Sep 17 00:00:00 2001 From: Marc Alvarez Date: Tue, 20 Oct 2026 20:12:38 -0600 Subject: [PATCH 1/2] internal/compat: a vault, export and sync folder made by v0.1.1, and tests that sesh still reads them --- AGENTS.md | 6 + docs/USAGE_AND_CONFIGURATION.md | 2 +- internal/compat/compat_test.go | 212 ++++++++++++++++++ internal/compat/testdata/v0.1.1/export.sesh | 12 + internal/compat/testdata/v0.1.1/make.sh | 78 +++++++ .../2168f0b61c0ec2e6d14300784c37755d.sesh | Bin 0 -> 900 bytes .../4d7a46fdb55d700f46e71837016b2744.sesh | Bin 0 -> 425 bytes .../5819a0d01cf40cb79f9e4440cc716517.sesh | Bin 0 -> 385 bytes .../610b7188de096b6077561d2d20267a58.sesh | Bin 0 -> 422 bytes .../82949d2b40eba42608e745a3b1902e06.sesh | Bin 0 -> 393 bytes .../ca8d678358042e5b449fdae013301522.sesh | Bin 0 -> 384 bytes .../testdata/v0.1.1/sync/sesh-sync.json | 14 ++ internal/compat/testdata/v0.1.1/vault.db | Bin 0 -> 172032 bytes 13 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 internal/compat/compat_test.go create mode 100644 internal/compat/testdata/v0.1.1/export.sesh create mode 100755 internal/compat/testdata/v0.1.1/make.sh create mode 100644 internal/compat/testdata/v0.1.1/sync/entries/2168f0b61c0ec2e6d14300784c37755d.sesh create mode 100644 internal/compat/testdata/v0.1.1/sync/entries/4d7a46fdb55d700f46e71837016b2744.sesh create mode 100644 internal/compat/testdata/v0.1.1/sync/entries/5819a0d01cf40cb79f9e4440cc716517.sesh create mode 100644 internal/compat/testdata/v0.1.1/sync/entries/610b7188de096b6077561d2d20267a58.sesh create mode 100644 internal/compat/testdata/v0.1.1/sync/entries/82949d2b40eba42608e745a3b1902e06.sesh create mode 100644 internal/compat/testdata/v0.1.1/sync/entries/ca8d678358042e5b449fdae013301522.sesh create mode 100644 internal/compat/testdata/v0.1.1/sync/sesh-sync.json create mode 100644 internal/compat/testdata/v0.1.1/vault.db diff --git a/AGENTS.md b/AGENTS.md index 5e177c70..2a1c5297 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,6 +117,12 @@ nothing, so the extension only works after the `browser install` above. ## Changes +- **What a released sesh wrote keeps working.** A change to how the vault, an + export or a sync folder is stored comes with a way to read what earlier + releases wrote: for the vault, a new upgrade step (`migrateVN` in + `internal/database/schema.go`, with `currentSchemaVersion` bumped), never an + edit to an old one. `internal/compat` keeps files written by released + versions, made-up, and tests that this sesh still reads them. - **One pull request per change**, each on its own branch (a git worktree per branch keeps them apart). Each is reviewed before it's merged, and CI must pass. diff --git a/docs/USAGE_AND_CONFIGURATION.md b/docs/USAGE_AND_CONFIGURATION.md index b31696db..9fb45933 100644 --- a/docs/USAGE_AND_CONFIGURATION.md +++ b/docs/USAGE_AND_CONFIGURATION.md @@ -864,7 +864,7 @@ An export to a file, in any format, doesn't replace an existing one without `--f Encrypted exports use Argon2id and AES-256-GCM, as the vault does. The file holds its own salt and Argon2id settings, so it opens on any computer, whatever its master password, given the export password. -Every export holds everything about each entry (CSV leaves out file entries, and says how many; JSON and the encrypted export include them): its kind, service name, username, secret, times, settings, folder, tags, URL, notes, and custom fields (secret ones included). A TOTP entry's settings (algorithm, digits, period, issuer) decide which codes are right, so they come back with it on import. In JSON they're the `settings` field; in CSV, a `settings` column holding the same JSON. +Every export holds everything about each entry (CSV leaves out file entries, and says how many; JSON and the encrypted export include them): its kind, service name, username, secret, times, settings, folder, tags, URL, notes, and custom fields (secret ones included). Its earlier secrets (its [history](#password-history)) aren't included. A TOTP entry's settings (algorithm, digits, period, issuer) decide which codes are right, so they come back with it on import. In JSON they're the `settings` field; in CSV, a `settings` column holding the same JSON. ### Changing your master password (`sesh passwd`) diff --git a/internal/compat/compat_test.go b/internal/compat/compat_test.go new file mode 100644 index 00000000..b47ae14f --- /dev/null +++ b/internal/compat/compat_test.go @@ -0,0 +1,212 @@ +// Package compat holds files written by released versions of sesh, and +// tests that this one still reads them: a vault, an encrypted export and a +// sync folder, each with made-up entries. A change to how any of them is +// stored needs a way to read what a release wrote; these tests fail +// without one. +package compat + +import ( + "bytes" + "os" + "path/filepath" + "slices" + "testing" + + "github.com/bashhack/sesh/internal/database" + "github.com/bashhack/sesh/internal/password" + "github.com/bashhack/sesh/internal/syncfolder" + "github.com/bashhack/sesh/internal/vault" +) + +// The made-up passwords v0.1.1's files were written with. +const ( + masterPassword = "fixture-v0.1.1-master" + exportPassword = "fixture-v0.1.1-export" +) + +// What v0.1.1's vault was given, entry by entry: each kept secret, and for +// the GitHub login, its earlier ones, newest first. +var ( + github = vault.Key{Kind: vault.KindPassword, Service: "GitHub", Username: "maya"} + githubTOTP = vault.Key{Kind: vault.KindTOTP, Service: "GitHub", Username: "maya"} + openai = vault.Key{Kind: vault.KindAPIKey, Service: "OpenAI"} + house = vault.Key{Kind: vault.KindNote, Service: "House"} + kube = vault.Key{Kind: vault.KindFile, Service: "kubeconfig"} + shop = vault.Key{Kind: vault.KindPassword, Service: "Shop", Username: "maya@example.org"} + secrets = map[vault.Key]string{ + github: "gh-pass-3", + githubTOTP: "JBSWY3DPEHPK3PXP", + openai: "sk-test-0123456789abcdef", + house: "Door code 4711\nAlarm 2580\n", + kube: "apiVersion: v1\nkind: Config\n", + } + githubEarlier = []string{"gh-pass-2", "gh-pass-1"} +) + +// copied is the release's file or folder at name, copied to a temporary +// folder, so a test's writes never reach the one kept here. +func copied(t *testing.T, release, name string) string { + t.Helper() + src := filepath.Join("testdata", release, name) + dst := filepath.Join(t.TempDir(), name) + err := filepath.WalkDir(src, func(p string, d os.DirEntry, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(src, p) + if err != nil { + return err + } + to := filepath.Join(dst, rel) + if d.IsDir() { + return os.MkdirAll(to, 0o700) + } + b, err := os.ReadFile(p) //nolint:gosec // the test's own files + if err != nil { + return err + } + return os.WriteFile(to, b, 0o600) + }) + if err != nil { + t.Fatal(err) + } + return dst +} + +// open opens the vault at path with password, and returns its key too. +func open(t *testing.T, path, pw string) (*database.Store, []byte) { + t.Helper() + src := database.NewMasterPasswordSource(path, func(string) ([]byte, error) { return []byte(pw), nil }) + st, err := database.Open(path, database.NewKeySourceOracle(src)) + if err != nil { + t.Fatalf("open: %v", err) + } + t.Cleanup(func() { + if err := st.Close(); err != nil { + t.Errorf("close: %v", err) + } + }) + key, err := src.GetEncryptionKey() + if err != nil { + t.Fatalf("key: %v", err) + } + return st, key +} + +// checkSecrets fails for each entry whose secret isn't the one v0.1.1 kept. +func checkSecrets(t *testing.T, st *database.Store) { + t.Helper() + for k, want := range secrets { + got, err := st.Get(k) + if err != nil || string(got) != want { + t.Errorf("%v: %q, %v; want %q", k, got, err, want) + } + } +} + +// A vault v0.1.1 made opens, and every entry, its details, its earlier secrets and the trash read back. +func TestVaultV011(t *testing.T) { + st, _ := open(t, copied(t, "v0.1.1", "vault.db"), masterPassword) + checkSecrets(t, st) + + entries, err := st.List(&vault.Filter{}) + if err != nil { + t.Fatal(err) + } + var keys []vault.Key + for _, e := range entries { + keys = append(keys, e.Key) + if e.Key == github && (e.Folder != "work" || !slices.Equal(e.Tags, []string{"dev"})) { + t.Errorf("GitHub filed in %q, tagged %q", e.Folder, e.Tags) + } + } + if len(keys) != len(secrets) { + t.Errorf("entries %v, want the %d v0.1.1 kept", keys, len(secrets)) + } + + d, err := st.Details(github, "compat test") + if err != nil { + t.Fatal(err) + } + if d.URL != "https://github.com/login" || string(d.Notes) != "Personal account. Recovery codes in the safe.\n" { + t.Errorf("GitHub's URL %q, notes %q", d.URL, d.Notes) + } + fields := map[string]string{} + for _, f := range d.Fields { + fields[f.Name] = string(f.Value) + if f.Secret != (f.Name == "pin") { + t.Errorf("field %s secret=%v", f.Name, f.Secret) + } + } + if fields["recovery-email"] != "maya@example.com" || fields["pin"] != "4711" { + t.Errorf("GitHub's fields %v", fields) + } + + for i, want := range githubEarlier { + got, err := st.ReadVersion(github, i+2, "compat test") // version 1 is the secret now + if err != nil || string(got) != want { + t.Errorf("GitHub's version %d: %q, %v; want %q", i+2, got, err, want) + } + } + + trash, err := st.Trash() + if err != nil { + t.Fatal(err) + } + if len(trash) != 1 || trash[0].Key != shop { + t.Errorf("trash %v, want Shop", trash) + } +} + +// An encrypted export v0.1.1 wrote imports into a new vault, every entry with it (its exports carry no earlier secrets). +func TestExportV011(t *testing.T) { + data, err := os.ReadFile(filepath.Join("testdata", "v0.1.1", "export.sesh")) + if err != nil { + t.Fatal(err) + } + st, _ := open(t, filepath.Join(t.TempDir(), "new.db"), "a-new-vault-password") + m := password.NewManager(st) + if _, err := m.ImportEncrypted(bytes.NewReader(data), password.ImportOptions{}, []byte(exportPassword)); err != nil { + t.Fatalf("import: %v", err) + } + checkSecrets(t, st) +} + +// A sync folder v0.1.1 made reads, every entry's file opening with the vault's key. +func TestSyncFolderV011(t *testing.T) { + _, key := open(t, copied(t, "v0.1.1", "vault.db"), masterPassword) + f := syncfolder.New(copied(t, "v0.1.1", "sync")) + if _, err := f.ReadInfo(); err != nil { + t.Fatalf("sesh-sync.json: %v", err) + } + c, err := f.Scan() + if err != nil { + t.Fatal(err) + } + got := map[vault.Key]*syncfolder.Record{} + for i := range c.Files { + b, err := f.Read(&c.Files[i]) + if err != nil { + t.Fatal(err) + } + _, rec, err := syncfolder.Open(database.KeySealer(key), b) + if err != nil { + t.Fatalf("%s: %v", c.Files[i].Path, err) + } + got[vault.Key{Kind: rec.Kind, Service: rec.Service, Username: rec.Username}] = rec + } + for k, want := range secrets { + if r := got[k]; r == nil || string(r.Secret) != want { + t.Errorf("%v: %+v, want secret %q", k, r, want) + } + } + if r := got[github]; r == nil || len(r.History) != len(githubEarlier) { + t.Errorf("GitHub's earlier secrets in the folder: %+v", r) + } + if r := got[shop]; r == nil || r.Deleted.IsZero() { + t.Errorf("Shop, in the trash, isn't marked so in the folder: %+v", r) + } + if len(c.Problems) != 0 { + t.Errorf("problems: %v", c.Problems) + } +} diff --git a/internal/compat/testdata/v0.1.1/export.sesh b/internal/compat/testdata/v0.1.1/export.sesh new file mode 100644 index 00000000..7a6078bf --- /dev/null +++ b/internal/compat/testdata/v0.1.1/export.sesh @@ -0,0 +1,12 @@ +{ + "algorithm": "argon2id", + "salt": "JWHosPsN+KnhS9gobw7BcDcgUM/yAQBTu0gAE3Tz1jI=", + "ciphertext": "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", + "params": { + "time": 2, + "memory": 19456, + "threads": 1, + "key_len": 32 + }, + "version": 1 +} diff --git a/internal/compat/testdata/v0.1.1/make.sh b/internal/compat/testdata/v0.1.1/make.sh new file mode 100755 index 00000000..ae46b774 --- /dev/null +++ b/internal/compat/testdata/v0.1.1/make.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# make.sh writes this folder's files with sesh v0.1.1, all made-up: the +# vault (vault.db), an encrypted export (export.sesh) and a sync folder +# (sync/). They're kept as v0.1.1 wrote them; compat_test.go checks that +# today's sesh still reads them. Run it only to remake them, with v0.1.1: +# +# internal/compat/testdata/v0.1.1/make.sh /path/to/sesh-0.1.1 +# +# Nothing personal goes in: a throwaway home for everything, made-up +# passwords, and a made-up computer name (sync records which computer made +# each change, and anyone can open these files with the password below). +set -euo pipefail + +sesh=$1 +out=$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +"$sesh" --version | grep -q '^sesh version 0.1.1 ' || { + echo "make.sh: $sesh isn't sesh 0.1.1" >&2 + exit 2 +} + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +mkdir -p "$tmp/home" "$tmp/config" "$tmp/out" +export HOME="$tmp/home" XDG_CONFIG_HOME="$tmp/config" +export SESH_DB_PATH="$tmp/out/vault.db" SESH_AUTH_SOCK="$tmp/agent.sock" +export SESH_MASTER_PASSWORD=fixture-v0.1.1-master SESH_SYNC_NAME=test-laptop +export SESH_KDF_MEMORY=19MiB SESH_KDF_TIME=2 SESH_KDF_THREADS=1 +unset SESH_BACKUP_DIR SESH_SYNC_DIR + +printf 'gh-pass-1\n' | "$sesh" add password GitHub --username maya --url https://github.com/login --folder work --tag dev +"$sesh" edit password/GitHub/maya --field recovery-email=maya@example.com +printf '4711\n' | "$sesh" edit password/GitHub/maya --secret-field pin +printf 'Personal account. Recovery codes in the safe.\n' | "$sesh" edit password/GitHub/maya --notes +printf 'gh-pass-2\n' | "$sesh" edit password/GitHub/maya --secret +printf 'gh-pass-3\n' | "$sesh" edit password/GitHub/maya --secret +printf 'JBSWY3DPEHPK3PXP\n' | "$sesh" add totp GitHub --username maya --folder work +printf 'sk-test-0123456789abcdef\n' | "$sesh" add key OpenAI --folder work --tag dev +printf 'Door code 4711\nAlarm 2580\n' | "$sesh" add note House --folder home +printf 'apiVersion: v1\nkind: Config\n' >"$tmp/kubeconfig" +"$sesh" add file kubeconfig --from "$tmp/kubeconfig" --folder work +printf 'old-shop-pass\n' | "$sesh" add password Shop --username maya@example.org +"$sesh" trash --force password/Shop/maya@example.org + +# The export asks for its password at a terminal: a pseudo-terminal answers. +python3 - "$sesh" export --file "$tmp/out/export.sesh" <<'EOF' +import os, pty, select, signal, sys, time +pid, fd = pty.fork() +if pid == 0: + os.execvp(sys.argv[1], sys.argv[1:]) +out, seen, deadline = b"", 0, time.monotonic() + 60 +while time.monotonic() < deadline: + if not select.select([fd], [], [], 1)[0]: + continue + try: + data = os.read(fd, 4096) + except OSError: + break + if not data: + break + out += data + last = out[seen:].rstrip().lower().split(b"\n")[-1] + if b"password" in last and last.endswith(b":"): + os.write(fd, b"fixture-v0.1.1-export\n") + seen = len(out) +else: + os.kill(pid, signal.SIGKILL) +_, status = os.waitpid(pid, 0) +sys.exit(os.waitstatus_to_exitcode(status)) +EOF + +mkdir "$tmp/sync" +"$sesh" sync init "$tmp/sync" #qSx!R%A!zWn91ghX4`z^A1gD+xCVJBwd4s)fuk--8^YOro$2l(S$+mfh} zjRCZ>p5jE>0!3#LhM9u@Ap3KH#z=v`q0i79(NzA_^IdXQtx?T4Qk6U$EFaHmN8y2W zeGa)Q8fGUf9qGfhTy)Zk#f51>oNDedE0$md9rTRzTGvT?jpUP;RBHgdRGHq0K;-B^ zRW>d#0p|u>97F=ZzjWEo6DyPJo0#b*@Ccc~&o%%;opY6HxrH=m;pV`mt2Z9+@UOfW zeqOdNZ_*WJF_blahFG2;G7g+?Dc1W7^sA}B@yB)phs)m3Jy+OpelxQYDLWd@RgbTM z%x$jBdLkDbM&n-q%0%RN?Sj_)JwX6M9;@=sprhkB3C2J|H>txelud9zOv_xR)~-n1 z>j$EvSI=CUazMy9lX69LKhceWo*jNeIbEWpi@*r&{H5eD_$E0HDh&~ybKY|79{CP$ zTl018kTa`(cM(q&;~q4B7#Vaab$yi+KRxy`Vz!m{Z>gpy2nUbMAtI0ji?9ZMgb>l{ zGOcC6_NPTR&}8r12D{O-9oORfRzYf=qMO+;fBzqw2=*cSt7U3~@FzyM-t1`|dW(XPS92=l!57=ID97sWepPl*1}^~}%t zducu}a01n>6_Hkn>dvQO^cQfe4s{Pugqmmu2k!ykp;dl5ZT)VjD}6Y3;TeFi`D-w2`u9%EhUci;YxZ a{nQ(|igr8*GnRxx3Z-Rp4mo0*=oExawyuN# literal 0 HcmV?d00001 diff --git a/internal/compat/testdata/v0.1.1/sync/entries/4d7a46fdb55d700f46e71837016b2744.sesh b/internal/compat/testdata/v0.1.1/sync/entries/4d7a46fdb55d700f46e71837016b2744.sesh new file mode 100644 index 0000000000000000000000000000000000000000..40e8e8d81f055ce9d49ed5c66b9bacb590d17bf0 GIT binary patch literal 425 zcmV;a0apG~MN>#qSx!R%O?pQCwOw!zM&}qe0c$5j+E)@f%62kL_uT=Ui~}=3S1C_+ zE!cAFk+T3JA7thxdZQyDMu^0-boTi139-Ak00048*&Zg}^-sN=u{WIkKf$L;0OlTx z#=!aszHZZvsl9ccNjd7QKWV3|ixIk}0vW@+vBUEpinftD2VnGMQ ztnR36G=8Uv{08Nb7@?OS$_m<*(m2;eUMon^dfK@Ys-%bFm_JGDW<$@2(Cmf$>XvX+ zn2(F&gi9KX$R+7CZo7Ev6{3odjUzQg_{-eS6-Z~Wu;&Hi?T&)9HVa8h-QeO}j=X2T z;x33xGgqn;eo4@vQ0fI{;1gv$%AX1Uq_ZO{%oW{{ADfTBVvYkbBk15pPC>B}sq5$c zk4MKg28}oVu>60!Tr?ds0<+lT&oN$bYExmMHX literal 0 HcmV?d00001 diff --git a/internal/compat/testdata/v0.1.1/sync/entries/5819a0d01cf40cb79f9e4440cc716517.sesh b/internal/compat/testdata/v0.1.1/sync/entries/5819a0d01cf40cb79f9e4440cc716517.sesh new file mode 100644 index 0000000000000000000000000000000000000000..6d04156037574662a720296b77bc37fa5a9f7ffa GIT binary patch literal 385 zcmV-{0e=2dMN>#qSx!R%SQ((u9P|vgpPocO%yDHG+E)@f%62kL_uT=Ui~}=3S1C_+ zE!cAFk+T3JA7thx)JtX{0U;q$Tr;j-YDTuf0003r-_kF3dR%7fg{MJzS3jW(+~~DE zFB5#+D11=7XH$|7syrjayPaKOoytNXaQ_dtUyoOQN zfS3l~kbs(7me{5#>Y{4a#SNr&b*ekR<1b!_R*jZ5!Eys>2z6(m*RKL2g(ObrGk6C> zu|hK5&HoDI%jRwXD^0j#aXB7PcCvFsOcb|@0g6jko}#qSx!R%VGD7H-U(}9cUB!OASQZP+E)@f%62kL_uT=Ui~}=3S1C_+ zE!cAFk+T3JA7thxyuac+f>fW=98Vhm6)bSW00045nq58%iG3b|3-DcbkcJl={U$D0 zFbY@e<}i%T#p7u#`$i!<#i$nirPR}-?KjL7qZSlBu`z1O4`Sg$)tRMzj|0DQHMZa` z$wa+XM`ueVT#ZW#^wb9xSCoGqL9gN4-m*qchox&!mB1dGx9?A`-Y=ZSb3Wg*1&n;Z zcbT+B#CoYLibe}nuosFwUDsAZ?V<)wm%*YYco=q61CkInv1GB(_S`0Yph)1ky?fnU z@sfb-8Q@7M0D<_a7)GY-ZEMZp@*ANiQ9j{rYP>d{THjr18LrWu?RIN=ST}&)FTx*$ zU<$6IhTooye0ntt8!nqWFkB=akR{=K4?QD~)Qqz(7k}Bb@CyI>!H^E^2P~RdCm@m& z2hyI$F(N_-StwW=_{L*JF5+VvNEmcKWZ^#qSx!R%f|Q*rK(#E!(4~E_^6s>jMYqpI`^y6jcy&zlvknCYDD%8Gq1Ep zFr+lY{2^Te7w;!qyUXi-l@x)S-Ky?8CSs0c@DhZdA#I-6 zm!{v3Jk?#)wx?9Zx6P-g8&iI6!rf{h?4VYxQ8R|ha=;5^>&kxMeJ73GN+h`_5*t&QLWJR`{Ica4~#oWU! literal 0 HcmV?d00001 diff --git a/internal/compat/testdata/v0.1.1/sync/entries/ca8d678358042e5b449fdae013301522.sesh b/internal/compat/testdata/v0.1.1/sync/entries/ca8d678358042e5b449fdae013301522.sesh new file mode 100644 index 0000000000000000000000000000000000000000..84c7bd4af7bc8ff1020bfdb7c54f6cbabf0ed8fb GIT binary patch literal 384 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 bDK}T02(S Date: Tue, 20 Oct 2026 20:26:25 -0600 Subject: [PATCH 2/2] internal/compat checks every format's details and TOTP settings, the vault's layout, and syncing and joining; AGENTS.md says to add a release's files and never loosen these tests --- AGENTS.md | 6 +- docs/USAGE_AND_CONFIGURATION.md | 8 +- internal/compat/compat_test.go | 219 ++++++++++++++---- internal/compat/testdata/v0.1.1/export.sesh | 4 +- internal/compat/testdata/v0.1.1/make.sh | 10 +- .../2168f0b61c0ec2e6d14300784c37755d.sesh | Bin 900 -> 0 bytes .../4d7a46fdb55d700f46e71837016b2744.sesh | Bin 425 -> 0 bytes .../5819a0d01cf40cb79f9e4440cc716517.sesh | Bin 385 -> 0 bytes .../610b7188de096b6077561d2d20267a58.sesh | Bin 422 -> 0 bytes .../6fc004559cd45c21905573369f24ff54.sesh | Bin 0 -> 469 bytes .../82949d2b40eba42608e745a3b1902e06.sesh | Bin 393 -> 0 bytes .../8a110c1a30af88555aa2a05345904f15.sesh | Bin 0 -> 425 bytes .../a2deb45d5804a42e0750c7fb0b178a91.sesh | Bin 0 -> 393 bytes .../a68c4e4522833ccfaa130357c6efefa4.sesh | Bin 0 -> 900 bytes .../b735eff353386ce13489603057254b89.sesh | Bin 0 -> 385 bytes .../ca8d678358042e5b449fdae013301522.sesh | Bin 384 -> 0 bytes .../e317e85c45e68250ac258fa1472e0c1e.sesh | Bin 0 -> 422 bytes .../testdata/v0.1.1/sync/sesh-sync.json | 6 +- internal/compat/testdata/v0.1.1/vault.db | Bin 172032 -> 172032 bytes 19 files changed, 199 insertions(+), 54 deletions(-) delete mode 100644 internal/compat/testdata/v0.1.1/sync/entries/2168f0b61c0ec2e6d14300784c37755d.sesh delete mode 100644 internal/compat/testdata/v0.1.1/sync/entries/4d7a46fdb55d700f46e71837016b2744.sesh delete mode 100644 internal/compat/testdata/v0.1.1/sync/entries/5819a0d01cf40cb79f9e4440cc716517.sesh delete mode 100644 internal/compat/testdata/v0.1.1/sync/entries/610b7188de096b6077561d2d20267a58.sesh create mode 100644 internal/compat/testdata/v0.1.1/sync/entries/6fc004559cd45c21905573369f24ff54.sesh delete mode 100644 internal/compat/testdata/v0.1.1/sync/entries/82949d2b40eba42608e745a3b1902e06.sesh create mode 100644 internal/compat/testdata/v0.1.1/sync/entries/8a110c1a30af88555aa2a05345904f15.sesh create mode 100644 internal/compat/testdata/v0.1.1/sync/entries/a2deb45d5804a42e0750c7fb0b178a91.sesh create mode 100644 internal/compat/testdata/v0.1.1/sync/entries/a68c4e4522833ccfaa130357c6efefa4.sesh create mode 100644 internal/compat/testdata/v0.1.1/sync/entries/b735eff353386ce13489603057254b89.sesh delete mode 100644 internal/compat/testdata/v0.1.1/sync/entries/ca8d678358042e5b449fdae013301522.sesh create mode 100644 internal/compat/testdata/v0.1.1/sync/entries/e317e85c45e68250ac258fa1472e0c1e.sesh diff --git a/AGENTS.md b/AGENTS.md index 2a1c5297..94fb9449 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -122,7 +122,11 @@ nothing, so the extension only works after the `browser install` above. releases wrote: for the vault, a new upgrade step (`migrateVN` in `internal/database/schema.go`, with `currentSchemaVersion` bumped), never an edit to an old one. `internal/compat` keeps files written by released - versions, made-up, and tests that this sesh still reads them. + versions, made-up, and tests that this sesh still reads them. A release + that changes a format adds its own (`testdata//`, made with that + release, as `testdata/v0.1.1/make.sh` does) and tests for them. Never edit + or remake a released version's files, or loosen these tests, to get a + failing one to pass: the failure is the point. - **One pull request per change**, each on its own branch (a git worktree per branch keeps them apart). Each is reviewed before it's merged, and CI must pass. diff --git a/docs/USAGE_AND_CONFIGURATION.md b/docs/USAGE_AND_CONFIGURATION.md index 9fb45933..68cecab4 100644 --- a/docs/USAGE_AND_CONFIGURATION.md +++ b/docs/USAGE_AND_CONFIGURATION.md @@ -435,7 +435,7 @@ sesh can't reset it for you: the master password derives the vault's key, sesh d sesh recover ``` -- **Restore from an encrypted export**, if you made one. Start a new vault (below), then import the export. It asks for the export's own password, which you chose when exporting: +- **Restore from an encrypted export**, if you made one. Start a new vault (below), then import the export (entries only: the trash and earlier secrets aren't in it). It asks for the export's own password, which you chose when exporting: ```bash sesh import --from sesh backup.enc @@ -745,7 +745,7 @@ What to do FAIL: 2 problems, 2 warnings ``` -It names every entry it can't read, and never shows a secret. It exits 1 when the config doesn't load, there's no vault, the vault file can't be read, the vault can't be unlocked (a wrong master password, say), or the file, an entry, the trash, an earlier secret, or the recovery key has a problem; warnings exit 0. Whatever stops the vault part, the setup's steps and the tally still show. If the file is damaged but every entry still reads, it says to export them now and start a new vault from that. If nothing can unlock the vault without asking (no `SESH_MASTER_PASSWORD`, no unlocked agent) and there's no terminal to ask at, as in a script, the setup is still checked, the last line says the vault wasn't, and it exits 0 unless the setup has a problem. If the agent locks or another command changes the master password while it checks, it says to run it again rather than blaming any entry. Run it when something seems off, after moving or restoring the vault, after a crash, or now and then. It writes nothing while it checks, then one `doctor` event to the audit log (not when the file itself is damaged). +It names every entry it can't read, and never shows a secret. It exits 1 when the config doesn't load, there's no vault, the vault file can't be read, the vault can't be unlocked (a wrong master password, say), or the file, an entry, the trash, an earlier secret, or the recovery key has a problem; warnings exit 0. Whatever stops the vault part, the setup's steps and the tally still show. If the file is damaged but every entry still reads, it says to export them now and start a new vault from that (the trash and earlier secrets stay in the old file). If nothing can unlock the vault without asking (no `SESH_MASTER_PASSWORD`, no unlocked agent) and there's no terminal to ask at, as in a script, the setup is still checked, the last line says the vault wasn't, and it exits 0 unless the setup has a problem. If the agent locks or another command changes the master password while it checks, it says to run it again rather than blaming any entry. Run it when something seems off, after moving or restoring the vault, after a crash, or now and then. It writes nothing while it checks, then one `doctor` event to the audit log (not when the file itself is damaged). ### Backups @@ -825,7 +825,7 @@ An automatic sync with nothing to do isn't written to the [audit log](#the-audit - **What a sync tool does is handled:** its copies of a file (Dropbox's "conflicted copy", Syncthing's, iCloud's "name 2") are merged and removed; a file it loses is written again, never taken for a deletion; an older copy it puts back is ignored, so an old password can't quietly return. - **iCloud Drive can keep files in the cloud** to save space ("Optimise Mac Storage"): sesh waits for those, says how many in the summary, and writes nothing over them. Keep the sync folder downloaded: in Finder, Control-click it and choose Keep Downloaded (macOS 15 or later). - **Each computer is named** in summaries and in history by its own name; `sync.name` changes it. -- **Joining needs a new or empty vault,** or one joining again the folder it left: a vault's own entries, never synced with the folder, would bring back, as edits, any deleted elsewhere. To bring a vault's entries along: `sesh export --file F`, move the vault's files aside (the vault, any `-wal` and `-shm` files beside it, and `touchid.key`), `sesh sync join `, then `sesh import --from sesh F`. +- **Joining needs a new or empty vault,** or one joining again the folder it left: a vault's own entries, never synced with the folder, would bring back, as edits, any deleted elsewhere. To bring a vault's entries along: `sesh export --file F`, move the vault's files aside (the vault, any `-wal` and `-shm` files beside it, and `touchid.key`), `sesh sync join `, then `sesh import --from sesh F`; the trash and earlier secrets stay with the vault moved aside. - **A master password changed on one computer** reaches the others: they ask for it at their next `sesh sync`, at a terminal, and switch to it (see [Changing your master password](#changing-your-master-password-sesh-passwd)). - **Joining again** works for a vault that left the folder, even one whose entries were all deleted since. One whose master password changed while it was away is switched to the folder's as it joins (it asks for the folder's, then its own): its own change isn't kept, so to change every computer's password, change it while syncing. - **`sync.dir` set by hand** (`sesh config set sync.dir`, or `SESH_SYNC_DIR`) to a folder the vault hasn't synced with is held to the same rule as joining: a vault with entries of its own that synced with another folder, or never synced, doesn't sync with one holding entry files (even ones it can't read), as that would bring back what was deleted in either; a new, empty folder starts afresh. A folder inside the vault's own, or holding the vault, is refused. @@ -1940,7 +1940,7 @@ sesh add --help | Message | What it means | What to do | |---|---|---| | "this vault doesn't sync: …" | `sync.dir` isn't set | `sesh sync init` makes a sync folder from this vault; `sesh sync join ` joins one | -| "this vault already has entries of its own, never synced with this folder. Joining takes a new or empty vault, …" (from `sesh sync join`) | Joining would bring back, as edits, entries deleted on the other computers | Follow the steps it gives: export, move the vault's files aside, join, import | +| "this vault already has entries of its own, never synced with this folder. Joining takes a new or empty vault, …" (from `sesh sync join`) | Joining would bring back, as edits, entries deleted on the other computers | Follow the steps it gives: export, move the vault's files aside, join, import (the trash and earlier secrets stay with the vault moved aside) | | "the master password was changed on another computer: …", or "⚠️ Sync: the master password was changed on another computer; nothing syncs until it's taken: sesh sync, or the Sync row in sesh ui's settings." | Another computer changed it; this vault still opens with its own, and doesn't sync | Run `sesh sync` at a terminal, or in `sesh ui` take it from the Sync row in settings and safety: each asks for this vault's password, then the new one. Forgotten your own? `sesh recover` takes the new one with the recovery key | | "⚠️ Sync: the master password was changed here and on another computer at once, and the other's is kept; nothing syncs until it's taken: sesh sync, or the Sync row in sesh ui's settings." | Two changes were made from the same master password, each computer's sync tool not yet showing the other's; the other one is kept on every computer | Run `sesh sync` at a terminal, or take it from the Sync row in `sesh ui`, and type the other change's password. If no computer of yours made it, `sesh passwd` puts yours back | | "this computer's master password change hasn't reached the sync folder yet: …" | The password was changed here with the folder out of reach | `sesh sync` sends it, as does any command that syncs on its own (`sync.auto`) once the folder is back | diff --git a/internal/compat/compat_test.go b/internal/compat/compat_test.go index b47ae14f..c8886bb6 100644 --- a/internal/compat/compat_test.go +++ b/internal/compat/compat_test.go @@ -7,15 +7,21 @@ package compat import ( "bytes" + "database/sql" "os" "path/filepath" "slices" "testing" + _ "modernc.org/sqlite" + "github.com/bashhack/sesh/internal/database" + "github.com/bashhack/sesh/internal/importer/seshexport" "github.com/bashhack/sesh/internal/password" "github.com/bashhack/sesh/internal/syncfolder" + "github.com/bashhack/sesh/internal/totp" "github.com/bashhack/sesh/internal/vault" + "github.com/bashhack/sesh/internal/vaultsync" ) // The made-up passwords v0.1.1's files were written with. @@ -24,8 +30,7 @@ const ( exportPassword = "fixture-v0.1.1-export" ) -// What v0.1.1's vault was given, entry by entry: each kept secret, and for -// the GitHub login, its earlier ones, newest first. +// What v0.1.1's vault was given (make.sh), entry by entry. var ( github = vault.Key{Kind: vault.KindPassword, Service: "GitHub", Username: "maya"} githubTOTP = vault.Key{Kind: vault.KindTOTP, Service: "GitHub", Username: "maya"} @@ -33,19 +38,28 @@ var ( house = vault.Key{Kind: vault.KindNote, Service: "House"} kube = vault.Key{Kind: vault.KindFile, Service: "kubeconfig"} shop = vault.Key{Kind: vault.KindPassword, Service: "Shop", Username: "maya@example.org"} - secrets = map[vault.Key]string{ + + // Each entry's secret. + secrets = map[vault.Key]string{ github: "gh-pass-3", githubTOTP: "JBSWY3DPEHPK3PXP", openai: "sk-test-0123456789abcdef", house: "Door code 4711\nAlarm 2580\n", kube: "apiVersion: v1\nkind: Config\n", } + // Each entry's settings: the TOTP's code settings decide which codes + // are right. + settings = map[vault.Key]vault.Settings{ + githubTOTP: {TOTP: totp.Params{Issuer: "GitHub", Algorithm: "SHA256", Digits: 8, Period: 60}}, + kube: {FileName: "kubeconfig"}, + } + // GitHub's earlier secrets, the newest first. githubEarlier = []string{"gh-pass-2", "gh-pass-1"} ) // copied is the release's file or folder at name, copied to a temporary // folder, so a test's writes never reach the one kept here. -func copied(t *testing.T, release, name string) string { +func copied(t *testing.T, release, name string) string { //nolint:unparam // a release that changes a format adds files of its own t.Helper() src := filepath.Join("testdata", release, name) dst := filepath.Join(t.TempDir(), name) @@ -73,7 +87,8 @@ func copied(t *testing.T, release, name string) string { return dst } -// open opens the vault at path with password, and returns its key too. +// open opens the vault at path with password, making it when there's none, +// and returns its key too. func open(t *testing.T, path, pw string) (*database.Store, []byte) { t.Helper() src := database.NewMasterPasswordSource(path, func(string) ([]byte, error) { return []byte(pw), nil }) @@ -93,41 +108,37 @@ func open(t *testing.T, path, pw string) (*database.Store, []byte) { return st, key } -// checkSecrets fails for each entry whose secret isn't the one v0.1.1 kept. -func checkSecrets(t *testing.T, st *database.Store) { +// checkVault fails for each entry whose secret, settings or filing isn't +// what v0.1.1 kept, and for GitHub's details. +func checkVault(t *testing.T, st *database.Store) { t.Helper() for k, want := range secrets { got, err := st.Get(k) if err != nil || string(got) != want { t.Errorf("%v: %q, %v; want %q", k, got, err, want) } - } -} - -// A vault v0.1.1 made opens, and every entry, its details, its earlier secrets and the trash read back. -func TestVaultV011(t *testing.T) { - st, _ := open(t, copied(t, "v0.1.1", "vault.db"), masterPassword) - checkSecrets(t, st) - - entries, err := st.List(&vault.Filter{}) - if err != nil { - t.Fatal(err) - } - var keys []vault.Key - for _, e := range entries { - keys = append(keys, e.Key) - if e.Key == github && (e.Folder != "work" || !slices.Equal(e.Tags, []string{"dev"})) { + e, err := st.Lookup(k) + if err != nil { + t.Errorf("%v: %v", k, err) + continue + } + if e.Settings != settings[k] { + t.Errorf("%v's settings: %+v, want %+v", k, e.Settings, settings[k]) + } + if k == github && (e.Folder != "work" || !slices.Equal(e.Tags, []string{"dev"})) { t.Errorf("GitHub filed in %q, tagged %q", e.Folder, e.Tags) } } - if len(keys) != len(secrets) { - t.Errorf("entries %v, want the %d v0.1.1 kept", keys, len(secrets)) - } - d, err := st.Details(github, "compat test") if err != nil { t.Fatal(err) } + checkGitHubDetails(t, &d) +} + +// checkGitHubDetails fails unless d is GitHub's URL, notes and fields. +func checkGitHubDetails(t *testing.T, d *vault.Details) { + t.Helper() if d.URL != "https://github.com/login" || string(d.Notes) != "Personal account. Recovery codes in the safe.\n" { t.Errorf("GitHub's URL %q, notes %q", d.URL, d.Notes) } @@ -138,17 +149,29 @@ func TestVaultV011(t *testing.T) { t.Errorf("field %s secret=%v", f.Name, f.Secret) } } - if fields["recovery-email"] != "maya@example.com" || fields["pin"] != "4711" { + if len(fields) != 2 || fields["recovery-email"] != "maya@example.com" || fields["pin"] != "4711" { t.Errorf("GitHub's fields %v", fields) } +} + +// A vault v0.1.1 made opens, and every entry, its details, its earlier secrets and the trash read back. +func TestVaultV011(t *testing.T) { + st, _ := open(t, copied(t, "v0.1.1", "vault.db"), masterPassword) + checkVault(t, st) + entries, err := st.List(&vault.Filter{}) + if err != nil { + t.Fatal(err) + } + if len(entries) != len(secrets) { + t.Errorf("%d entries, want the %d v0.1.1 kept", len(entries), len(secrets)) + } for i, want := range githubEarlier { got, err := st.ReadVersion(github, i+2, "compat test") // version 1 is the secret now if err != nil || string(got) != want { t.Errorf("GitHub's version %d: %q, %v; want %q", i+2, got, err, want) } } - trash, err := st.Trash() if err != nil { t.Fatal(err) @@ -158,21 +181,98 @@ func TestVaultV011(t *testing.T) { } } -// An encrypted export v0.1.1 wrote imports into a new vault, every entry with it (its exports carry no earlier secrets). +// schema is the tables, indexes and triggers of the vault at path, as +// SQLite holds their definitions. +func schema(t *testing.T, path string) []string { + t.Helper() + db, err := sql.Open("sqlite", "file:"+path+"?mode=ro") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := db.Close(); err != nil { + t.Error(err) + } + }() + rows, err := db.Query(`SELECT type || ' ' || name || ': ' || COALESCE(sql, '') FROM sqlite_master ORDER BY type, name`) + if err != nil { + t.Fatal(err) + } + var out []string + for rows.Next() { + var s string + if err := rows.Scan(&s); err != nil { + t.Fatal(err) + } + out = append(out, s) + } + if err := rows.Err(); err != nil { + t.Fatal(err) + } + return out +} + +// v0.1.1's vault, opened (and so upgraded), is laid out as a new vault is: a change to an upgrade step already +// released, rather than a new step, leaves the two apart. +func TestVaultSchemaV011(t *testing.T) { + old := copied(t, "v0.1.1", "vault.db") + open(t, old, masterPassword) + fresh := filepath.Join(t.TempDir(), "new.db") + open(t, fresh, "a-new-vault-password") + a, b := schema(t, old), schema(t, fresh) + for _, s := range a { + if !slices.Contains(b, s) { + t.Errorf("only in the upgraded v0.1.1 vault: %s", s) + } + } + for _, s := range b { + if !slices.Contains(a, s) { + t.Errorf("only in a new vault: %s", s) + } + } +} + +// An encrypted export v0.1.1 wrote reads, as sesh import reads it: every entry with its secret, settings, filing and +// details (its exports carry no earlier secrets). func TestExportV011(t *testing.T) { data, err := os.ReadFile(filepath.Join("testdata", "v0.1.1", "export.sesh")) if err != nil { t.Fatal(err) } - st, _ := open(t, filepath.Join(t.TempDir(), "new.db"), "a-new-vault-password") - m := password.NewManager(st) - if _, err := m.ImportEncrypted(bytes.NewReader(data), password.ImportOptions{}, []byte(exportPassword)); err != nil { - t.Fatalf("import: %v", err) + sealed, err := password.ReadSealed(bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + list, err := sealed.Entries([]byte(exportPassword)) + if err != nil { + t.Fatal(err) + } + got := map[vault.Key]bool{} + for _, e := range seshexport.Entries(list) { + got[e.Key] = true + if e.Skip != "" { + t.Errorf("%v skipped: %s", e.Key, e.Skip) + continue + } + if string(e.Secret) != secrets[e.Key] || e.Settings != settings[e.Key] { + t.Errorf("%v: secret %q, settings %+v", e.Key, e.Secret, e.Settings) + } + if e.Key == github { + if e.Folder != "work" || !slices.Equal(e.Tags, []string{"dev"}) { + t.Errorf("GitHub filed in %q, tagged %q", e.Folder, e.Tags) + } + checkGitHubDetails(t, &e.Details) + } + } + for k := range secrets { + if !got[k] { + t.Errorf("%v isn't in the export", k) + } } - checkSecrets(t, st) } -// A sync folder v0.1.1 made reads, every entry's file opening with the vault's key. +// A sync folder v0.1.1 made reads: every entry's file opens with the vault's key, with its secret, settings and +// earlier secrets, and the trashed entry marked so. func TestSyncFolderV011(t *testing.T) { _, key := open(t, copied(t, "v0.1.1", "vault.db"), masterPassword) f := syncfolder.New(copied(t, "v0.1.1", "sync")) @@ -183,6 +283,9 @@ func TestSyncFolderV011(t *testing.T) { if err != nil { t.Fatal(err) } + if len(c.Problems) != 0 { + t.Errorf("problems: %v", c.Problems) + } got := map[vault.Key]*syncfolder.Record{} for i := range c.Files { b, err := f.Read(&c.Files[i]) @@ -196,17 +299,49 @@ func TestSyncFolderV011(t *testing.T) { got[vault.Key{Kind: rec.Kind, Service: rec.Service, Username: rec.Username}] = rec } for k, want := range secrets { - if r := got[k]; r == nil || string(r.Secret) != want { - t.Errorf("%v: %+v, want secret %q", k, r, want) + r := got[k] + if r == nil || string(r.Secret) != want || r.Settings != settings[k] { + t.Errorf("%v: %+v, want secret %q, settings %+v", k, r, want, settings[k]) } } - if r := got[github]; r == nil || len(r.History) != len(githubEarlier) { - t.Errorf("GitHub's earlier secrets in the folder: %+v", r) + if r := got[github]; r == nil || len(r.History) != len(githubEarlier) || r.URL != "https://github.com/login" { + t.Errorf("GitHub in the folder: %+v", r) } if r := got[shop]; r == nil || r.Deleted.IsZero() { t.Errorf("Shop, in the trash, isn't marked so in the folder: %+v", r) } - if len(c.Problems) != 0 { - t.Errorf("problems: %v", c.Problems) +} + +// v0.1.1's vault syncs with its own folder and finds nothing to do: what it kept of the folder still reads. +func TestSyncV011(t *testing.T) { + st, _ := open(t, copied(t, "v0.1.1", "vault.db"), masterPassword) + sum, err := vaultsync.SyncWith(st, syncfolder.New(copied(t, "v0.1.1", "sync")), "test-laptop", vaultsync.Options{}) + if err != nil { + t.Fatal(err) + } + if len(sum.Received)+len(sum.Sent)+len(sum.Merged)+len(sum.Problems)+len(sum.Warnings) != 0 { + t.Errorf("syncing v0.1.1's vault with its own folder did something: %+v", *sum) + } +} + +// Another computer joins v0.1.1's sync folder with its master password, and gets every entry. +func TestJoinV011(t *testing.T) { + f := syncfolder.New(copied(t, "v0.1.1", "sync")) + info, err := f.ReadInfo() + if err != nil { + t.Fatal(err) + } + m := database.UnlockMaterial{Salt: info.Key.Salt, Verify: info.Key.Verify, Params: info.Key.Params} + if err := database.CheckMasterPassword([]byte(masterPassword), m); err != nil { + t.Fatalf("the folder's key record: %v", err) + } + p := filepath.Join(t.TempDir(), "joined.db") + if _, err := database.JoinVault(p, m, info.FolderID, info.Links); err != nil { + t.Fatal(err) + } + st, _ := open(t, p, masterPassword) + if _, err := vaultsync.SyncWith(st, f, "other-laptop", vaultsync.Options{}); err != nil { + t.Fatal(err) } + checkVault(t, st) } diff --git a/internal/compat/testdata/v0.1.1/export.sesh b/internal/compat/testdata/v0.1.1/export.sesh index 7a6078bf..1736f4c9 100644 --- a/internal/compat/testdata/v0.1.1/export.sesh +++ b/internal/compat/testdata/v0.1.1/export.sesh @@ -1,7 +1,7 @@ { "algorithm": "argon2id", - "salt": "JWHosPsN+KnhS9gobw7BcDcgUM/yAQBTu0gAE3Tz1jI=", - "ciphertext": "TgongGqHPSDwex4+YuOgt0Dd+SbF4zK9+tJwcRvfG9skKtKwBTTR7Y+5Wgec+AfDp4g7vXnnKKGi0Z/+6PMKIcnLgDx++3ZB5TlAV88S08eWZwRCL0W0GUFQ9Rl7f2r57Hb+bfotF49lvtKm2T8R8gOskFxTKNmHkPEeuH0iMO0yhR3RHv/JSASMumR51QLeOromoA+NELHrQyrR69ujDiN1gck096Goh+jHHtOB9VrZm4K1vvRkSKzfKTl5+2LF77hgE2DyKooptjB91MmXAxOiHVInD9Ng0Xk7sVEozUp0Ym/sX/PJ3otzzWp5lBxvWHKv1ZqNfP+Rj8LgnYL5/h1NjE/IjrYkn53udU2tptWIbKUOEgy9DJRqbSHYocUaIqiTJuRRXMMbGbONI7sexjmjds5T1SMGXYzR5tNaskvX0IMjqLo2Dko9AqoCxDv4zjRVZkXkI/7lZtEehZpNnmYzBp7yd3AMll6Gi3OKczNZCNkik26VPRMAlkU6Qtihlqa5hxvMJp9PSKMvBrn6XKQVHum8Ji0s5VvZSnEX6xaVsoniaBCa9Y3txh7bKidbGLHejLfYXSeG/dqONhwAyTxyFYVcCAC01dC1ED38s8Trgrw5gMdQFeB1gqjibhk1o5ywM3cf8htpBJQx/xp3LK19R2W8gXJB2f1AJKXL0J5ViJgzuPpDNHxQaKakR5wU1WWtrlpM6CNKEEAmsz3tjVEKl0xYoTOsKnckxyXt4Z24wpdReDkkOQyu6Hy8Oqj5cRJe8jG8W0jp9FHmZAbM8KR6++VhubzFsXhS7YO2UxSxq64aVxR5UfbkckcvYN+xrpYzc0K3VcW5hEEpn48P27jl80npUpuKmcLB6j9ulBekKsw7CShiLKU3ed5U5k75pQ/wlsBrRztBn8WyCTa/eB9X8vv+KjlAQSJbXCGGWSJ0ngUPLwOUC54ZnMVYTzcTUPUBEwQ3kngTf2Cc1cawTBs3rXR+MGw8t6LJQhAtyEwaTQIu9ZmZV/TFgwTLi6EB3IZa8xGytVN6E1pX28OrDiPNIDE6yUvHmKIlny0oFXg0VA6+Hxtcrs6o/z6dZokgspG+jwXpsYucReIwP0KNwBeNsE/dpoGyVH7WBEP0ubtHXe2aTIjc+wUKBnQwh/QeTM7EijK5ZJco5eaNX4KUpxA3C4xXxlnuL0rlzO96lDang616vxb3LHb7ouCogNx23bvY7mCLEBG6F4u1tQesumsgRTzSMNcSLCEZh/L+IVTTd81JCaqz72wRyqle+q2PIXQtiXx27zui2cdKCc7Zpl0oDYgzAp1zFlbvUCn9ak8BUzp2FMR3eNepsliMFUgNEQwZIm8uPE6YIe5eC8tFQ1+Uq91xHiWAgL5XBTPUP18iXht9Gz0/5C7uoCAE5CVuBwmf8pRuDVis4NXUR1V7sU+Hl3ipRw7sxi8jUh1v13xFTdpB9uBg6cKx5mjVZ4jZiPNdc5MBN+UB0RwZrUXWtQyW3WkLYR6OUnasRprg97kmYv4aN/0lfTFB20p63NKDyoNpWree5uH3iPJFI/m65WgGLDZwAqadfA2+f9J2P5oGyv1kNcDVjaIaJcjkB1k7Axg8l272/3PQdyKx41g13q0GHzxgXZIM9Lew1Vax5AeFDVQoVyG7Mz/nUsCDeyJobG8z8HDzER65A3CON7pqcuRJTobnEk0LiE3Jm3WO5Sg88erjzNQjU4bbklCwwqEwGYaayWOZOFIltzDV1YJsU75HdLY1dbnHYPpCQ7bMBBT8gDMSUz2BkG5x/UCrV24nRt2+NfHkINc3eh6IhDxmwc80TlvDIMiKlJS7Hyyv0dBivrJE37IVVS0/pv6e1SJP+jUfeAF+Du68B8+evNdgunGltvOdOcBjrgughFxcrM/GKq/xNBuDm/sPh75Znt7Zz8UV9izY1I6jYJEQnhmJH4RrKLVvAdIrJv6SKhGd+qWxZP+PBD5F3REH7fKTeTyPBTpqVHTdtennw5YL3eXSu8o6+nV5ItnkvJocrOWhyaAiswTcm0Uab9iRGIWvLq18OYGk4iirTpzQnNEAQ7f8EtCtTKyPVNRu/XRUp3DOVQnGYxCkggIx8OwiEBlr5x55SYfYa9yV8HBeqj6OhoBARr1ktrGaOWyHMcyJUZIlfSXg", + "salt": "eEZmjhpAu8RLsFov6v/MEwzfP5Vhd6X85R4yP+P26/A=", + "ciphertext": "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", "params": { "time": 2, "memory": 19456, diff --git a/internal/compat/testdata/v0.1.1/make.sh b/internal/compat/testdata/v0.1.1/make.sh index ae46b774..d4b4f140 100755 --- a/internal/compat/testdata/v0.1.1/make.sh +++ b/internal/compat/testdata/v0.1.1/make.sh @@ -18,6 +18,11 @@ out=$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) exit 2 } +# Nothing from the caller's own sesh settings. +for v in $(compgen -e | grep '^SESH_' || true); do + unset "$v" +done + tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT mkdir -p "$tmp/home" "$tmp/config" "$tmp/out" @@ -25,7 +30,6 @@ export HOME="$tmp/home" XDG_CONFIG_HOME="$tmp/config" export SESH_DB_PATH="$tmp/out/vault.db" SESH_AUTH_SOCK="$tmp/agent.sock" export SESH_MASTER_PASSWORD=fixture-v0.1.1-master SESH_SYNC_NAME=test-laptop export SESH_KDF_MEMORY=19MiB SESH_KDF_TIME=2 SESH_KDF_THREADS=1 -unset SESH_BACKUP_DIR SESH_SYNC_DIR printf 'gh-pass-1\n' | "$sesh" add password GitHub --username maya --url https://github.com/login --folder work --tag dev "$sesh" edit password/GitHub/maya --field recovery-email=maya@example.com @@ -33,7 +37,9 @@ printf '4711\n' | "$sesh" edit password/GitHub/maya --secret-field pin printf 'Personal account. Recovery codes in the safe.\n' | "$sesh" edit password/GitHub/maya --notes printf 'gh-pass-2\n' | "$sesh" edit password/GitHub/maya --secret printf 'gh-pass-3\n' | "$sesh" edit password/GitHub/maya --secret -printf 'JBSWY3DPEHPK3PXP\n' | "$sesh" add totp GitHub --username maya --folder work +# A TOTP entry with settings of its own, which decide which codes are right. +printf 'otpauth://totp/GitHub:maya?secret=JBSWY3DPEHPK3PXP&issuer=GitHub&algorithm=SHA256&digits=8&period=60\n' | + "$sesh" add totp GitHub --username maya --folder work printf 'sk-test-0123456789abcdef\n' | "$sesh" add key OpenAI --folder work --tag dev printf 'Door code 4711\nAlarm 2580\n' | "$sesh" add note House --folder home printf 'apiVersion: v1\nkind: Config\n' >"$tmp/kubeconfig" diff --git a/internal/compat/testdata/v0.1.1/sync/entries/2168f0b61c0ec2e6d14300784c37755d.sesh b/internal/compat/testdata/v0.1.1/sync/entries/2168f0b61c0ec2e6d14300784c37755d.sesh deleted file mode 100644 index c803511a3cd04197d92a5130d3e564452faa0f81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 900 zcmV-~1AF{aMN>#qSx!R%A!zWn91ghX4`z^A1gD+xCVJBwd4s)fuk--8^YOro$2l(S$+mfh} zjRCZ>p5jE>0!3#LhM9u@Ap3KH#z=v`q0i79(NzA_^IdXQtx?T4Qk6U$EFaHmN8y2W zeGa)Q8fGUf9qGfhTy)Zk#f51>oNDedE0$md9rTRzTGvT?jpUP;RBHgdRGHq0K;-B^ zRW>d#0p|u>97F=ZzjWEo6DyPJo0#b*@Ccc~&o%%;opY6HxrH=m;pV`mt2Z9+@UOfW zeqOdNZ_*WJF_blahFG2;G7g+?Dc1W7^sA}B@yB)phs)m3Jy+OpelxQYDLWd@RgbTM z%x$jBdLkDbM&n-q%0%RN?Sj_)JwX6M9;@=sprhkB3C2J|H>txelud9zOv_xR)~-n1 z>j$EvSI=CUazMy9lX69LKhceWo*jNeIbEWpi@*r&{H5eD_$E0HDh&~ybKY|79{CP$ zTl018kTa`(cM(q&;~q4B7#Vaab$yi+KRxy`Vz!m{Z>gpy2nUbMAtI0ji?9ZMgb>l{ zGOcC6_NPTR&}8r12D{O-9oORfRzYf=qMO+;fBzqw2=*cSt7U3~@FzyM-t1`|dW(XPS92=l!57=ID97sWepPl*1}^~}%t zducu}a01n>6_Hkn>dvQO^cQfe4s{Pugqmmu2k!ykp;dl5ZT)VjD}6Y3;TeFi`D-w2`u9%EhUci;YxZ a{nQ(|igr8*GnRxx3Z-Rp4mo0*=oExawyuN# diff --git a/internal/compat/testdata/v0.1.1/sync/entries/4d7a46fdb55d700f46e71837016b2744.sesh b/internal/compat/testdata/v0.1.1/sync/entries/4d7a46fdb55d700f46e71837016b2744.sesh deleted file mode 100644 index 40e8e8d81f055ce9d49ed5c66b9bacb590d17bf0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 425 zcmV;a0apG~MN>#qSx!R%O?pQCwOw!zM&}qe0c$5j+E)@f%62kL_uT=Ui~}=3S1C_+ zE!cAFk+T3JA7thxdZQyDMu^0-boTi139-Ak00048*&Zg}^-sN=u{WIkKf$L;0OlTx z#=!aszHZZvsl9ccNjd7QKWV3|ixIk}0vW@+vBUEpinftD2VnGMQ ztnR36G=8Uv{08Nb7@?OS$_m<*(m2;eUMon^dfK@Ys-%bFm_JGDW<$@2(Cmf$>XvX+ zn2(F&gi9KX$R+7CZo7Ev6{3odjUzQg_{-eS6-Z~Wu;&Hi?T&)9HVa8h-QeO}j=X2T z;x33xGgqn;eo4@vQ0fI{;1gv$%AX1Uq_ZO{%oW{{ADfTBVvYkbBk15pPC>B}sq5$c zk4MKg28}oVu>60!Tr?ds0<+lT&oN$bYExmMHX diff --git a/internal/compat/testdata/v0.1.1/sync/entries/5819a0d01cf40cb79f9e4440cc716517.sesh b/internal/compat/testdata/v0.1.1/sync/entries/5819a0d01cf40cb79f9e4440cc716517.sesh deleted file mode 100644 index 6d04156037574662a720296b77bc37fa5a9f7ffa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 385 zcmV-{0e=2dMN>#qSx!R%SQ((u9P|vgpPocO%yDHG+E)@f%62kL_uT=Ui~}=3S1C_+ zE!cAFk+T3JA7thx)JtX{0U;q$Tr;j-YDTuf0003r-_kF3dR%7fg{MJzS3jW(+~~DE zFB5#+D11=7XH$|7syrjayPaKOoytNXaQ_dtUyoOQN zfS3l~kbs(7me{5#>Y{4a#SNr&b*ekR<1b!_R*jZ5!Eys>2z6(m*RKL2g(ObrGk6C> zu|hK5&HoDI%jRwXD^0j#aXB7PcCvFsOcb|@0g6jko}#qSx!R%VGD7H-U(}9cUB!OASQZP+E)@f%62kL_uT=Ui~}=3S1C_+ zE!cAFk+T3JA7thxyuac+f>fW=98Vhm6)bSW00045nq58%iG3b|3-DcbkcJl={U$D0 zFbY@e<}i%T#p7u#`$i!<#i$nirPR}-?KjL7qZSlBu`z1O4`Sg$)tRMzj|0DQHMZa` z$wa+XM`ueVT#ZW#^wb9xSCoGqL9gN4-m*qchox&!mB1dGx9?A`-Y=ZSb3Wg*1&n;Z zcbT+B#CoYLibe}nuosFwUDsAZ?V<)wm%*YYco=q61CkInv1GB(_S`0Yph)1ky?fnU z@sfb-8Q@7M0D<_a7)GY-ZEMZp@*ANiQ9j{rYP>d{THjr18LrWu?RIN=ST}&)FTx*$ zU<$6IhTooye0ntt8!nqWFkB=akR{=K4?QD~)Qqz(7k}Bb@CyI>!H^E^2P~RdCm@m& z2hyI$F(N_-StwW=_{L*JF5+VvNEmcKWZ^#qSx!R%Z@>gqoYY()kX3UwpCtcOa{vETE>V{Fat}E}IWD{v*#;aZ z{TbZ)pg>}2u%;rc)CeTK*tWRP>h3oMNGv~30004q(YMsV!+RHb^lj=0{^~VMiS{sL z=o+d&EBF8Yu7*f>q>YEx`*VcD59NXI+-jSv9C}J>`pNPUsaTQq;C6|km7pd_OkVPU zryg&ZWntMK5Lqfc|HAlOb$Z#F?I2fkP3=a~?lQA8nkJaObZ9e_*3~jUUH8sCsHRq} zR!IzECSYb+bGS(Aq-8OkKp*fPL~wT4mb+(NAdZ;+b&p_>V>r$ZVEKLFU^J9`hnZ(J zNfh)n2stGySX!RJ&1R6a$uc8ejngsN$cOTtPlm~=Wc5|YVW2Y9BF{Uh0#Z(_;hCh} z_e^HTpq~EZSDes-itk$xoM`i{VzlmX)K{+pHk(tqR7_CVFrV*CG=A~kStZt L<7-Ie2-^!G!^80} literal 0 HcmV?d00001 diff --git a/internal/compat/testdata/v0.1.1/sync/entries/82949d2b40eba42608e745a3b1902e06.sesh b/internal/compat/testdata/v0.1.1/sync/entries/82949d2b40eba42608e745a3b1902e06.sesh deleted file mode 100644 index ba44a116360fc20b0cf9e03e0ff054581731c523..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 393 zcmV;40e1dVMN>#qSx!R%f|Q*rK(#E!(4~E_^6s>jMYqpI`^y6jcy&zlvknCYDD%8Gq1Ep zFr+lY{2^Te7w;!qyUXi-l@x)S-Ky?8CSs0c@DhZdA#I-6 zm!{v3Jk?#)wx?9Zx6P-g8&iI6!rf{h?4VYxQ8R|ha=;5^>&kxMeJ73GN+h`_5*t&QLWJR`{Ica4~#oWU! diff --git a/internal/compat/testdata/v0.1.1/sync/entries/8a110c1a30af88555aa2a05345904f15.sesh b/internal/compat/testdata/v0.1.1/sync/entries/8a110c1a30af88555aa2a05345904f15.sesh new file mode 100644 index 0000000000000000000000000000000000000000..8e2367e4be92650432495880d6a27249b70a3534 GIT binary patch literal 425 zcmV;a0apG~MN>#qSx!R%iV+MNFt3PJTB4v+MUYPwa{vETE>V{Fat}E}IWD{v*#;aZ z{TbZ)pg>}2u%;rc5{IPuEYh5XJEv*3;w{O1000488~&&mEF-fb1XWacet)~;b@q@Y zNNEC7=sOA{5DaJl50l;5PK@)thDG|+dGwD}$e*vP*GCiT@ynTp)?Q3Seq1KcLpWvF zV8bpzsA{C&A_K@-N2XDhsuh_p^!6xTlsjfZIfo|&xxCdKT^XyUX=n80@zEtoAvm5g z=S@-uA{A5}Sl#j`ef$Jd*2lhHJWn_+MSg8R&Fqp`%PDK)}7qypAiqEb&oGF9*O zJsA)d8ThSA&5w00l>eXvpkH32kVVYMfD!{^&=Jw`;^_EQCwSh&t}%Jm1;TD@LAl?n z1lI8Dev>kljW*I#M`syUAqoL;nCN1?WrZJC(mX78=J8I-J*iAo6rNQ8$Wj={U`D_D0L!DF`Y21N5eW9x6y6~3R0iBGhXUmRkc>SQUDoVof(8bR T;UA!ge@X&Uj@nvcVo(D06fnk* literal 0 HcmV?d00001 diff --git a/internal/compat/testdata/v0.1.1/sync/entries/a2deb45d5804a42e0750c7fb0b178a91.sesh b/internal/compat/testdata/v0.1.1/sync/entries/a2deb45d5804a42e0750c7fb0b178a91.sesh new file mode 100644 index 0000000000000000000000000000000000000000..8eede645f9e0f23cbafb79a39775e0eefff61282 GIT binary patch literal 393 zcmV;40e1dVMN>#qSx!R%qTaM!SOlal2T;fR3m1x!a{vETE>V{Fat}E}IWD{v*#;aZ z{TbZ)pg>}2u%;rc&OeP@{x&AM7ov+vgJhx80003zdxIsR1Y8cYR)8j}yV{L=D|X={ z8Ao^AQ~#-?g z*fh5)GI3iO*YXEzETK8Nc(@Zur8SSF2;BAj>Y9w; nPvt^j3D%DMqIb*bv>-H=#s9Wb^FOgO0r*a`hf1o>as^j;in+OY literal 0 HcmV?d00001 diff --git a/internal/compat/testdata/v0.1.1/sync/entries/a68c4e4522833ccfaa130357c6efefa4.sesh b/internal/compat/testdata/v0.1.1/sync/entries/a68c4e4522833ccfaa130357c6efefa4.sesh new file mode 100644 index 0000000000000000000000000000000000000000..28821d3146df1e0b6ddbf6247461c316ce0b6806 GIT binary patch literal 900 zcmV-~1AF{aMN>#qSx!R%ri@NSB7;27suKfO#_#W>a{vETE>V{Fat}E}IWD{v*#;aZ z{TbZ)pg>}2u%;rcu;x(>=k{bK^R{ua4%E~N0009wa7x(?f5V@|*d7U8VCJa--z2^Y zfjSI$(KUKCu=k2VPS-ztK}d;Y|55~t9jys0Ij-J2AH42}I4r9^Vky?F?do^OY$TV@ z=@Gs3io^QIkKvUJ2`2uUyku1U1GK(ZmqSFhnvMr?sN zXa^ndBCszlD{qx08B!YgPt&vAl_c{dm1e&L<*~9s2TFjRB2ES;lC{p-x}@FIic4+C zK|^XS=Tl)Sy4=53bu%uvLot_<3{&eXp3xYbn9I}gha*vxO@#cU?T;3=MPn%x=b|(( zg*#*MvTP%iZ+dLx`7sDOa`zWuB5S_)!cB5|gEh+H#be-b2;>_zwU~%U%R4ZqEUh_& zi;u8;W<)d*S5qx5eyrqhDx||HG!Icj{%q+E?GFQ5BEmRp%Z)YQY)yxOGJ+K+4V>3i z{#3(}DcHl*_V=PqX^ND2(gZvkW-~rVlIPcaAz>$7w~kZow$+~3s2SX8j=J>59};EQ zhl20+c{hkVFKT?i1pU~YD(Knh~p5&jPi z@K;ICZ3aVx863m&tb4AM?BXN}#v?WZVR%AN_8BvPnmiFU?gbVEq-yVV>n*Nil(lsS z;LyTgvGoH9?ydVZfXOxdgsJhzAl~$eF;((RN8O3i)7JdWGrzCa8boo$DEV%q8h93F z?FFdspu(s_#0pVk-Mg%RbJYB+8bd)u&8pj?G_Ua#%}%P%voHKLz1CKEJUnxEu>EGa z7j~sjsY+deznVB)fdPcjAjB|6mCRO7c>?|+24BrP?jPz`&(6b!;wW<) z9StX>?=B-P1p+(y9pZ_RF-%J9P4g>3z{_L(&|mj)J&}&8@~M=@5E56>q1WkWXj?{E zozzh=pIiG^$Hd8w`l-g$X1lucwy!yk%V;4`;Nb@+a+*1ln&_AZoK8tAGZ a2B7){fO!5MXBq(;>21Uk-qT9bVdoB0@w`<4 literal 0 HcmV?d00001 diff --git a/internal/compat/testdata/v0.1.1/sync/entries/b735eff353386ce13489603057254b89.sesh b/internal/compat/testdata/v0.1.1/sync/entries/b735eff353386ce13489603057254b89.sesh new file mode 100644 index 0000000000000000000000000000000000000000..7840a3dc744b3aa685ab957b0ac650f8b5899383 GIT binary patch literal 385 zcmV-{0e=2dMN>#qSx!R%w>9tcQ#fqlG>Kp^S0zh{a{vETE>V{Fat}E}IWD{v*#;aZ z{TbZ)pg>}2u%;rcdMvK41^8ah8>4w8`)wU)0003r+mev*f%dx+G%Hvi@zcy@4yugx z;SV;#Thd--!qMx_=@>S_EPV*C8K0g;u4MamXV{ZcJTpE8oo_z8R(lbWKiijD(p=zl zI}|M`iOszp-4}^{McjYqMmoByg?~-riIZ^Ty61k-dPT{Wf7sQ1#UsBc=3I&DsSc*Q zLXGHh*Jy-6cPUwzH>wI1a_CuFURacBO6U^6HELJJVgSUkf2xdQ5qY?)NZjyr#w&H> z+#MX`5fZ!3b1*jDb)MsdnG+Zf_{c2o4YnStNK1rA+-3M#rVV?eN=iKOwW2mlH+gFK zV#qSx!R%%8h4(SOhLxM4#H=6EGDb+E)@f%62kL_uT=Ui~}=3S1C_+ zE!cAFk+T3JA7thxY4p{nu5PUyyCWsO>j)Uo0003q$pz3!{kyxIeA^JW3V-atK6QE{ z8vzChn@S3l1@3ZA+Ku1WU$6LWy_c?5yf5e=s^{+$P$ba`PC11lCSaCf!!x*x;sSjuE+yxdN}=wiZea2T ze|S{=1$@zhNg#@gueTZ?AqkM(LdE~*XQ|eUaRxbeg8#!b?XBss=WYY>n}vs42MUu?$h)rL~16^ zzCkV5IZdUo#WX8z{M;Li|2s|UZlUKxrK!)sPY#3v#@fz`2Ft%&g)xze^uCJml38Wa ehZzm;b{*Cit4N|Wf8}XYj9X@^RNF~24p`*}^tZ#qSx!R%;}_^$MdpG~tR;`3M=lH=a{vETE>V{Fat}E}IWD{v*#;aZ z{TbZ)pg>}2u%;rc`p><83=w2%MtQfFxoh>r00045oQ+f?U+c|CoAAHqxm`J7gz){& ziW+$_FR^)s%*!&rfPzo#{WsMJULMB8_mK~>eg;}VhA+`}GWHQsV zo3&ROj#^QJJ4D+aD3*aYvaqi=vbK=&mZ76LcRc^rZv=D#`Uqp;0$t)x9RiOMh3gvF z!!z}pL35uty=K?#t}A+0J>{<)MtHT?K{}*CbwXaI)bP`JUi0vUj_%f$CI0EbAh{*# z<39Glh;FjfA$-_z^3DDKdAgR_oTzB6t4nB#P}4RGKzkq+ihBIsCQe#Yp)2c}X*!0T zQqHrctkW1RqyL+kR)1#m0FC&0W|X!COGx=fed0GW%f!sip_S2{p3r;bxr^wHm%DMGxBAUZuXNt4>;aJhCayELsN+|La{=)f|@JZjD)P Qkc4ae@)RxN=ApCaeC=D-p#T5? literal 0 HcmV?d00001 diff --git a/internal/compat/testdata/v0.1.1/sync/sesh-sync.json b/internal/compat/testdata/v0.1.1/sync/sesh-sync.json index a3c3d919..fb58975b 100644 --- a/internal/compat/testdata/v0.1.1/sync/sesh-sync.json +++ b/internal/compat/testdata/v0.1.1/sync/sesh-sync.json @@ -1,8 +1,8 @@ { - "folder_id": "99c4e96e8369b1ca", + "folder_id": "008ef4fe22eec16d", "key": { - "salt": "I2IBrQZ+T2iZ/QfLkDQl2L+xpnc+mldkt3xtnSszcOg=", - "verify": "BypsOOiF4aObyh6vnqkkKi4ny8gCy9fv2U+9IgIgApLDcSTVBINk", + "salt": "2qBS2Ib2jxrx/spyT53sZynrljn0Q7kQ+nxviRR2wog=", + "verify": "/pWJF9VN5tgL7vymNJ8DPzm3c3um3s5qWx2M6F9/iDv5NKOF6SZe", "params": { "time": 2, "memory": 19456, diff --git a/internal/compat/testdata/v0.1.1/vault.db b/internal/compat/testdata/v0.1.1/vault.db index 2fa010595ffd298e7a197d1e291218e3400b1188..fa743a74c8fded4e8b30ccfc519c662d6fc811ff 100644 GIT binary patch delta 12364 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 delta 12126 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 cJn&-afI5o