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
@@ -13,6 +13,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.veillax.com/Veillax/mako/internal/vault"
|
||||
)
|
||||
|
||||
// OIDC is the OAuth client Authentik gives you for Mako.
|
||||
@@ -37,6 +39,10 @@ type Config struct {
|
||||
|
||||
// Secret signs session cookies, so a restart does not sign you out. It is made on first run.
|
||||
Secret string `json:"secret,omitempty"`
|
||||
|
||||
// Key encrypts the pages on disk (each person's key is made from it and who they are). It is made
|
||||
// on first run. Unlike Secret it must never change or be lost: without it the pages cannot be read.
|
||||
Key string `json:"key,omitempty"`
|
||||
}
|
||||
|
||||
// Defaults fills in what is missing.
|
||||
@@ -52,6 +58,11 @@ func (c *Config) Defaults() {
|
||||
rand.Read(b)
|
||||
c.Secret = base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
if c.Key == "" {
|
||||
b := make([]byte, 32)
|
||||
rand.Read(b)
|
||||
c.Key = base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
c.PublicURL = strings.TrimRight(strings.TrimSpace(c.PublicURL), "/")
|
||||
c.OIDC.Issuer = strings.TrimSpace(c.OIDC.Issuer)
|
||||
c.OIDC.ClientID = strings.TrimSpace(c.OIDC.ClientID)
|
||||
@@ -83,6 +94,9 @@ func checkURL(name, raw string) error {
|
||||
|
||||
// Check says what is still missing before Mako can sign anyone in, in words a person can act on.
|
||||
func (c Config) Check() error {
|
||||
if len(c.Key) < vault.MinKey {
|
||||
return fmt.Errorf("the encryption key (key, or MAKO_KEY) must be at least %d characters; leave it out to have one made, or make one with: openssl rand -base64 32", vault.MinKey)
|
||||
}
|
||||
var missing []string
|
||||
if c.PublicURL == "" {
|
||||
missing = append(missing, " public_url (MAKO_PUBLIC_URL) where you open Mako, such as https://notes.example.com")
|
||||
@@ -121,6 +135,7 @@ type Holder struct {
|
||||
path string
|
||||
mu sync.RWMutex
|
||||
file Config
|
||||
made bool // the file is missing, or lacked a secret or key that was just made
|
||||
}
|
||||
|
||||
// Load reads the file. A missing file gives the defaults (and is created by the first Update).
|
||||
@@ -136,10 +151,21 @@ func Load(path string) (*Holder, error) {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
}
|
||||
h.made = err != nil || h.file.Secret == "" || (h.file.Key == "" && !keyInEnv())
|
||||
keyless := h.file.Key == "" && keyInEnv() // a key in the environment is used instead, so none is made
|
||||
h.file.Defaults()
|
||||
if keyless {
|
||||
h.file.Key = ""
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func keyInEnv() bool { return strings.TrimSpace(os.Getenv("MAKO_KEY")) != "" }
|
||||
|
||||
// NeedsSave says whether the file should be written now: it is missing, or a secret or key was
|
||||
// made that is only in memory. Writing it is what makes them stick.
|
||||
func (h *Holder) NeedsSave() bool { return h.made }
|
||||
|
||||
func (h *Holder) Path() string { return h.path }
|
||||
|
||||
// Exists says whether the file is on disk yet.
|
||||
@@ -162,6 +188,7 @@ func (h *Holder) Get() Config {
|
||||
env("MAKO_OIDC_ISSUER", &c.OIDC.Issuer)
|
||||
env("MAKO_OIDC_CLIENT_ID", &c.OIDC.ClientID)
|
||||
env("MAKO_OIDC_CLIENT_SECRET", &c.OIDC.ClientSecret)
|
||||
env("MAKO_KEY", &c.Key)
|
||||
if v := os.Getenv("MAKO_ALLOWED"); strings.TrimSpace(v) != "" {
|
||||
c.Allowed = nil
|
||||
for _, a := range strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == ';' || r == '\n' }) {
|
||||
|
||||
Reference in new issue
Block a user