Initial commit

This commit is contained in:
WeebDataHoarder
2025-03-31 16:24:08 +02:00
commit 06bc5107d6
27 changed files with 2500 additions and 0 deletions

74
challenge/generic.go Normal file
View File

@@ -0,0 +1,74 @@
//go:build !tinygo
package challenge
import (
"context"
"encoding/json"
"errors"
"github.com/tetratelabs/wazero/api"
)
func MakeChallengeCall(ctx context.Context, mod api.Module, in MakeChallengeInput) (*MakeChallengeOutput, error) {
makeChallengeFunc := mod.ExportedFunction("MakeChallenge")
malloc := mod.ExportedFunction("malloc")
free := mod.ExportedFunction("free")
inData, err := json.Marshal(in)
mallocResult, err := malloc.Call(ctx, uint64(len(inData)))
if err != nil {
return nil, err
}
defer free.Call(ctx, mallocResult[0])
if !mod.Memory().Write(uint32(mallocResult[0]), inData) {
return nil, errors.New("could not write memory")
}
result, err := makeChallengeFunc.Call(ctx, uint64(NewAllocation(uint32(mallocResult[0]), uint32(len(inData)))))
if err != nil {
return nil, err
}
resultPtr := Allocation(result[0])
outData, ok := mod.Memory().Read(resultPtr.Pointer(), resultPtr.Size())
if !ok {
return nil, errors.New("could not read result")
}
defer free.Call(ctx, uint64(resultPtr.Pointer()))
var out MakeChallengeOutput
err = json.Unmarshal(outData, &out)
if err != nil {
return nil, err
}
return &out, nil
}
func VerifyChallengeCall(ctx context.Context, mod api.Module, in VerifyChallengeInput) (VerifyChallengeOutput, error) {
verifyChallengeFunc := mod.ExportedFunction("VerifyChallenge")
malloc := mod.ExportedFunction("malloc")
free := mod.ExportedFunction("free")
inData, err := json.Marshal(in)
mallocResult, err := malloc.Call(ctx, uint64(len(inData)))
if err != nil {
return VerifyChallengeOutputError, err
}
defer free.Call(ctx, mallocResult[0])
if !mod.Memory().Write(uint32(mallocResult[0]), inData) {
return VerifyChallengeOutputError, errors.New("could not write memory")
}
result, err := verifyChallengeFunc.Call(ctx, uint64(NewAllocation(uint32(mallocResult[0]), uint32(len(inData)))))
if err != nil {
return VerifyChallengeOutputError, err
}
return VerifyChallengeOutput(result[0]), nil
}
func PtrToBytes(ptr uint32, size uint32) []byte { panic("not implemented") }
func BytesToPtr(s []byte) (uint32, uint32) { panic("not implemented") }
func BytesToLeakedPtr(s []byte) (uint32, uint32) { panic("not implemented") }
func PtrToString(ptr uint32, size uint32) string { panic("not implemented") }
func StringToPtr(s string) (uint32, uint32) { panic("not implemented") }
func StringToLeakedPtr(s string) (uint32, uint32) { panic("not implemented") }

123
challenge/interface.go Normal file
View File

@@ -0,0 +1,123 @@
package challenge
import (
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
)
const ChallengeKeySize = sha256.Size
type MakeChallenge func(in Allocation) (out Allocation)
type Allocation uint64
func NewAllocation(ptr, size uint32) Allocation {
return Allocation((uint64(ptr) << uint64(32)) | uint64(size))
}
func (p Allocation) Pointer() uint32 {
return uint32(p >> 32)
}
func (p Allocation) Size() uint32 {
return uint32(p)
}
func MakeChallengeDecode(callback func(in MakeChallengeInput, out *MakeChallengeOutput), in Allocation) (out Allocation) {
outStruct := &MakeChallengeOutput{}
var inStruct MakeChallengeInput
inData := PtrToBytes(in.Pointer(), in.Size())
err := json.Unmarshal(inData, &inStruct)
if err != nil {
outStruct.Code = 500
outStruct.Error = err.Error()
} else {
outStruct.Code = 200
outStruct.Headers = make(http.Header)
func() {
// encapsulate err
defer func() {
if recovered := recover(); recovered != nil {
if outStruct.Code == 200 {
outStruct.Code = 500
}
if err, ok := recovered.(error); ok {
outStruct.Error = err.Error()
} else {
outStruct.Error = fmt.Sprintf("%v", recovered)
}
}
}()
callback(inStruct, outStruct)
}()
}
if len(outStruct.Headers) == 0 {
outStruct.Headers = nil
}
outData, err := json.Marshal(outStruct)
if err != nil {
panic(err)
}
return NewAllocation(BytesToLeakedPtr(outData))
}
func VerifyChallengeDecode(callback func(in VerifyChallengeInput) VerifyChallengeOutput, in Allocation) (out VerifyChallengeOutput) {
var inStruct VerifyChallengeInput
inData := PtrToBytes(in.Pointer(), in.Size())
err := json.Unmarshal(inData, &inStruct)
if err != nil {
return VerifyChallengeOutputError
} else {
func() {
// encapsulate err
defer func() {
if recovered := recover(); recovered != nil {
out = VerifyChallengeOutputError
}
}()
out = callback(inStruct)
}()
}
return out
}
type MakeChallengeInput struct {
Key []byte `json:"key"`
Parameters map[string]string `json:"parameters,omitempty"`
Headers http.Header `json:"headers,omitempty"`
Data []byte `json:"data,omitempty"`
}
type MakeChallengeOutput struct {
Data []byte `json:"data"`
Code int `json:"code"`
Headers http.Header `json:"headers,omitempty"`
Error string `json:"error,omitempty"`
}
type VerifyChallengeInput struct {
Key []byte `json:"key"`
Parameters map[string]string `json:"parameters,omitempty"`
Result []byte `json:"result,omitempty"`
}
type VerifyChallengeOutput uint64
const (
VerifyChallengeOutputOK = VerifyChallengeOutput(iota)
VerifyChallengeOutputFailed
VerifyChallengeOutputError
)

