Skip to content

Commit

Permalink
Merge pull request #28 from proton-jsadun/update-pow
Browse files Browse the repository at this point in the history
Update to new specifications for proof-of-work challenges
  • Loading branch information
wussler authored Apr 19, 2022
2 parents d0fe76f + 9aee6a7 commit cc36f69
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 23 deletions.
6 changes: 5 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

## Unreleased

## Changed
### Added
* New function `Argon2PreimageChallenge` to solve new hash preimage challenges.

### Changed
* Update `github.com/cronokirby/saferith` dependency to v0.33.0. Adds assembly routines support for more platforms
* Update `ECDLPChallenge` to the new specification.

## v0.0.3 (2021-12-15)

Expand Down
108 changes: 92 additions & 16 deletions challenge.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,119 @@ package srp

import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"errors"
"math"
"time"

"golang.org/x/crypto/argon2"
"golang.org/x/crypto/curve25519"
)

const ECDLPEphemeralSize = 24
// Implementation following the "context" package
var DeadlineExceeded error = deadlineExceededError{}

func ECDLPChallenge(b64Challenge string) (solution int64, err error) {
if len(b64Challenge) != 76 { // 56 bytes in base64
return 0, errors.New("srp:invalid ECDLP challenge length")
type deadlineExceededError struct{}

func (deadlineExceededError) Error() string { return "srp: deadline exceeded calculating proof-of-work challenge" }
func (deadlineExceededError) Timeout() bool { return true }
func (deadlineExceededError) Temporary() bool { return true }

const ecdlpPRFKeySize = 32

// ECDLPChallenge computes the base64 solution for a given ECDLP base64 challenge
// within deadlineUnixMilli milliseconds, if any was found. Deadlines are measured on the
// wall clock, not the monotonic clock, due to unreliability on mobile devices.
func ECDLPChallenge(b64Challenge string, deadlineUnixMilli uint64) (b64Solution string, err error) {
challenge, err := base64.StdEncoding.DecodeString(b64Challenge)
if err != nil {
return "", err
}

if len(challenge) != 2 * ecdlpPRFKeySize + sha256.Size {
return "", errors.New("srp: invalid ECDLP challenge length")
}

var i uint64
var point []byte
buffer := make([]byte, 8)

for i = 0;; i++ {
if deadlineUnixMilli <= math.MaxInt64 && time.Now().UnixMilli() > int64(deadlineUnixMilli) {
return "", DeadlineExceeded
}

prePRF := hmac.New(sha256.New, challenge[:ecdlpPRFKeySize])
binary.LittleEndian.PutUint64(buffer, i)
_, _ = prePRF.Write(buffer)
point, err = curve25519.X25519(prePRF.Sum(nil), curve25519.Basepoint)
if err != nil {
return "", err
}
postPRF := hmac.New(sha256.New, challenge[ecdlpPRFKeySize:2*ecdlpPRFKeySize])
_, _ = postPRF.Write(point)

if bytes.Equal(postPRF.Sum(nil), challenge[2*ecdlpPRFKeySize:]) {
break
}
}
solution := []byte{}
solution = append(solution, buffer...)
solution = append(solution, point...)

return base64.StdEncoding.EncodeToString(solution), nil
}

const argon2PRFKeySize = 32

// Argon2PreimageChallenge computes the base64 solution for a given Argon2 base64
// challenge within deadlineUnixMilli milliseconds, if any was found. Deadlines are measured
// on the wall clock, not the monotonic clock, due to unreliability on mobile devices.
func Argon2PreimageChallenge(b64Challenge string, deadlineUnixMilli uint64) (b64Solution string, err error) {
challenge, err := base64.StdEncoding.DecodeString(b64Challenge)
if err != nil {
return 0, err
return "", err
}

// Argon2 challenges consist of 3 PRF keys, the hash output, and 4 32-bit argon2 parameters
if len(challenge) != 3 * argon2PRFKeySize + sha256.Size + 4 * 4 {
return "", errors.New("srp: invalid Argon2 preimage challenge length")
}
prfKeys := challenge[:3*argon2PRFKeySize]
goal := challenge[3*argon2PRFKeySize:][:sha256.Size]
argon2Params := challenge[3*argon2PRFKeySize + sha256.Size:]

threads := binary.LittleEndian.Uint32(argon2Params[0:])
argon2OutputSize := binary.LittleEndian.Uint32(argon2Params[4:])
memoryCost := binary.LittleEndian.Uint32(argon2Params[8:])
timeCost := binary.LittleEndian.Uint32(argon2Params[12:])

var i uint64
var stage2 []byte
buffer := make([]byte, 8)
point := make([]byte, curve25519.PointSize)

for i = 0; bytes.Compare(point, challenge[ECDLPEphemeralSize:]) != 0; i++ {
hash := sha256.New()
for i = 0;; i++ {
if deadlineUnixMilli <= math.MaxInt64 && time.Now().UnixMilli() > int64(deadlineUnixMilli) {
return "", DeadlineExceeded
}

prePRF := hmac.New(sha256.New, prfKeys[:argon2PRFKeySize])
binary.LittleEndian.PutUint64(buffer, i)
_, _ = hash.Write(buffer) // hash writer interface never returns errors
_, _ = hash.Write(challenge[:ECDLPEphemeralSize])
_, _ = prePRF.Write(buffer)
stage2 = argon2.IDKey(prePRF.Sum(nil), prfKeys[argon2PRFKeySize:2*argon2PRFKeySize], timeCost, memoryCost, uint8(threads), argon2OutputSize)
postPRF := hmac.New(sha256.New, prfKeys[2*argon2PRFKeySize:])
_, _ = postPRF.Write(stage2)

point, err = curve25519.X25519(hash.Sum(nil), curve25519.Basepoint)
if err != nil {
return 0, err
if bytes.Equal(postPRF.Sum(nil), goal) {
break
}
}
solution := []byte{}
solution = append(solution, buffer...)
solution = append(solution, stage2...)

// Last iteration increments i by 1 too much
// We cast to int64 for gomobile, possible because the challenges don't go above 2^63
return int64(i - 1), nil
return base64.StdEncoding.EncodeToString(solution), nil
}
49 changes: 43 additions & 6 deletions challenge_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,52 @@
package srp

import "testing"
import (
"math"
"strings"
"testing"
"time"
)

func TestECDLPChallenge(t *testing.T) {
challenge := "Qwr8NfpwpxeC3ulvVNIQlhJiKou7WUV1YLrwE8K94wf+RGrY9NJyR/HFBSNM6GZuzrZ3vdTdJkA="
result, err := ECDLPChallenge(challenge)
b64Challenge := "qfGBXLcNQMRqs/Krzx+EL87++Unwy5PGlnWxK2/BRIckF+Zlqmo7eIczHzAfm66MIZk5hkRVDVXMmEfy7dB++pkn3Ht+4bm3UtbBws/R43xZn23E2rSvPACxnjGFxMar"
b64Target := "ewAAAAAAAACsasMixdYBr/9Fb4SMM8urvjPUEUCVOjGqzwQyRdUafg=="

result, err := ECDLPChallenge(b64Challenge, math.MaxUint64)
if err != nil {
t.Fatal("Expected no error in processing challenge")
}

if result != b64Target {
t.Fatalf("Expected result to be %s, returned %s", b64Target, result)
}
}

func TestECDLPChallengeTimeout(t *testing.T) {
b64Challenge := strings.Repeat("A", 128)
_, err := ECDLPChallenge(b64Challenge, uint64(time.Now().UnixMilli() + 5))
if err != DeadlineExceeded {
t.Fatal("Expected timeout in ECDLP challenge")
}
}

func TestArgon2PreimageChallenge(t *testing.T) {
b64Challenge := "qbYJSn07JQGfol0u8MJTZ16fDRyFo2AR6phcgqlZCr44RBpz/odJc17EROMfMOpz2dE8oHW2JHeqoRax2ha4bpGusDBkEySSWJU+cmuWePzUC58fTY+VJMLBMDLhdqV9QKvozeqKcoPzqDoHZZYmyWQf4DIAKfgaha/WwzMikQMBAAAAIAAAAOEQAAABAAAA"
b64Target := "ewAAAAAAAABXe+n/4g0Hfz40eEw7h5d3XeiKdWilfCJvz0izj7p0YA=="

result, err := Argon2PreimageChallenge(b64Challenge, math.MaxUint64)
if err != nil {
t.Fatal("Expected no error in processing challenge")
}

if result != 123 {
t.Fatalf("Expected result to be 123, returned %d", result)
if result != b64Target {
t.Fatalf("Expected result to be %s, returned %s", b64Target, result)
}
}

func TestArgon2PreimageChallengeTimeout(t *testing.T) {
b64Challenge := strings.Repeat("A", 170) + "MBAAAAIAAAAOEQAAABAAAA"
_, err := Argon2PreimageChallenge(b64Challenge, uint64(time.Now().UnixMilli() + 5))
if err != DeadlineExceeded {
t.Fatal("Expected timeout in Argon2 preimage challenge")
}
}
}
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
Expand Down

0 comments on commit cc36f69

Please sign in to comment.