|
|
@ -5,6 +5,7 @@ import ( |
|
|
|
"crypto/ecdsa" |
|
|
|
"crypto/rand" |
|
|
|
"crypto/rsa" |
|
|
|
"crypto/sha1" |
|
|
|
"crypto/sha256" |
|
|
|
"crypto/sha512" |
|
|
|
"encoding/base64" |
|
|
@ -15,14 +16,19 @@ import ( |
|
|
|
"math/big" |
|
|
|
"net/http" |
|
|
|
"net/url" |
|
|
|
"os" |
|
|
|
"path/filepath" |
|
|
|
"strconv" |
|
|
|
"strings" |
|
|
|
"sync" |
|
|
|
"time" |
|
|
|
|
|
|
|
"git.rootprojects.org/root/keypairs" |
|
|
|
"git.rootprojects.org/root/keypairs/keyfetch" |
|
|
|
|
|
|
|
//jwt "github.com/dgrijalva/jwt-go"
|
|
|
|
|
|
|
|
"github.com/google/uuid" |
|
|
|
) |
|
|
|
|
|
|
|
type PublicJWK struct { |
|
|
@ -53,13 +59,39 @@ func (t *InspectableToken) MarshalJSON() ([]byte, error) { |
|
|
|
)), nil |
|
|
|
} |
|
|
|
|
|
|
|
var nonces map[string]int64 |
|
|
|
var defaultFrom string |
|
|
|
var defaultReplyTo string |
|
|
|
|
|
|
|
//var nonces map[string]int64
|
|
|
|
//var nonCh chan string
|
|
|
|
var nonces sync.Map |
|
|
|
var salt []byte |
|
|
|
|
|
|
|
func init() { |
|
|
|
nonces = make(map[string]int64) |
|
|
|
var err error |
|
|
|
salt64 := os.Getenv("SALT") |
|
|
|
salt, err = base64.RawURLEncoding.DecodeString(salt64) |
|
|
|
if len(salt64) < 22 || nil != err { |
|
|
|
panic("SALT must be set as 22+ character base64") |
|
|
|
} |
|
|
|
defaultFrom = os.Getenv("MAILER_FROM") |
|
|
|
defaultReplyTo = os.Getenv("MAILER_REPLY_TO") |
|
|
|
//nonces = make(map[string]int64)
|
|
|
|
//nonCh = make(chan string)
|
|
|
|
|
|
|
|
/* |
|
|
|
go func() { |
|
|
|
for { |
|
|
|
nonce := <- nonCh |
|
|
|
nonces[nonce] = time.Now().Unix() |
|
|
|
} |
|
|
|
}() |
|
|
|
*/ |
|
|
|
} |
|
|
|
|
|
|
|
func Route(jwksPrefix string, privkey keypairs.PrivateKey) { |
|
|
|
// TODO get from main()
|
|
|
|
tokPrefix := jwksPrefix |
|
|
|
pubkey := keypairs.NewPublicKey(privkey.Public()) |
|
|
|
|
|
|
|
http.HandleFunc("/api/new-nonce", func(w http.ResponseWriter, r *http.Request) { |
|
|
@ -87,7 +119,84 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) { |
|
|
|
}) |
|
|
|
|
|
|
|
http.HandleFunc("/api/new-account", requireNonce(func(w http.ResponseWriter, r *http.Request) { |
|
|
|
http.Error(w, "Not Implemented", http.StatusNotImplemented) |
|
|
|
// Try to decode the request body into the struct. If there is an error,
|
|
|
|
// respond to the client with the error message and a 400 status code.
|
|
|
|
data := map[string]string{} |
|
|
|
err := json.NewDecoder(r.Body).Decode(&data) |
|
|
|
if nil != err { |
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// TODO check DNS for MX records
|
|
|
|
parts := strings.Split(data["to"], ", <>\n\r\t") |
|
|
|
to := parts[0] |
|
|
|
if len(parts) > 1 || !strings.Contains(to, "@") { |
|
|
|
http.Error(w, "invalid email address", http.StatusBadRequest) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
token, err := uuid.NewRandom() |
|
|
|
if nil != err { |
|
|
|
// nothing else to do if we run out of random
|
|
|
|
// or are on a platform that doesn't support random
|
|
|
|
panic(fmt.Errorf("random bytes read failure: %w", err)) |
|
|
|
} |
|
|
|
token64 := base64.RawURLEncoding.EncodeToString([]byte(token[:])) |
|
|
|
// hash token to prevent fs read timing attacks
|
|
|
|
hash := sha1.Sum(append(token[:], salt...)) |
|
|
|
tokname := base64.RawURLEncoding.EncodeToString(hash[:]) |
|
|
|
if err := ioutil.WriteFile( |
|
|
|
filepath.Join(tokPrefix, tokname+".tok.txt"), |
|
|
|
[]byte(`{"comment":"I have no idea..."}`), |
|
|
|
os.FileMode(0600), |
|
|
|
); nil != err { |
|
|
|
http.Error(w, "database connection failed when writing verification token", http.StatusInternalServerError) |
|
|
|
return |
|
|
|
} |
|
|
|
subject := "Verify New Account" |
|
|
|
// TODO go tpl
|
|
|
|
// TODO determine OS and Browser from user agent
|
|
|
|
baseURL := getBaseURL(r) |
|
|
|
text := fmt.Sprintf( |
|
|
|
"It looks like you just tried to register a new Pocket ID account.\n\n Verify account: %s/verify/%s\n\nNot you? Just ignore this message.", |
|
|
|
baseURL, token64, |
|
|
|
) |
|
|
|
_, err = SendSimpleMessage(to, defaultFrom, subject, text, defaultReplyTo) |
|
|
|
if nil != err { |
|
|
|
// TODO neuter mailgun output
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
fmt.Fprintf(w, `{ "success": true, "error": "" }%s`, "\n") |
|
|
|
})) |
|
|
|
|
|
|
|
// TODO use chi
|
|
|
|
http.HandleFunc("/verify/", requireNonce(func(w http.ResponseWriter, r *http.Request) { |
|
|
|
parts := strings.Split(r.URL.Path, "/") |
|
|
|
if 3 != len(parts) { |
|
|
|
http.Error(w, "invalid url path", http.StatusBadRequest) |
|
|
|
return |
|
|
|
} |
|
|
|
token64 := parts[2] |
|
|
|
token, err := base64.RawURLEncoding.DecodeString(token64) |
|
|
|
if err != nil || 0 == len(token) { |
|
|
|
http.Error(w, "invalid url path", http.StatusBadRequest) |
|
|
|
return |
|
|
|
} |
|
|
|
// hash token to prevent fs read timing attacks
|
|
|
|
hash := sha1.Sum(append(token, salt...)) |
|
|
|
tokname := base64.RawURLEncoding.EncodeToString(hash[:]) |
|
|
|
tokfile := filepath.Join(tokPrefix, tokname+".tok.txt") |
|
|
|
_, err = ioutil.ReadFile(tokfile) |
|
|
|
if nil != err { |
|
|
|
http.Error(w, "database connection failed when reading verification token", http.StatusInternalServerError) |
|
|
|
return |
|
|
|
} |
|
|
|
os.Remove(tokfile) |
|
|
|
|
|
|
|
fmt.Fprintf(w, `{ "success": true, "error": "" }%s`, "\n") |
|
|
|
})) |
|
|
|
|
|
|
|
http.HandleFunc("/api/jwks", func(w http.ResponseWriter, r *http.Request) { |
|
|
@ -521,6 +630,7 @@ func JOSEVerify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool { |
|
|
|
case *rsa.PublicKey: |
|
|
|
// TODO keypairs.Size(key) to detect key size ?
|
|
|
|
//alg := "SHA256"
|
|
|
|
// TODO: this hasn't been tested yet
|
|
|
|
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err { |
|
|
|
verified = true |
|
|
|
} |
|
|
@ -566,7 +676,8 @@ func issueNonce(w http.ResponseWriter, r *http.Request) { |
|
|
|
b := make([]byte, 16) |
|
|
|
_, _ = rand.Read(b) |
|
|
|
nonce := base64.RawURLEncoding.EncodeToString(b) |
|
|
|
nonces[nonce] = time.Now().Unix() |
|
|
|
//nonCh <- nonce
|
|
|
|
nonces.Store(nonce, time.Now()) |
|
|
|
|
|
|
|
w.Header().Set("Replay-Nonce", nonce) |
|
|
|
} |
|
|
@ -575,8 +686,13 @@ func requireNonce(next http.HandlerFunc) http.HandlerFunc { |
|
|
|
return func(w http.ResponseWriter, r *http.Request) { |
|
|
|
nonce := r.Header.Get("Replay-Nonce") |
|
|
|
// TODO expire nonces every so often
|
|
|
|
t := nonces[nonce] |
|
|
|
if 0 == t { |
|
|
|
//t := nonces[nonce]
|
|
|
|
var t time.Time |
|
|
|
tmp, ok := nonces.Load(nonce) |
|
|
|
if ok { |
|
|
|
t = tmp.(time.Time) |
|
|
|
} |
|
|
|
if !ok || time.Now().Sub(t) > 15*time.Minute { |
|
|
|
http.Error( |
|
|
|
w, |
|
|
|
`{ "error": "invalid or expired nonce", "error_code": "ENONCE" }`, |
|
|
@ -585,7 +701,8 @@ func requireNonce(next http.HandlerFunc) http.HandlerFunc { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
delete(nonces, nonce) |
|
|
|
//delete(nonces, nonce)
|
|
|
|
nonces.Delete(nonce) |
|
|
|
issueNonce(w, r) |
|
|
|
|
|
|
|
next(w, r) |
|
|
|