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
+29
-7
@@ -1,5 +1,5 @@
|
||||
// Package store keeps pages as plain markdown files, one per page, so they stay readable and
|
||||
// portable with or without Mako. An in-memory index makes listing, searching and linking quick.
|
||||
// Package store keeps pages as markdown files, one per page, encrypted at rest (see package vault)
|
||||
// and exportable as plain markdown. An in-memory index makes listing, searching and linking quick.
|
||||
package store
|
||||
|
||||
import (
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.veillax.com/Veillax/mako/internal/vault"
|
||||
)
|
||||
|
||||
// MaxBody is the longest page, in bytes. MaxTitle is the longest title, in characters.
|
||||
@@ -60,6 +62,7 @@ type Note struct {
|
||||
|
||||
type Store struct {
|
||||
dir string
|
||||
box *vault.Cipher
|
||||
mu sync.RWMutex
|
||||
by map[string]*Note
|
||||
}
|
||||
@@ -72,11 +75,16 @@ var (
|
||||
// ValidID says whether s can be used as a page id (and so as a file name).
|
||||
func ValidID(s string) bool { return idRe.MatchString(s) }
|
||||
|
||||
// Open reads every page under dir, creating the folders if needed.
|
||||
func Open(dir string) (*Store, error) {
|
||||
s := &Store{dir: dir, by: map[string]*Note{}}
|
||||
// Open reads every page under dir, creating the folders if needed. Pages are encrypted with box. A
|
||||
// plain markdown file found there is still read, and is saved again encrypted.
|
||||
func Open(dir string, box *vault.Cipher) (*Store, error) {
|
||||
if box == nil {
|
||||
return nil, errors.New("a notebook needs an encryption key")
|
||||
}
|
||||
s := &Store{dir: dir, box: box, by: map[string]*Note{}}
|
||||
var plain []*Note
|
||||
for _, sub := range []string{"notes", "trash"} {
|
||||
if err := os.MkdirAll(filepath.Join(dir, sub), 0o755); err != nil {
|
||||
if err := os.MkdirAll(filepath.Join(dir, sub), 0o700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(filepath.Join(dir, sub))
|
||||
@@ -97,12 +105,26 @@ func Open(dir string) (*Store, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sealed := vault.IsSealed(b)
|
||||
if sealed {
|
||||
if b, err = box.Open(b, id); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w; put the original key back (key in the settings file, or MAKO_KEY)", path, err)
|
||||
}
|
||||
}
|
||||
n := parse(id, string(b), info.ModTime())
|
||||
n.Trashed = sub == "trash"
|
||||
s.by[id] = n
|
||||
if !sealed {
|
||||
plain = append(plain, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
s.mend()
|
||||
for _, n := range plain {
|
||||
if err := s.write(n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -339,7 +361,7 @@ func isNumber(s string) bool {
|
||||
func (s *Store) write(n *Note) error {
|
||||
path := s.path(n)
|
||||
tmp := filepath.Join(filepath.Dir(path), "."+n.ID+".tmp")
|
||||
if err := os.WriteFile(tmp, format(n), 0o644); err != nil {
|
||||
if err := os.WriteFile(tmp, s.box.Seal(format(n), n.ID), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
|
||||
Reference in new issue
Block a user