From 09517318b87e39506cbcb3232a395ac7d43c25f5 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Tue, 23 Sep 2025 21:18:49 -0700 Subject: Use pbkdf2 for password hashing instead of sha512 --- cmd/finditers/finditers.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ cmd/hashpw/hashpw.go | 34 ++++++++++++++++++++++++++++++++++ flake.lock | 6 +++--- flake.nix | 2 +- go.mod | 6 +++++- go.sum | 4 ++++ internal/pwhash/pwhash.go | 22 ++++++++++++++++++++++ roseh.moe.go | 21 ++++++++++++++++----- 8 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 cmd/finditers/finditers.go create mode 100644 cmd/hashpw/hashpw.go create mode 100644 go.sum create mode 100644 internal/pwhash/pwhash.go diff --git a/cmd/finditers/finditers.go b/cmd/finditers/finditers.go new file mode 100644 index 0000000..2a256c8 --- /dev/null +++ b/cmd/finditers/finditers.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/hex" + "fmt" + "os" + "sort" + "testing" + "time" + + "gitlab.com/rhogenson/roseh.moe/internal/pwhash" +) + +var ( + iterations int + + salt, _ = hex.DecodeString("3fb84513fc3afcd6d3b230bf9ece91aaae2d2a99da17efbf7de83b21fafe3f08") +) + +func BenchmarkHashIter(b *testing.B) { + const password = "oboe shortness ether ideology undesired fresh freezable catching mashing glimpse" + for b.Loop() { + pwhash.HashIter(password, salt, iterations) + } +} + +func run() error { + for iterations = 8192; time.Duration(testing.Benchmark(BenchmarkHashIter).NsPerOp()) < 500*time.Millisecond; iterations *= 2 { + } + lo := iterations / 2 + hi := iterations + fmt.Println(lo + sort.Search(hi-lo, func(i int) bool { + iterations = lo + i + return time.Duration(testing.Benchmark(BenchmarkHashIter).NsPerOp()) > 500*time.Millisecond + })) + return nil +} + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cmd/hashpw/hashpw.go b/cmd/hashpw/hashpw.go new file mode 100644 index 0000000..ffbdf79 --- /dev/null +++ b/cmd/hashpw/hashpw.go @@ -0,0 +1,34 @@ +package main + +import ( + "crypto/rand" + "fmt" + "os" + + "gitlab.com/rhogenson/roseh.moe/internal/pwhash" + "golang.org/x/term" +) + +func run() error { + fmt.Print("Enter password:") + password, err := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + if err != nil { + return err + } + salt := make([]byte, 32) + rand.Read(salt) + hashedPassword, err := pwhash.Hash(string(password), salt) + if err != nil { + return err + } + fmt.Printf("notepad-password=%x\nsalt=%x\n", hashedPassword, salt) + return nil +} + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/flake.lock b/flake.lock index ff4cd92..5f475fe 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1757545623, - "narHash": "sha256-mCxPABZ6jRjUQx3bPP4vjA68ETbPLNz9V2pk9tO7pRQ=", + "lastModified": 1758589230, + "narHash": "sha256-zMTCFGe8aVGTEr2RqUi/QzC1nOIQ0N1HRsbqB4f646k=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8cd5ce828d5d1d16feff37340171a98fc3bf6526", + "rev": "d1d883129b193f0b495d75c148c2c3a7d95789a0", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index f6122c4..0a0d016 100644 --- a/flake.nix +++ b/flake.nix @@ -48,7 +48,7 @@ # remember to bump this hash when your dependencies change. # vendorHash = pkgs.lib.fakeHash; - vendorHash = null; + vendorHash = "sha256-L7nK+w4CB2H3b6vL0ZoFfaRMgCmpqzQo8ThMM60C76I="; }; }); diff --git a/go.mod b/go.mod index f50ecfa..5e702ba 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ module gitlab.com/rhogenson/roseh.moe -go 1.23.2 +go 1.24.0 + +require golang.org/x/term v0.35.0 + +require golang.org/x/sys v0.36.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a7fae6a --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= diff --git a/internal/pwhash/pwhash.go b/internal/pwhash/pwhash.go new file mode 100644 index 0000000..a17ed2e --- /dev/null +++ b/internal/pwhash/pwhash.go @@ -0,0 +1,22 @@ +package pwhash + +import ( + "crypto/pbkdf2" + "crypto/sha256" + "crypto/sha512" +) + +const defaultIterations = 3670016 // from cmd/finditers + +func HashIter(password string, salt []byte, iter int) ([]byte, error) { + return pbkdf2.Key(sha256.New, password, salt, iter, 32) +} + +func Hash(password string, salt []byte) ([]byte, error) { + hashed, err := HashIter(password, salt, defaultIterations) + if err != nil { + return nil, err + } + sha := sha512.Sum512(hashed) + return sha[:], nil +} diff --git a/roseh.moe.go b/roseh.moe.go index b8e6e21..a15fd9b 100644 --- a/roseh.moe.go +++ b/roseh.moe.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/ed25519" "crypto/rand" - "crypto/sha512" "crypto/subtle" "embed" "encoding/base64" @@ -19,6 +18,8 @@ import ( "path/filepath" "strings" "time" + + "gitlab.com/rhogenson/roseh.moe/internal/pwhash" ) var ( @@ -30,9 +31,10 @@ var ( ) var ( - notepadPassword []byte - privateKey ed25519.PrivateKey - publicKey ed25519.PublicKey + notepadPassword []byte + notepadPasswordSalt []byte + privateKey ed25519.PrivateKey + publicKey ed25519.PublicKey ) func loadSecrets() error { @@ -56,6 +58,11 @@ func loadSecrets() error { } privateKey = ed25519.NewKeyFromSeed(seed) publicKey = privateKey.Public().(ed25519.PublicKey) + } else if salt, ok := bytes.CutPrefix(line, []byte("salt=")); ok { + notepadPasswordSalt = make([]byte, hex.DecodedLen(len(salt))) + if _, err := hex.Decode(notepadPasswordSalt, salt); err != nil { + return err + } } } return nil @@ -204,7 +211,11 @@ type loginTemplateArgs struct { } func login(w http.ResponseWriter, r *http.Request) { - pwHash := sha512.Sum512([]byte(r.FormValue("password"))) + pwHash, err := pwhash.Hash(r.FormValue("password"), notepadPasswordSalt) + 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, Redirect: r.FormValue("redirect")}); err != nil { log.Printf("Warning: login: %s", err) -- cgit v1.3.1