From 25cdced2d759d8b598d710dec03338d856f3f9b1 Mon Sep 17 00:00:00 2001 From: Marc Alvarez Date: Tue, 20 Oct 2026 21:12:55 -0600 Subject: [PATCH 1/2] internal/compat: an export made by v0.1.2, the first with earlier secrets, and a test that it imports with them --- internal/compat/testdata/v0.1.2/export.sesh | 12 +++ internal/compat/testdata/v0.1.2/make.sh | 75 +++++++++++++++++ internal/compat/v012_test.go | 91 +++++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 internal/compat/testdata/v0.1.2/export.sesh create mode 100755 internal/compat/testdata/v0.1.2/make.sh create mode 100644 internal/compat/v012_test.go diff --git a/internal/compat/testdata/v0.1.2/export.sesh b/internal/compat/testdata/v0.1.2/export.sesh new file mode 100644 index 00000000..3f4d4ac3 --- /dev/null +++ b/internal/compat/testdata/v0.1.2/export.sesh @@ -0,0 +1,12 @@ +{ + "algorithm": "argon2id", + "salt": "517Ug4POerFZ9L9hoSnRYuONQMIgzMyxIaPtT/ixtVk=", + "ciphertext": "1FTznWDjItS4py4V/b7kOorHE3BQwxGPQzHl9Pd4IrVoUC0J5hkMXrDfvX4dZPMKXLaHuZdz8Ym4+sHzD7XmD3ZNw2n1WiY+ey/Kis+u2naCCsxr8s8AEZiJ+FgArepzAEBt0MHG6vP7N/aT3AdjTWffvntPrS5bvijNEuOlCw9LHLl5bTdpvyF32u3j59Db9ZOK083hxGUVIUuUB1cVJ9q0MXLU+UzrzKNch7Ks4nCZ9o3wogUweID45XT78PYUJbK0ZYCM8MYASJdRNqsjUw+99oyC5m/iO0WbWPb3ijn07qhGaaqAqAIaOWfXmZEcdeZPGQBFxGosneS5BpJ1iCh2IlDcZWNav2TtPygdi8fWF3DzIYpaSoMyHvdyvnxr40y4Xg808PNos96T2DrtafefiFYl82miVDpC8IwFRE5BbHxQxY10vq/BsAftVAli5BNtRwap9aQHE6/ljoG5WLFE7HBGAHMR4P3z+8TlRzrd4x0WWhUJ+uRsIqKcfJnTkn77kmGVp3DAB3VFbETYGT+h6iqi8SGoNp65xKQfupRU4MA8caGQex+La8ggOi19csWvxuqMFA1Q1tBLcT+e3NojQSyZUoqZZM51kZDz+RoYi5GFRvwA26GOYsXMg3FI4AbQeLEtRzQG+nQmoT2l9Z3KMTNLB/5GVeIVw+trGTsPtmwIDpGXYbkIqAUtPUE56M6wW2vx7eK5yibiBvMo4FCOGnEQvEmWYPBJJdb6in5mi6pPMPH6nUw2Z8hviUph21AcOWXjOSZstD1NY7zk7QFo3Yfij4wqpSQjZ2fv6chBkIkY+mW4rN2/ujLWdtRmUKAdFmWiOOR5I+7Gppk4pzbwAoI7pYLHv5JVlJRNwxiTik/+7dQPG1s+SKra1AzDx+iXwRNXiHPxaNKW2qpxvDbGr5uU1cxIBwOLCjQBlipObe2MPw7PTOd0RSZ8H6yjp1oVL8ZSbGWigXsmrAYRfwD0CUpTQvr+fHSGsa12rtLSINg7ZRrqc/bcUT6SwmPdfX8HakxlI2LycpHBWWdsEw3tyIyWaFTFDb3Ln2ZpkBptEbZAW1Jg8YFFdnoF4Kb+C9rKH04vaCf/s3sh3DDBXi/eGSmmpslUyS4c6xIgpF6AxO3Nf1hDaiDDnepmXcoLc3rTO6Rsa3SbMkCRbKS2pjxCpndhx2y7n2lHwPlSbBZJa90Sm3RIL0wj2pNqqJQa/64gWaf9O3u9oE3ZprjpAyTzr3dMd4KRGE0YnPcf2tPDjS0JD4BvldlmlrYmcn/70NcDS6WbxSCY3/Esql1Bh+gQdf/V+g1o0Rw7iO1yPRhebVFn0IH9U3TpNDpW74W0OOBgEjMTE+ppBdhWIX6lMRSh5xb2war5OY36Z0Mz9ymI4J40h54IXFjuAUZH4vPXXLVKNQAz/iExtstmgJHUCaqOsDJol2wzbSAMJrZKDJFjf4BUj8zg5rb1xK07zu1OFAKEW9MlJpPcxsiYZ+nhIbSsbhVT8U8eUtKLk3YTMF9dcqKUg9pdYRUKrM2zpd2Zq58catwujKWV4bguVmEGd3D8O2d3h1FzePTqJsjNne9Rpb3eRAh60QAzBH/4sj1i8dAUZOV1Rf3SJLxs", + "params": { + "time": 2, + "memory": 19456, + "threads": 1, + "key_len": 32 + }, + "version": 1 +} diff --git a/internal/compat/testdata/v0.1.2/make.sh b/internal/compat/testdata/v0.1.2/make.sh new file mode 100755 index 00000000..a04ac2de --- /dev/null +++ b/internal/compat/testdata/v0.1.2/make.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# make.sh writes this folder's export.sesh with sesh v0.1.2, all made-up: an +# encrypted export whose entries carry earlier secrets (v0.1.2 is the first +# release whose encrypted exports hold them). It's kept as v0.1.2 wrote it; +# compat_test.go checks that today's sesh still reads it. Run it only to +# remake it, with v0.1.2: +# +# internal/compat/testdata/v0.1.2/make.sh /path/to/sesh-0.1.2 +# +# Nothing personal goes in: a throwaway home for everything, made-up +# passwords, and a made-up computer name. +set -euo pipefail + +sesh=$1 +out=$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +"$sesh" --version | grep -q '^sesh version 0.1.2 ' || { + echo "make.sh: $sesh isn't sesh 0.1.2" >&2 + 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" +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.2-master SESH_SYNC_NAME=test-laptop +export SESH_KDF_MEMORY=19MiB SESH_KDF_TIME=2 SESH_KDF_THREADS=1 + +# A password with two earlier ones, and a file with an earlier version under +# another name. +printf 'gh-pass-1\n' | "$sesh" add password GitHub --username maya --url https://github.com/login --folder work --tag dev +printf 'gh-pass-2\n' | "$sesh" edit password/GitHub/maya --secret +printf 'gh-pass-3\n' | "$sesh" edit password/GitHub/maya --secret +mkdir -p "$tmp/files" +printf 'apiVersion: v1\nkind: Config\n# old\n' >"$tmp/files/old.yaml" +printf 'apiVersion: v1\nkind: Config\n' >"$tmp/files/config" +"$sesh" add file kubeconfig --from "$tmp/files/old.yaml" --folder work +"$sesh" edit file/kubeconfig --from "$tmp/files/config" + +# 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.2-export\n") + seen = len(out) +else: + os.kill(pid, signal.SIGKILL) +_, status = os.waitpid(pid, 0) +sys.exit(os.waitstatus_to_exitcode(status)) +EOF + +rm -f "$out/export.sesh" +cp "$tmp/out/export.sesh" "$out/" +chmod u=rw,go=r "$out/export.sesh" +echo "Wrote $out/export.sesh" diff --git a/internal/compat/v012_test.go b/internal/compat/v012_test.go new file mode 100644 index 00000000..e3ad26fc --- /dev/null +++ b/internal/compat/v012_test.go @@ -0,0 +1,91 @@ +package compat + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/bashhack/sesh/internal/importer/seshexport" + "github.com/bashhack/sesh/internal/password" + "github.com/bashhack/sesh/internal/vault" +) + +// What v0.1.2's export was given (testdata/v0.1.2/make.sh): GitHub's +// password with two earlier ones, and a file with an earlier version +// under another name. +const exportPasswordV012 = "fixture-v0.1.2-export" + +var ( + kubeNowV012 = "apiVersion: v1\nkind: Config\n" + kubeEarlierV012 = "apiVersion: v1\nkind: Config\n# old\n" +) + +// An encrypted export v0.1.2 wrote, the first to hold earlier secrets, imports into a new vault with them: their +// order, when each was replaced and began, and a file's earlier name. +func TestExportV012(t *testing.T) { + data, err := os.ReadFile(filepath.Join("testdata", "v0.1.2", "export.sesh")) + if err != nil { + t.Fatal(err) + } + sealed, err := password.ReadSealed(bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + list, err := sealed.Entries([]byte(exportPasswordV012)) + if err != nil { + t.Fatal(err) + } + st, _ := open(t, filepath.Join(t.TempDir(), "new.db"), "a-new-vault-password") + st.SetHistoryKeep(10) + found := map[vault.Key]bool{} + for _, e := range seshexport.Entries(list) { + if e.Skip != "" { + t.Fatalf("%v skipped: %s", e.Key, e.Skip) + } + found[e.Key] = true + for _, h := range e.Earlier { + if h.ReplacedAt.IsZero() || h.SetAt.IsZero() { + t.Errorf("%v: an earlier secret without its times: %+v", e.Key, h) + } + } + ent := vault.Entry{Key: e.Key, Folder: e.Folder, Tags: e.Tags, Settings: e.Settings, CreatedAt: e.Created, UpdatedAt: e.Updated} + if _, err := st.SaveImported(&ent, e.Secret, &e.Details, e.Earlier); err != nil { + t.Fatalf("%v: %v", e.Key, err) + } + } + if !found[github] || !found[kube] || len(found) != 2 { + t.Fatalf("entries %v", found) + } + + for i, want := range append([]string{"gh-pass-3"}, githubEarlier...) { + var got []byte + var err error + if i == 0 { + got, err = st.Get(github) + } else { + got, err = st.ReadVersion(github, i+1, "compat test") + } + if err != nil || string(got) != want { + t.Errorf("GitHub's version %d: %q, %v; want %q", i+1, got, err, want) + } + } + + e, err := st.Lookup(kube) + if err != nil { + t.Fatal(err) + } + if got, err := st.Get(kube); err != nil || string(got) != kubeNowV012 || e.Settings.FileName != "config" { + t.Errorf("kubeconfig now: %q named %q (%v)", got, e.Settings.FileName, err) + } + if _, err := st.RestoreVersion(kube, 2); err != nil { + t.Fatal(err) + } + e, err = st.Lookup(kube) + if err != nil { + t.Fatal(err) + } + if got, err := st.Get(kube); err != nil || string(got) != kubeEarlierV012 || e.Settings.FileName != "old.yaml" { + t.Errorf("kubeconfig's earlier version, restored: %q named %q (%v)", got, e.Settings.FileName, err) + } +} From 276ca75720514d407804798294b50848b82e4494 Mon Sep 17 00:00:00 2001 From: Marc Alvarez Date: Tue, 20 Oct 2026 21:15:33 -0600 Subject: [PATCH 2/2] make.sh names the test that reads v0.1.2's export --- internal/compat/testdata/v0.1.2/make.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/compat/testdata/v0.1.2/make.sh b/internal/compat/testdata/v0.1.2/make.sh index a04ac2de..2a3eeaac 100755 --- a/internal/compat/testdata/v0.1.2/make.sh +++ b/internal/compat/testdata/v0.1.2/make.sh @@ -2,7 +2,7 @@ # make.sh writes this folder's export.sesh with sesh v0.1.2, all made-up: an # encrypted export whose entries carry earlier secrets (v0.1.2 is the first # release whose encrypted exports hold them). It's kept as v0.1.2 wrote it; -# compat_test.go checks that today's sesh still reads it. Run it only to +# v012_test.go checks that today's sesh still reads it. Run it only to # remake it, with v0.1.2: # # internal/compat/testdata/v0.1.2/make.sh /path/to/sesh-0.1.2