summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--roseh.moe.go164
-rw-r--r--templates/register/register.html.template4
2 files changed, 106 insertions, 62 deletions
diff --git a/roseh.moe.go b/roseh.moe.go
index f497c8e..67b13c1 100644
--- a/roseh.moe.go
+++ b/roseh.moe.go
@@ -36,6 +36,7 @@ import (
"embed"
"encoding/base64"
"encoding/binary"
+ "encoding/json"
"encoding/xml"
"errors"
"flag"
@@ -188,7 +189,7 @@ type serviceConfiguration struct {
PackageRedirects []packageConfig `xml:"Redirects>Package"`
CommandRedirects []commandConfig `xml:"Redirects>Command"`
Passkey []passkey
- passkeys map[int64][]byte
+ passkeys map[string][]byte
}
var (
@@ -220,8 +221,8 @@ func loadConfig() (*serviceConfiguration, error) {
return nil, err
}
names := make(map[string]bool)
- config.passkeys = make(map[int64][]byte)
- namesByID := make(map[int64]string)
+ 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)
@@ -229,21 +230,20 @@ func loadConfig() (*serviceConfiguration, error) {
names[passkey.ID] = true
keyBytes, err := base64.RawURLEncoding.DecodeString(passkey.Key)
if err != nil {
- return nil, fmt.Errorf("%s: invalid key for device %q: %s", *configFile, passkey.ID, err)
+ return nil, fmt.Errorf("%s: invalid key for passkey %q: %s", *configFile, passkey.ID, err)
}
- id, n := binary.Varint(keyBytes)
- if n <= 0 {
- return nil, fmt.Errorf("%s: invalid key for device %q: bad device ID", *configFile, passkey.ID)
+ if len(keyBytes) < 16 {
+ return nil, fmt.Errorf("%s: invalid key for passkey %q: bad user ID: too short", *configFile, passkey.ID)
}
- key := keyBytes[n:]
+ id, key := keyBytes[:16], keyBytes[16:]
if _, err := sigVerifier(key); err != nil {
- return nil, fmt.Errorf("%s: invalid key for device %q: %s", *configFile, passkey.ID, err)
+ return nil, fmt.Errorf("%s: invalid key for passkey %q: %s", *configFile, passkey.ID, err)
}
- if _, ok := config.passkeys[id]; ok {
- return nil, fmt.Errorf("%s: passkeys %q and %q have duplicate ID %d", *configFile, namesByID[id], passkey.ID, id)
+ 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[id] = key
- namesByID[id] = passkey.ID
+ config.passkeys[string(id)] = key
+ namesByID[string(id)] = passkey.ID
}
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) })
@@ -774,7 +774,7 @@ func verifyHMAC(signedMsg, purpose string) (string, bool) {
const (
authVersion = "authv3"
- authCookieName = "a"
+ authCookieName = "__Secure-a"
)
func makeHTTPCookie(token string, expiry time.Time) *http.Cookie {
@@ -791,9 +791,16 @@ func makeHTTPCookie(token string, expiry time.Time) *http.Cookie {
}
}
-func attachCookie(w http.ResponseWriter, expiry time.Time) {
- token := signMsg(base64.RawURLEncoding.EncodeToString(binary.AppendVarint(nil, expiry.Unix())), authVersion)
- http.SetCookie(w, makeHTTPCookie(token, expiry))
+type authToken struct {
+ expiry time.Time
+ userID []byte
+}
+
+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)
+ http.SetCookie(w, makeHTTPCookie(token, tok.expiry))
}
func cookieAuth(r *http.Request) bool {
@@ -813,10 +820,25 @@ func cookieAuth(r *http.Request) bool {
if n <= 0 {
return false
}
+ msgBytes = msgBytes[n:]
expiry := time.Unix(expiryUnix, 0)
if time.Now().After(expiry) {
return false
}
+ if len(msgBytes) > 0 {
+ if len(msgBytes) < 16 {
+ return false
+ }
+ userID := msgBytes
+ config, err := loadConfig()
+ if err != nil {
+ log.Printf("Error: failed to load config: %s", err)
+ return false
+ }
+ if _, ok := config.passkeys[string(userID)]; !ok {
+ return false
+ }
+ }
return true
}
@@ -872,16 +894,11 @@ func login(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Challenge is too old. Please try again", http.StatusConflict)
return
}
- userBytes, err := base64.StdEncoding.DecodeString(r.FormValue("user"))
+ user, err := base64.StdEncoding.DecodeString(r.FormValue("user"))
if err != nil {
http.Error(w, "Bad user", http.StatusBadRequest)
return
}
- user, n := binary.Varint(userBytes)
- if n <= 0 {
- http.Error(w, "Bad user", http.StatusBadRequest)
- return
- }
authData, err := base64.StdEncoding.DecodeString(r.FormValue("auth-data"))
if err != nil {
http.Error(w, "Bad auth data", http.StatusBadRequest)
@@ -892,9 +909,25 @@ func login(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad client data: invalid base64", 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
+ }
challengeBase64 := base64.RawURLEncoding.EncodeToString(myChallenge)
- if !strings.Contains(string(clientData), challengeBase64) {
- http.Error(w, "Bad client data: bad challenge", http.StatusBadRequest)
+ if cd.Type != "webauthn.get" || cd.Challenge != challengeBase64 || 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)
return
}
sig, err := base64.StdEncoding.DecodeString(r.FormValue("sig"))
@@ -908,7 +941,7 @@ func login(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
- pubKey, ok := config.passkeys[user]
+ pubKey, ok := config.passkeys[string(user)]
if !ok {
http.Error(w, "No user with the given ID", http.StatusBadRequest)
return
@@ -918,7 +951,10 @@ func login(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Challenge failed", http.StatusBadRequest)
return
}
- attachCookie(w, time.Now().Add(30*24*time.Hour))
+ attachCookie(w, &authToken{
+ expiry: time.Now().Add(30 * 24 * time.Hour),
+ userID: user,
+ })
redirect, ok := verifyHMAC(r.FormValue("redirect"), "redirect")
if !ok {
redirect = "/"
@@ -938,22 +974,12 @@ type registerTemplateArgs struct {
}
func serveRegister(w http.ResponseWriter, r *http.Request) {
- serviceConfig, err := loadConfig()
- if err != nil {
- log.Printf("Error: failed to load service config: %s", err)
- http.Error(w, "Internal error", http.StatusInternalServerError)
- return
- }
- var maxID int64
- for id := range serviceConfig.passkeys {
- maxID = max(maxID, id)
- }
- myID := maxID + 1
- buf := make([]byte, 32)
+ buf := make([]byte, 32+16)
rand.Read(buf)
+ challenge, userID := buf[:32], buf[32:]
if err := registerTemplate.Execute(w, registerTemplateArgs{
- ID: base64.StdEncoding.EncodeToString(binary.AppendVarint(nil, myID)),
- Challenge: base64.StdEncoding.EncodeToString(buf),
+ ID: base64.StdEncoding.EncodeToString(userID),
+ Challenge: base64.StdEncoding.EncodeToString(challenge),
}); err != nil {
log.Printf("Warning: serveRegister: %s", err)
}
@@ -976,18 +1002,6 @@ func register(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Passkey name is required", http.StatusBadRequest)
return
}
- config, err := loadConfig()
- if err != nil {
- log.Printf("Error: failed to load config: %s", err)
- http.Error(w, "Internal error", http.StatusInternalServerError)
- return
- }
- for _, passkey := range config.Passkey {
- if passkey.ID == username {
- http.Error(w, "Passkey name already used", http.StatusBadRequest)
- return
- }
- }
userID, err := base64.StdEncoding.DecodeString(r.FormValue("id"))
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
@@ -1066,7 +1080,7 @@ func otpLogin(w http.ResponseWriter, r *http.Request) {
http.Error(w, "OTP is too old. Please generate a new link", http.StatusConflict)
return
}
- attachCookie(w, time.Now().Add(24*time.Hour))
+ attachCookie(w, &authToken{expiry: time.Now().Add(24 * time.Hour)})
fmt.Fprintf(w, "Temporary 24 hour token issued. You are now logged in.\n")
}
@@ -1330,7 +1344,15 @@ func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not logged in", http.StatusUnauthorized)
} else {
u := *r.URL
- u.Host = r.Host
+ switch r.Host {
+ case
+ "cinema." + selfURL.Host,
+ "downloads." + selfURL.Host:
+
+ u.Host = r.Host
+ default:
+ u.Host = selfURL.Host
+ }
u.Scheme = "http"
if *https != "" {
u.Scheme = "https"
@@ -1376,6 +1398,24 @@ func (h *loggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.h.ServeHTTP(w, r)
}
+type removeAuthCookieHandler struct {
+ h http.Handler
+}
+
+func (h *removeAuthCookieHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ /*var cookies []*http.Cookie
+ for _, c := range r.Cookies() {
+ if c.Name != authCookieName {
+ cookies = append(cookies, c)
+ }
+ }
+ r.Header.Del("Cookie")
+ for _, c := range cookies {
+ r.AddCookie(c)
+ }*/
+ h.h.ServeHTTP(w, r)
+}
+
func main() {
flag.Parse()
@@ -1457,17 +1497,17 @@ func main() {
mux.Handle("/api/", apiMux)
mux.Handle("/", &timeoutHandler{h: http.HandlerFunc(sharedFileHandler), timeout: 5 * time.Minute})
serverMux := http.NewServeMux()
- serverMux.Handle(selfURL.Hostname()+"/", http.NewCrossOriginProtection().Handler(&gzipHandler{mux}))
+ serverMux.Handle(selfURL.Hostname()+"/", &gzipHandler{mux})
jellyfinURL, err := url.Parse("http://127.0.0.1:8096")
if err != nil {
log.Fatal(err)
}
- serverMux.Handle("cinema."+selfURL.Hostname()+"/", &authHandler{h: &httputil.ReverseProxy{
+ serverMux.Handle("cinema."+selfURL.Hostname()+"/", &authHandler{h: &removeAuthCookieHandler{&httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(jellyfinURL)
r.SetXForwarded()
},
- }})
+ }}})
codeMux := http.NewServeMux()
cgitStaticFiles := http.FileServer(http.Dir(*cgit))
codeMux.Handle("/cgit.css", &timeoutHandler{h: cgitStaticFiles, timeout: 5 * time.Minute})
@@ -1483,20 +1523,20 @@ func main() {
Env: []string{"CGIT_CONFIG=" + cgitConfigAbsPath},
}
cgitCGI.PathLocationHandler = cgitCGI
- codeMux.Handle("/", &timeoutHandler{h: cgitCGI, timeout: 5 * time.Minute})
+ codeMux.Handle("/", &timeoutHandler{h: &removeAuthCookieHandler{cgitCGI}, timeout: 5 * time.Minute})
serverMux.Handle("code."+selfURL.Hostname()+"/", &gzipHandler{codeMux})
transmissionURL, err := url.Parse("http://127.0.0.1:9091")
if err != nil {
log.Fatal(err)
}
- serverMux.Handle("downloads."+selfURL.Hostname()+"/", &authHandler{h: &httputil.ReverseProxy{
+ serverMux.Handle("downloads."+selfURL.Hostname()+"/", &authHandler{h: &removeAuthCookieHandler{&httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(transmissionURL)
r.SetXForwarded()
},
- }})
+ }}})
server := &http.Server{
- Handler: &loggingHandler{serverMux},
+ Handler: &loggingHandler{http.NewCrossOriginProtection().Handler(serverMux)},
ReadHeaderTimeout: 5 * time.Minute,
IdleTimeout: 10 * time.Minute,
diff --git a/templates/register/register.html.template b/templates/register/register.html.template
index 3b4d959..833a1a7 100644
--- a/templates/register/register.html.template
+++ b/templates/register/register.html.template
@@ -40,6 +40,10 @@
async function generateKey() {
const name = document.getElementById("name").value;
const publicKeyCredential = await navigator.credentials.create({publicKey: {
+ authenticatorSelection: {
+ residentKey: "required",
+ userVerification: "required",
+ },
challenge: Uint8Array.fromBase64("{{.Challenge}}").buffer,
pubKeyCredParams: [
{alg: -9, type: "public-key"},