View File

@@ -0,0 +1 @@
*.wasm

View File

@@ -0,0 +1,92 @@
package main
import (
"crypto/sha256"
"crypto/subtle"
"encoding/binary"
"encoding/hex"
"encoding/json"
"git.gammaspectra.live/git/go-away/challenge"
"strconv"
"strings"
)
//go:generate tinygo build -target wasip1 -buildmode=c-shared -scheduler=none -gc=leaking -o runtime.wasm runtime.go
func main() {
}
func getChallenge(key []byte, params map[string]string) ([]byte, uint64) {
difficulty := uint64(5)
var err error
if diffStr, ok := params["difficulty"]; ok {
difficulty, err = strconv.ParseUint(diffStr, 10, 64)
if err != nil {
panic(err)
}
}
hasher := sha256.New()
hasher.Write(binary.LittleEndian.AppendUint64(nil, difficulty))
hasher.Write(key)
return hasher.Sum(nil), difficulty
}
//go:wasmexport MakeChallenge
func MakeChallenge(in challenge.Allocation) (out challenge.Allocation) {
return challenge.MakeChallengeDecode(func(in challenge.MakeChallengeInput, out *challenge.MakeChallengeOutput) {
type Result struct {
Challenge string `json:"challenge"`
Difficulty uint64 `json:"difficulty"`
}
challenge, difficulty := getChallenge(in.Key, in.Parameters)
data, err := json.Marshal(Result{
Challenge: hex.EncodeToString(challenge),
Difficulty: difficulty,
})
if err != nil {
panic(err)
}
out.Data = data
out.Headers.Set("Content-Type", "text/javascript; charset=utf-8")
}, in)
}
//go:wasmexport VerifyChallenge
func VerifyChallenge(in challenge.Allocation) (out challenge.VerifyChallengeOutput) {
return challenge.VerifyChallengeDecode(func(in challenge.VerifyChallengeInput) challenge.VerifyChallengeOutput {
c, difficulty := getChallenge(in.Key, in.Parameters)
type Result struct {
Hash string `json:"hash"`
Nonce uint64 `json:"nonce"`
}
var result Result
err := json.Unmarshal(in.Result, &result)
if err != nil {
panic(err)
}
if !strings.HasPrefix(result.Hash, strings.Repeat("0", int(difficulty))) {
return challenge.VerifyChallengeOutputFailed
}
resultBinary, err := hex.DecodeString(result.Hash)
if err != nil {
panic(err)
}
buf := make([]byte, 0, len(c)+8)
buf = append(buf, c[:]...)
buf = binary.LittleEndian.AppendUint64(buf, result.Nonce)
calculated := sha256.Sum256(buf)
if subtle.ConstantTimeCompare(resultBinary, calculated[:]) != 1 {
return challenge.VerifyChallengeOutputFailed
}
return challenge.VerifyChallengeOutputOK
}, in)
}

View File

