mirror of
https://github.com/sequentialread/pow-captcha.git
synced 2025-10-13 16:49:18 +00:00
hack together first draft of pow captcha server
This commit is contained in:
parent
f83e4b2806
commit
81127c77c5
@ -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
|
||||
|
||||

|
||||
|
||||
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
223
main.go
Normal 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
1
readme/sequence.drawio
Normal 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
BIN
readme/sequence.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
Loading…
x
Reference in New Issue
Block a user