Encrypt pages at rest with a key per person
CI / test (push) Successful in 2m2s
Release / release (push) Successful in 2m15s

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:
VeillaxandClaude Sonnet 5.5 committed 2026-11-01 00:25:37 -06:00
1 parent e3147597e8
commit 9f2fe4a416
11 files changed
+396 -33

No files matched your search

+12 -7
View File
@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
@@ -18,6 +19,7 @@ import (
"git.veillax.com/Veillax/mako/internal/config"
"git.veillax.com/Veillax/mako/internal/oidc"
"git.veillax.com/Veillax/mako/internal/store"
"git.veillax.com/Veillax/mako/internal/vault"
)
//go:embed static
@@ -63,16 +65,19 @@ func (s *Server) notebook(c config.Config, sub, name, email string) (*store.Stor
if st, ok := s.books[key]; ok {
return st, nil
}
dir := filepath.Join(c.DataDir, "users", key)
st, err := store.Open(dir)
box, err := vault.New(c.Key, c.OIDC.Issuer, sub)
if err != nil {
return nil, err
}
// A note for whoever looks after the server, so a folder can be matched to a person.
who := fmt.Sprintf("%s <%s>\n", name, email)
if b, _ := os.ReadFile(filepath.Join(dir, "owner.txt")); string(b) != who {
os.WriteFile(filepath.Join(dir, "owner.txt"), []byte(who), 0o644)
dir := filepath.Join(c.DataDir, "users", key)
st, err := store.Open(dir, box)
if err != nil {
log.Printf("could not open the notebook of %s <%s>: %v", name, email, err)
return nil, err
}
// Earlier versions left a plain note here naming the owner, which does not belong next to
// encrypted pages.
os.Remove(filepath.Join(dir, "owner.txt"))
s.books[key] = st
return st, nil
}
@@ -142,7 +147,7 @@ func (s *Server) api(fn handlerFunc) http.HandlerFunc {
}
st, err := s.notebook(c, ses.Sub, ses.Name, ses.Email)
if err != nil {
fail(w, http.StatusInternalServerError, "could not open your notes: "+err.Error())
fail(w, http.StatusInternalServerError, "could not open your notes; the server's log says why")
return
}
r.Body = http.MaxBytesReader(w, r.Body, maxRequestBody)