summaryrefslogtreecommitdiffstats
path: root/roseh.moe.go
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2025-09-25 21:53:40 -0700
committerRose Hogenson <rosehogenson@posteo.net>2025-09-25 21:53:40 -0700
commit240e6e995745185c34249f1939d4c0d469a6746e (patch)
treeb739a4ab16f3b6ab9981c6c433dbe92ff6bdeddc /roseh.moe.go
parent251ea4f8fcfd0960f6a627501eb556e109a3e2b6 (diff)
downloadroseh.moe-240e6e995745185c34249f1939d4c0d469a6746e.tar.zst
Move complex crypto logic into a helper package
Diffstat (limited to 'roseh.moe.go')
-rw-r--r--roseh.moe.go146
1 files changed, 36 insertions, 110 deletions
diff --git a/roseh.moe.go b/roseh.moe.go
index 3c5e7a9..6624706 100644
--- a/roseh.moe.go
+++ b/roseh.moe.go
@@ -2,12 +2,7 @@ package main
import (
"bytes"
- "crypto/aes"
- "crypto/cipher"
- "crypto/hkdf"
- "crypto/hmac"
"crypto/rand"
- "crypto/sha256"
"crypto/subtle"
"embed"
"encoding/base64"
@@ -25,7 +20,7 @@ import (
"sync"
"time"
- "gitlab.com/rhogenson/roseh.moe/internal/pwhash"
+ "gitlab.com/rhogenson/roseh.moe/internal/cryptoutil"
)
var (
@@ -37,12 +32,11 @@ var (
)
var (
- notepadPassword []byte
- notepadPasswordSalt []byte
- privateKey []byte
+ notepadPassword cryptoutil.PasswordHash
+ secretKey cryptoutil.HMACKey
encryptionKeyMu sync.Mutex
- encryptionKey []byte
+ encryptionKey cryptoutil.EncryptionKey
)
func loadSecrets() error {
@@ -52,20 +46,19 @@ func loadSecrets() error {
}
for _, line := range bytes.Split(bytes.TrimSuffix(secrets, []byte("\n")), []byte("\n")) {
if pw, ok := bytes.CutPrefix(line, []byte("notepad-password=")); ok {
- buf := make([]byte, hex.DecodedLen(len(pw)))
- if _, err := hex.Decode(buf, pw); err != nil {
- return err
+ if hex.DecodedLen(len(pw)) != cryptoutil.PasswordHashSize {
+ return fmt.Errorf("invalid PBKDF2-SHA256 hash")
}
- if len(buf) < pwhash.SaltLen {
- return fmt.Errorf("bad password hash")
+ notepadPassword = make(cryptoutil.PasswordHash, hex.DecodedLen(len(pw)))
+ if _, err := hex.Decode(notepadPassword, pw); err != nil {
+ return err
}
- notepadPasswordSalt, notepadPassword = buf[:pwhash.SaltLen], buf[pwhash.SaltLen:]
} else if key, ok := bytes.CutPrefix(line, []byte("secret-key=")); ok {
- if hex.DecodedLen(len(key)) != sha256.Size {
+ if hex.DecodedLen(len(key)) != cryptoutil.HMACKeySize {
return fmt.Errorf("invalid HMAC-SHA256 key")
}
- privateKey = make([]byte, hex.DecodedLen(len(key)))
- if _, err := hex.Decode(privateKey, key); err != nil {
+ secretKey = make(cryptoutil.HMACKey, hex.DecodedLen(len(key)))
+ if _, err := hex.Decode(secretKey, key); err != nil {
return err
}
}
@@ -73,78 +66,6 @@ func loadSecrets() error {
return nil
}
-func sign(msg []byte) []byte {
- mac := hmac.New(sha256.New, privateKey)
- mac.Write(msg)
- return mac.Sum(msg)
-}
-
-func verify(msg []byte) ([]byte, bool) {
- if len(msg) < sha256.Size {
- return nil, false
- }
- msg, sig := msg[:len(msg)-sha256.Size], msg[len(msg)-sha256.Size:]
- mac := hmac.New(sha256.New, privateKey)
- mac.Write(msg)
- return msg, hmac.Equal(sig, mac.Sum(nil))
-}
-
-var errNoKey = errors.New("not logged in")
-
-const (
- aesKeyLen = 32
- nonceLen = aes.BlockSize
- certLen = sha256.Size
-)
-
-func encrypt(msg string) ([]byte, error) {
- encryptionKeyMu.Lock()
- key := encryptionKey
- encryptionKeyMu.Unlock()
- if key == nil {
- return nil, errNoKey
- }
- block, err := aes.NewCipher(key[:aesKeyLen])
- if err != nil {
- return nil, err
- }
- buf := make([]byte, nonceLen+len(msg)+certLen)
- nonce := buf[:nonceLen]
- rand.Read(nonce)
- cipher.NewCTR(block, nonce).XORKeyStream(buf[nonceLen:], []byte(msg))
- cipherText := buf[:nonceLen+len(msg)]
- mac := hmac.New(sha256.New, key[aesKeyLen:])
- mac.Write(cipherText)
- return mac.Sum(cipherText), nil
-}
-
-func decrypt(msg []byte) (string, error) {
- encryptionKeyMu.Lock()
- key := encryptionKey
- encryptionKeyMu.Unlock()
- if key == nil {
- return "", errNoKey
- }
- if len(msg) < certLen+nonceLen {
- return "", fmt.Errorf("message too short")
- }
- msg, messageMAC := msg[:len(msg)-certLen], msg[len(msg)-certLen:]
- mac := hmac.New(sha256.New, key[aesKeyLen:])
- mac.Write(msg)
- expectedMAC := mac.Sum(nil)
- if !hmac.Equal(messageMAC, expectedMAC) {
- return "", fmt.Errorf("bad signature")
- }
- block, err := aes.NewCipher(key[:aesKeyLen])
- if err != nil {
- return "", err
- }
- nonce, msg := msg[:nonceLen], msg[nonceLen:]
- buf := make([]byte, len(msg))
- cipher.NewCTR(block, nonce).XORKeyStream(buf, msg)
- return string(buf), nil
-}
-
var (
//go:embed templates/404.html.template
notFoundString string
@@ -219,7 +140,7 @@ func attachCookie(w http.ResponseWriter) error {
}
http.SetCookie(w, &http.Cookie{
Name: "auth",
- Value: base64.RawStdEncoding.EncodeToString(sign(nowBytes)),
+ Value: base64.RawStdEncoding.EncodeToString(secretKey.Sign(nowBytes)),
Path: "/notepad",
Expires: time.Now().Add(cookieExpiration),
Secure: true,
@@ -241,7 +162,7 @@ func cookieAuth(w http.ResponseWriter, r *http.Request) (string, bool) {
if err != nil {
return "", false
}
- msg, ok := verify(authCookie)
+ msg, ok := secretKey.Verify(authCookie)
if !ok {
return "", false
}
@@ -284,12 +205,8 @@ type loginTemplateArgs struct {
}
func login(w http.ResponseWriter, r *http.Request) {
- rawKey, pwHash, err := pwhash.Hash(r.FormValue("password"), notepadPasswordSalt)
+ rawKey, err := cryptoutil.CheckPassword(r.FormValue("password"), notepadPassword)
if err != nil {
- http.Error(w, fmt.Sprintf("Unable to hash password: %s", err), http.StatusInternalServerError)
- return
- }
- if subtle.ConstantTimeCompare(pwHash[:], notepadPassword) == 0 {
if err := loginTemplate.Execute(w, loginTemplateArgs{Error: true}); err != nil {
log.Printf("Warning: login: %s", err)
}
@@ -299,7 +216,7 @@ func login(w http.ResponseWriter, r *http.Request) {
currentKey := encryptionKey
encryptionKeyMu.Unlock()
if currentKey == nil {
- key, err := hkdf.Expand(sha256.New, rawKey, "encrypt", aesKeyLen+certLen)
+ key, err := rawKey.EncryptionKey()
if err != nil {
http.Error(w, fmt.Sprintf("failed to derive encryption key: %s", err), http.StatusInternalServerError)
return
@@ -314,16 +231,16 @@ func login(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/notepad", http.StatusSeeOther)
}
-func readNotepad() (string, error) {
+func readNotepad(key cryptoutil.EncryptionKey) (string, error) {
encrypted, err := os.ReadFile(*notepadFile)
if err != nil {
return "", err
}
- decrypted, err := decrypt(encrypted)
+ decrypted, err := key.Decrypt(encrypted)
if err != nil {
return "", err
}
- return decrypted, nil
+ return string(decrypted), nil
}
var (
@@ -345,14 +262,17 @@ func notepad(w http.ResponseWriter, r *http.Request) {
}
return
}
- currentContent, err := readNotepad()
- if err != nil {
- if err == errNoKey {
- if err := loginTemplate.Execute(w, loginTemplateArgs{}); err != nil {
- log.Printf("Warning: login: %s", err)
- }
- return
+ encryptionKeyMu.Lock()
+ key := encryptionKey
+ encryptionKeyMu.Unlock()
+ if key == nil {
+ if err := loginTemplate.Execute(w, loginTemplateArgs{}); err != nil {
+ log.Printf("Warning: login: %s", err)
}
+ return
+ }
+ currentContent, err := readNotepad(key)
+ if err != nil {
currentContent = fmt.Sprintf("Error reading notepad file: %s", err)
}
if err := notepadTemplate.Execute(w, notepadTemplateArgs{Content: currentContent, CSRFToken: csrfToken}); err != nil {
@@ -367,7 +287,13 @@ func saveNote(w http.ResponseWriter, r *http.Request) error {
if _, ok := cookieAuth(w, r); !ok {
return fmt.Errorf("not logged in")
}
- encrypted, err := encrypt(r.FormValue("content"))
+ encryptionKeyMu.Lock()
+ key := encryptionKey
+ encryptionKeyMu.Unlock()
+ if key == nil {
+ return errors.New("not logged in")
+ }
+ encrypted, err := key.Encrypt([]byte(r.FormValue("content")))
if err != nil {
return err
}