Encrypt pages at rest with a key per person
Pages are sealed with AES-256-GCM. Each person's key is derived (HKDF) from a master key in the settings file plus their Authentik issuer and subject. Plain files from earlier versions are read and re-saved encrypted. Mako refuses to open a notebook it cannot decrypt rather than overwrite it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e3147597e8
commit
9f2fe4a416
11 files changed
+396
-33
No files matched your search
@@ -10,12 +10,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.veillax.com/Veillax/mako/internal/vault"
|
||||
)
|
||||
|
||||
func open(t *testing.T) (*Store, string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
s, err := Open(dir)
|
||||
s, err := Open(dir, testBox(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -100,7 +102,7 @@ func TestCreateRoundTripsThroughDisk(t *testing.T) {
|
||||
if _, err := os.Stat(filepath.Join(dir, "notes", n.ID+".md")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s2, err := Open(dir)
|
||||
s2, err := Open(dir, testBox(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -126,7 +128,7 @@ func TestPlainMarkdownFileIsAPage(t *testing.T) {
|
||||
os.WriteFile(path, []byte("# Dropped in\r\nby hand #manual\r\n"), 0o644)
|
||||
mod := time.Date(2025, 3, 4, 5, 6, 7, 0, time.UTC)
|
||||
os.Chtimes(path, mod, mod)
|
||||
s2, err := Open(dir)
|
||||
s2, err := Open(dir, testBox(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -209,7 +211,7 @@ func TestRenameRewritesLinks(t *testing.T) {
|
||||
if other, _ := s.Get(b.ID); other.Body != b.Body || !other.Updated.Equal(b.Updated) {
|
||||
t.Fatal("unrelated pages must not be touched")
|
||||
}
|
||||
s2, _ := Open(s.dir)
|
||||
s2, _ := Open(s.dir, testBox(t))
|
||||
if again, _ := s2.Get(a.ID); again.Body != want {
|
||||
t.Fatal("rewrite was not saved")
|
||||
}
|
||||
@@ -255,7 +257,7 @@ func TestTreeParentsAndCycles(t *testing.T) {
|
||||
os.WriteFile(filepath.Join(dir, "notes", "x1.md"), []byte("---\nparent: x2\n---\none"), 0o644)
|
||||
os.WriteFile(filepath.Join(dir, "notes", "x2.md"), []byte("---\nparent: x1\n---\ntwo"), 0o644)
|
||||
os.WriteFile(filepath.Join(dir, "notes", "x3.md"), []byte("---\nparent: ghost\n---\nthree"), 0o644)
|
||||
s2, err := Open(dir)
|
||||
s2, err := Open(dir, testBox(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -299,7 +301,7 @@ func TestTrashRestorePurge(t *testing.T) {
|
||||
if _, err := s.Create(NewNote{Title: "Child"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s2, _ := Open(dir)
|
||||
s2, _ := Open(dir, testBox(t))
|
||||
back, err := s2.Restore(n.ID)
|
||||
if err != nil || back.Title != "Child 2" || back.Parent != parent.ID {
|
||||
t.Fatalf("restore: %v %+v", err, back)
|
||||
@@ -355,7 +357,7 @@ func TestDaily(t *testing.T) {
|
||||
t.Fatalf("a day can be linked by its date: %+v", got)
|
||||
}
|
||||
// reload keeps the title from the date, and an empty day stays out of navigation
|
||||
s2, _ := Open(s.dir)
|
||||
s2, _ := Open(s.dir, testBox(t))
|
||||
if n, _ := s2.Get(d1.ID); n.Title != "2026-10-31" {
|
||||
t.Fatalf("title after reload: %q", n.Title)
|
||||
}
|
||||
@@ -465,7 +467,7 @@ func TestIDsCannotEscape(t *testing.T) {
|
||||
}
|
||||
_, dir := open(t)
|
||||
os.WriteFile(filepath.Join(dir, "notes", "Bad Name.md"), []byte("ignored"), 0o644)
|
||||
s2, _ := Open(dir)
|
||||
s2, _ := Open(dir, testBox(t))
|
||||
if list, _ := s2.List(Query{}); len(list) != 0 {
|
||||
t.Fatal("a file whose name is not a valid id should be ignored")
|
||||
}
|
||||
@@ -502,3 +504,75 @@ func TestZipIsAnObsidianVault(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testBox(t *testing.T) *vault.Cipher {
|
||||
t.Helper()
|
||||
b, err := vault.New("0123456789abcdef0123456789abcdef", "https://auth.example.com/", "alice")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestFilesAreEncrypted(t *testing.T) {
|
||||
s, dir := open(t)
|
||||
n := mk(t, s, "Diary", "meet at the #harbour")
|
||||
b, err := os.ReadFile(filepath.Join(dir, "notes", n.ID+".md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !vault.IsSealed(b) || bytes.Contains(b, []byte("harbour")) || bytes.Contains(b, []byte("Diary")) {
|
||||
t.Fatalf("the page is on disk in the clear: %q", b)
|
||||
}
|
||||
// trashing keeps it encrypted, and everything reads back
|
||||
if _, err := s.Trash(n.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, _ = os.ReadFile(filepath.Join(dir, "trash", n.ID+".md"))
|
||||
if !vault.IsSealed(b) {
|
||||
t.Fatalf("a trashed page is in the clear: %q", b)
|
||||
}
|
||||
s2, err := Open(dir, testBox(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, err := s2.Get(n.ID); err != nil || got.Body != "meet at the #harbour" || !got.Trashed {
|
||||
t.Fatalf("reload: %+v %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlainFilesAreEncryptedWhenOpened(t *testing.T) {
|
||||
_, dir := open(t)
|
||||
path := filepath.Join(dir, "notes", "old-page.md")
|
||||
os.WriteFile(path, []byte("---\ntitle: \"Old\"\n---\n\nwritten before encryption\n"), 0o644)
|
||||
s, err := Open(dir, testBox(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n, err := s.Get("old-page"); err != nil || n.Title != "Old" {
|
||||
t.Fatalf("%+v %v", n, err)
|
||||
}
|
||||
b, _ := os.ReadFile(path)
|
||||
if !vault.IsSealed(b) || bytes.Contains(b, []byte("before encryption")) {
|
||||
t.Fatalf("still in the clear: %q", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrongKeyRefusesInsteadOfLosingPages(t *testing.T) {
|
||||
s, dir := open(t)
|
||||
n := mk(t, s, "Keep", "me")
|
||||
other, _ := vault.New("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "https://auth.example.com/", "alice")
|
||||
if _, err := Open(dir, other); !errors.Is(err, vault.ErrWrongKey) {
|
||||
t.Fatalf("want ErrWrongKey, got %v", err)
|
||||
}
|
||||
// a page file copied over another page's name does not open either
|
||||
src := filepath.Join(dir, "notes", n.ID+".md")
|
||||
b, _ := os.ReadFile(src)
|
||||
os.WriteFile(filepath.Join(dir, "notes", "other-id.md"), b, 0o600)
|
||||
if _, err := Open(dir, testBox(t)); !errors.Is(err, vault.ErrWrongKey) {
|
||||
t.Fatalf("a swapped file opened: %v", err)
|
||||
}
|
||||
if _, err := Open(dir, nil); err == nil {
|
||||
t.Fatal("opened with no key")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user