Add a passphrase-locked vault
CI / test (push) Successful in 2m2s
Release / release (push) Successful in 2m52s

A second notebook whose pages are encrypted with a key made from a passphrase plus the server key. It is unlocked into memory only (15 minutes idle, Lock, or sign-out), is not in search, lists or downloads of the ordinary notebook, and limits wrong guesses.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-11-01 01:40:35 -07:00
1 parent 7ff424fa55
commit c08a8b8718
12 files changed
+706 -28

No files matched your search

+28 -2
View File
@@ -15,6 +15,7 @@ import (
"path/filepath"
"strings"
"sync"
"time"
"git.veillax.com/Veillax/mako/internal/config"
"git.veillax.com/Veillax/mako/internal/oidc"
@@ -34,6 +35,11 @@ type Server struct {
mu sync.Mutex
books map[string]*store.Store
vmu sync.Mutex
vstate map[string]*vaultState // unlocked vaults, by the folder name of their owner
vtoken map[string]string // vault cookie value to that folder name
vfails map[string][]time.Time // recent wrong passphrases, by the same name
}
// principal is who is making a request, with the notebook that is theirs alone.
@@ -49,17 +55,23 @@ func New(h *config.Holder, version string) (*Server, error) {
return nil, err
}
return &Server{cfg: h, version: version, books: map[string]*store.Store{},
vstate: map[string]*vaultState{}, vtoken: map[string]string{}, vfails: map[string][]time.Time{},
oidc: oidc.New(c.OIDC.Issuer, c.OIDC.ClientID, c.OIDC.ClientSecret, c.RedirectURL())}, nil
}
// OIDC is the sign-in client, for the check command.
func (s *Server) OIDC() *oidc.Client { return s.oidc }
// userKey names a person's folder, from a hash of who they are.
func userKey(c config.Config, sub string) string {
sum := sha256.Sum256([]byte(c.OIDC.Issuer + "|" + sub))
return hex.EncodeToString(sum[:16])
}
// notebook opens the notes that belong to one person. Their folder is named from a hash of who they
// are, so nothing a person controls ever becomes part of a path.
func (s *Server) notebook(c config.Config, sub, name, email string) (*store.Store, error) {
sum := sha256.Sum256([]byte(c.OIDC.Issuer + "|" + sub))
key := hex.EncodeToString(sum[:16])
key := userKey(c, sub)
s.mu.Lock()
defer s.mu.Unlock()
if st, ok := s.books[key]; ok {
@@ -104,6 +116,20 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("GET /api/find", s.api(s.find))
mux.HandleFunc("GET /api/export", s.api(s.export))
mux.HandleFunc("GET /api/vault", s.api(s.vaultInfo))
mux.HandleFunc("POST /api/vault", s.api(s.vaultCreate))
mux.HandleFunc("POST /api/vault/unlock", s.api(s.vaultUnlock))
mux.HandleFunc("POST /api/vault/lock", s.api(s.vaultLock))
mux.HandleFunc("GET /api/vault/pages", s.vaultAPI(s.pages))
mux.HandleFunc("GET /api/vault/notes", s.vaultAPI(s.listNotes))
mux.HandleFunc("POST /api/vault/notes", s.vaultAPI(s.createNote))
mux.HandleFunc("GET /api/vault/notes/{id}", s.vaultAPI(s.getNote))
mux.HandleFunc("PUT /api/vault/notes/{id}", s.vaultAPI(s.updateNote))
mux.HandleFunc("DELETE /api/vault/notes/{id}", s.vaultAPI(s.deleteNote))
mux.HandleFunc("POST /api/vault/notes/{id}/restore", s.vaultAPI(s.restoreNote))
mux.HandleFunc("DELETE /api/vault/trash", s.vaultAPI(s.emptyTrash))
mux.HandleFunc("GET /api/vault/find", s.vaultAPI(s.find))
static, _ := fs.Sub(staticFiles, "static")
mux.Handle("GET /", http.FileServerFS(static))
return s.secure(mux)