diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..6b56df0 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,39 @@ +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: '0 19 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..e77120c --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,42 @@ +name: Lint + +on: + push: + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + strategy: + matrix: + go: [1.16, 1.17, 1.18] + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 2 + + - name: Setup go + id: go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + + - name: Cache go + uses: actions/cache@v2 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-v${{ matrix.go }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Golangci lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.45.2 + skip-pkg-cache: true + skip-build-cache: true + only-new-issues: true + args: -v ./... diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..af4f778 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,51 @@ +name: Tests + +on: + push: + schedule: + - cron: '0 19 * * 0' + +jobs: + test: + name: Tests + runs-on: ubuntu-latest + strategy: + matrix: + go: [1.16, 1.17, 1.18] + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 2 + + - name: Setup go + id: go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + + - name: Cache go + uses: actions/cache@v2 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-v${{ matrix.go }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Run tests + id: test + run: | + make test + echo "::set-output name=coverage::$(make coverage)" + + - name: Create Coverage Badge + uses: schneegans/dynamic-badges-action@v1.2.0 + with: + auth: ${{ secrets.GIST_SECRET }} + gistID: cce912d48b587ba656b45a0cba34510b + filename: pow-hc-test-coverage.json + label: Coverage + message: ${{ steps.test.outputs.coverage }} + color: green diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b8c71a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +.DS_Store + +coverage.txt \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..c031dc4 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,46 @@ +# This file contains all available configuration options +# with their default values. + +# options for analysis running +run: + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 5m + + # include test files or not, default is true + tests: false + +linters: + disable: + - scopelint + enable: + - errcheck + - goimports + - gofmt + - revive + - exportloopref + - prealloc + - lll + - whitespace + - unconvert + - goconst + - staticcheck + - govet + - gocritic + presets: + - bugs + - unused + +linters-settings: + lll: + line-length: 180 + revive: + ignore-generated-header: true + rules: + - name: unexported-return + disabled: true + +issues: + exclude-rules: + - linters: + - lll + source: "^//go:generate " \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dd3d9c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Kirill Rodin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..118a87e --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +.PHONY: install +install: + @go mod tidy + @go install \ + github.com/abice/go-enum \ + github.com/golang/mock/mockgen + +.PHONY: gen +gen: install + @go generate ./... + +.PHONY: test +test: gen + @go test $$(go list ./... | grep -v /mock) -coverprofile=coverage.txt -covermode=atomic -timeout 60s + +.PHONY: coverage +coverage: + @make test | tr -d '\n' | sed -e 's/.*coverage:\(.*\)of statements.*/\1/' | tr -d '\n ' + +.PHONY: lint +lint: + @golangci-lint --timeout 10m0s run diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3c23d0 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Hashcash + +[![GoDoc](https://img.shields.io/badge/reference-007d9c.svg?logo=go&logoColor=white&label=doc)](https://pkg.go.dev/github.com/PoW-HC/hashcash) +[![Go Report Card](https://goreportcard.com/badge/github.com/PoW-HC/hashcash)](https://goreportcard.com/report/github.com/PoW-HC/hashcash) +[![Lint](https://github.com/PoW-HC/hashcash/actions/workflows/lint.yml/badge.svg)](https://github.com/PoW-HC/hashcash/actions/workflows/lint.yml) +[![Tests](https://github.com/PoW-HC/hashcash/actions/workflows/tests.yml/badge.svg)](https://github.com/PoW-HC/hashcash/actions/workflows/tests.yml) +![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/halfi/cce912d48b587ba656b45a0cba34510b/raw/pow-hc-test-coverage.json) +[![Code quality](https://github.com/PoW-HC/hashcash/actions/workflows/codeql.yml/badge.svg)](https://github.com/PoW-HC/hashcash/actions/workflows/codeql.yml) +[![license](https://img.shields.io/github/license/PoW-HC/hashcash)](https://github.com/PoW-HC/hashcash/blob/main/LICENSE) + +--- + +Hashcash is a Go library which implements the hashcash [proof-of-work](https://en.wikipedia.org/wiki/Proof_of_work) +algorithm. Hashcash has been used as a denial-of-service counter measure technique in a number of systems. To learn more +about hashcash visit [official page](http://hashcash.org/). + +## Usage + +Computing a valid hashcash: + +```go +package main + +import ( + "context" + "fmt" + + "github.com/PoW-HC/hashcash/pkg/hash" + "github.com/PoW-HC/hashcash/pkg/pow" +) + +const maxIterations = 1 << 30 + +func main() { + hasher, err := hash.NewHasher("sha256") + if err != nil { + // handle error + } + + p := pow.New(hasher) + + hashcash, err := pow.InitHashcash(5, "127.0.0.1", pow.SignExt("secret", hasher)) + if err != nil { + // handle error + } + + solution, err := p.Compute(context.Background(), hashcash, maxIterations) + if err != nil { + // handle error + } + fmt.Println(solution) +} + +``` + +Outputs: + +``` +1:5:1649257375:127.0.0.1:41965c8500f67f79b35672d7fb7e19fe5af0d51da582cbfcdbedb0f5944198bb:MgjmhCBiRV4=:MTk3YzA0 +``` + +```shell +echo -n "1:5:1649257375:127.0.0.1:41965c8500f67f79b35672d7fb7e19fe5af0d51da582cbfcdbedb0f5944198bb:MgjmhCBiRV4=:MTk3YzA0" | shasum -a 256 +0000067c716fa612cee5eb31eab6163e804c002841cfe96cc81f4f8034fd6006 - +``` + +Verification token: + +```go + err := p.Verify(solution, "127.0.0.1") + if err != nil { + // hashcash token failed verification. + } +``` + +## Documentation + +https://pkg.go.dev/github.com/PoW-HC/hashcash + +## License + +See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT). diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..39c509f --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module github.com/PoW-HC/hashcash + +go 1.16 + +require ( + github.com/abice/go-enum v0.4.0 + github.com/golang/mock v1.6.0 + github.com/minio/sha256-simd v1.0.0 + github.com/stretchr/testify v1.7.1 +) + +require ( + github.com/google/uuid v1.3.0 // indirect + github.com/klauspost/cpuid/v2 v2.0.12 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/mod v0.6.0-dev.0.20220330205332-605edab4323b // indirect + golang.org/x/sys v0.0.0-20220405210540-1e041c57c461 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..252ffde --- /dev/null +++ b/go.sum @@ -0,0 +1,131 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/abice/go-enum v0.4.0 h1:+ZFgSv7CrCe5O24x/Vkfx10ANOF48hjHIMrgFIsFd3M= +github.com/abice/go-enum v0.4.0/go.mod h1:KV6EE8ZkAwoDSoShP5u/LZ0j5zf7N13bc9QeSGxrM7o= +github.com/bradleyjkemp/cupaloy v2.3.0+incompatible h1:UafIjBvWQmS9i/xRg+CamMrnLTKNzo+bdmT/oH34c2Y= +github.com/bradleyjkemp/cupaloy v2.3.0+incompatible/go.mod h1:Au1Xw1sgaJ5iSFktEhYsS0dbQiS1B0/XMXl+42y9Ilk= +github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/kevinburke/go-bindata v3.23.0+incompatible h1:rqNOXZlqrYhMVVAsQx8wuc+LaA73YcfbQ407wAykyS8= +github.com/kevinburke/go-bindata v3.23.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= +github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/goveralls v0.0.11 h1:eJXea6R6IFlL1QMKNMzDvvHv/hwGrnvyig4N+0+XiMM= +github.com/mattn/goveralls v0.0.11/go.mod h1:gU8SyhNswsJKchEV93xRQxX6X3Ei4PJdQk/6ZHvrvRk= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/copystructure v1.1.2/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I= +github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220330205332-605edab4323b h1:1MSqJBxq66rbEP29+EBWKhTAoif7TI9xPRDjX+M0c8g= +golang.org/x/mod v0.6.0-dev.0.20220330205332-605edab4323b/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220405210540-1e041c57c461 h1:kHVeDEnfKn3T238CvrUcz6KeEsFHVaKh4kMTt6Wsysg= +golang.org/x/sys v0.0.0-20220405210540-1e041c57c461/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/tools/tools.go b/internal/tools/tools.go new file mode 100644 index 0000000..8cf0604 --- /dev/null +++ b/internal/tools/tools.go @@ -0,0 +1,9 @@ +//go:build tools +// +build tools + +package tools + +import ( + _ "github.com/abice/go-enum" + _ "github.com/golang/mock/mockgen" +) diff --git a/pkg/hash/factory.go b/pkg/hash/factory.go new file mode 100644 index 0000000..b0ae9b2 --- /dev/null +++ b/pkg/hash/factory.go @@ -0,0 +1,26 @@ +package hash + +import ( + "fmt" + "strings" + + "github.com/PoW-HC/hashcash/pkg/hash/hasher" +) + +func NewHasher(hasherName string) (Hasher, error) { + h, err := hasher.ParseHasher(strings.ToUpper(hasherName)) + if err != nil { + return nil, fmt.Errorf("parse hasher by name error: %w", err) + } + + switch h { + case hasher.HasherSHA1: + return NewSHA1(), nil + case hasher.HasherSHA256: + return NewSHA256(), nil + case hasher.HasherSHA512: + return NewSHA512(), nil + default: + return NewSHA256(), nil + } +} diff --git a/pkg/hash/hasher.go b/pkg/hash/hasher.go new file mode 100644 index 0000000..8b9f63a --- /dev/null +++ b/pkg/hash/hasher.go @@ -0,0 +1,8 @@ +package hash + +//go:generate mockgen -package=mock -destination=./mock/hasher.go github.com/PoW-HC/hashcash/pkg/hash Hasher + +// Hasher interface to hash function +type Hasher interface { + Hash(str string) (string, error) +} diff --git a/pkg/hash/hasher/const.go b/pkg/hash/hasher/const.go new file mode 100644 index 0000000..f6a1208 --- /dev/null +++ b/pkg/hash/hasher/const.go @@ -0,0 +1,12 @@ +package hasher + +//go:generate go-enum -f=$GOFILE --marshal + +// Hasher is an enumeration of hash types that are allowed. +/* ENUM( + SHA1, + SHA256, + SHA512, +) +*/ +type Hasher int diff --git a/pkg/hash/hasher/const_enum.go b/pkg/hash/hasher/const_enum.go new file mode 100644 index 0000000..6fee3f5 --- /dev/null +++ b/pkg/hash/hasher/const_enum.go @@ -0,0 +1,66 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: +// Revision: +// Build Date: +// Built By: + +package hasher + +import ( + "fmt" +) + +const ( + // HasherSHA1 is a Hasher of type SHA1. + HasherSHA1 Hasher = iota + // HasherSHA256 is a Hasher of type SHA256. + HasherSHA256 + // HasherSHA512 is a Hasher of type SHA512. + HasherSHA512 +) + +const _HasherName = "SHA1SHA256SHA512" + +var _HasherMap = map[Hasher]string{ + HasherSHA1: _HasherName[0:4], + HasherSHA256: _HasherName[4:10], + HasherSHA512: _HasherName[10:16], +} + +// String implements the Stringer interface. +func (x Hasher) String() string { + if str, ok := _HasherMap[x]; ok { + return str + } + return fmt.Sprintf("Hasher(%d)", x) +} + +var _HasherValue = map[string]Hasher{ + _HasherName[0:4]: HasherSHA1, + _HasherName[4:10]: HasherSHA256, + _HasherName[10:16]: HasherSHA512, +} + +// ParseHasher attempts to convert a string to a Hasher. +func ParseHasher(name string) (Hasher, error) { + if x, ok := _HasherValue[name]; ok { + return x, nil + } + return Hasher(0), fmt.Errorf("%s is not a valid Hasher", name) +} + +// MarshalText implements the text marshaller method. +func (x Hasher) MarshalText() ([]byte, error) { + return []byte(x.String()), nil +} + +// UnmarshalText implements the text unmarshaller method. +func (x *Hasher) UnmarshalText(text []byte) error { + name := string(text) + tmp, err := ParseHasher(name) + if err != nil { + return err + } + *x = tmp + return nil +} diff --git a/pkg/hash/mock/hasher.go b/pkg/hash/mock/hasher.go new file mode 100644 index 0000000..0d5f8f9 --- /dev/null +++ b/pkg/hash/mock/hasher.go @@ -0,0 +1,49 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/PoW-HC/hashcash/pkg/hash (interfaces: Hasher) + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockHasher is a mock of Hasher interface. +type MockHasher struct { + ctrl *gomock.Controller + recorder *MockHasherMockRecorder +} + +// MockHasherMockRecorder is the mock recorder for MockHasher. +type MockHasherMockRecorder struct { + mock *MockHasher +} + +// NewMockHasher creates a new mock instance. +func NewMockHasher(ctrl *gomock.Controller) *MockHasher { + mock := &MockHasher{ctrl: ctrl} + mock.recorder = &MockHasherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHasher) EXPECT() *MockHasherMockRecorder { + return m.recorder +} + +// Hash mocks base method. +func (m *MockHasher) Hash(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Hash", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Hash indicates an expected call of Hash. +func (mr *MockHasherMockRecorder) Hash(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Hash", reflect.TypeOf((*MockHasher)(nil).Hash), arg0) +} diff --git a/pkg/hash/mock/hasher_params.go b/pkg/hash/mock/hasher_params.go new file mode 100644 index 0000000..fc64444 --- /dev/null +++ b/pkg/hash/mock/hasher_params.go @@ -0,0 +1,29 @@ +package mock + +import ( + "github.com/golang/mock/gomock" +) + +type HasherMockParams struct { + HashTimes int + HashAnyTimes bool + HashReq gomock.Matcher + HashRes string + HashResErr error +} + +func (p HasherMockParams) NewHasher(ctrl *gomock.Controller) *MockHasher { + mock := NewMockHasher(ctrl) + + callTimes(mock.EXPECT().Hash(p.HashReq), p.HashTimes, p.HashAnyTimes).Return(p.HashRes, p.HashResErr) + + return mock +} + +func callTimes(c *gomock.Call, times int, anyTimes bool) *gomock.Call { + if anyTimes { + return c.AnyTimes() + } + + return c.Times(times) +} diff --git a/pkg/hash/sha1.go b/pkg/hash/sha1.go new file mode 100644 index 0000000..80980c9 --- /dev/null +++ b/pkg/hash/sha1.go @@ -0,0 +1,27 @@ +package hash + +import ( + "crypto/sha1" //nolint:gosec + "encoding/hex" + "fmt" +) + +type SHA1 struct { +} + +// NewSHA1 sha1 hash function +func NewSHA1() *SHA1 { + return new(SHA1) +} + +func (s *SHA1) Hash(str string) (string, error) { + // Weak cryptographic primitive. I know, I know.. + sha := sha1.New() //nolint:gosec + + _, err := sha.Write([]byte(str)) + if err != nil { + return "", fmt.Errorf("sha256 hash error: %w", err) + } + + return hex.EncodeToString(sha.Sum(nil)), err +} diff --git a/pkg/hash/sha256.go b/pkg/hash/sha256.go new file mode 100644 index 0000000..f94fa50 --- /dev/null +++ b/pkg/hash/sha256.go @@ -0,0 +1,27 @@ +package hash + +import ( + "encoding/hex" + "fmt" + + "github.com/minio/sha256-simd" +) + +type SHA256 struct { +} + +// NewSHA256 sha256 hash function +func NewSHA256() *SHA256 { + return new(SHA256) +} + +func (s *SHA256) Hash(str string) (string, error) { + sha := sha256.New() + + _, err := sha.Write([]byte(str)) + if err != nil { + return "", fmt.Errorf("sha256 hash error: %w", err) + } + + return hex.EncodeToString(sha.Sum(nil)), err +} diff --git a/pkg/hash/sha512.go b/pkg/hash/sha512.go new file mode 100644 index 0000000..8d3b5d7 --- /dev/null +++ b/pkg/hash/sha512.go @@ -0,0 +1,26 @@ +package hash + +import ( + "crypto/sha512" + "encoding/hex" + "fmt" +) + +type SHA512 struct { +} + +// NewSHA512 sha512 hash function +func NewSHA512() *SHA512 { + return new(SHA512) +} + +func (s *SHA512) Hash(str string) (string, error) { + sha := sha512.New() + + _, err := sha.Write([]byte(str)) + if err != nil { + return "", fmt.Errorf("sha512 hash error: %w", err) + } + + return hex.EncodeToString(sha.Sum(nil)), err +} diff --git a/pkg/pow/hashcach.go b/pkg/pow/hashcach.go new file mode 100644 index 0000000..ebcf2d5 --- /dev/null +++ b/pkg/pow/hashcach.go @@ -0,0 +1,108 @@ +package pow + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "fmt" + "math" + "math/big" + mrand "math/rand" + "strconv" + "time" +) + +const ( + versionV1 = 1 +) + +var ( + ErrExtInvalid = fmt.Errorf("extension sum invalid") + ErrHashcashEmpty = fmt.Errorf("hashcash empty") +) + +// Hashcach struct to marshal and unmarshal hashcach to string or proto buf +type Hashcach struct { + Version int32 + Bits int32 + Date time.Time + Resource string + Ext string + Rand []byte + Counter int64 +} + +func NewHashcach( + version int32, + bits int32, + date time.Time, + resource string, + ext string, + rand []byte, + counter int64, +) *Hashcach { + return &Hashcach{ + Version: version, + Bits: bits, + Date: date, + Resource: resource, + Ext: ext, + Rand: rand, + Counter: counter, + } +} + +// InitHashcash initiate new hashcash +func InitHashcash(bits int32, resource string, extGenerator ExtGeneratorFunc) (*Hashcach, error) { + t := time.Now() + randBytes := randomBytes() + + hc := NewHashcach( + versionV1, + bits, + t, + resource, + "", + randBytes, + 0, + ) + + if extGenerator != nil { + ext, err := extGenerator(hc) + if err != nil { + return nil, fmt.Errorf("ext generator error: %w", err) + } + hc.Ext = ext + } + + return hc, nil +} + +// String implements fmt.Stringer interface to get string hashcash +func (h *Hashcach) String() string { + var buf bytes.Buffer + buf.WriteString(strconv.Itoa(int(h.Version))) + buf.WriteString(":") + buf.WriteString(strconv.Itoa(int(h.Bits))) + buf.WriteString(":") + buf.WriteString(strconv.Itoa(int(h.Date.Unix()))) + buf.WriteString(":") + buf.WriteString(h.Resource) + buf.WriteString(":") + buf.WriteString(h.Ext) + buf.WriteString(":") + buf.WriteString(base64.StdEncoding.EncodeToString(h.Rand)) + buf.WriteString(":") + buf.WriteString(base64.StdEncoding.EncodeToString([]byte(strconv.FormatInt(h.Counter, 16)))) + return buf.String() +} + +func randomBytes() []byte { + b, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + // this is fallback in case of unweak rand function fall with error (exceptional case) + b = big.NewInt(mrand.Int63n(math.MaxInt64)) //nolint:gosec + } + + return b.Bytes() +} diff --git a/pkg/pow/hashcash_test.go b/pkg/pow/hashcash_test.go new file mode 100644 index 0000000..d0c62ff --- /dev/null +++ b/pkg/pow/hashcash_test.go @@ -0,0 +1,22 @@ +package pow + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRandomBytes(t *testing.T) { + assert.Greater(t, len(randomBytes()), 0) +} + +func TestInitHashcash(t *testing.T) { + var bits int32 = 3 + var resource = "127.0.0.1" + a := assert.New(t) + + actual, err := InitHashcash(bits, resource, nil) + a.Nil(err) + a.Equal(bits, actual.Bits) + a.Equal(resource, actual.Resource) +} diff --git a/pkg/pow/options.go b/pkg/pow/options.go new file mode 100644 index 0000000..010edd0 --- /dev/null +++ b/pkg/pow/options.go @@ -0,0 +1,29 @@ +package pow + +import ( + "time" +) + +type options struct { + validateExtFunc ExtValidatorFunc + challengeExpDuration time.Duration +} + +type Options func(*options) + +func WithValidateExtFunc(callback ExtValidatorFunc) Options { + return func(pow *options) { + pow.validateExtFunc = callback + } +} +func WithChallengeExpDuration(callback time.Duration) Options { + return func(pow *options) { + pow.challengeExpDuration = callback + } +} + +func setDefaultOptions(o *options) { + if o.challengeExpDuration == 0 { + o.challengeExpDuration = defaultChallengeDuration + } +} diff --git a/pkg/pow/pow.go b/pkg/pow/pow.go new file mode 100644 index 0000000..2ec7adf --- /dev/null +++ b/pkg/pow/pow.go @@ -0,0 +1,112 @@ +package pow + +import ( + "context" + "fmt" + "time" + + "github.com/PoW-HC/hashcash/pkg/hash" +) + +const ( + zero rune = 48 // ASCII code for number zero + + defaultChallengeDuration = 120 * time.Second + maxHashSize = 1 << 7 // max length of sha512 +) + +var ( + ErrMaxIterationsExceeded = fmt.Errorf("max iterations exceeded") + ErrWrongResource = fmt.Errorf("wrong resource") + ErrChallengeExpired = fmt.Errorf("challenge expired") + ErrWrongChallenge = fmt.Errorf("wrong challenge") +) + +// POW proof of work class +type POW struct { + s hash.Hasher + zeros []rune + + options options +} + +// New constructor +func New(s hash.Hasher, opts ...Options) *POW { + p := &POW{ + s: s, + zeros: make([]rune, maxHashSize), + } + + for i := range p.zeros { + p.zeros[i] = zero + } + + for i := range opts { + opts[i](&p.options) + } + + setDefaultOptions(&p.options) + + return p +} + +// Compute time waster. Do all useless load. +func (p *POW) Compute(ctx context.Context, h *Hashcach, max int64) (*Hashcach, error) { + if max > 0 { + for h.Counter <= max { + if err := ctx.Err(); err != nil { + break + } + + hashString, err := p.s.Hash(h.String()) + if err != nil { + return nil, fmt.Errorf("calculate pow hash sum error: %w", err) + } + + if isHashCorrect(hashString, p.zeros, int(h.Bits)) { + return h, nil + } + + h.Counter++ + } + } + + return nil, ErrMaxIterationsExceeded +} + +// Verify that hashcash correct and provided by server +func (p *POW) Verify(h *Hashcach, resource string) error { + if h == nil || h.Resource != resource { + return ErrWrongResource + } + + if h.Date.Add(p.options.challengeExpDuration).Before(time.Now()) { + return ErrChallengeExpired + } + + hashString, err := p.s.Hash(h.String()) + if err != nil { + return fmt.Errorf("calculate pow hash sum error: %w", err) + } + + if !isHashCorrect(hashString, p.zeros, int(h.Bits)) { + return ErrWrongChallenge + } + + if p.options.validateExtFunc != nil { + err = p.options.validateExtFunc(h) + if err != nil { + return fmt.Errorf("validation extension error: %w", err) + } + } + + return nil +} + +func isHashCorrect(hash string, zeroHash []rune, zerosCount int) bool { + if zerosCount > len(hash) || zerosCount > len(zeroHash) { + return false + } + + return hash[:zerosCount] == string(zeroHash[:zerosCount]) +} diff --git a/pkg/pow/pow_test.go b/pkg/pow/pow_test.go new file mode 100644 index 0000000..4d132b2 --- /dev/null +++ b/pkg/pow/pow_test.go @@ -0,0 +1,368 @@ +package pow + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/PoW-HC/hashcash/pkg/hash/mock" +) + +var zeros = make([]rune, maxHashSize) + +func TestMain(m *testing.M) { + for i := range zeros { + zeros[i] = zero + } + os.Exit(m.Run()) +} + +func TestIsHashCorrect(t *testing.T) { + for _, tCase := range []struct { + name string + hash string + zeros int + expected bool + }{ + { + name: "positive", + hash: "00000e89df98a05e524fdcd29d8040d64d0259e2d5109ca1998e567a3c1c1c68", + zeros: 5, + expected: true, + }, + { + name: "wrong 5 zeros", + hash: "00000e89df98a05e524fdcd29d8040d64d0259e2d5109ca1998e567a3c1c1c68", + zeros: 6, + expected: false, + }, + { + name: "wrong 0", + hash: "d59d15c9a1842bc4563897803799e94f1f242d7e7e8c618f047e068211543998", + zeros: 5, + expected: false, + }, + { + name: "too short", + hash: "0000", + zeros: 6, + expected: false, + }, + } { + t.Run(tCase.name, func(t *testing.T) { + actual := isHashCorrect(tCase.hash, zeros, tCase.zeros) + assert.Equal(t, tCase.expected, actual) + }) + } +} + +func TestPowCompute(t *testing.T) { + hasherErr := fmt.Errorf("expected error") + ctrl := gomock.NewController(t) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + deadCTX, deadCancel := context.WithCancel(context.Background()) + deadCancel() + + for _, tCase := range []struct { + name string + ctx context.Context + hashcash *Hashcach + max int64 + hasherMock mock.HasherMockParams + expected *Hashcach + expectedErr error + }{ + { + name: "positive", + ctx: ctx, + hashcash: &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + Ext: "resource\nsecret1648762844", + }, + max: 1, + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Eq("0:5:1648762844:resource:resource\nsecret1648762844:Cg==:MA=="), + HashRes: "00000e89df98a05e524fdcd29d8040d64d0259e2d5109ca1998e567a3c1c1c68", + HashResErr: nil, + }, + expected: &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + Ext: "resource\nsecret1648762844", + Counter: 0, + }, + expectedErr: nil, + }, + { + name: "hasher error", + ctx: ctx, + hashcash: &Hashcach{ + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + Ext: "resource\nsecret1648762844", + }, + max: 1, + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Eq("0:0:1648762844:resource:resource\nsecret1648762844:Cg==:MA=="), + HashRes: "", + HashResErr: hasherErr, + }, + expected: nil, + expectedErr: hasherErr, + }, + { + name: "deadline exceeded", + ctx: ctx, + hashcash: &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + Ext: "resource\nsecret1648762844", + }, + max: 1, + hasherMock: mock.HasherMockParams{ + HashTimes: 2, + HashReq: gomock.Any(), + HashRes: "d59d15c9a1842bc4563897803799e94f1f242d7e7e8c618f047e068211543998", + HashResErr: nil, + }, + expected: nil, + expectedErr: ErrMaxIterationsExceeded, + }, + { + name: "dead ctx", + ctx: deadCTX, + hashcash: &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + Ext: "resource\nsecret1648762844", + }, + max: 1, + expected: nil, + expectedErr: ErrMaxIterationsExceeded, + }, + } { + t.Run(tCase.name, func(t *testing.T) { + var ( + a = assert.New(t) + hasher = tCase.hasherMock.NewHasher(ctrl) + pow = New(hasher) + ) + + actual, err := pow.Compute(tCase.ctx, tCase.hashcash, tCase.max) + a.Equal(tCase.expected, actual) + a.ErrorIs(err, tCase.expectedErr) + }) + } +} + +func TestPowVerify(t *testing.T) { + hasherErr := fmt.Errorf("expected error") + ctrl := gomock.NewController(t) + now := time.Now() + + for _, tCase := range []struct { + name string + powOptions []Options + hashcash *Hashcach + resource string + hasherMock mock.HasherMockParams + expectedErr error + }{ + { + name: "positive", + hashcash: &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: now, + Ext: "resource\nsecret1648762844", + }, + resource: "resource", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Any(), + HashRes: "00000e89df98a05e524fdcd29d8040d64d0259e2d5109ca1998e567a3c1c1c68", + HashResErr: nil, + }, + expectedErr: nil, + }, + { + name: "positive validate ext", + hashcash: &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: now, + Ext: "resource\nsecret1648762844", + }, + powOptions: []Options{ + WithValidateExtFunc(func(h *Hashcach) error { + assert.Equal( + t, + &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: now, + Ext: "resource\nsecret1648762844", + }, + h, + ) + return nil + }), + }, + resource: "resource", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Any(), + HashRes: "00000e89df98a05e524fdcd29d8040d64d0259e2d5109ca1998e567a3c1c1c68", + HashResErr: nil, + }, + expectedErr: nil, + }, + { + name: "positive duration", + hashcash: &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: now.Add(50 * time.Second), + Ext: "resource\nsecret1648762844", + }, + powOptions: []Options{ + WithChallengeExpDuration(time.Minute), + }, + resource: "resource", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Any(), + HashRes: "00000e89df98a05e524fdcd29d8040d64d0259e2d5109ca1998e567a3c1c1c68", + HashResErr: nil, + }, + expectedErr: nil, + }, + { + name: "wrong resource", + hashcash: &Hashcach{ + Resource: "resource", + }, + resource: "resource2", + expectedErr: ErrWrongResource, + }, + { + name: "challenge expired", + hashcash: &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + Ext: "resource\nsecret1648762844", + }, + resource: "resource", + expectedErr: ErrChallengeExpired, + }, + { + name: "hasher error", + hashcash: &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: time.Now(), + Ext: "resource\nsecret1648762844", + }, + resource: "resource", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Any(), + HashRes: "", + HashResErr: hasherErr, + }, + expectedErr: hasherErr, + }, + { + name: "wrong hash", + hashcash: &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: time.Now(), + Ext: "resource\nsecret1648762844", + }, + resource: "resource", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Any(), + HashRes: "d59d15c9a1842bc4563897803799e94f1f242d7e7e8c618f047e068211543998", + HashResErr: nil, + }, + expectedErr: ErrWrongChallenge, + }, + { + name: "validate ext error", + hashcash: &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: time.Now(), + Ext: "resource\nsecret1648762844", + }, + powOptions: []Options{ + WithValidateExtFunc(func(h *Hashcach) error { + return hasherErr + }), + }, + resource: "resource", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Any(), + HashRes: "00000e89df98a05e524fdcd29d8040d64d0259e2d5109ca1998e567a3c1c1c68", + HashResErr: nil, + }, + expectedErr: hasherErr, + }, + { + name: "error duration", + hashcash: &Hashcach{ + Bits: 5, + Resource: "resource", + Rand: []byte{10}, + Date: now.Add(-2 * time.Minute), + Ext: "resource\nsecret1648762844", + }, + powOptions: []Options{ + WithChallengeExpDuration(time.Minute), + }, + resource: "resource", + expectedErr: ErrChallengeExpired, + }, + } { + t.Run(tCase.name, func(t *testing.T) { + var ( + a = assert.New(t) + hasher = tCase.hasherMock.NewHasher(ctrl) + pow = New(hasher, tCase.powOptions...) + ) + + err := pow.Verify(tCase.hashcash, tCase.resource) + a.ErrorIs(err, tCase.expectedErr) + }) + } +} diff --git a/pkg/pow/sign.go b/pkg/pow/sign.go new file mode 100644 index 0000000..2e35c01 --- /dev/null +++ b/pkg/pow/sign.go @@ -0,0 +1,64 @@ +package pow + +import ( + "bytes" + "fmt" + "strconv" + "time" + + "github.com/PoW-HC/hashcash/pkg/hash" +) + +type ExtGeneratorFunc func(*Hashcach) (string, error) +type ExtValidatorFunc func(*Hashcach) error + +// SignExt creates signed extension +// See extSum description for hash generating details +func SignExt(secret string, hasher hash.Hasher) ExtGeneratorFunc { + return func(h *Hashcach) (string, error) { + ext, err := extSum(h.Resource, secret, h.Bits, h.Rand, h.Date, hasher) + if err != nil { + return "", err + } + + return ext, nil + } +} + +// VerifyExt verify extension from hashcash to validate hashcash was provided by server. +// See extSum description for hash generating details +func VerifyExt(secret string, hasher hash.Hasher) ExtValidatorFunc { + return func(h *Hashcach) error { + extSum, err := extSum(h.Resource, secret, h.Bits, h.Rand, h.Date, hasher) + if err != nil { + return fmt.Errorf("verify ext sum error: %w", err) + } + + if h.Ext != extSum { + return ErrExtInvalid + } + + return nil + } +} + +// extSum generates hash sum with hasher interface from fields: +// - resource - ip address +// - randBytes - random number +// - secret - secret known only on server +// - time - timestamp +func extSum(resource, secret string, bits int32, randBytes []byte, t time.Time, hasher hash.Hasher) (string, error) { + var ext bytes.Buffer + ext.WriteString(resource) + ext.Write(randBytes) + ext.WriteString(secret) + ext.WriteString(strconv.Itoa(int(t.Unix()))) + ext.WriteString(strconv.Itoa(int(bits))) + + extSum, err := hasher.Hash(ext.String()) + if err != nil { + return "", fmt.Errorf("calculate hashcash ext hash sum error: %w", err) + } + + return extSum, nil +} diff --git a/pkg/pow/sign_test.go b/pkg/pow/sign_test.go new file mode 100644 index 0000000..4c8888f --- /dev/null +++ b/pkg/pow/sign_test.go @@ -0,0 +1,226 @@ +package pow + +import ( + "fmt" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/PoW-HC/hashcash/pkg/hash/mock" +) + +func TestExtSum(t *testing.T) { + var ( + resource = "resource1" + secret = "secret" + rand = []byte{10} + bits int32 = 5 + date = time.Unix(1648762844, 0) + expected = fmt.Sprintf("%s%s%s%d%d", resource, rand, secret, date.Unix(), bits) + a = assert.New(t) + hasher = (mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Eq(expected), + HashRes: expected, + HashResErr: nil, + }).NewHasher(gomock.NewController(t)) + ) + + actual, err := extSum(resource, secret, bits, rand, date, hasher) + a.Nil(err) + a.Equal(expected, actual) +} + +func TestExtSumErr(t *testing.T) { + var ( + resource = "resource" + secret = "secret" + rand = []byte{10} + bits int32 = 5 + date = time.Unix(1648762844, 0) + expected = fmt.Sprintf("%s%s%s%d%d", resource, rand, secret, date.Unix(), bits) + expectedErr = fmt.Errorf("expected error") + a = assert.New(t) + hasher = (mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Eq(expected), + HashRes: "", + HashResErr: expectedErr, + }).NewHasher(gomock.NewController(t)) + ) + + actual, err := extSum(resource, secret, bits, rand, date, hasher) + a.Empty(actual) + a.ErrorIs(err, expectedErr) +} + +func TestVerifyExt(t *testing.T) { + hasherErr := fmt.Errorf("expected error") + ctrl := gomock.NewController(t) + + for _, tCase := range []struct { + name string + hashcash *Hashcach + secret string + hasherMock mock.HasherMockParams + expectedErr error + }{ + { + name: "positive", + hashcash: &Hashcach{ + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + Ext: "resource\nsecret16487628440", + }, + secret: "secret", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Eq("resource\nsecret16487628440"), + HashRes: "resource\nsecret16487628440", + HashResErr: nil, + }, + expectedErr: nil, + }, + { + name: "wrong ext", + hashcash: &Hashcach{ + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + Ext: "wrong", + }, + secret: "secret", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Eq("resource\nsecret16487628440"), + HashRes: "resource\nsecret16487628440", + HashResErr: nil, + }, + expectedErr: ErrExtInvalid, + }, + { + name: "wrong hasher response", + hashcash: &Hashcach{ + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + Ext: "resource\nsecret16487628440", + }, + secret: "secret", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Eq("resource\nsecret16487628440"), + HashRes: "wrong", + HashResErr: nil, + }, + expectedErr: ErrExtInvalid, + }, + { + name: "wrong hasher response", + hashcash: &Hashcach{ + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + Ext: "resource\nsecret16487628440", + }, + secret: "secret", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Eq("resource\nsecret16487628440"), + HashRes: "wrong", + HashResErr: nil, + }, + expectedErr: ErrExtInvalid, + }, + { + name: "hasher error", + hashcash: &Hashcach{ + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + Ext: "resource\nsecret16487628440", + }, + secret: "secret", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Eq("resource\nsecret16487628440"), + HashRes: "", + HashResErr: hasherErr, + }, + expectedErr: hasherErr, + }, + } { + t.Run(tCase.name, func(t *testing.T) { + var ( + a = assert.New(t) + hasher = tCase.hasherMock.NewHasher(ctrl) + ) + + err := VerifyExt(tCase.secret, hasher)(tCase.hashcash) + a.ErrorIs(err, tCase.expectedErr) + }) + } +} + +func TestSignExt(t *testing.T) { + hasherErr := fmt.Errorf("expected error") + _ = hasherErr + ctrl := gomock.NewController(t) + + for _, tCase := range []struct { + name string + hashcash *Hashcach + secret string + hasherMock mock.HasherMockParams + expected string + expectedErr error + }{ + { + name: "positive", + hashcash: &Hashcach{ + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + }, + secret: "secret", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Eq("resource\nsecret16487628440"), + HashRes: "resource\nsecret16487628440", + HashResErr: nil, + }, + expected: "resource\nsecret16487628440", + }, + { + name: "hasher error", + hashcash: &Hashcach{ + Resource: "resource", + Rand: []byte{10}, + Date: time.Unix(1648762844, 0), + Ext: "resource\nsecret16487628440", + }, + secret: "secret", + hasherMock: mock.HasherMockParams{ + HashTimes: 1, + HashReq: gomock.Eq("resource\nsecret16487628440"), + HashRes: "", + HashResErr: hasherErr, + }, + expectedErr: hasherErr, + }, + } { + t.Run(tCase.name, func(t *testing.T) { + var ( + a = assert.New(t) + hasher = tCase.hasherMock.NewHasher(ctrl) + ) + + ext, err := SignExt(tCase.secret, hasher)(tCase.hashcash) + a.ErrorIs(err, tCase.expectedErr) + a.Equal(tCase.expected, ext) + }) + } +}