hack together first draft of pow captcha server

This commit is contained in:
forest 2021-02-22 17:21:15 -06:00
parent f83e4b2806
commit 81127c77c5
4 changed files with 230 additions and 1 deletions

View File

@ -1,3 +1,8 @@
# sequentialread-pow-captcha
A proof of work based captcha similar to friendly captcha, but lightweight and FLOSS
A proof of work based captcha similar to [friendly captcha](https://github.com/FriendlyCaptcha/friendly-challenge), but lightweight and FLOSS
![sequence diagram](readme/sequence.png)
This diagram was created with https://app.diagrams.net/.
To edit it, download the <a download href="readme/sequence.drawio">diagram file</a> and edit it with the https://app.diagrams.net/ web application, or you may run the application from [source](https://github.com/jgraph/drawio) if you wish.

223
main.go Normal file
View File

@ -0,0 +1,223 @@
package main
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"golang.org/x/crypto/scrypt"
)
const numberOfChallenges = 100
const deprecateAfterGenerations = 10
const portNumber = 2370
// https://en.wikipedia.org/wiki/Scrypt
type ScryptParameters struct {
CPUAndMemoryCost int `json:"N"`
BlockSize int `json:"r"`
Paralellization int `json:"p"`
KeyLength int `json:"klen"`
}
type Challenge struct {
ScryptParameters
Preimage string `json:"i"`
Difficulty string `json:"d"`
}
var currentChallengesGeneration = 0
var challenges = map[string]int{}
func main() {
scryptParameters := ScryptParameters{
CPUAndMemoryCost: 2048,
BlockSize: 8,
Paralellization: 1,
KeyLength: 16,
}
http.HandleFunc("/GetChallenges", func(responseWriter http.ResponseWriter, request *http.Request) {
if request.Method != "POST" {
responseWriter.Header().Set("Allow", "POST")
http.Error(responseWriter, "405 Method Not Allowed, try POST", http.StatusMethodNotAllowed)
}
currentChallengesGeneration++
requestQuery := request.URL.Query()
difficultyLevelString := requestQuery.Get("difficultyLevel")
difficultyLevel, err := strconv.Atoi(difficultyLevelString)
if err != nil {
http.Error(
responseWriter,
fmt.Sprintf(
"400 url param ?difficultyLevel=%s value could not be converted to an integer",
difficultyLevelString,
),
http.StatusInternalServerError,
)
return
}
toReturn := make([]string, numberOfChallenges)
for i := 0; i < numberOfChallenges; i++ {
preimageBytes := make([]byte, 8)
_, err := rand.Read(preimageBytes)
if err != nil {
http.Error(responseWriter, "500 internal server error", http.StatusInternalServerError)
log.Printf("read random bytes failed: %v", err)
return
}
preimage := hex.EncodeToString(preimageBytes)
difficulty := fmt.Sprintf(fmt.Sprintf("%%0%dd", difficultyLevel), 0)
challenge := Challenge{
Preimage: preimage,
Difficulty: difficulty,
}
challenge.CPUAndMemoryCost = scryptParameters.CPUAndMemoryCost
challenge.BlockSize = scryptParameters.BlockSize
challenge.Paralellization = scryptParameters.Paralellization
challenge.KeyLength = scryptParameters.KeyLength
challengeBytes, err := json.Marshal(challenge)
if err != nil {
http.Error(responseWriter, "500 internal server error", http.StatusInternalServerError)
log.Printf("serialize challenge as json failed: %v", err)
return
}
challengeBase64 := base64.StdEncoding.EncodeToString(challengeBytes)
challenges[challengeBase64] = currentChallengesGeneration
toReturn[i] = challengeBase64
}
toRemove := []string{}
for k, generation := range challenges {
if generation+deprecateAfterGenerations < currentChallengesGeneration {
toRemove = append(toRemove, k)
}
}
for _, k := range toRemove {
delete(challenges, k)
}
responseBytes, err := json.Marshal(toReturn)
if err != nil {
http.Error(responseWriter, "500 internal doodoo error", http.StatusInternalServerError)
log.Printf("json marshal failed: %v", err)
return
}
responseWriter.Write(responseBytes)
})
http.HandleFunc("/Verify", func(responseWriter http.ResponseWriter, request *http.Request) {
if request.Method != "POST" {
responseWriter.Header().Set("Allow", "POST")
http.Error(responseWriter, "405 Method Not Allowed, try POST", http.StatusMethodNotAllowed)
}
requestQuery := request.URL.Query()
challengeBase64 := requestQuery.Get("challenge")
nonceBase64 := requestQuery.Get("nonce")
if _, has := challenges[challengeBase64]; !has {
http.Error(
responseWriter,
fmt.Sprintf(
"404 challenge given by url param ?challenge=%s was not found",
challengeBase64,
),
http.StatusNotFound,
)
return
}
delete(challenges, challengeBase64)
nonceBuffer := make([]byte, 8)
bytesWritten, err := base64.StdEncoding.Decode(nonceBuffer, []byte(nonceBase64))
if nonceBase64 == "" || err != nil {
http.Error(
responseWriter,
fmt.Sprintf(
"400 bad request: nonce given by url param ?nonce=%s could not be base64 decoded",
nonceBase64,
),
http.StatusBadRequest,
)
return
}
nonceBytes := nonceBuffer[:bytesWritten]
challengeJson, err := base64.StdEncoding.DecodeString(challengeBase64)
if err != nil {
http.Error(responseWriter, "500 challenge couldn't be decoded", http.StatusInternalServerError)
log.Printf("challenge %s couldn't be parsed: %v\n", challengeBase64, err)
return
}
var challenge Challenge
err = json.Unmarshal([]byte(challengeJson), &challenge)
if err != nil {
http.Error(responseWriter, "500 challenge couldn't be parsed", http.StatusInternalServerError)
log.Printf("challenge %s (%s) couldn't be parsed: %v\n", challengeJson, challenge, err)
return
}
preimageBytes := make([]byte, 8)
n, err := base64.StdEncoding.Decode(preimageBytes, []byte(challenge.Preimage))
if n != 8 || err != nil {
http.Error(responseWriter, "500 invalid preimage", http.StatusInternalServerError)
log.Printf("invalid preimage %s: %v\n", challenge.Preimage, err)
return
}
hash, err := scrypt.Key(
nonceBytes,
preimageBytes,
challenge.CPUAndMemoryCost,
challenge.BlockSize,
challenge.Paralellization,
challenge.KeyLength,
)
if err != nil {
http.Error(responseWriter, "500 scrypt returned error", http.StatusInternalServerError)
log.Printf("scrypt returned error: %v\n", challengeJson, challenge, err)
return
}
hashHex := hex.EncodeToString(hash)
if !strings.HasSuffix(hashHex, challenge.Difficulty) {
http.Error(
responseWriter,
fmt.Sprintf(
"400 bad request: nonce given by url param ?nonce=%s did not result in a hash that meets the required difficulty",
nonceBase64,
),
http.StatusBadRequest,
)
return
}
responseWriter.WriteHeader(200)
responseWriter.Write([]byte("OK"))
})
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
err := http.ListenAndServe(fmt.Sprintf(":%d", portNumber), nil)
// if got this far it means server crashed!
panic(err)
}