@@ -0,0 +1,125 @@
let _worker;
let _webWorkerURL;
let _challenge;
let _difficulty;
async function setup(config) {
const status = document.getElementById('status');
const image = document.getElementById('image');
const title = document.getElementById('title');
const spinner = document.getElementById('spinner');
const { challenge, difficulty } = await fetch(config.Path + "/make-challenge", { method: "POST" })
.then(r => {
if (!r.ok) {
throw new Error("Failed to fetch config");
}
return r.json();
})
.catch(err => {
throw err;
});
_challenge = challenge;
_difficulty = difficulty;
_webWorkerURL = URL.createObjectURL(new Blob([
'(', processTask(challenge, difficulty), ')()'
], { type: 'application/javascript' }));
_worker = new Worker(_webWorkerURL);
return `Difficulty ${difficulty}`
}
function challenge() {
return new Promise((resolve, reject) => {
_worker.onmessage = (event) => {
_worker.terminate();
resolve(event.data);
};
_worker.onerror = (event) => {
_worker.terminate();
reject();
};
_worker.postMessage({
challenge: _challenge,
difficulty: _difficulty,
});
URL.revokeObjectURL(_webWorkerURL);
});
}
function processTask() {
return function () {
const decodeHex = (str) => {
let result = new Uint8Array(str.length>>1)
for (let i = 0; i < str.length; i += 2){
result[i>>1] = parseInt(str.substring(i, i + 2), 16)
}
return result
}
const encodeHex = (buf) => {
return buf.reduce((a, b) => a + b.toString(16).padStart(2, '0'), '')
}
const lessThan = (buf, target) => {
for(let i = 0; i < buf.length; ++i){
if (buf[i] < target[i]){
return true;
} else if (buf[i] > target[i]){
return false;
}
}
return false
}
const increment = (number) => {
for ( let i = 0; i < number.length; i++ ) {
if(number[i]===255){
number[i] = 0;
} else {
number[i]++;
break;
}
}
}
addEventListener('message', async (event) => {
let data = decodeHex(event.data.challenge);
let target = decodeHex("0".repeat(event.data.difficulty) + "f".repeat(64 - event.data.difficulty));
let nonce = new Uint8Array(8);
let buf = new Uint8Array(data.length + nonce.length);
buf.set(data, 0);
while(true) {
buf.set(nonce, data.length);
let result = new Uint8Array(await crypto.subtle.digest("SHA-256", buf))
if (lessThan(result, target)){
const nonceNumber = Number(new BigUint64Array(nonce.buffer).at(0))
postMessage({
result: {
hash: encodeHex(result),
nonce: nonceNumber,
},
info: `iterations ${nonceNumber}`,
});
return
}
increment(nonce)
}
});
}.toString();
}
export { setup, challenge }

59
challenge/tinygo.go Normal file
View File

@@ -0,0 +1,59 @@
//go:build tinygo
package challenge
// #include <stdlib.h>
import "C"
import (
"unsafe"
)
// PtrToBytes returns a byte slice from WebAssembly compatible numeric types
// representing its pointer and length.
func PtrToBytes(ptr uint32, size uint32) []byte {
return unsafe.Slice((*byte)(unsafe.Pointer(uintptr(ptr))), size)
}
// BytesToPtr returns a pointer and size pair for the given byte slice in a way
// compatible with WebAssembly numeric types.
// The returned pointer aliases the slice hence the slice must be kept alive
// until ptr is no longer needed.
func BytesToPtr(s []byte) (uint32, uint32) {
ptr := unsafe.Pointer(unsafe.SliceData(s))
return uint32(uintptr(ptr)), uint32(len(s))
}
// BytesToLeakedPtr returns a pointer and size pair for the given byte slice in a way
// compatible with WebAssembly numeric types.
// The pointer is not automatically managed by TinyGo hence it must be freed by the host.
func BytesToLeakedPtr(s []byte) (uint32, uint32) {
size := C.ulong(len(s))
ptr := unsafe.Pointer(C.malloc(size))
copy(unsafe.Slice((*byte)(ptr), size), s)
return uint32(uintptr(ptr)), uint32(size)
}
// PtrToString returns a string from WebAssembly compatible numeric types
// representing its pointer and length.
func PtrToString(ptr uint32, size uint32) string {
return unsafe.String((*byte)(unsafe.Pointer(uintptr(ptr))), size)
}
// StringToPtr returns a pointer and size pair for the given string in a way
// compatible with WebAssembly numeric types.
// The returned pointer aliases the string hence the string must be kept alive
// until ptr is no longer needed.
func StringToPtr(s string) (uint32, uint32) {
ptr := unsafe.Pointer(unsafe.StringData(s))
return uint32(uintptr(ptr)), uint32(len(s))
}
// StringToLeakedPtr returns a pointer and size pair for the given string in a way
// compatible with WebAssembly numeric types.
// The pointer is not automatically managed by TinyGo hence it must be freed by the host.
func StringToLeakedPtr(s string) (uint32, uint32) {
size := C.ulong(len(s))
ptr := unsafe.Pointer(C.malloc(size))
copy(unsafe.Slice((*byte)(ptr), size), s)
return uint32(uintptr(ptr)), uint32(size)
}