// Package vault encrypts pages at rest. Every person gets a key of their own, made from the server's // master key and who they are in Authentik, so one person's files cannot be opened (or swapped in) // with another's key. Who they are is not secret; the master key is what keeps the files private. package vault import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/hkdf" "crypto/pbkdf2" "crypto/rand" "crypto/sha256" "encoding/hex" "errors" "fmt" ) // ErrWrongKey means a file is encrypted but this key cannot open it: the master key was changed or // lost, or the file was altered. var ErrWrongKey = errors.New("these notes were saved with a different encryption key") // magic starts every encrypted file, so a plain markdown file can still be told apart and read. var magic = []byte("MAKO1\n") // MinKey is the shortest master key accepted, in characters. const MinKey = 32 // Cipher seals and opens the files of one person. type Cipher struct{ aead cipher.AEAD } // New makes the cipher for one person from the master key, the Authentik issuer and their subject. func New(master, issuer, sub string) (*Cipher, error) { if len(master) < MinKey { return nil, fmt.Errorf("the encryption key must be at least %d characters", MinKey) } if sub == "" { return nil, errors.New("no user to make an encryption key for") } key, err := hkdf.Key(sha256.New, []byte(master), nil, "mako notes v1|"+issuer+"|"+sub, 32) if err != nil { return nil, err } block, err := aes.NewCipher(key) if err != nil { return nil, err } aead, err := cipher.NewGCM(block) if err != nil { return nil, err } return &Cipher{aead}, nil } // Passphrase stretches a passphrase into key material (hex) with PBKDF2-HMAC-SHA256, which is slow on // purpose so that guessing passphrases is expensive. func Passphrase(pass string, salt []byte, iterations int) (string, error) { k, err := pbkdf2.Key(sha256.New, pass, salt, iterations, 32) return hex.EncodeToString(k), err } // IsSealed says whether data is an encrypted file. func IsSealed(data []byte) bool { return bytes.HasPrefix(data, magic) } // Seal encrypts a file. The name (the page id) is signed into it, so a file copied over another // page's name no longer opens. func (c *Cipher) Seal(plain []byte, name string) []byte { nonce := make([]byte, c.aead.NonceSize()) rand.Read(nonce) out := make([]byte, 0, len(magic)+len(nonce)+len(plain)+c.aead.Overhead()) out = append(out, magic...) out = append(out, nonce...) return c.aead.Seal(out, nonce, plain, []byte(name)) } // Open decrypts a file made by Seal. func (c *Cipher) Open(data []byte, name string) ([]byte, error) { n := len(magic) + c.aead.NonceSize() if !IsSealed(data) || len(data) < n+c.aead.Overhead() { return nil, ErrWrongKey } plain, err := c.aead.Open(nil, data[len(magic):n], data[n:], []byte(name)) if err != nil { return nil, ErrWrongKey } return plain, nil }