1
readme/sequence.drawio Normal file
View File

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2021-02-22T21:13:21.920Z" agent="5.0 (X11)" etag="tw2PtMxtdaydmWXPzKbo" version="14.4.2" type="device"><diagram id="GVFBTWp1bs8fEg8jQywl" name="Page-1">5VpRk6I4EP41Po4FBBQfd5zZvavarZ27qat9jhCBnUjcEEbdX38dCCDE0dEBasUXC9rwBfrrL51uGKH5avuF43X4jfmEjizD347Qw8iynMkEfqVhlxvQbJobAh75ucmsDM/Rb6KMhrKmkU+S2kDBGBXRum70WBwTT9RsmHO2qQ9bMlqfdY0DohmePUx164/IF2FudR2jsv9FoiAsZjYN9c8KF4OVIQmxzzZ7JvQ4QnPOmMiPVts5odJ3hV/y6z6/8W95Y5zE4j0XBLHx8Ovu+e/45b/4H/O7O1t8f7xTKK+YpuqB1c2KXeEBEvufpCPhLGYxGO9DsaJwZsJhPpr4mhOruzLLZ4UYIWxFBN/BkE3lzcKZ4Z4jCxsnFIvotQ6PFalBCVfO8MQimNgyVPyZtsLZlfzUIRKWco+oq/a91wBy0AkggXlAhAYEB3uPXZkycs4gyrqEKB8nIfEVVdfE2qzubMe5kLUmUN+soVtiDVktsdYE6ps1+5ZYsyctsdYE6pu1icaah9fCC7FkjvBXwjUWBdmKOluJ4OyFzBllvKJ2GVHaMGEaBTGcekAiAKN7gBcRbBs+qT9Wke/Lae43YSTI8xp7cs4N7JHAxlka+1mwGGWESACyPTdGCt+bdd+76nQvhMqNyX4MWcbb4VLj51wyphoZG7KQE63XFNwkIhYPlw1r+oex4WpspKCIRN5SxspC7pOHrA/TOs2I1Scjs7NSjEdxkkTedSUWLYvbbSWWJlDHiaVYW2tkTahUhh+9wmEgD78QMQ8xpSQOoF5V/8N8e0N0ioG8r3gBFXON2feLh5Mk+o0XGZ6Uylr6IPOKcz9yHo6JRxXM6uJRGer78fN26L6ptDtj7DjIrRFWFC8fDSh37EwPAhcYbLlMSDcxcF6hfJWC1XQ2aWv/3gTqWrAHimXQw/R+PB5N4QxQjfoZHDoPw5CneTwTgj4t27brMjJbiZ87czJ2J70p8rza+ioVqbUv3LYU2QTqWpF6Sf0v+ZWSRN7jk2zBgkvlrJbxmfHVQJSITioRoTot7UQNAM8O4fYgSmf4omxqSSsSLm1OakBdi1LvmCwz8YEjwY8Q+cV+diB6dI7qETauM9RJYrTR2Ea9SVBvvQQcyyo/ltW8ZaQxFP/+UXo/XvdTshQfq/qXLBbqdeCsnSaAlgQL/e2tG+6BdaOzJoCp92UGt1pqi1xb7WUNqOvVUm/Z+GwTU4allkQotzBVw/lnMpA10z0qqQ6r/Qbd/ZX6hW+Ot3sSCDeRUU69VD5oHMiqUg1c8Krnc7gTpAGuOWNLgMh+Noy/jOTXCry+TsvHh6BisnueH+eBl2bdWyO7KWmVK3N+RywV5ag8u7/3DlvPCTxfXP6spFA24otl5UBr+FBSaK4+rSUF6wY7TeULkw9voZtAXX+VoXeaVA4YDyUDnNBPl/0kezqe9la6WjfYT0JtfaGhAXWtu0OfaOgplSWHk9/JZKx6UfmVT+zHxVnzOiV/snHV2Sseo+wg97/vu8HeFbr0FU8zg2tAXS8Aeu8KFBAtdwNR4PFW1dW9ZIXT6kPnfHj1tTh6/B8=</diagram></mxfile>

BIN
readme/sequence.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB