summaryrefslogtreecommitdiffstats
path: root/roseh.moe.go
diff options
context:
space:
mode:
Diffstat (limited to 'roseh.moe.go')
-rw-r--r--roseh.moe.go193
1 files changed, 18 insertions, 175 deletions
diff --git a/roseh.moe.go b/roseh.moe.go
index e8fb802..54b4ea6 100644
--- a/roseh.moe.go
+++ b/roseh.moe.go
@@ -22,17 +22,12 @@ import (
"bytes"
"compress/gzip"
"context"
- "crypto"
- "crypto/ecdsa"
"crypto/hmac"
"crypto/rand"
- "crypto/rsa"
"crypto/sha256"
- "crypto/x509"
"embed"
"encoding/base64"
"encoding/binary"
- "encoding/json/v2"
"encoding/xml"
"flag"
"fmt"
@@ -55,6 +50,7 @@ import (
"unicode"
"github.com/skip2/go-qrcode"
+ "roseh.moe/pkg/roseh.moe/internal/auth"
"roseh.moe/pkg/roseh.moe/internal/hole"
"roseh.moe/pkg/wordlist"
)
@@ -104,40 +100,6 @@ func urlFlag(name, def, doc string) *url.URL {
return u
}
-func sigVerifier(keyBytes []byte) (func(msg, sig []byte) bool, error) {
- key, err := x509.ParsePKIXPublicKey(keyBytes)
- if err != nil {
- return nil, err
- }
- switch key := key.(type) {
- case *ecdsa.PublicKey:
- return func(msg, sig []byte) bool {
- hash := sha256.Sum256(msg)
- return ecdsa.VerifyASN1(key, hash[:], sig)
- }, nil
- case *rsa.PublicKey:
- return func(msg, sig []byte) bool {
- hash := sha256.Sum256(msg)
- return rsa.VerifyPKCS1v15(key, crypto.SHA256, hash[:], sig) == nil
- }, nil
- default:
- return nil, fmt.Errorf("unsupported key type")
- }
-}
-
-func verifyPubKey(pubKey, msg, sig []byte) error {
- verify, err := sigVerifier(pubKey)
- if err != nil {
- return err
- }
- if !verify(msg, sig) {
- return fmt.Errorf("message failed validation")
- }
- return nil
-}
-
-var secretKey []byte
-
func loadSecrets() error {
fileBytes, err := os.ReadFile(*secretsFile)
if err != nil {
@@ -152,7 +114,7 @@ func loadSecrets() error {
if secretsConfig.SecretKey == "" {
return fmt.Errorf("%s: missing SecretKey", *secretsFile)
}
- if secretKey, err = base64.StdEncoding.DecodeString(secretsConfig.SecretKey); err != nil {
+ if auth.SecretKey, err = base64.StdEncoding.DecodeString(secretsConfig.SecretKey); err != nil {
return fmt.Errorf("%s: bad SecretKey (try base64)", *secretsFile)
}
return nil
@@ -173,15 +135,10 @@ type commandConfig struct {
Multi bool `xml:",attr"`
}
-type passkey struct {
- ID string `xml:",attr"`
- Key string `xml:",attr"`
-}
-
type serviceConfiguration struct {
PackageRedirects []packageConfig `xml:"Redirects>Package"`
CommandRedirects []commandConfig `xml:"Redirects>Command"`
- Passkey []passkey
+ Passkey []auth.Passkey
passkeys map[string][]byte
}
@@ -213,35 +170,9 @@ func loadConfig() (*serviceConfiguration, error) {
if err := xml.Unmarshal(fileBytes, config); err != nil {
return nil, err
}
- names := make(map[string]bool)
- config.passkeys = make(map[string][]byte)
- namesByID := make(map[string]string)
- for _, passkey := range config.Passkey {
- if names[passkey.ID] {
- return nil, fmt.Errorf("%s: duplicate name %q", *configFile, passkey.ID)
- }
- names[passkey.ID] = true
- keyBytes, err := base64.RawURLEncoding.DecodeString(strings.Map(func(r rune) rune {
- if unicode.IsSpace(r) {
- return -1
- }
- return r
- }, passkey.Key))
- if err != nil {
- return nil, fmt.Errorf("%s: invalid key for passkey %q: %s", *configFile, passkey.ID, err)
- }
- if len(keyBytes) < 16 {
- return nil, fmt.Errorf("%s: invalid key for passkey %q: bad user ID: too short", *configFile, passkey.ID)
- }
- id, key := keyBytes[:16], keyBytes[16:]
- if _, err := sigVerifier(key); err != nil {
- return nil, fmt.Errorf("%s: invalid key for passkey %q: %s", *configFile, passkey.ID, err)
- }
- if _, ok := config.passkeys[string(id)]; ok {
- return nil, fmt.Errorf("%s: passkeys %q and %q have duplicate ID %d", *configFile, namesByID[string(id)], passkey.ID, id)
- }
- config.passkeys[string(id)] = key
- namesByID[string(id)] = passkey.ID
+ config.passkeys, err = auth.LoadPasskeys(config.Passkey)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %s", *configFile, err)
}
slices.SortFunc(config.PackageRedirects, func(x, y packageConfig) int { return strings.Compare(x.Name, y.Name) })
slices.SortFunc(config.CommandRedirects, func(x, y commandConfig) int { return strings.Compare(x.Name, y.Name) })
@@ -407,7 +338,7 @@ func sharedFileHandler(w http.ResponseWriter, r *http.Request) {
}
func qrHandler(w http.ResponseWriter, r *http.Request) {
- text, ok := verifyHMAC(r.FormValue("t"), "qr")
+ text, ok := auth.VerifyHMAC(r.FormValue("t"), "qr")
if !ok {
http.Error(w, "Bad signature", http.StatusBadRequest)
return
@@ -485,7 +416,7 @@ func wormholeUpload(w http.ResponseWriter, r *http.Request) {
downloadURL := selfURL.String() + "/wormhole/" + holeName
if err := wormholeSuccessTemplate.Execute(w, wormholeSuccessTemplateArgs{
URL: downloadURL,
- QR: "/qr.png?t=" + url.QueryEscape(signMsg(downloadURL, "qr")),
+ QR: "/qr.png?t=" + url.QueryEscape(auth.SignMsg(downloadURL, "qr")),
}); err != nil {
log.Printf("Warning: wormholeSend: %s", err)
}
@@ -517,31 +448,6 @@ func wormholeDownload(w http.ResponseWriter, r *http.Request) {
}
}
-func mac(msg []byte, purpose string) []byte {
- mac := hmac.New(sha256.New, secretKey)
- io.WriteString(mac, purpose)
- io.WriteString(mac, ":")
- mac.Write(msg)
- return mac.Sum(nil)
-}
-
-func signMsg(msg, purpose string) string {
- return msg + base64.RawURLEncoding.EncodeToString(mac([]byte(msg), purpose))
-}
-
-func verifyHMAC(signedMsg, purpose string) (string, bool) {
- macSize := base64.RawURLEncoding.EncodedLen(sha256.Size)
- if len(signedMsg) < macSize {
- return "", false
- }
- msg, msgMAC := signedMsg[:len(signedMsg)-macSize], signedMsg[len(signedMsg)-macSize:]
- expectedMAC := base64.RawURLEncoding.EncodeToString(mac([]byte(msg), purpose))
- if !hmac.Equal([]byte(msgMAC), []byte(expectedMAC)) {
- return "", false
- }
- return msg, true
-}
-
const (
authVersion = "authv3"
authCookieName = "__Secure-a"
@@ -569,7 +475,7 @@ type authToken struct {
func attachCookie(w http.ResponseWriter, tok *authToken) {
buf := binary.AppendVarint(nil, tok.expiry.Unix())
buf = append(buf, tok.userID...)
- token := signMsg(base64.RawURLEncoding.EncodeToString(buf), authVersion)
+ token := auth.SignMsg(base64.RawURLEncoding.EncodeToString(buf), authVersion)
http.SetCookie(w, makeHTTPCookie(token, tok.expiry))
}
@@ -578,7 +484,7 @@ func cookieAuth(r *http.Request) bool {
if err != nil {
return false
}
- msg, ok := verifyHMAC(cookie.Value, authVersion)
+ msg, ok := auth.VerifyHMAC(cookie.Value, authVersion)
if !ok {
return false
}
@@ -628,13 +534,6 @@ func verifyRedirect(redirect string) bool {
return false
}
-const challengeExpiry = 10 * time.Minute
-
-var (
- challengeMu sync.Mutex
- challenges = make(map[string]time.Time)
-)
-
var (
//go:embed templates/login.html.template
loginTemplateString string
@@ -654,19 +553,13 @@ func serveLogin(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, redirect, http.StatusSeeOther)
return
}
- buf := make([]byte, 32)
- rand.Read(buf)
- now := time.Now()
- challengeMu.Lock()
- if len(challenges) >= 100 {
- challengeMu.Unlock()
+ challenge, ok := auth.NewChallenge()
+ if !ok {
http.Error(w, "Too many active requests", http.StatusTooManyRequests)
return
}
- challenges[string(buf)] = now
- challengeMu.Unlock()
if err := loginTemplate.Execute(w, loginTemplateArgs{
- Challenge: base64.StdEncoding.EncodeToString(buf),
+ Challenge: base64.StdEncoding.EncodeToString(challenge),
}); err != nil {
log.Printf("Warning: login: %s", err)
}
@@ -699,51 +592,8 @@ func login(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
- pubKey, ok := config.passkeys[string(user)]
- if !ok {
- http.Error(w, "No user with the given ID", http.StatusBadRequest)
- return
- }
- clientDataHash := sha256.Sum256(clientData)
- if err := verifyPubKey(pubKey, slices.Concat(authData, clientDataHash[:]), sig); err != nil {
- http.Error(w, "Challenge failed", http.StatusBadRequest)
- return
- }
- var cd struct {
- Type string `json:"type"`
- Challenge string `json:"challenge"`
- Origin string `json:"origin"`
- CrossOrigin bool `json:"crossOrigin"`
- }
- if err := json.Unmarshal(clientData, &cd); err != nil {
- http.Error(w, "Bad client data: invalid JSON", http.StatusBadRequest)
- return
- }
- challenge, err := base64.RawURLEncoding.DecodeString(cd.Challenge)
- if err != nil {
- http.Error(w, "Bad client data: bad challenge", http.StatusBadRequest)
- return
- }
- challengeMu.Lock()
- initiated, ok := challenges[string(challenge)]
- delete(challenges, string(challenge))
- challengeMu.Unlock()
- if !ok {
- http.Error(w, "Request already consumed", http.StatusConflict)
- return
- }
- if time.Since(initiated) > challengeExpiry {
- http.Error(w, "Challenge is too old. Please try again", http.StatusConflict)
- return
- }
- if cd.Type != "webauthn.get" || cd.Origin != selfURL.String() || cd.CrossOrigin {
- http.Error(w, "Bad client data", http.StatusBadRequest)
- return
- }
- expectedRPHash := sha256.Sum256([]byte(selfURL.Hostname()))
- const flagUserPresence, flagUserVerification = 0x01, 0x04
- if len(authData) < 37 || !bytes.Equal(authData[:32], expectedRPHash[:]) || authData[32]&(flagUserPresence|flagUserVerification) != (flagUserPresence|flagUserVerification) {
- http.Error(w, "Bad authenticator data", http.StatusBadRequest)
+ if err := auth.Login(user, authData, clientData, sig, config.passkeys, selfURL); err != nil {
+ http.Error(w, "Failed challenge", http.StatusBadRequest)
return
}
attachCookie(w, &authToken{
@@ -861,7 +711,7 @@ func serveOTP(w http.ResponseWriter, r *http.Request) {
otpURL := selfURL.String() + "/otp/" + l
if err := otpTemplate.Execute(w, otpTemplateArgs{
URL: otpURL,
- QR: "/qr.png?t=" + url.QueryEscape(signMsg(otpURL, "qr")),
+ QR: "/qr.png?t=" + url.QueryEscape(auth.SignMsg(otpURL, "qr")),
}); err != nil {
log.Printf("Warning: serveOTP: %s", err)
}
@@ -1243,15 +1093,8 @@ func main() {
}()
go func() {
- for now := range time.NewTicker(time.Hour).C {
- log.Printf("Cleaning expired challenges")
- challengeMu.Lock()
- for challenge, initiated := range challenges {
- if now.Sub(initiated) > challengeExpiry {
- delete(challenges, challenge)
- }
- }
- challengeMu.Unlock()
+ for range time.NewTicker(time.Hour).C {
+ auth.CleanChallenges()
}
}()