summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2025-10-07 08:31:39 -0700
committerRose Hogenson <rosehogenson@posteo.net>2025-10-07 08:41:15 -0700
commitbcc1c2d9dd7fefa538647c0d4e9e621511f1fde6 (patch)
tree64bd8f2b15dd3a737ae8d2db683641474acaff3f
parent65b1ed1ff56e4d795c67d4d0b31634decec7767b (diff)
downloadroseh.moe-bcc1c2d9dd7fefa538647c0d4e9e621511f1fde6.tar.zst
Bring back pbkdf2
-rw-r--r--go.mod4
-rw-r--r--go.sum4
-rw-r--r--internal/hole/hole.go25
-rw-r--r--internal/hole/wordlist.txt (renamed from internal/wordlist/wordlist.txt)0
-rw-r--r--internal/pwhash/pwhash.go18
-rw-r--r--internal/wordlist/wordlist.go11
-rw-r--r--roseh.moe.go26
-rw-r--r--tools/finditer/finditer.go43
-rw-r--r--tools/hashpw/hashpw.go34
-rw-r--r--tools/hole/hole.go94
10 files changed, 204 insertions, 55 deletions
diff --git a/go.mod b/go.mod
index 4e975bb..5e702ba 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,7 @@
module gitlab.com/rhogenson/roseh.moe
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
index e69de29..a7fae6a 100644
--- a/go.sum
+++ 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/hole/hole.go b/internal/hole/hole.go
new file mode 100644
index 0000000..4824459
--- /dev/null
+++ b/internal/hole/hole.go
@@ -0,0 +1,25 @@
+package hole
+
+import (
+ "crypto/rand"
+ _ "embed"
+ "encoding/binary"
+ "strings"
+)
+
+var (
+ //go:embed wordlist.txt
+ wordListString string
+ wordList = strings.Split(strings.TrimSuffix(wordListString, "\n"), "\n")
+)
+
+func New() string {
+ const nWords = 10
+ buf := make([]byte, 2*nWords)
+ rand.Read(buf)
+ words := make([]string, nWords)
+ for i := range words {
+ words[i] = wordList[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff]
+ }
+ return strings.Join(words, "-")
+}
diff --git a/internal/wordlist/wordlist.txt b/internal/hole/wordlist.txt
index 5e304ce..5e304ce 100644
--- a/internal/wordlist/wordlist.txt
+++ b/internal/hole/wordlist.txt
diff --git a/internal/pwhash/pwhash.go b/internal/pwhash/pwhash.go
new file mode 100644
index 0000000..576d052
--- /dev/null
+++ b/internal/pwhash/pwhash.go
@@ -0,0 +1,18 @@
+package pwhash
+
+import (
+ "crypto/pbkdf2"
+ "crypto/sha512"
+)
+
+const SaltSize = 8
+
+const defaultIter = 12059332 // from tools/finditer
+
+func HashIter(password string, salt []byte, iter int) ([]byte, error) {
+ return pbkdf2.Key(sha512.New, password, salt, iter, 32)
+}
+
+func Hash(password string, salt []byte) ([]byte, error) {
+ return HashIter(password, salt, defaultIter)
+}
diff --git a/internal/wordlist/wordlist.go b/internal/wordlist/wordlist.go
deleted file mode 100644
index de929bc..0000000
--- a/internal/wordlist/wordlist.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package wordlist
-
-import (
- _ "embed"
- "strings"
-)
-
-//go:embed wordlist.txt
-var wordList string
-
-var Words = strings.Split(strings.TrimSuffix(wordList, "\n"), "\n")
diff --git a/roseh.moe.go b/roseh.moe.go
index 140b1de..d3e96bf 100644
--- a/roseh.moe.go
+++ b/roseh.moe.go
@@ -8,7 +8,6 @@ import (
"crypto/subtle"
"embed"
"encoding/base64"
- "encoding/binary"
"encoding/hex"
"flag"
"fmt"
@@ -21,7 +20,8 @@ import (
"sync"
"time"
- "gitlab.com/rhogenson/roseh.moe/internal/wordlist"
+ "gitlab.com/rhogenson/roseh.moe/internal/hole"
+ "gitlab.com/rhogenson/roseh.moe/internal/pwhash"
)
var (
@@ -131,19 +131,8 @@ type wormholeTemplateArgs struct {
Hole string
}
-func makeHole() string {
- const nWords = 10
- buf := make([]byte, 2*nWords)
- rand.Read(buf)
- words := make([]string, nWords)
- for i := range words {
- words[i] = wordlist.Words[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff]
- }
- return strings.Join(words, "-")
-}
-
func wormhole(w http.ResponseWriter, r *http.Request) {
- if err := wormholeTemplate.Execute(w, wormholeTemplateArgs{Self: *selfURL, Hole: makeHole()}); err != nil {
+ if err := wormholeTemplate.Execute(w, wormholeTemplateArgs{Self: *selfURL, Hole: hole.New()}); err != nil {
log.Printf("Warning: wormhole: %s", err)
}
}
@@ -358,6 +347,12 @@ func checkCSRFToken(r *http.Request) error {
return nil
}
+func checkPassword(password string) bool {
+ expectedHash, salt := notepadPassword[:len(notepadPassword)-pwhash.SaltSize], notepadPassword[len(notepadPassword)-pwhash.SaltSize:]
+ hash, err := pwhash.Hash(password, salt)
+ return err == nil && subtle.ConstantTimeCompare(hash, expectedHash) != 0
+}
+
var (
//go:embed templates/login.html.template
loginTemplateString string
@@ -380,8 +375,7 @@ func login(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bad CSRF token", http.StatusBadRequest)
return
}
- hash := sha256.Sum256([]byte(r.FormValue("password")))
- if subtle.ConstantTimeCompare(notepadPassword, hash[:]) == 0 {
+ if !checkPassword(r.FormValue("password")) {
if err := loginTemplate.Execute(w, loginTemplateArgs{
Error: true,
CSRFToken: r.FormValue("csrf-token"),
diff --git a/tools/finditer/finditer.go b/tools/finditer/finditer.go
new file mode 100644
index 0000000..fbd0363
--- /dev/null
+++ b/tools/finditer/finditer.go
@@ -0,0 +1,43 @@
+package main
+
+import (
+ "encoding/hex"
+ "fmt"
+ "sort"
+ "testing"
+ "time"
+
+ "gitlab.com/rhogenson/roseh.moe/internal/pwhash"
+)
+
+func mustHex(s string) []byte {
+ b, err := hex.DecodeString(s)
+ if err != nil {
+ panic(err)
+ }
+ return b
+}
+
+const password = "emcee polio cardiac disclose superglue clapper cruelness stonework tingly unarmored"
+
+var salt = mustHex("a0aa6971f38827e5")
+
+var iterations int
+
+func BenchmarkHashIter(b *testing.B) {
+ for b.Loop() {
+ pwhash.HashIter(password, salt, iterations)
+ }
+}
+
+func main() {
+ const targetDuration = 5 * time.Second
+ for iterations = 8192; time.Duration(testing.Benchmark(BenchmarkHashIter).NsPerOp()) < targetDuration; 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()) > targetDuration
+ }))
+}
diff --git a/tools/hashpw/hashpw.go b/tools/hashpw/hashpw.go
new file mode 100644
index 0000000..c75fa2e
--- /dev/null
+++ b/tools/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 hashpw() error {
+ fmt.Fprint(os.Stderr, "Enter password: ")
+ password, err := term.ReadPassword(int(os.Stdin.Fd()))
+ fmt.Fprintln(os.Stderr)
+ if err != nil {
+ return err
+ }
+ salt := make([]byte, pwhash.SaltSize)
+ rand.Read(salt)
+ hash, err := pwhash.Hash(string(password), salt)
+ if err != nil {
+ return err
+ }
+ fmt.Printf("%x%x\n", hash, salt)
+ return nil
+}
+
+func main() {
+ if err := hashpw(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
diff --git a/tools/hole/hole.go b/tools/hole/hole.go
index d701c40..c6946f0 100644
--- a/tools/hole/hole.go
+++ b/tools/hole/hole.go
@@ -1,36 +1,54 @@
+// The hole command is a CLI uploader tool for the roseh.moe wormhole.
+//
+// # Magic wormhole
+//
+// The name is inspired by the original "magic wormhole" library. The goal is to
+// get a file from one computer to another without a USB stick, Including cases
+// where scp might not be available. This includes transferring files to your
+// phone, for example, although a USB cord might still be more convenient when
+// moving many files. It should also work over restrictive firewalls. You should
+// be able to use the wormhole as long as you can access http://roseh.moe in
+// a browser.
+//
+// # Usage example
+//
+// The following shows a typical interaction with the tool
+//
+// $ hole file.pdf
+// https://roseh.moe/wormhole/dv-32-ethos-aztec-shako-spoils-2y-dwarf-sand-any
+// Waiting for upload...
+// <tool hangs, waiting to upload the file>
+//
+// Then, on another machine:
+//
+// $ curl https://roseh.moe/wormhole/dv-32-ethos-aztec-shako-spoils-2y-dwarf-sand-any > file.pdf
+//
+// # Web interface
+//
+// There's also a web interface available for the wormhole, available at
+// https://roseh.moe/wormhole.
package main
import (
- "crypto/rand"
- "encoding/binary"
"flag"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
- "strings"
- "gitlab.com/rhogenson/roseh.moe/internal/wordlist"
+ "gitlab.com/rhogenson/roseh.moe/internal/hole"
)
var url = flag.String("url", "https://roseh.moe", "server URL")
-func makeHole() string {
- const nWords = 10
- buf := make([]byte, 2*nWords)
- rand.Read(buf)
- words := make([]string, nWords)
- for i := range words {
- words[i] = wordlist.Words[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff]
- }
- return strings.Join(words, "-")
-}
-
-func hole() error {
+func upload() error {
args := flag.Args()
- if len(args) != 1 {
- return fmt.Errorf("usage error")
+ if len(args) == 0 {
+ return fmt.Errorf("usage error: missing filename")
+ }
+ if len(args) > 1 {
+ return fmt.Errorf("usage error: too many positional arguments")
}
fileName := args[0]
f, err := os.Open(fileName)
@@ -38,18 +56,19 @@ func hole() error {
return err
}
defer f.Close()
- hole := makeHole()
+ hole := hole.New()
fmt.Println(*url + "/wormhole/" + hole)
+ fmt.Fprintln(os.Stderr, "Waiting for upload...")
resp, err := http.Get(*url + "/wormhole/" + hole + "/ready")
if err != nil {
- return err
+ return fmt.Errorf("check wormhole ready: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
- return fmt.Errorf("%s", resp.Status)
+ return fmt.Errorf("check wormhole ready: status: %s", resp.Status)
}
if _, err := io.Copy(io.Discard, resp.Body); err != nil {
- return err
+ return fmt.Errorf("check wormhole ready: read response: %s", err)
}
pipeReader, pipeWriter := io.Pipe()
multipartWriter := multipart.NewWriter(pipeWriter)
@@ -57,15 +76,15 @@ func hole() error {
defer pipeWriter.Close()
part, err := multipartWriter.CreateFormFile("file", fileName)
if err != nil {
- fmt.Fprintln(os.Stderr, err)
+ pipeWriter.CloseWithError(fmt.Errorf("create multipart reader: %s", err))
return
}
if _, err := io.Copy(part, f); err != nil {
- fmt.Fprintln(os.Stderr, err)
+ pipeWriter.CloseWithError(fmt.Errorf("write body: %s", err))
return
}
if err := multipartWriter.Close(); err != nil {
- fmt.Fprintln(os.Stderr, err)
+ pipeWriter.CloseWithError(fmt.Errorf("write trailer: %s", err))
return
}
}()
@@ -75,7 +94,7 @@ func hole() error {
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
- return fmt.Errorf("%s", resp.Status)
+ return fmt.Errorf("upload: status: %s", resp.Status)
}
io.Copy(os.Stderr, resp.Body)
fmt.Fprintln(os.Stderr)
@@ -83,9 +102,28 @@ func hole() error {
}
func main() {
+ flag.Usage = func() {
+ fmt.Fprint(os.Stderr, `Usage: hole FILE
+Create a one-time wormhole that allows a specific client to download a
+file. Prints the URL to download the file and then hangs until the file
+is downloaded.
+
+Example:
+ $ hole file.pdf
+ https://roseh.moe/wormhole/dv-32-ethos-aztec-shako-spoils-2y-dwarf-sand-any
+ Waiting for upload...
+ <tool hangs, waiting to upload the file>
+
+Then, on another machine:
+ $ curl https://roseh.moe/wormhole/dv-32-ethos-aztec-shako-spoils-2y-dwarf-sand-any > file.pdf
+
+Flags:
+`)
+ flag.PrintDefaults()
+ }
flag.Parse()
- if err := hole(); err != nil {
- fmt.Fprintln(os.Stderr, err)
+ if err := upload(); err != nil {
+ fmt.Fprintf(os.Stderr, "hole: %s\n", err)
os.Exit(1)
}
}