chore: modernize CI and update Go toolchain

- Bump Go from 1.19 to 1.26 and update all dependencies
- Rewrite CI workflow with matrix strategy (Linux, macOS, Windows)
- Update GitHub Actions to current versions (checkout@v4, setup-go@v5)
- Update CodeQL actions from v1 to v3
- Fix cross-platform bug in mock/path.go (path.Join -> filepath.Join)
- Clean up dependabot config (weekly schedule, remove stale ignore)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christopher Allen Lane
2026-02-14 20:58:51 -05:00
parent cc85a4bdb1
commit 2a19755804
657 changed files with 49050 additions and 32001 deletions

View File

@@ -3,9 +3,5 @@ updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily
interval: weekly
open-pull-requests-limit: 10
ignore:
- dependency-name: github.com/alecthomas/chroma
versions:
- 0.9.1

View File

@@ -1,5 +1,5 @@
---
name: Go
name: CI
on:
push:
@@ -8,39 +8,33 @@ on:
branches: [master]
jobs:
# TODO: is it possible to DRY out these jobs? Aside from `runs-on`, they are
# identical.
# See: https://github.com/actions/runner/issues/1182
build-linux:
runs-on: [ubuntu-latest]
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.19
- name: Set up Revive (linter)
run: go get -u github.com/boyter/scc github.com/mgechev/revive
env:
GO111MODULE: "off"
- name: Build
run: make build
- name: Test
run: make test
go-version: stable
- name: Install revive
run: go install github.com/mgechev/revive@latest
- name: Lint
run: revive -exclude vendor/... ./...
- name: Vet
run: go vet ./...
- name: Check formatting
run: test -z "$(gofmt -l . | grep -v vendor/)"
build-osx:
runs-on: [macos-latest]
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.19
- name: Set up Revive (linter)
run: go get -u github.com/boyter/scc github.com/mgechev/revive
env:
GO111MODULE: "off"
go-version: stable
- name: Build
run: make build
run: go build -mod vendor ./cmd/cheat
- name: Test
run: make test
run: go test ./...

View File

@@ -19,12 +19,12 @@ jobs:
language: [go]
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v3

39
go.mod
View File

@@ -1,38 +1,37 @@
module github.com/cheat/cheat
go 1.19
go 1.26
require (
github.com/alecthomas/chroma/v2 v2.12.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/davecgh/go-spew v1.1.1
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
github.com/go-git/go-git/v5 v5.11.0
github.com/go-git/go-git/v5 v5.16.5
github.com/mattn/go-isatty v0.0.20
github.com/mitchellh/go-homedir v1.1.0
gopkg.in/yaml.v3 v3.0.1
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/go-git/go-billy/v5 v5.7.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/skeema/knownhosts v1.2.1 // indirect
github.com/kevinburke/ssh_config v1.5.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/tools v0.16.1 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.41.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

152
go.sum
View File

@@ -1,140 +1,118 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
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/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kevinburke/ssh_config v1.5.0 h1:3cPZmE54xb5j3G5xQCjSvokqNwU2uW+3ry1+PRLSPpA=
github.com/kevinburke/ssh_config v1.5.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
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/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
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/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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-20190902080502-41f04d3bba15/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/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -3,7 +3,6 @@ package mock
import (
"fmt"
"path"
"path/filepath"
"runtime"
)
@@ -16,7 +15,7 @@ func Path(filename string) string {
// compute the mock path
file, err := filepath.Abs(
path.Join(
filepath.Join(
filepath.Dir(thisfile),
"../../mocks",
filename,

View File

@@ -13,6 +13,9 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Golang/Intellij
.idea
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/

7
vendor/dario.cat/mergo/FUNDING.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"drips": {
"ethereum": {
"ownedBy": "0x6160020e7102237aC41bdb156e94401692D76930"
}
}
}

View File

@@ -44,13 +44,21 @@ Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the
## Status
It is ready for production use. [It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, Microsoft, etc](https://github.com/imdario/mergo#mergo-in-the-wild).
Mergo is stable and frozen, ready for production. Check a short list of the projects using at large scale it [here](https://github.com/imdario/mergo#mergo-in-the-wild).
No new features are accepted. They will be considered for a future v2 that improves the implementation and fixes bugs for corner cases.
### Important notes
#### 1.0.0
In [1.0.0](//github.com/imdario/mergo/releases/tag/1.0.0) Mergo moves to a vanity URL `dario.cat/mergo`.
In [1.0.0](//github.com/imdario/mergo/releases/tag/1.0.0) Mergo moves to a vanity URL `dario.cat/mergo`. No more v1 versions will be released.
If the vanity URL is causing issues in your project due to a dependency pulling Mergo - it isn't a direct dependency in your project - it is recommended to use [replace](https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive) to pin the version to the last one with the old import URL:
```
replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16
```
#### 0.3.9
@@ -64,55 +72,23 @@ If you were using Mergo before April 6th, 2015, please check your project works
If Mergo is useful to you, consider buying me a coffee, a beer, or making a monthly donation to allow me to keep building great free software. :heart_eyes:
<a href='https://ko-fi.com/B0B58839' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://az743702.vo.msecnd.net/cdn/kofi1.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
<a href="https://liberapay.com/dario/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
<a href='https://github.com/sponsors/imdario' target='_blank'><img alt="Become my sponsor" src="https://img.shields.io/github/sponsors/imdario?style=for-the-badge" /></a>
### Mergo in the wild
- [moby/moby](https://github.com/moby/moby)
- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes)
- [vmware/dispatch](https://github.com/vmware/dispatch)
- [Shopify/themekit](https://github.com/Shopify/themekit)
- [imdario/zas](https://github.com/imdario/zas)
- [matcornic/hermes](https://github.com/matcornic/hermes)
- [OpenBazaar/openbazaar-go](https://github.com/OpenBazaar/openbazaar-go)
- [kataras/iris](https://github.com/kataras/iris)
- [michaelsauter/crane](https://github.com/michaelsauter/crane)
- [go-task/task](https://github.com/go-task/task)
- [sensu/uchiwa](https://github.com/sensu/uchiwa)
- [ory/hydra](https://github.com/ory/hydra)
- [sisatech/vcli](https://github.com/sisatech/vcli)
- [dairycart/dairycart](https://github.com/dairycart/dairycart)
- [projectcalico/felix](https://github.com/projectcalico/felix)
- [resin-os/balena](https://github.com/resin-os/balena)
- [go-kivik/kivik](https://github.com/go-kivik/kivik)
- [Telefonica/govice](https://github.com/Telefonica/govice)
- [supergiant/supergiant](supergiant/supergiant)
- [SergeyTsalkov/brooce](https://github.com/SergeyTsalkov/brooce)
- [soniah/dnsmadeeasy](https://github.com/soniah/dnsmadeeasy)
- [ohsu-comp-bio/funnel](https://github.com/ohsu-comp-bio/funnel)
- [EagerIO/Stout](https://github.com/EagerIO/Stout)
- [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api)
- [russross/canvasassignments](https://github.com/russross/canvasassignments)
- [rdegges/cryptly-api](https://github.com/rdegges/cryptly-api)
- [casualjim/exeggutor](https://github.com/casualjim/exeggutor)
- [divshot/gitling](https://github.com/divshot/gitling)
- [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl)
- [andrerocker/deploy42](https://github.com/andrerocker/deploy42)
- [elwinar/rambler](https://github.com/elwinar/rambler)
- [tmaiaroto/gopartman](https://github.com/tmaiaroto/gopartman)
- [jfbus/impressionist](https://github.com/jfbus/impressionist)
- [Jmeyering/zealot](https://github.com/Jmeyering/zealot)
- [godep-migrator/rigger-host](https://github.com/godep-migrator/rigger-host)
- [Dronevery/MultiwaySwitch-Go](https://github.com/Dronevery/MultiwaySwitch-Go)
- [thoas/picfit](https://github.com/thoas/picfit)
- [mantasmatelis/whooplist-server](https://github.com/mantasmatelis/whooplist-server)
- [jnuthong/item_search](https://github.com/jnuthong/item_search)
- [bukalapak/snowboard](https://github.com/bukalapak/snowboard)
- [containerssh/containerssh](https://github.com/containerssh/containerssh)
- [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)
- [tjpnz/structbot](https://github.com/tjpnz/structbot)
Mergo is used by [thousands](https://deps.dev/go/dario.cat%2Fmergo/v1.0.0/dependents) [of](https://deps.dev/go/github.com%2Fimdario%2Fmergo/v0.3.16/dependents) [projects](https://deps.dev/go/github.com%2Fimdario%2Fmergo/v0.3.12), including:
* [containerd/containerd](https://github.com/containerd/containerd)
* [datadog/datadog-agent](https://github.com/datadog/datadog-agent)
* [docker/cli/](https://github.com/docker/cli/)
* [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)
* [go-micro/go-micro](https://github.com/go-micro/go-micro)
* [grafana/loki](https://github.com/grafana/loki)
* [masterminds/sprig](github.com/Masterminds/sprig)
* [moby/moby](https://github.com/moby/moby)
* [slackhq/nebula](https://github.com/slackhq/nebula)
* [volcano-sh/volcano](https://github.com/volcano-sh/volcano)
## Install
@@ -141,6 +117,39 @@ if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil {
}
```
If you need to override pointers, so the source pointer's value is assigned to the destination's pointer, you must use `WithoutDereference`:
```go
package main
import (
"fmt"
"dario.cat/mergo"
)
type Foo struct {
A *string
B int64
}
func main() {
first := "first"
second := "second"
src := Foo{
A: &first,
B: 2,
}
dest := Foo{
A: &second,
B: 1,
}
mergo.Merge(&dest, src, mergo.WithOverride, mergo.WithoutDereference)
}
```
Additionally, you can map a `map[string]interface{}` to a struct (and otherwise, from struct to map), following the same restrictions as in `Merge()`. Keys are capitalized to find each corresponding exported field.
```go
@@ -181,10 +190,6 @@ func main() {
}
```
Note: if test are failing due missing package, please execute:
go get gopkg.in/yaml.v3
### Transformers
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`?

View File

@@ -4,8 +4,8 @@
| Version | Supported |
| ------- | ------------------ |
| 0.3.x | :white_check_mark: |
| < 0.3 | :x: |
| 1.x.x | :white_check_mark: |
| < 1.0 | :x: |
## Security contact information

View File

@@ -58,7 +58,7 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, conf
}
fieldName := field.Name
fieldName = changeInitialCase(fieldName, unicode.ToLower)
if v, ok := dstMap[fieldName]; !ok || (isEmptyValue(reflect.ValueOf(v), !config.ShouldNotDereference) || overwrite) {
if _, ok := dstMap[fieldName]; !ok || (!isEmptyValue(reflect.ValueOf(src.Field(i).Interface()), !config.ShouldNotDereference) && overwrite) || config.overwriteWithEmptyValue {
dstMap[fieldName] = src.Field(i).Interface()
}
}

View File

@@ -269,7 +269,7 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co
if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil {
return
}
} else {
} else if src.Elem().Kind() != reflect.Struct {
if overwriteWithEmptySrc || (overwrite && !src.IsNil()) || dst.IsNil() {
dst.Set(src)
}

View File

@@ -1,7 +1,3 @@
run:
skip-dirs:
- pkg/etw/sample
linters:
enable:
# style
@@ -20,9 +16,13 @@ linters:
- gofmt # files are gofmt'ed
- gosec # security
- nilerr # returns nil even with non-nil error
- thelper # test helpers without t.Helper()
- unparam # unused function params
issues:
exclude-dirs:
- pkg/etw/sample
exclude-rules:
# err is very often shadowed in nested scopes
- linters:
@@ -69,9 +69,7 @@ linters-settings:
# struct order is often for Win32 compat
# also, ignore pointer bytes/GC issues for now until performance becomes an issue
- fieldalignment
check-shadowing: true
nolintlint:
allow-leading-space: false
require-explanation: true
require-specific: true
revive:

View File

@@ -10,14 +10,14 @@ import (
"io"
"os"
"runtime"
"syscall"
"unicode/utf16"
"github.com/Microsoft/go-winio/internal/fs"
"golang.org/x/sys/windows"
)
//sys backupRead(h syscall.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead
//sys backupWrite(h syscall.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite
//sys backupRead(h windows.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead
//sys backupWrite(h windows.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite
const (
BackupData = uint32(iota + 1)
@@ -104,7 +104,7 @@ func (r *BackupStreamReader) Next() (*BackupHeader, error) {
if err := binary.Read(r.r, binary.LittleEndian, name); err != nil {
return nil, err
}
hdr.Name = syscall.UTF16ToString(name)
hdr.Name = windows.UTF16ToString(name)
}
if wsi.StreamID == BackupSparseBlock {
if err := binary.Read(r.r, binary.LittleEndian, &hdr.Offset); err != nil {
@@ -205,7 +205,7 @@ func NewBackupFileReader(f *os.File, includeSecurity bool) *BackupFileReader {
// Read reads a backup stream from the file by calling the Win32 API BackupRead().
func (r *BackupFileReader) Read(b []byte) (int, error) {
var bytesRead uint32
err := backupRead(syscall.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx)
err := backupRead(windows.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx)
if err != nil {
return 0, &os.PathError{Op: "BackupRead", Path: r.f.Name(), Err: err}
}
@@ -220,7 +220,7 @@ func (r *BackupFileReader) Read(b []byte) (int, error) {
// the underlying file.
func (r *BackupFileReader) Close() error {
if r.ctx != 0 {
_ = backupRead(syscall.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx)
_ = backupRead(windows.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx)
runtime.KeepAlive(r.f)
r.ctx = 0
}
@@ -244,7 +244,7 @@ func NewBackupFileWriter(f *os.File, includeSecurity bool) *BackupFileWriter {
// Write restores a portion of the file using the provided backup stream.
func (w *BackupFileWriter) Write(b []byte) (int, error) {
var bytesWritten uint32
err := backupWrite(syscall.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx)
err := backupWrite(windows.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx)
if err != nil {
return 0, &os.PathError{Op: "BackupWrite", Path: w.f.Name(), Err: err}
}
@@ -259,7 +259,7 @@ func (w *BackupFileWriter) Write(b []byte) (int, error) {
// close the underlying file.
func (w *BackupFileWriter) Close() error {
if w.ctx != 0 {
_ = backupWrite(syscall.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx)
_ = backupWrite(windows.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx)
runtime.KeepAlive(w.f)
w.ctx = 0
}
@@ -271,17 +271,14 @@ func (w *BackupFileWriter) Close() error {
//
// If the file opened was a directory, it cannot be used with Readdir().
func OpenForBackup(path string, access uint32, share uint32, createmode uint32) (*os.File, error) {
winPath, err := syscall.UTF16FromString(path)
if err != nil {
return nil, err
}
h, err := syscall.CreateFile(&winPath[0],
access,
share,
h, err := fs.CreateFile(path,
fs.AccessMask(access),
fs.FileShareMode(share),
nil,
createmode,
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OPEN_REPARSE_POINT,
0)
fs.FileCreationDisposition(createmode),
fs.FILE_FLAG_BACKUP_SEMANTICS|fs.FILE_FLAG_OPEN_REPARSE_POINT,
0,
)
if err != nil {
err = &os.PathError{Op: "open", Path: path, Err: err}
return nil, err

View File

@@ -15,26 +15,11 @@ import (
"golang.org/x/sys/windows"
)
//sys cancelIoEx(file syscall.Handle, o *syscall.Overlapped) (err error) = CancelIoEx
//sys createIoCompletionPort(file syscall.Handle, port syscall.Handle, key uintptr, threadCount uint32) (newport syscall.Handle, err error) = CreateIoCompletionPort
//sys getQueuedCompletionStatus(port syscall.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus
//sys setFileCompletionNotificationModes(h syscall.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
//sys wsaGetOverlappedResult(h syscall.Handle, o *syscall.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
type atomicBool int32
func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 }
func (b *atomicBool) setFalse() { atomic.StoreInt32((*int32)(b), 0) }
func (b *atomicBool) setTrue() { atomic.StoreInt32((*int32)(b), 1) }
//revive:disable-next-line:predeclared Keep "new" to maintain consistency with "atomic" pkg
func (b *atomicBool) swap(new bool) bool {
var newInt int32
if new {
newInt = 1
}
return atomic.SwapInt32((*int32)(b), newInt) == 1
}
//sys cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) = CancelIoEx
//sys createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) = CreateIoCompletionPort
//sys getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus
//sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
//sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
var (
ErrFileClosed = errors.New("file has already been closed")
@@ -50,7 +35,7 @@ func (*timeoutError) Temporary() bool { return true }
type timeoutChan chan struct{}
var ioInitOnce sync.Once
var ioCompletionPort syscall.Handle
var ioCompletionPort windows.Handle
// ioResult contains the result of an asynchronous IO operation.
type ioResult struct {
@@ -60,12 +45,12 @@ type ioResult struct {
// ioOperation represents an outstanding asynchronous Win32 IO.
type ioOperation struct {
o syscall.Overlapped
o windows.Overlapped
ch chan ioResult
}
func initIO() {
h, err := createIoCompletionPort(syscall.InvalidHandle, 0, 0, 0xffffffff)
h, err := createIoCompletionPort(windows.InvalidHandle, 0, 0, 0xffffffff)
if err != nil {
panic(err)
}
@@ -76,10 +61,10 @@ func initIO() {
// win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall.
// It takes ownership of this handle and will close it if it is garbage collected.
type win32File struct {
handle syscall.Handle
handle windows.Handle
wg sync.WaitGroup
wgLock sync.RWMutex
closing atomicBool
closing atomic.Bool
socket bool
readDeadline deadlineHandler
writeDeadline deadlineHandler
@@ -90,11 +75,11 @@ type deadlineHandler struct {
channel timeoutChan
channelLock sync.RWMutex
timer *time.Timer
timedout atomicBool
timedout atomic.Bool
}
// makeWin32File makes a new win32File from an existing file handle.
func makeWin32File(h syscall.Handle) (*win32File, error) {
func makeWin32File(h windows.Handle) (*win32File, error) {
f := &win32File{handle: h}
ioInitOnce.Do(initIO)
_, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff)
@@ -110,7 +95,12 @@ func makeWin32File(h syscall.Handle) (*win32File, error) {
return f, nil
}
// Deprecated: use NewOpenFile instead.
func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
return NewOpenFile(windows.Handle(h))
}
func NewOpenFile(h windows.Handle) (io.ReadWriteCloser, error) {
// If we return the result of makeWin32File directly, it can result in an
// interface-wrapped nil, rather than a nil interface value.
f, err := makeWin32File(h)
@@ -124,13 +114,13 @@ func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
func (f *win32File) closeHandle() {
f.wgLock.Lock()
// Atomically set that we are closing, releasing the resources only once.
if !f.closing.swap(true) {
if !f.closing.Swap(true) {
f.wgLock.Unlock()
// cancel all IO and wait for it to complete
_ = cancelIoEx(f.handle, nil)
f.wg.Wait()
// at this point, no new IO can start
syscall.Close(f.handle)
windows.Close(f.handle)
f.handle = 0
} else {
f.wgLock.Unlock()
@@ -145,14 +135,14 @@ func (f *win32File) Close() error {
// IsClosed checks if the file has been closed.
func (f *win32File) IsClosed() bool {
return f.closing.isSet()
return f.closing.Load()
}
// prepareIO prepares for a new IO operation.
// The caller must call f.wg.Done() when the IO is finished, prior to Close() returning.
func (f *win32File) prepareIO() (*ioOperation, error) {
f.wgLock.RLock()
if f.closing.isSet() {
if f.closing.Load() {
f.wgLock.RUnlock()
return nil, ErrFileClosed
}
@@ -164,12 +154,12 @@ func (f *win32File) prepareIO() (*ioOperation, error) {
}
// ioCompletionProcessor processes completed async IOs forever.
func ioCompletionProcessor(h syscall.Handle) {
func ioCompletionProcessor(h windows.Handle) {
for {
var bytes uint32
var key uintptr
var op *ioOperation
err := getQueuedCompletionStatus(h, &bytes, &key, &op, syscall.INFINITE)
err := getQueuedCompletionStatus(h, &bytes, &key, &op, windows.INFINITE)
if op == nil {
panic(err)
}
@@ -182,11 +172,11 @@ func ioCompletionProcessor(h syscall.Handle) {
// asyncIO processes the return value from ReadFile or WriteFile, blocking until
// the operation has actually completed.
func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) {
if err != syscall.ERROR_IO_PENDING { //nolint:errorlint // err is Errno
if err != windows.ERROR_IO_PENDING { //nolint:errorlint // err is Errno
return int(bytes), err
}
if f.closing.isSet() {
if f.closing.Load() {
_ = cancelIoEx(f.handle, &c.o)
}
@@ -201,8 +191,8 @@ func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, er
select {
case r = <-c.ch:
err = r.err
if err == syscall.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
if f.closing.isSet() {
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
if f.closing.Load() {
err = ErrFileClosed
}
} else if err != nil && f.socket {
@@ -214,7 +204,7 @@ func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, er
_ = cancelIoEx(f.handle, &c.o)
r = <-c.ch
err = r.err
if err == syscall.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
err = ErrTimeout
}
}
@@ -235,23 +225,22 @@ func (f *win32File) Read(b []byte) (int, error) {
}
defer f.wg.Done()
if f.readDeadline.timedout.isSet() {
if f.readDeadline.timedout.Load() {
return 0, ErrTimeout
}
var bytes uint32
err = syscall.ReadFile(f.handle, b, &bytes, &c.o)
err = windows.ReadFile(f.handle, b, &bytes, &c.o)
n, err := f.asyncIO(c, &f.readDeadline, bytes, err)
runtime.KeepAlive(b)
// Handle EOF conditions.
if err == nil && n == 0 && len(b) != 0 {
return 0, io.EOF
} else if err == syscall.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
} else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
return 0, io.EOF
} else {
return n, err
}
return n, err
}
// Write writes to a file handle.
@@ -262,12 +251,12 @@ func (f *win32File) Write(b []byte) (int, error) {
}
defer f.wg.Done()
if f.writeDeadline.timedout.isSet() {
if f.writeDeadline.timedout.Load() {
return 0, ErrTimeout
}
var bytes uint32
err = syscall.WriteFile(f.handle, b, &bytes, &c.o)
err = windows.WriteFile(f.handle, b, &bytes, &c.o)
n, err := f.asyncIO(c, &f.writeDeadline, bytes, err)
runtime.KeepAlive(b)
return n, err
@@ -282,7 +271,7 @@ func (f *win32File) SetWriteDeadline(deadline time.Time) error {
}
func (f *win32File) Flush() error {
return syscall.FlushFileBuffers(f.handle)
return windows.FlushFileBuffers(f.handle)
}
func (f *win32File) Fd() uintptr {
@@ -299,7 +288,7 @@ func (d *deadlineHandler) set(deadline time.Time) error {
}
d.timer = nil
}
d.timedout.setFalse()
d.timedout.Store(false)
select {
case <-d.channel:
@@ -314,7 +303,7 @@ func (d *deadlineHandler) set(deadline time.Time) error {
}
timeoutIO := func() {
d.timedout.setTrue()
d.timedout.Store(true)
close(d.channel)
}

View File

@@ -18,9 +18,18 @@ type FileBasicInfo struct {
_ uint32 // padding
}
// alignedFileBasicInfo is a FileBasicInfo, but aligned to uint64 by containing
// uint64 rather than windows.Filetime. Filetime contains two uint32s. uint64
// alignment is necessary to pass this as FILE_BASIC_INFO.
type alignedFileBasicInfo struct {
CreationTime, LastAccessTime, LastWriteTime, ChangeTime uint64
FileAttributes uint32
_ uint32 // padding
}
// GetFileBasicInfo retrieves times and attributes for a file.
func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
bi := &FileBasicInfo{}
bi := &alignedFileBasicInfo{}
if err := windows.GetFileInformationByHandleEx(
windows.Handle(f.Fd()),
windows.FileBasicInfo,
@@ -30,16 +39,21 @@ func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
}
runtime.KeepAlive(f)
return bi, nil
// Reinterpret the alignedFileBasicInfo as a FileBasicInfo so it matches the
// public API of this module. The data may be unnecessarily aligned.
return (*FileBasicInfo)(unsafe.Pointer(bi)), nil
}
// SetFileBasicInfo sets times and attributes for a file.
func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error {
// Create an alignedFileBasicInfo based on a FileBasicInfo. The copy is
// suitable to pass to GetFileInformationByHandleEx.
biAligned := *(*alignedFileBasicInfo)(unsafe.Pointer(bi))
if err := windows.SetFileInformationByHandle(
windows.Handle(f.Fd()),
windows.FileBasicInfo,
(*byte)(unsafe.Pointer(bi)),
uint32(unsafe.Sizeof(*bi)),
(*byte)(unsafe.Pointer(&biAligned)),
uint32(unsafe.Sizeof(biAligned)),
); err != nil {
return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err}
}

View File

@@ -10,7 +10,6 @@ import (
"io"
"net"
"os"
"syscall"
"time"
"unsafe"
@@ -181,13 +180,13 @@ type HvsockConn struct {
var _ net.Conn = &HvsockConn{}
func newHVSocket() (*win32File, error) {
fd, err := syscall.Socket(afHVSock, syscall.SOCK_STREAM, 1)
fd, err := windows.Socket(afHVSock, windows.SOCK_STREAM, 1)
if err != nil {
return nil, os.NewSyscallError("socket", err)
}
f, err := makeWin32File(fd)
if err != nil {
syscall.Close(fd)
windows.Close(fd)
return nil, err
}
f.socket = true
@@ -197,16 +196,24 @@ func newHVSocket() (*win32File, error) {
// ListenHvsock listens for connections on the specified hvsock address.
func ListenHvsock(addr *HvsockAddr) (_ *HvsockListener, err error) {
l := &HvsockListener{addr: *addr}
sock, err := newHVSocket()
var sock *win32File
sock, err = newHVSocket()
if err != nil {
return nil, l.opErr("listen", err)
}
defer func() {
if err != nil {
_ = sock.Close()
}
}()
sa := addr.raw()
err = socket.Bind(windows.Handle(sock.handle), &sa)
err = socket.Bind(sock.handle, &sa)
if err != nil {
return nil, l.opErr("listen", os.NewSyscallError("socket", err))
}
err = syscall.Listen(sock.handle, 16)
err = windows.Listen(sock.handle, 16)
if err != nil {
return nil, l.opErr("listen", os.NewSyscallError("listen", err))
}
@@ -246,7 +253,7 @@ func (l *HvsockListener) Accept() (_ net.Conn, err error) {
var addrbuf [addrlen * 2]byte
var bytes uint32
err = syscall.AcceptEx(l.sock.handle, sock.handle, &addrbuf[0], 0 /* rxdatalen */, addrlen, addrlen, &bytes, &c.o)
err = windows.AcceptEx(l.sock.handle, sock.handle, &addrbuf[0], 0 /* rxdatalen */, addrlen, addrlen, &bytes, &c.o)
if _, err = l.sock.asyncIO(c, nil, bytes, err); err != nil {
return nil, l.opErr("accept", os.NewSyscallError("acceptex", err))
}
@@ -263,7 +270,7 @@ func (l *HvsockListener) Accept() (_ net.Conn, err error) {
conn.remote.fromRaw((*rawHvsockAddr)(unsafe.Pointer(&addrbuf[addrlen])))
// initialize the accepted socket and update its properties with those of the listening socket
if err = windows.Setsockopt(windows.Handle(sock.handle),
if err = windows.Setsockopt(sock.handle,
windows.SOL_SOCKET, windows.SO_UPDATE_ACCEPT_CONTEXT,
(*byte)(unsafe.Pointer(&l.sock.handle)), int32(unsafe.Sizeof(l.sock.handle))); err != nil {
return nil, conn.opErr("accept", os.NewSyscallError("setsockopt", err))
@@ -334,7 +341,7 @@ func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *Hvsock
}()
sa := addr.raw()
err = socket.Bind(windows.Handle(sock.handle), &sa)
err = socket.Bind(sock.handle, &sa)
if err != nil {
return nil, conn.opErr(op, os.NewSyscallError("bind", err))
}
@@ -347,7 +354,7 @@ func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *Hvsock
var bytes uint32
for i := uint(0); i <= d.Retries; i++ {
err = socket.ConnectEx(
windows.Handle(sock.handle),
sock.handle,
&sa,
nil, // sendBuf
0, // sendDataLen
@@ -367,7 +374,7 @@ func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *Hvsock
// update the connection properties, so shutdown can be used
if err = windows.Setsockopt(
windows.Handle(sock.handle),
sock.handle,
windows.SOL_SOCKET,
windows.SO_UPDATE_CONNECT_CONTEXT,
nil, // optvalue
@@ -378,7 +385,7 @@ func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *Hvsock
// get the local name
var sal rawHvsockAddr
err = socket.GetSockName(windows.Handle(sock.handle), &sal)
err = socket.GetSockName(sock.handle, &sal)
if err != nil {
return nil, conn.opErr(op, os.NewSyscallError("getsockname", err))
}
@@ -421,7 +428,7 @@ func (d *HvsockDialer) redialWait(ctx context.Context) (err error) {
return ctx.Err()
}
// assumes error is a plain, unwrapped syscall.Errno provided by direct syscall.
// assumes error is a plain, unwrapped windows.Errno provided by direct syscall.
func canRedial(err error) bool {
//nolint:errorlint // guaranteed to be an Errno
switch err {
@@ -447,9 +454,9 @@ func (conn *HvsockConn) Read(b []byte) (int, error) {
return 0, conn.opErr("read", err)
}
defer conn.sock.wg.Done()
buf := syscall.WSABuf{Buf: &b[0], Len: uint32(len(b))}
buf := windows.WSABuf{Buf: &b[0], Len: uint32(len(b))}
var flags, bytes uint32
err = syscall.WSARecv(conn.sock.handle, &buf, 1, &bytes, &flags, &c.o, nil)
err = windows.WSARecv(conn.sock.handle, &buf, 1, &bytes, &flags, &c.o, nil)
n, err := conn.sock.asyncIO(c, &conn.sock.readDeadline, bytes, err)
if err != nil {
var eno windows.Errno
@@ -482,9 +489,9 @@ func (conn *HvsockConn) write(b []byte) (int, error) {
return 0, conn.opErr("write", err)
}
defer conn.sock.wg.Done()
buf := syscall.WSABuf{Buf: &b[0], Len: uint32(len(b))}
buf := windows.WSABuf{Buf: &b[0], Len: uint32(len(b))}
var bytes uint32
err = syscall.WSASend(conn.sock.handle, &buf, 1, &bytes, 0, &c.o, nil)
err = windows.WSASend(conn.sock.handle, &buf, 1, &bytes, 0, &c.o, nil)
n, err := conn.sock.asyncIO(c, &conn.sock.writeDeadline, bytes, err)
if err != nil {
var eno windows.Errno
@@ -511,7 +518,7 @@ func (conn *HvsockConn) shutdown(how int) error {
return socket.ErrSocketClosed
}
err := syscall.Shutdown(conn.sock.handle, how)
err := windows.Shutdown(conn.sock.handle, how)
if err != nil {
// If the connection was closed, shutdowns fail with "not connected"
if errors.Is(err, windows.WSAENOTCONN) ||
@@ -525,7 +532,7 @@ func (conn *HvsockConn) shutdown(how int) error {
// CloseRead shuts down the read end of the socket, preventing future read operations.
func (conn *HvsockConn) CloseRead() error {
err := conn.shutdown(syscall.SHUT_RD)
err := conn.shutdown(windows.SHUT_RD)
if err != nil {
return conn.opErr("closeread", err)
}
@@ -535,7 +542,7 @@ func (conn *HvsockConn) CloseRead() error {
// CloseWrite shuts down the write end of the socket, preventing future write operations and
// notifying the other endpoint that no more data will be written.
func (conn *HvsockConn) CloseWrite() error {
err := conn.shutdown(syscall.SHUT_WR)
err := conn.shutdown(windows.SHUT_WR)
if err != nil {
return conn.opErr("closewrite", err)
}

View File

@@ -11,12 +11,14 @@ import (
//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go fs.go
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew
//sys CreateFile(name string, access AccessMask, mode FileShareMode, sa *syscall.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = CreateFileW
//sys CreateFile(name string, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = CreateFileW
const NullHandle windows.Handle = 0
// AccessMask defines standard, specific, and generic rights.
//
// Used with CreateFile and NtCreateFile (and co.).
//
// Bitmask:
// 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
@@ -47,6 +49,12 @@ const (
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#parameters
FILE_ANY_ACCESS AccessMask = 0
GENERIC_READ AccessMask = 0x8000_0000
GENERIC_WRITE AccessMask = 0x4000_0000
GENERIC_EXECUTE AccessMask = 0x2000_0000
GENERIC_ALL AccessMask = 0x1000_0000
ACCESS_SYSTEM_SECURITY AccessMask = 0x0100_0000
// Specific Object Access
// from ntioapi.h
@@ -124,14 +132,32 @@ const (
TRUNCATE_EXISTING FileCreationDisposition = 0x05
)
// Create disposition values for NtCreate*
type NTFileCreationDisposition uint32
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
const (
// From ntioapi.h
FILE_SUPERSEDE NTFileCreationDisposition = 0x00
FILE_OPEN NTFileCreationDisposition = 0x01
FILE_CREATE NTFileCreationDisposition = 0x02
FILE_OPEN_IF NTFileCreationDisposition = 0x03
FILE_OVERWRITE NTFileCreationDisposition = 0x04
FILE_OVERWRITE_IF NTFileCreationDisposition = 0x05
FILE_MAXIMUM_DISPOSITION NTFileCreationDisposition = 0x05
)
// CreateFile and co. take flags or attributes together as one parameter.
// Define alias until we can use generics to allow both
//
// https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
type FileFlagOrAttribute uint32
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
const ( // from winnt.h
const (
// from winnt.h
FILE_FLAG_WRITE_THROUGH FileFlagOrAttribute = 0x8000_0000
FILE_FLAG_OVERLAPPED FileFlagOrAttribute = 0x4000_0000
FILE_FLAG_NO_BUFFERING FileFlagOrAttribute = 0x2000_0000
@@ -145,17 +171,51 @@ const ( // from winnt.h
FILE_FLAG_FIRST_PIPE_INSTANCE FileFlagOrAttribute = 0x0008_0000
)
// NtCreate* functions take a dedicated CreateOptions parameter.
//
// https://learn.microsoft.com/en-us/windows/win32/api/Winternl/nf-winternl-ntcreatefile
//
// https://learn.microsoft.com/en-us/windows/win32/devnotes/nt-create-named-pipe-file
type NTCreateOptions uint32
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
const (
// From ntioapi.h
FILE_DIRECTORY_FILE NTCreateOptions = 0x0000_0001
FILE_WRITE_THROUGH NTCreateOptions = 0x0000_0002
FILE_SEQUENTIAL_ONLY NTCreateOptions = 0x0000_0004
FILE_NO_INTERMEDIATE_BUFFERING NTCreateOptions = 0x0000_0008
FILE_SYNCHRONOUS_IO_ALERT NTCreateOptions = 0x0000_0010
FILE_SYNCHRONOUS_IO_NONALERT NTCreateOptions = 0x0000_0020
FILE_NON_DIRECTORY_FILE NTCreateOptions = 0x0000_0040
FILE_CREATE_TREE_CONNECTION NTCreateOptions = 0x0000_0080
FILE_COMPLETE_IF_OPLOCKED NTCreateOptions = 0x0000_0100
FILE_NO_EA_KNOWLEDGE NTCreateOptions = 0x0000_0200
FILE_DISABLE_TUNNELING NTCreateOptions = 0x0000_0400
FILE_RANDOM_ACCESS NTCreateOptions = 0x0000_0800
FILE_DELETE_ON_CLOSE NTCreateOptions = 0x0000_1000
FILE_OPEN_BY_FILE_ID NTCreateOptions = 0x0000_2000
FILE_OPEN_FOR_BACKUP_INTENT NTCreateOptions = 0x0000_4000
FILE_NO_COMPRESSION NTCreateOptions = 0x0000_8000
)
type FileSQSFlag = FileFlagOrAttribute
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
const ( // from winbase.h
const (
// from winbase.h
SECURITY_ANONYMOUS FileSQSFlag = FileSQSFlag(SecurityAnonymous << 16)
SECURITY_IDENTIFICATION FileSQSFlag = FileSQSFlag(SecurityIdentification << 16)
SECURITY_IMPERSONATION FileSQSFlag = FileSQSFlag(SecurityImpersonation << 16)
SECURITY_DELEGATION FileSQSFlag = FileSQSFlag(SecurityDelegation << 16)
SECURITY_SQOS_PRESENT FileSQSFlag = 0x00100000
SECURITY_VALID_SQOS_FLAGS FileSQSFlag = 0x001F0000
SECURITY_SQOS_PRESENT FileSQSFlag = 0x0010_0000
SECURITY_VALID_SQOS_FLAGS FileSQSFlag = 0x001F_0000
)
// GetFinalPathNameByHandle flags

View File

@@ -33,9 +33,6 @@ func errnoErr(e syscall.Errno) error {
case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING
}
// TODO: add more here, after collecting data on the common
// error values see on Windows. (perhaps when running
// all.bat?)
return e
}
@@ -45,7 +42,7 @@ var (
procCreateFileW = modkernel32.NewProc("CreateFileW")
)
func CreateFile(name string, access AccessMask, mode FileShareMode, sa *syscall.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) {
func CreateFile(name string, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) {
var _p0 *uint16
_p0, err = syscall.UTF16PtrFromString(name)
if err != nil {
@@ -54,8 +51,8 @@ func CreateFile(name string, access AccessMask, mode FileShareMode, sa *syscall.
return _CreateFile(_p0, access, mode, sa, createmode, attrs, templatefile)
}
func _CreateFile(name *uint16, access AccessMask, mode FileShareMode, sa *syscall.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) {
r0, _, e1 := syscall.Syscall9(procCreateFileW.Addr(), 7, uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile), 0, 0)
func _CreateFile(name *uint16, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) {
r0, _, e1 := syscall.SyscallN(procCreateFileW.Addr(), uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile))
handle = windows.Handle(r0)
if handle == windows.InvalidHandle {
err = errnoErr(e1)

View File

@@ -156,9 +156,7 @@ func connectEx(
bytesSent *uint32,
overlapped *windows.Overlapped,
) (err error) {
// todo: after upgrading to 1.18, switch from syscall.Syscall9 to syscall.SyscallN
r1, _, e1 := syscall.Syscall9(connectExFunc.addr,
7,
r1, _, e1 := syscall.SyscallN(connectExFunc.addr,
uintptr(s),
uintptr(name),
uintptr(namelen),
@@ -166,8 +164,8 @@ func connectEx(
uintptr(sendDataLen),
uintptr(unsafe.Pointer(bytesSent)),
uintptr(unsafe.Pointer(overlapped)),
0,
0)
)
if r1 == 0 {
if e1 != 0 {
err = error(e1)

View File

@@ -33,9 +33,6 @@ func errnoErr(e syscall.Errno) error {
case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING
}
// TODO: add more here, after collecting data on the common
// error values see on Windows. (perhaps when running
// all.bat?)
return e
}
@@ -48,7 +45,7 @@ var (
)
func bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) {
r1, _, e1 := syscall.Syscall(procbind.Addr(), 3, uintptr(s), uintptr(name), uintptr(namelen))
r1, _, e1 := syscall.SyscallN(procbind.Addr(), uintptr(s), uintptr(name), uintptr(namelen))
if r1 == socketError {
err = errnoErr(e1)
}
@@ -56,7 +53,7 @@ func bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) {
}
func getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) {
r1, _, e1 := syscall.Syscall(procgetpeername.Addr(), 3, uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen)))
r1, _, e1 := syscall.SyscallN(procgetpeername.Addr(), uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen)))
if r1 == socketError {
err = errnoErr(e1)
}
@@ -64,7 +61,7 @@ func getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err err
}
func getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) {
r1, _, e1 := syscall.Syscall(procgetsockname.Addr(), 3, uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen)))
r1, _, e1 := syscall.SyscallN(procgetsockname.Addr(), uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen)))
if r1 == socketError {
err = errnoErr(e1)
}

View File

@@ -62,7 +62,7 @@ func (b *WString) Free() {
// ResizeTo grows the buffer to at least c and returns the new capacity, freeing the
// previous buffer back into pool.
func (b *WString) ResizeTo(c uint32) uint32 {
// allready sufficient (or n is 0)
// already sufficient (or n is 0)
if c <= b.Cap() {
return b.Cap()
}

View File

@@ -11,7 +11,6 @@ import (
"net"
"os"
"runtime"
"syscall"
"time"
"unsafe"
@@ -20,20 +19,44 @@ import (
"github.com/Microsoft/go-winio/internal/fs"
)
//sys connectNamedPipe(pipe syscall.Handle, o *syscall.Overlapped) (err error) = ConnectNamedPipe
//sys createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *syscall.SecurityAttributes) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = CreateNamedPipeW
//sys getNamedPipeInfo(pipe syscall.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) = GetNamedPipeInfo
//sys getNamedPipeHandleState(pipe syscall.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW
//sys localAlloc(uFlags uint32, length uint32) (ptr uintptr) = LocalAlloc
//sys ntCreateNamedPipeFile(pipe *syscall.Handle, access uint32, oa *objectAttributes, iosb *ioStatusBlock, share uint32, disposition uint32, options uint32, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (status ntStatus) = ntdll.NtCreateNamedPipeFile
//sys connectNamedPipe(pipe windows.Handle, o *windows.Overlapped) (err error) = ConnectNamedPipe
//sys createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = CreateNamedPipeW
//sys disconnectNamedPipe(pipe windows.Handle) (err error) = DisconnectNamedPipe
//sys getNamedPipeInfo(pipe windows.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) = GetNamedPipeInfo
//sys getNamedPipeHandleState(pipe windows.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW
//sys ntCreateNamedPipeFile(pipe *windows.Handle, access ntAccessMask, oa *objectAttributes, iosb *ioStatusBlock, share ntFileShareMode, disposition ntFileCreationDisposition, options ntFileOptions, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (status ntStatus) = ntdll.NtCreateNamedPipeFile
//sys rtlNtStatusToDosError(status ntStatus) (winerr error) = ntdll.RtlNtStatusToDosErrorNoTeb
//sys rtlDosPathNameToNtPathName(name *uint16, ntName *unicodeString, filePart uintptr, reserved uintptr) (status ntStatus) = ntdll.RtlDosPathNameToNtPathName_U
//sys rtlDefaultNpAcl(dacl *uintptr) (status ntStatus) = ntdll.RtlDefaultNpAcl
type PipeConn interface {
net.Conn
Disconnect() error
Flush() error
}
// type aliases for mkwinsyscall code
type (
ntAccessMask = fs.AccessMask
ntFileShareMode = fs.FileShareMode
ntFileCreationDisposition = fs.NTFileCreationDisposition
ntFileOptions = fs.NTCreateOptions
)
type ioStatusBlock struct {
Status, Information uintptr
}
// typedef struct _OBJECT_ATTRIBUTES {
// ULONG Length;
// HANDLE RootDirectory;
// PUNICODE_STRING ObjectName;
// ULONG Attributes;
// PVOID SecurityDescriptor;
// PVOID SecurityQualityOfService;
// } OBJECT_ATTRIBUTES;
//
// https://learn.microsoft.com/en-us/windows/win32/api/ntdef/ns-ntdef-_object_attributes
type objectAttributes struct {
Length uintptr
RootDirectory uintptr
@@ -49,6 +72,17 @@ type unicodeString struct {
Buffer uintptr
}
// typedef struct _SECURITY_DESCRIPTOR {
// BYTE Revision;
// BYTE Sbz1;
// SECURITY_DESCRIPTOR_CONTROL Control;
// PSID Owner;
// PSID Group;
// PACL Sacl;
// PACL Dacl;
// } SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR;
//
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-security_descriptor
type securityDescriptor struct {
Revision byte
Sbz1 byte
@@ -80,6 +114,8 @@ type win32Pipe struct {
path string
}
var _ PipeConn = (*win32Pipe)(nil)
type win32MessageBytePipe struct {
win32Pipe
writeClosed bool
@@ -103,6 +139,10 @@ func (f *win32Pipe) SetDeadline(t time.Time) error {
return f.SetWriteDeadline(t)
}
func (f *win32Pipe) Disconnect() error {
return disconnectNamedPipe(f.win32File.handle)
}
// CloseWrite closes the write side of a message pipe in byte mode.
func (f *win32MessageBytePipe) CloseWrite() error {
if f.writeClosed {
@@ -146,7 +186,7 @@ func (f *win32MessageBytePipe) Read(b []byte) (int, error) {
// zero-byte message, ensure that all future Read() calls
// also return EOF.
f.readEOF = true
} else if err == syscall.ERROR_MORE_DATA { //nolint:errorlint // err is Errno
} else if err == windows.ERROR_MORE_DATA { //nolint:errorlint // err is Errno
// ERROR_MORE_DATA indicates that the pipe's read mode is message mode
// and the message still has more bytes. Treat this as a success, since
// this package presents all named pipes as byte streams.
@@ -164,21 +204,20 @@ func (s pipeAddress) String() string {
}
// tryDialPipe attempts to dial the pipe at `path` until `ctx` cancellation or timeout.
func tryDialPipe(ctx context.Context, path *string, access fs.AccessMask) (syscall.Handle, error) {
func tryDialPipe(ctx context.Context, path *string, access fs.AccessMask, impLevel PipeImpLevel) (windows.Handle, error) {
for {
select {
case <-ctx.Done():
return syscall.Handle(0), ctx.Err()
return windows.Handle(0), ctx.Err()
default:
wh, err := fs.CreateFile(*path,
h, err := fs.CreateFile(*path,
access,
0, // mode
nil, // security attributes
fs.OPEN_EXISTING,
fs.FILE_FLAG_OVERLAPPED|fs.SECURITY_SQOS_PRESENT|fs.SECURITY_ANONYMOUS,
fs.FILE_FLAG_OVERLAPPED|fs.SECURITY_SQOS_PRESENT|fs.FileSQSFlag(impLevel),
0, // template file handle
)
h := syscall.Handle(wh)
if err == nil {
return h, nil
}
@@ -214,15 +253,33 @@ func DialPipe(path string, timeout *time.Duration) (net.Conn, error) {
// DialPipeContext attempts to connect to a named pipe by `path` until `ctx`
// cancellation or timeout.
func DialPipeContext(ctx context.Context, path string) (net.Conn, error) {
return DialPipeAccess(ctx, path, syscall.GENERIC_READ|syscall.GENERIC_WRITE)
return DialPipeAccess(ctx, path, uint32(fs.GENERIC_READ|fs.GENERIC_WRITE))
}
// PipeImpLevel is an enumeration of impersonation levels that may be set
// when calling DialPipeAccessImpersonation.
type PipeImpLevel uint32
const (
PipeImpLevelAnonymous = PipeImpLevel(fs.SECURITY_ANONYMOUS)
PipeImpLevelIdentification = PipeImpLevel(fs.SECURITY_IDENTIFICATION)
PipeImpLevelImpersonation = PipeImpLevel(fs.SECURITY_IMPERSONATION)
PipeImpLevelDelegation = PipeImpLevel(fs.SECURITY_DELEGATION)
)
// DialPipeAccess attempts to connect to a named pipe by `path` with `access` until `ctx`
// cancellation or timeout.
func DialPipeAccess(ctx context.Context, path string, access uint32) (net.Conn, error) {
return DialPipeAccessImpLevel(ctx, path, access, PipeImpLevelAnonymous)
}
// DialPipeAccessImpLevel attempts to connect to a named pipe by `path` with
// `access` at `impLevel` until `ctx` cancellation or timeout. The other
// DialPipe* implementations use PipeImpLevelAnonymous.
func DialPipeAccessImpLevel(ctx context.Context, path string, access uint32, impLevel PipeImpLevel) (net.Conn, error) {
var err error
var h syscall.Handle
h, err = tryDialPipe(ctx, &path, fs.AccessMask(access))
var h windows.Handle
h, err = tryDialPipe(ctx, &path, fs.AccessMask(access), impLevel)
if err != nil {
return nil, err
}
@@ -235,7 +292,7 @@ func DialPipeAccess(ctx context.Context, path string, access uint32) (net.Conn,
f, err := makeWin32File(h)
if err != nil {
syscall.Close(h)
windows.Close(h)
return nil, err
}
@@ -255,7 +312,7 @@ type acceptResponse struct {
}
type win32PipeListener struct {
firstHandle syscall.Handle
firstHandle windows.Handle
path string
config PipeConfig
acceptCh chan (chan acceptResponse)
@@ -263,8 +320,8 @@ type win32PipeListener struct {
doneCh chan int
}
func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (syscall.Handle, error) {
path16, err := syscall.UTF16FromString(path)
func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (windows.Handle, error) {
path16, err := windows.UTF16FromString(path)
if err != nil {
return 0, &os.PathError{Op: "open", Path: path, Err: err}
}
@@ -280,16 +337,20 @@ func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (sy
).Err(); err != nil {
return 0, &os.PathError{Op: "open", Path: path, Err: err}
}
defer localFree(ntPath.Buffer)
defer windows.LocalFree(windows.Handle(ntPath.Buffer)) //nolint:errcheck
oa.ObjectName = &ntPath
oa.Attributes = windows.OBJ_CASE_INSENSITIVE
// The security descriptor is only needed for the first pipe.
if first {
if sd != nil {
//todo: does `sdb` need to be allocated on the heap, or can go allocate it?
l := uint32(len(sd))
sdb := localAlloc(0, l)
defer localFree(sdb)
sdb, err := windows.LocalAlloc(0, l)
if err != nil {
return 0, fmt.Errorf("LocalAlloc for security descriptor with of length %d: %w", l, err)
}
defer windows.LocalFree(windows.Handle(sdb)) //nolint:errcheck
copy((*[0xffff]byte)(unsafe.Pointer(sdb))[:], sd)
oa.SecurityDescriptor = (*securityDescriptor)(unsafe.Pointer(sdb))
} else {
@@ -298,7 +359,7 @@ func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (sy
if err := rtlDefaultNpAcl(&dacl).Err(); err != nil {
return 0, fmt.Errorf("getting default named pipe ACL: %w", err)
}
defer localFree(dacl)
defer windows.LocalFree(windows.Handle(dacl)) //nolint:errcheck
sdb := &securityDescriptor{
Revision: 1,
@@ -314,27 +375,27 @@ func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (sy
typ |= windows.FILE_PIPE_MESSAGE_TYPE
}
disposition := uint32(windows.FILE_OPEN)
access := uint32(syscall.GENERIC_READ | syscall.GENERIC_WRITE | syscall.SYNCHRONIZE)
disposition := fs.FILE_OPEN
access := fs.GENERIC_READ | fs.GENERIC_WRITE | fs.SYNCHRONIZE
if first {
disposition = windows.FILE_CREATE
disposition = fs.FILE_CREATE
// By not asking for read or write access, the named pipe file system
// will put this pipe into an initially disconnected state, blocking
// client connections until the next call with first == false.
access = syscall.SYNCHRONIZE
access = fs.SYNCHRONIZE
}
timeout := int64(-50 * 10000) // 50ms
var (
h syscall.Handle
h windows.Handle
iosb ioStatusBlock
)
err = ntCreateNamedPipeFile(&h,
access,
&oa,
&iosb,
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE,
fs.FILE_SHARE_READ|fs.FILE_SHARE_WRITE,
disposition,
0,
typ,
@@ -359,7 +420,7 @@ func (l *win32PipeListener) makeServerPipe() (*win32File, error) {
}
f, err := makeWin32File(h)
if err != nil {
syscall.Close(h)
windows.Close(h)
return nil, err
}
return f, nil
@@ -418,7 +479,7 @@ func (l *win32PipeListener) listenerRoutine() {
closed = err == ErrPipeListenerClosed //nolint:errorlint // err is Errno
}
}
syscall.Close(l.firstHandle)
windows.Close(l.firstHandle)
l.firstHandle = 0
// Notify Close() and Accept() callers that the handle has been closed.
close(l.doneCh)

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"runtime"
"sync"
"syscall"
"unicode/utf16"
"golang.org/x/sys/windows"
@@ -18,8 +17,8 @@ import (
//sys adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) [true] = advapi32.AdjustTokenPrivileges
//sys impersonateSelf(level uint32) (err error) = advapi32.ImpersonateSelf
//sys revertToSelf() (err error) = advapi32.RevertToSelf
//sys openThreadToken(thread syscall.Handle, accessMask uint32, openAsSelf bool, token *windows.Token) (err error) = advapi32.OpenThreadToken
//sys getCurrentThread() (h syscall.Handle) = GetCurrentThread
//sys openThreadToken(thread windows.Handle, accessMask uint32, openAsSelf bool, token *windows.Token) (err error) = advapi32.OpenThreadToken
//sys getCurrentThread() (h windows.Handle) = GetCurrentThread
//sys lookupPrivilegeValue(systemName string, name string, luid *uint64) (err error) = advapi32.LookupPrivilegeValueW
//sys lookupPrivilegeName(systemName string, luid *uint64, buffer *uint16, size *uint32) (err error) = advapi32.LookupPrivilegeNameW
//sys lookupPrivilegeDisplayName(systemName string, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) = advapi32.LookupPrivilegeDisplayNameW
@@ -29,7 +28,7 @@ const (
SE_PRIVILEGE_ENABLED = windows.SE_PRIVILEGE_ENABLED
//revive:disable-next-line:var-naming ALL_CAPS
ERROR_NOT_ALL_ASSIGNED syscall.Errno = windows.ERROR_NOT_ALL_ASSIGNED
ERROR_NOT_ALL_ASSIGNED windows.Errno = windows.ERROR_NOT_ALL_ASSIGNED
SeBackupPrivilege = "SeBackupPrivilege"
SeRestorePrivilege = "SeRestorePrivilege"
@@ -177,7 +176,7 @@ func newThreadToken() (windows.Token, error) {
}
var token windows.Token
err = openThreadToken(getCurrentThread(), syscall.TOKEN_ADJUST_PRIVILEGES|syscall.TOKEN_QUERY, false, &token)
err = openThreadToken(getCurrentThread(), windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, false, &token)
if err != nil {
rerr := revertToSelf()
if rerr != nil {

View File

@@ -5,7 +5,7 @@ package winio
import (
"errors"
"syscall"
"fmt"
"unsafe"
"golang.org/x/sys/windows"
@@ -15,10 +15,6 @@ import (
//sys lookupAccountSid(systemName *uint16, sid *byte, name *uint16, nameSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) = advapi32.LookupAccountSidW
//sys convertSidToStringSid(sid *byte, str **uint16) (err error) = advapi32.ConvertSidToStringSidW
//sys convertStringSidToSid(str *uint16, sid **byte) (err error) = advapi32.ConvertStringSidToSidW
//sys convertStringSecurityDescriptorToSecurityDescriptor(str string, revision uint32, sd *uintptr, size *uint32) (err error) = advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW
//sys convertSecurityDescriptorToStringSecurityDescriptor(sd *byte, revision uint32, secInfo uint32, sddl **uint16, sddlSize *uint32) (err error) = advapi32.ConvertSecurityDescriptorToStringSecurityDescriptorW
//sys localFree(mem uintptr) = LocalFree
//sys getSecurityDescriptorLength(sd uintptr) (len uint32) = advapi32.GetSecurityDescriptorLength
type AccountLookupError struct {
Name string
@@ -64,7 +60,7 @@ func LookupSidByName(name string) (sid string, err error) {
var sidSize, sidNameUse, refDomainSize uint32
err = lookupAccountName(nil, name, nil, &sidSize, nil, &refDomainSize, &sidNameUse)
if err != nil && err != syscall.ERROR_INSUFFICIENT_BUFFER { //nolint:errorlint // err is Errno
if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { //nolint:errorlint // err is Errno
return "", &AccountLookupError{name, err}
}
sidBuffer := make([]byte, sidSize)
@@ -78,8 +74,8 @@ func LookupSidByName(name string) (sid string, err error) {
if err != nil {
return "", &AccountLookupError{name, err}
}
sid = syscall.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(strBuffer))[:])
localFree(uintptr(unsafe.Pointer(strBuffer)))
sid = windows.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(strBuffer))[:])
_, _ = windows.LocalFree(windows.Handle(unsafe.Pointer(strBuffer)))
return sid, nil
}
@@ -100,7 +96,7 @@ func LookupNameBySid(sid string) (name string, err error) {
if err = convertStringSidToSid(sidBuffer, &sidPtr); err != nil {
return "", &AccountLookupError{sid, err}
}
defer localFree(uintptr(unsafe.Pointer(sidPtr)))
defer windows.LocalFree(windows.Handle(unsafe.Pointer(sidPtr))) //nolint:errcheck
var nameSize, refDomainSize, sidNameUse uint32
err = lookupAccountSid(nil, sidPtr, nil, &nameSize, nil, &refDomainSize, &sidNameUse)
@@ -120,25 +116,18 @@ func LookupNameBySid(sid string) (name string, err error) {
}
func SddlToSecurityDescriptor(sddl string) ([]byte, error) {
var sdBuffer uintptr
err := convertStringSecurityDescriptorToSecurityDescriptor(sddl, 1, &sdBuffer, nil)
sd, err := windows.SecurityDescriptorFromString(sddl)
if err != nil {
return nil, &SddlConversionError{sddl, err}
return nil, &SddlConversionError{Sddl: sddl, Err: err}
}
defer localFree(sdBuffer)
sd := make([]byte, getSecurityDescriptorLength(sdBuffer))
copy(sd, (*[0xffff]byte)(unsafe.Pointer(sdBuffer))[:len(sd)])
return sd, nil
b := unsafe.Slice((*byte)(unsafe.Pointer(sd)), sd.Length())
return b, nil
}
func SecurityDescriptorToSddl(sd []byte) (string, error) {
var sddl *uint16
// The returned string length seems to include an arbitrary number of terminating NULs.
// Don't use it.
err := convertSecurityDescriptorToStringSecurityDescriptor(&sd[0], 1, 0xff, &sddl, nil)
if err != nil {
return "", err
if l := int(unsafe.Sizeof(windows.SECURITY_DESCRIPTOR{})); len(sd) < l {
return "", fmt.Errorf("SecurityDescriptor (%d) smaller than expected (%d): %w", len(sd), l, windows.ERROR_INCORRECT_SIZE)
}
defer localFree(uintptr(unsafe.Pointer(sddl)))
return syscall.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(sddl))[:]), nil
s := (*windows.SECURITY_DESCRIPTOR)(unsafe.Pointer(&sd[0]))
return s.String(), nil
}

View File

@@ -1,5 +0,0 @@
//go:build tools
package winio
import _ "golang.org/x/tools/cmd/stringer"

View File

@@ -33,9 +33,6 @@ func errnoErr(e syscall.Errno) error {
case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING
}
// TODO: add more here, after collecting data on the common
// error values see on Windows. (perhaps when running
// all.bat?)
return e
}
@@ -45,38 +42,34 @@ var (
modntdll = windows.NewLazySystemDLL("ntdll.dll")
modws2_32 = windows.NewLazySystemDLL("ws2_32.dll")
procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges")
procConvertSecurityDescriptorToStringSecurityDescriptorW = modadvapi32.NewProc("ConvertSecurityDescriptorToStringSecurityDescriptorW")
procConvertSidToStringSidW = modadvapi32.NewProc("ConvertSidToStringSidW")
procConvertStringSecurityDescriptorToSecurityDescriptorW = modadvapi32.NewProc("ConvertStringSecurityDescriptorToSecurityDescriptorW")
procConvertStringSidToSidW = modadvapi32.NewProc("ConvertStringSidToSidW")
procGetSecurityDescriptorLength = modadvapi32.NewProc("GetSecurityDescriptorLength")
procImpersonateSelf = modadvapi32.NewProc("ImpersonateSelf")
procLookupAccountNameW = modadvapi32.NewProc("LookupAccountNameW")
procLookupAccountSidW = modadvapi32.NewProc("LookupAccountSidW")
procLookupPrivilegeDisplayNameW = modadvapi32.NewProc("LookupPrivilegeDisplayNameW")
procLookupPrivilegeNameW = modadvapi32.NewProc("LookupPrivilegeNameW")
procLookupPrivilegeValueW = modadvapi32.NewProc("LookupPrivilegeValueW")
procOpenThreadToken = modadvapi32.NewProc("OpenThreadToken")
procRevertToSelf = modadvapi32.NewProc("RevertToSelf")
procBackupRead = modkernel32.NewProc("BackupRead")
procBackupWrite = modkernel32.NewProc("BackupWrite")
procCancelIoEx = modkernel32.NewProc("CancelIoEx")
procConnectNamedPipe = modkernel32.NewProc("ConnectNamedPipe")
procCreateIoCompletionPort = modkernel32.NewProc("CreateIoCompletionPort")
procCreateNamedPipeW = modkernel32.NewProc("CreateNamedPipeW")
procGetCurrentThread = modkernel32.NewProc("GetCurrentThread")
procGetNamedPipeHandleStateW = modkernel32.NewProc("GetNamedPipeHandleStateW")
procGetNamedPipeInfo = modkernel32.NewProc("GetNamedPipeInfo")
procGetQueuedCompletionStatus = modkernel32.NewProc("GetQueuedCompletionStatus")
procLocalAlloc = modkernel32.NewProc("LocalAlloc")
procLocalFree = modkernel32.NewProc("LocalFree")
procSetFileCompletionNotificationModes = modkernel32.NewProc("SetFileCompletionNotificationModes")
procNtCreateNamedPipeFile = modntdll.NewProc("NtCreateNamedPipeFile")
procRtlDefaultNpAcl = modntdll.NewProc("RtlDefaultNpAcl")
procRtlDosPathNameToNtPathName_U = modntdll.NewProc("RtlDosPathNameToNtPathName_U")
procRtlNtStatusToDosErrorNoTeb = modntdll.NewProc("RtlNtStatusToDosErrorNoTeb")
procWSAGetOverlappedResult = modws2_32.NewProc("WSAGetOverlappedResult")
procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges")
procConvertSidToStringSidW = modadvapi32.NewProc("ConvertSidToStringSidW")
procConvertStringSidToSidW = modadvapi32.NewProc("ConvertStringSidToSidW")
procImpersonateSelf = modadvapi32.NewProc("ImpersonateSelf")
procLookupAccountNameW = modadvapi32.NewProc("LookupAccountNameW")
procLookupAccountSidW = modadvapi32.NewProc("LookupAccountSidW")
procLookupPrivilegeDisplayNameW = modadvapi32.NewProc("LookupPrivilegeDisplayNameW")
procLookupPrivilegeNameW = modadvapi32.NewProc("LookupPrivilegeNameW")
procLookupPrivilegeValueW = modadvapi32.NewProc("LookupPrivilegeValueW")
procOpenThreadToken = modadvapi32.NewProc("OpenThreadToken")
procRevertToSelf = modadvapi32.NewProc("RevertToSelf")
procBackupRead = modkernel32.NewProc("BackupRead")
procBackupWrite = modkernel32.NewProc("BackupWrite")
procCancelIoEx = modkernel32.NewProc("CancelIoEx")
procConnectNamedPipe = modkernel32.NewProc("ConnectNamedPipe")
procCreateIoCompletionPort = modkernel32.NewProc("CreateIoCompletionPort")
procCreateNamedPipeW = modkernel32.NewProc("CreateNamedPipeW")
procDisconnectNamedPipe = modkernel32.NewProc("DisconnectNamedPipe")
procGetCurrentThread = modkernel32.NewProc("GetCurrentThread")
procGetNamedPipeHandleStateW = modkernel32.NewProc("GetNamedPipeHandleStateW")
procGetNamedPipeInfo = modkernel32.NewProc("GetNamedPipeInfo")
procGetQueuedCompletionStatus = modkernel32.NewProc("GetQueuedCompletionStatus")
procSetFileCompletionNotificationModes = modkernel32.NewProc("SetFileCompletionNotificationModes")
procNtCreateNamedPipeFile = modntdll.NewProc("NtCreateNamedPipeFile")
procRtlDefaultNpAcl = modntdll.NewProc("RtlDefaultNpAcl")
procRtlDosPathNameToNtPathName_U = modntdll.NewProc("RtlDosPathNameToNtPathName_U")
procRtlNtStatusToDosErrorNoTeb = modntdll.NewProc("RtlNtStatusToDosErrorNoTeb")
procWSAGetOverlappedResult = modws2_32.NewProc("WSAGetOverlappedResult")
)
func adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) {
@@ -84,7 +77,7 @@ func adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, ou
if releaseAll {
_p0 = 1
}
r0, _, e1 := syscall.Syscall6(procAdjustTokenPrivileges.Addr(), 6, uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(input)), uintptr(outputSize), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(requiredSize)))
r0, _, e1 := syscall.SyscallN(procAdjustTokenPrivileges.Addr(), uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(input)), uintptr(outputSize), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(requiredSize)))
success = r0 != 0
if true {
err = errnoErr(e1)
@@ -92,33 +85,8 @@ func adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, ou
return
}
func convertSecurityDescriptorToStringSecurityDescriptor(sd *byte, revision uint32, secInfo uint32, sddl **uint16, sddlSize *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procConvertSecurityDescriptorToStringSecurityDescriptorW.Addr(), 5, uintptr(unsafe.Pointer(sd)), uintptr(revision), uintptr(secInfo), uintptr(unsafe.Pointer(sddl)), uintptr(unsafe.Pointer(sddlSize)), 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func convertSidToStringSid(sid *byte, str **uint16) (err error) {
r1, _, e1 := syscall.Syscall(procConvertSidToStringSidW.Addr(), 2, uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(str)), 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func convertStringSecurityDescriptorToSecurityDescriptor(str string, revision uint32, sd *uintptr, size *uint32) (err error) {
var _p0 *uint16
_p0, err = syscall.UTF16PtrFromString(str)
if err != nil {
return
}
return _convertStringSecurityDescriptorToSecurityDescriptor(_p0, revision, sd, size)
}
func _convertStringSecurityDescriptorToSecurityDescriptor(str *uint16, revision uint32, sd *uintptr, size *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procConvertStringSecurityDescriptorToSecurityDescriptorW.Addr(), 4, uintptr(unsafe.Pointer(str)), uintptr(revision), uintptr(unsafe.Pointer(sd)), uintptr(unsafe.Pointer(size)), 0, 0)
r1, _, e1 := syscall.SyscallN(procConvertSidToStringSidW.Addr(), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(str)))
if r1 == 0 {
err = errnoErr(e1)
}
@@ -126,21 +94,15 @@ func _convertStringSecurityDescriptorToSecurityDescriptor(str *uint16, revision
}
func convertStringSidToSid(str *uint16, sid **byte) (err error) {
r1, _, e1 := syscall.Syscall(procConvertStringSidToSidW.Addr(), 2, uintptr(unsafe.Pointer(str)), uintptr(unsafe.Pointer(sid)), 0)
r1, _, e1 := syscall.SyscallN(procConvertStringSidToSidW.Addr(), uintptr(unsafe.Pointer(str)), uintptr(unsafe.Pointer(sid)))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func getSecurityDescriptorLength(sd uintptr) (len uint32) {
r0, _, _ := syscall.Syscall(procGetSecurityDescriptorLength.Addr(), 1, uintptr(sd), 0, 0)
len = uint32(r0)
return
}
func impersonateSelf(level uint32) (err error) {
r1, _, e1 := syscall.Syscall(procImpersonateSelf.Addr(), 1, uintptr(level), 0, 0)
r1, _, e1 := syscall.SyscallN(procImpersonateSelf.Addr(), uintptr(level))
if r1 == 0 {
err = errnoErr(e1)
}
@@ -157,7 +119,7 @@ func lookupAccountName(systemName *uint16, accountName string, sid *byte, sidSiz
}
func _lookupAccountName(systemName *uint16, accountName *uint16, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) {
r1, _, e1 := syscall.Syscall9(procLookupAccountNameW.Addr(), 7, uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(accountName)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(sidSize)), uintptr(unsafe.Pointer(refDomain)), uintptr(unsafe.Pointer(refDomainSize)), uintptr(unsafe.Pointer(sidNameUse)), 0, 0)
r1, _, e1 := syscall.SyscallN(procLookupAccountNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(accountName)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(sidSize)), uintptr(unsafe.Pointer(refDomain)), uintptr(unsafe.Pointer(refDomainSize)), uintptr(unsafe.Pointer(sidNameUse)))
if r1 == 0 {
err = errnoErr(e1)
}
@@ -165,7 +127,7 @@ func _lookupAccountName(systemName *uint16, accountName *uint16, sid *byte, sidS
}
func lookupAccountSid(systemName *uint16, sid *byte, name *uint16, nameSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) {
r1, _, e1 := syscall.Syscall9(procLookupAccountSidW.Addr(), 7, uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(nameSize)), uintptr(unsafe.Pointer(refDomain)), uintptr(unsafe.Pointer(refDomainSize)), uintptr(unsafe.Pointer(sidNameUse)), 0, 0)
r1, _, e1 := syscall.SyscallN(procLookupAccountSidW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(nameSize)), uintptr(unsafe.Pointer(refDomain)), uintptr(unsafe.Pointer(refDomainSize)), uintptr(unsafe.Pointer(sidNameUse)))
if r1 == 0 {
err = errnoErr(e1)
}
@@ -182,7 +144,7 @@ func lookupPrivilegeDisplayName(systemName string, name *uint16, buffer *uint16,
}
func _lookupPrivilegeDisplayName(systemName *uint16, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procLookupPrivilegeDisplayNameW.Addr(), 5, uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size)), uintptr(unsafe.Pointer(languageId)), 0)
r1, _, e1 := syscall.SyscallN(procLookupPrivilegeDisplayNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size)), uintptr(unsafe.Pointer(languageId)))
if r1 == 0 {
err = errnoErr(e1)
}
@@ -199,7 +161,7 @@ func lookupPrivilegeName(systemName string, luid *uint64, buffer *uint16, size *
}
func _lookupPrivilegeName(systemName *uint16, luid *uint64, buffer *uint16, size *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procLookupPrivilegeNameW.Addr(), 4, uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(luid)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size)), 0, 0)
r1, _, e1 := syscall.SyscallN(procLookupPrivilegeNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(luid)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size)))
if r1 == 0 {
err = errnoErr(e1)
}
@@ -221,19 +183,19 @@ func lookupPrivilegeValue(systemName string, name string, luid *uint64) (err err
}
func _lookupPrivilegeValue(systemName *uint16, name *uint16, luid *uint64) (err error) {
r1, _, e1 := syscall.Syscall(procLookupPrivilegeValueW.Addr(), 3, uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(luid)))
r1, _, e1 := syscall.SyscallN(procLookupPrivilegeValueW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(luid)))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func openThreadToken(thread syscall.Handle, accessMask uint32, openAsSelf bool, token *windows.Token) (err error) {
func openThreadToken(thread windows.Handle, accessMask uint32, openAsSelf bool, token *windows.Token) (err error) {
var _p0 uint32
if openAsSelf {
_p0 = 1
}
r1, _, e1 := syscall.Syscall6(procOpenThreadToken.Addr(), 4, uintptr(thread), uintptr(accessMask), uintptr(_p0), uintptr(unsafe.Pointer(token)), 0, 0)
r1, _, e1 := syscall.SyscallN(procOpenThreadToken.Addr(), uintptr(thread), uintptr(accessMask), uintptr(_p0), uintptr(unsafe.Pointer(token)))
if r1 == 0 {
err = errnoErr(e1)
}
@@ -241,14 +203,14 @@ func openThreadToken(thread syscall.Handle, accessMask uint32, openAsSelf bool,
}
func revertToSelf() (err error) {
r1, _, e1 := syscall.Syscall(procRevertToSelf.Addr(), 0, 0, 0, 0)
r1, _, e1 := syscall.SyscallN(procRevertToSelf.Addr())
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func backupRead(h syscall.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) {
func backupRead(h windows.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) {
var _p0 *byte
if len(b) > 0 {
_p0 = &b[0]
@@ -261,14 +223,14 @@ func backupRead(h syscall.Handle, b []byte, bytesRead *uint32, abort bool, proce
if processSecurity {
_p2 = 1
}
r1, _, e1 := syscall.Syscall9(procBackupRead.Addr(), 7, uintptr(h), uintptr(unsafe.Pointer(_p0)), uintptr(len(b)), uintptr(unsafe.Pointer(bytesRead)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(context)), 0, 0)
r1, _, e1 := syscall.SyscallN(procBackupRead.Addr(), uintptr(h), uintptr(unsafe.Pointer(_p0)), uintptr(len(b)), uintptr(unsafe.Pointer(bytesRead)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(context)))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func backupWrite(h syscall.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) {
func backupWrite(h windows.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) {
var _p0 *byte
if len(b) > 0 {
_p0 = &b[0]
@@ -281,39 +243,39 @@ func backupWrite(h syscall.Handle, b []byte, bytesWritten *uint32, abort bool, p
if processSecurity {
_p2 = 1
}
r1, _, e1 := syscall.Syscall9(procBackupWrite.Addr(), 7, uintptr(h), uintptr(unsafe.Pointer(_p0)), uintptr(len(b)), uintptr(unsafe.Pointer(bytesWritten)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(context)), 0, 0)
r1, _, e1 := syscall.SyscallN(procBackupWrite.Addr(), uintptr(h), uintptr(unsafe.Pointer(_p0)), uintptr(len(b)), uintptr(unsafe.Pointer(bytesWritten)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(context)))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func cancelIoEx(file syscall.Handle, o *syscall.Overlapped) (err error) {
r1, _, e1 := syscall.Syscall(procCancelIoEx.Addr(), 2, uintptr(file), uintptr(unsafe.Pointer(o)), 0)
func cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) {
r1, _, e1 := syscall.SyscallN(procCancelIoEx.Addr(), uintptr(file), uintptr(unsafe.Pointer(o)))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func connectNamedPipe(pipe syscall.Handle, o *syscall.Overlapped) (err error) {
r1, _, e1 := syscall.Syscall(procConnectNamedPipe.Addr(), 2, uintptr(pipe), uintptr(unsafe.Pointer(o)), 0)
func connectNamedPipe(pipe windows.Handle, o *windows.Overlapped) (err error) {
r1, _, e1 := syscall.SyscallN(procConnectNamedPipe.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(o)))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func createIoCompletionPort(file syscall.Handle, port syscall.Handle, key uintptr, threadCount uint32) (newport syscall.Handle, err error) {
r0, _, e1 := syscall.Syscall6(procCreateIoCompletionPort.Addr(), 4, uintptr(file), uintptr(port), uintptr(key), uintptr(threadCount), 0, 0)
newport = syscall.Handle(r0)
func createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) {
r0, _, e1 := syscall.SyscallN(procCreateIoCompletionPort.Addr(), uintptr(file), uintptr(port), uintptr(key), uintptr(threadCount))
newport = windows.Handle(r0)
if newport == 0 {
err = errnoErr(e1)
}
return
}
func createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *syscall.SecurityAttributes) (handle syscall.Handle, err error) {
func createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error) {
var _p0 *uint16
_p0, err = syscall.UTF16PtrFromString(name)
if err != nil {
@@ -322,96 +284,93 @@ func createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances ui
return _createNamedPipe(_p0, flags, pipeMode, maxInstances, outSize, inSize, defaultTimeout, sa)
}
func _createNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *syscall.SecurityAttributes) (handle syscall.Handle, err error) {
r0, _, e1 := syscall.Syscall9(procCreateNamedPipeW.Addr(), 8, uintptr(unsafe.Pointer(name)), uintptr(flags), uintptr(pipeMode), uintptr(maxInstances), uintptr(outSize), uintptr(inSize), uintptr(defaultTimeout), uintptr(unsafe.Pointer(sa)), 0)
handle = syscall.Handle(r0)
if handle == syscall.InvalidHandle {
func _createNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error) {
r0, _, e1 := syscall.SyscallN(procCreateNamedPipeW.Addr(), uintptr(unsafe.Pointer(name)), uintptr(flags), uintptr(pipeMode), uintptr(maxInstances), uintptr(outSize), uintptr(inSize), uintptr(defaultTimeout), uintptr(unsafe.Pointer(sa)))
handle = windows.Handle(r0)
if handle == windows.InvalidHandle {
err = errnoErr(e1)
}
return
}
func getCurrentThread() (h syscall.Handle) {
r0, _, _ := syscall.Syscall(procGetCurrentThread.Addr(), 0, 0, 0, 0)
h = syscall.Handle(r0)
return
}
func getNamedPipeHandleState(pipe syscall.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) {
r1, _, e1 := syscall.Syscall9(procGetNamedPipeHandleStateW.Addr(), 7, uintptr(pipe), uintptr(unsafe.Pointer(state)), uintptr(unsafe.Pointer(curInstances)), uintptr(unsafe.Pointer(maxCollectionCount)), uintptr(unsafe.Pointer(collectDataTimeout)), uintptr(unsafe.Pointer(userName)), uintptr(maxUserNameSize), 0, 0)
func disconnectNamedPipe(pipe windows.Handle) (err error) {
r1, _, e1 := syscall.SyscallN(procDisconnectNamedPipe.Addr(), uintptr(pipe))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func getNamedPipeInfo(pipe syscall.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procGetNamedPipeInfo.Addr(), 5, uintptr(pipe), uintptr(unsafe.Pointer(flags)), uintptr(unsafe.Pointer(outSize)), uintptr(unsafe.Pointer(inSize)), uintptr(unsafe.Pointer(maxInstances)), 0)
func getCurrentThread() (h windows.Handle) {
r0, _, _ := syscall.SyscallN(procGetCurrentThread.Addr())
h = windows.Handle(r0)
return
}
func getNamedPipeHandleState(pipe windows.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) {
r1, _, e1 := syscall.SyscallN(procGetNamedPipeHandleStateW.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(state)), uintptr(unsafe.Pointer(curInstances)), uintptr(unsafe.Pointer(maxCollectionCount)), uintptr(unsafe.Pointer(collectDataTimeout)), uintptr(unsafe.Pointer(userName)), uintptr(maxUserNameSize))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func getQueuedCompletionStatus(port syscall.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procGetQueuedCompletionStatus.Addr(), 5, uintptr(port), uintptr(unsafe.Pointer(bytes)), uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(o)), uintptr(timeout), 0)
func getNamedPipeInfo(pipe windows.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) {
r1, _, e1 := syscall.SyscallN(procGetNamedPipeInfo.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(flags)), uintptr(unsafe.Pointer(outSize)), uintptr(unsafe.Pointer(inSize)), uintptr(unsafe.Pointer(maxInstances)))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func localAlloc(uFlags uint32, length uint32) (ptr uintptr) {
r0, _, _ := syscall.Syscall(procLocalAlloc.Addr(), 2, uintptr(uFlags), uintptr(length), 0)
ptr = uintptr(r0)
return
}
func localFree(mem uintptr) {
syscall.Syscall(procLocalFree.Addr(), 1, uintptr(mem), 0, 0)
return
}
func setFileCompletionNotificationModes(h syscall.Handle, flags uint8) (err error) {
r1, _, e1 := syscall.Syscall(procSetFileCompletionNotificationModes.Addr(), 2, uintptr(h), uintptr(flags), 0)
func getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) {
r1, _, e1 := syscall.SyscallN(procGetQueuedCompletionStatus.Addr(), uintptr(port), uintptr(unsafe.Pointer(bytes)), uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(o)), uintptr(timeout))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func ntCreateNamedPipeFile(pipe *syscall.Handle, access uint32, oa *objectAttributes, iosb *ioStatusBlock, share uint32, disposition uint32, options uint32, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (status ntStatus) {
r0, _, _ := syscall.Syscall15(procNtCreateNamedPipeFile.Addr(), 14, uintptr(unsafe.Pointer(pipe)), uintptr(access), uintptr(unsafe.Pointer(oa)), uintptr(unsafe.Pointer(iosb)), uintptr(share), uintptr(disposition), uintptr(options), uintptr(typ), uintptr(readMode), uintptr(completionMode), uintptr(maxInstances), uintptr(inboundQuota), uintptr(outputQuota), uintptr(unsafe.Pointer(timeout)), 0)
func setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) {
r1, _, e1 := syscall.SyscallN(procSetFileCompletionNotificationModes.Addr(), uintptr(h), uintptr(flags))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func ntCreateNamedPipeFile(pipe *windows.Handle, access ntAccessMask, oa *objectAttributes, iosb *ioStatusBlock, share ntFileShareMode, disposition ntFileCreationDisposition, options ntFileOptions, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (status ntStatus) {
r0, _, _ := syscall.SyscallN(procNtCreateNamedPipeFile.Addr(), uintptr(unsafe.Pointer(pipe)), uintptr(access), uintptr(unsafe.Pointer(oa)), uintptr(unsafe.Pointer(iosb)), uintptr(share), uintptr(disposition), uintptr(options), uintptr(typ), uintptr(readMode), uintptr(completionMode), uintptr(maxInstances), uintptr(inboundQuota), uintptr(outputQuota), uintptr(unsafe.Pointer(timeout)))
status = ntStatus(r0)
return
}
func rtlDefaultNpAcl(dacl *uintptr) (status ntStatus) {
r0, _, _ := syscall.Syscall(procRtlDefaultNpAcl.Addr(), 1, uintptr(unsafe.Pointer(dacl)), 0, 0)
r0, _, _ := syscall.SyscallN(procRtlDefaultNpAcl.Addr(), uintptr(unsafe.Pointer(dacl)))
status = ntStatus(r0)
return
}
func rtlDosPathNameToNtPathName(name *uint16, ntName *unicodeString, filePart uintptr, reserved uintptr) (status ntStatus) {
r0, _, _ := syscall.Syscall6(procRtlDosPathNameToNtPathName_U.Addr(), 4, uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(ntName)), uintptr(filePart), uintptr(reserved), 0, 0)
r0, _, _ := syscall.SyscallN(procRtlDosPathNameToNtPathName_U.Addr(), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(ntName)), uintptr(filePart), uintptr(reserved))
status = ntStatus(r0)
return
}
func rtlNtStatusToDosError(status ntStatus) (winerr error) {
r0, _, _ := syscall.Syscall(procRtlNtStatusToDosErrorNoTeb.Addr(), 1, uintptr(status), 0, 0)
r0, _, _ := syscall.SyscallN(procRtlNtStatusToDosErrorNoTeb.Addr(), uintptr(status))
if r0 != 0 {
winerr = syscall.Errno(r0)
}
return
}
func wsaGetOverlappedResult(h syscall.Handle, o *syscall.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) {
func wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) {
var _p0 uint32
if wait {
_p0 = 1
}
r1, _, e1 := syscall.Syscall6(procWSAGetOverlappedResult.Addr(), 5, uintptr(h), uintptr(unsafe.Pointer(o)), uintptr(unsafe.Pointer(bytes)), uintptr(_p0), uintptr(unsafe.Pointer(flags)), 0)
r1, _, e1 := syscall.SyscallN(procWSAGetOverlappedResult.Addr(), uintptr(h), uintptr(unsafe.Pointer(o)), uintptr(unsafe.Pointer(bytes)), uintptr(_p0), uintptr(unsafe.Pointer(flags)))
if r1 == 0 {
err = errnoErr(e1)
}

View File

@@ -49,16 +49,16 @@ func ShiftNBytesLeft(dst, x []byte, n int) {
dst = append(dst, make([]byte, n/8)...)
}
// XorBytesMut assumes equal input length, replaces X with X XOR Y
// XorBytesMut replaces X with X XOR Y. len(X) must be >= len(Y).
func XorBytesMut(X, Y []byte) {
for i := 0; i < len(X); i++ {
for i := 0; i < len(Y); i++ {
X[i] ^= Y[i]
}
}
// XorBytes assumes equal input length, puts X XOR Y into Z
// XorBytes puts X XOR Y into Z. len(Z) and len(X) must be >= len(Y).
func XorBytes(Z, X, Y []byte) {
for i := 0; i < len(X); i++ {
for i := 0; i < len(Y); i++ {
Z[i] = X[i] ^ Y[i]
}
}

View File

@@ -18,8 +18,9 @@ import (
"crypto/cipher"
"crypto/subtle"
"errors"
"github.com/ProtonMail/go-crypto/internal/byteutil"
"math/bits"
"github.com/ProtonMail/go-crypto/internal/byteutil"
)
type ocb struct {
@@ -108,8 +109,10 @@ func (o *ocb) Seal(dst, nonce, plaintext, adata []byte) []byte {
if len(nonce) > o.nonceSize {
panic("crypto/ocb: Incorrect nonce length given to OCB")
}
ret, out := byteutil.SliceForAppend(dst, len(plaintext)+o.tagSize)
o.crypt(enc, out, nonce, adata, plaintext)
sep := len(plaintext)
ret, out := byteutil.SliceForAppend(dst, sep+o.tagSize)
tag := o.crypt(enc, out[:sep], nonce, adata, plaintext)
copy(out[sep:], tag)
return ret
}
@@ -121,12 +124,10 @@ func (o *ocb) Open(dst, nonce, ciphertext, adata []byte) ([]byte, error) {
return nil, ocbError("Ciphertext shorter than tag length")
}
sep := len(ciphertext) - o.tagSize
ret, out := byteutil.SliceForAppend(dst, len(ciphertext))
ret, out := byteutil.SliceForAppend(dst, sep)
ciphertextData := ciphertext[:sep]
tag := ciphertext[sep:]
o.crypt(dec, out, nonce, adata, ciphertextData)
if subtle.ConstantTimeCompare(ret[sep:], tag) == 1 {
ret = ret[:sep]
tag := o.crypt(dec, out, nonce, adata, ciphertextData)
if subtle.ConstantTimeCompare(tag, ciphertext[sep:]) == 1 {
return ret, nil
}
for i := range out {
@@ -136,7 +137,8 @@ func (o *ocb) Open(dst, nonce, ciphertext, adata []byte) ([]byte, error) {
}
// On instruction enc (resp. dec), crypt is the encrypt (resp. decrypt)
// function. It returns the resulting plain/ciphertext with the tag appended.
// function. It writes the resulting plain/ciphertext into Y and returns
// the tag.
func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte {
//
// Consider X as a sequence of 128-bit blocks
@@ -153,7 +155,7 @@ func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte {
truncatedNonce := make([]byte, len(nonce))
copy(truncatedNonce, nonce)
truncatedNonce[len(truncatedNonce)-1] &= 192
Ktop := make([]byte, blockSize)
var Ktop []byte
if bytes.Equal(truncatedNonce, o.reusableKtop.noncePrefix) {
Ktop = o.reusableKtop.Ktop
} else {
@@ -193,13 +195,14 @@ func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte {
byteutil.XorBytesMut(offset, o.mask.L[bits.TrailingZeros(uint(i+1))])
blockX := X[i*blockSize : (i+1)*blockSize]
blockY := Y[i*blockSize : (i+1)*blockSize]
byteutil.XorBytes(blockY, blockX, offset)
switch instruction {
case enc:
byteutil.XorBytesMut(checksum, blockX)
byteutil.XorBytes(blockY, blockX, offset)
o.block.Encrypt(blockY, blockY)
byteutil.XorBytesMut(blockY, offset)
byteutil.XorBytesMut(checksum, blockX)
case dec:
byteutil.XorBytes(blockY, blockX, offset)
o.block.Decrypt(blockY, blockY)
byteutil.XorBytesMut(blockY, offset)
byteutil.XorBytesMut(checksum, blockY)
@@ -215,31 +218,24 @@ func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte {
o.block.Encrypt(pad, offset)
chunkX := X[blockSize*m:]
chunkY := Y[blockSize*m : len(X)]
byteutil.XorBytes(chunkY, chunkX, pad[:len(chunkX)])
// P_* || bit(1) || zeroes(127) - len(P_*)
switch instruction {
case enc:
paddedY := append(chunkX, byte(128))
paddedY = append(paddedY, make([]byte, blockSize-len(chunkX)-1)...)
byteutil.XorBytesMut(checksum, paddedY)
byteutil.XorBytesMut(checksum, chunkX)
checksum[len(chunkX)] ^= 128
byteutil.XorBytes(chunkY, chunkX, pad[:len(chunkX)])
// P_* || bit(1) || zeroes(127) - len(P_*)
case dec:
paddedX := append(chunkY, byte(128))
paddedX = append(paddedX, make([]byte, blockSize-len(chunkY)-1)...)
byteutil.XorBytesMut(checksum, paddedX)
byteutil.XorBytes(chunkY, chunkX, pad[:len(chunkX)])
// P_* || bit(1) || zeroes(127) - len(P_*)
byteutil.XorBytesMut(checksum, chunkY)
checksum[len(chunkY)] ^= 128
}
byteutil.XorBytes(tag, checksum, offset)
byteutil.XorBytesMut(tag, o.mask.lDol)
o.block.Encrypt(tag, tag)
byteutil.XorBytesMut(tag, o.hash(adata))
copy(Y[blockSize*m+len(chunkY):], tag[:o.tagSize])
} else {
byteutil.XorBytes(tag, checksum, offset)
byteutil.XorBytesMut(tag, o.mask.lDol)
o.block.Encrypt(tag, tag)
byteutil.XorBytesMut(tag, o.hash(adata))
copy(Y[blockSize*m:], tag[:o.tagSize])
}
return Y
byteutil.XorBytes(tag, checksum, offset)
byteutil.XorBytesMut(tag, o.mask.lDol)
o.block.Encrypt(tag, tag)
byteutil.XorBytesMut(tag, o.hash(adata))
return tag[:o.tagSize]
}
// This hash function is used to compute the tag. Per design, on empty input it

View File

@@ -23,7 +23,7 @@ import (
// Headers
//
// base64-encoded Bytes
// '=' base64 encoded checksum
// '=' base64 encoded checksum (optional) not checked anymore
// -----END Type-----
//
// where Headers is a possibly empty sequence of Key: Value lines.
@@ -40,36 +40,15 @@ type Block struct {
var ArmorCorrupt error = errors.StructuralError("armor invalid")
const crc24Init = 0xb704ce
const crc24Poly = 0x1864cfb
const crc24Mask = 0xffffff
// crc24 calculates the OpenPGP checksum as specified in RFC 4880, section 6.1
func crc24(crc uint32, d []byte) uint32 {
for _, b := range d {
crc ^= uint32(b) << 16
for i := 0; i < 8; i++ {
crc <<= 1
if crc&0x1000000 != 0 {
crc ^= crc24Poly
}
}
}
return crc
}
var armorStart = []byte("-----BEGIN ")
var armorEnd = []byte("-----END ")
var armorEndOfLine = []byte("-----")
// lineReader wraps a line based reader. It watches for the end of an armor
// block and records the expected CRC value.
// lineReader wraps a line based reader. It watches for the end of an armor block
type lineReader struct {
in *bufio.Reader
buf []byte
eof bool
crc uint32
crcSet bool
in *bufio.Reader
buf []byte
eof bool
}
func (l *lineReader) Read(p []byte) (n int, err error) {
@@ -98,26 +77,9 @@ func (l *lineReader) Read(p []byte) (n int, err error) {
if len(line) == 5 && line[0] == '=' {
// This is the checksum line
var expectedBytes [3]byte
var m int
m, err = base64.StdEncoding.Decode(expectedBytes[0:], line[1:])
if m != 3 || err != nil {
return
}
l.crc = uint32(expectedBytes[0])<<16 |
uint32(expectedBytes[1])<<8 |
uint32(expectedBytes[2])
line, _, err = l.in.ReadLine()
if err != nil && err != io.EOF {
return
}
if !bytes.HasPrefix(line, armorEnd) {
return 0, ArmorCorrupt
}
// Don't check the checksum
l.eof = true
l.crcSet = true
return 0, io.EOF
}
@@ -138,23 +100,14 @@ func (l *lineReader) Read(p []byte) (n int, err error) {
return
}
// openpgpReader passes Read calls to the underlying base64 decoder, but keeps
// a running CRC of the resulting data and checks the CRC against the value
// found by the lineReader at EOF.
// openpgpReader passes Read calls to the underlying base64 decoder.
type openpgpReader struct {
lReader *lineReader
b64Reader io.Reader
currentCRC uint32
lReader *lineReader
b64Reader io.Reader
}
func (r *openpgpReader) Read(p []byte) (n int, err error) {
n, err = r.b64Reader.Read(p)
r.currentCRC = crc24(r.currentCRC, p[:n])
if err == io.EOF && r.lReader.crcSet && r.lReader.crc != uint32(r.currentCRC&crc24Mask) {
return 0, ArmorCorrupt
}
return
}
@@ -222,7 +175,6 @@ TryNextBlock:
}
p.lReader.in = r
p.oReader.currentCRC = crc24Init
p.oReader.lReader = &p.lReader
p.oReader.b64Reader = base64.NewDecoder(base64.StdEncoding, &p.lReader)
p.Body = &p.oReader

View File

@@ -7,6 +7,7 @@ package armor
import (
"encoding/base64"
"io"
"sort"
)
var armorHeaderSep = []byte(": ")
@@ -14,6 +15,23 @@ var blockEnd = []byte("\n=")
var newline = []byte("\n")
var armorEndOfLineOut = []byte("-----\n")
const crc24Init = 0xb704ce
const crc24Poly = 0x1864cfb
// crc24 calculates the OpenPGP checksum as specified in RFC 4880, section 6.1
func crc24(crc uint32, d []byte) uint32 {
for _, b := range d {
crc ^= uint32(b) << 16
for i := 0; i < 8; i++ {
crc <<= 1
if crc&0x1000000 != 0 {
crc ^= crc24Poly
}
}
}
return crc
}
// writeSlices writes its arguments to the given Writer.
func writeSlices(out io.Writer, slices ...[]byte) (err error) {
for _, s := range slices {
@@ -99,15 +117,18 @@ func (l *lineBreaker) Close() (err error) {
//
// encoding -> base64 encoder -> lineBreaker -> out
type encoding struct {
out io.Writer
breaker *lineBreaker
b64 io.WriteCloser
crc uint32
blockType []byte
out io.Writer
breaker *lineBreaker
b64 io.WriteCloser
crc uint32
crcEnabled bool
blockType []byte
}
func (e *encoding) Write(data []byte) (n int, err error) {
e.crc = crc24(e.crc, data)
if e.crcEnabled {
e.crc = crc24(e.crc, data)
}
return e.b64.Write(data)
}
@@ -118,28 +139,36 @@ func (e *encoding) Close() (err error) {
}
e.breaker.Close()
var checksumBytes [3]byte
checksumBytes[0] = byte(e.crc >> 16)
checksumBytes[1] = byte(e.crc >> 8)
checksumBytes[2] = byte(e.crc)
if e.crcEnabled {
var checksumBytes [3]byte
checksumBytes[0] = byte(e.crc >> 16)
checksumBytes[1] = byte(e.crc >> 8)
checksumBytes[2] = byte(e.crc)
var b64ChecksumBytes [4]byte
base64.StdEncoding.Encode(b64ChecksumBytes[:], checksumBytes[:])
var b64ChecksumBytes [4]byte
base64.StdEncoding.Encode(b64ChecksumBytes[:], checksumBytes[:])
return writeSlices(e.out, blockEnd, b64ChecksumBytes[:], newline, armorEnd, e.blockType, armorEndOfLine)
return writeSlices(e.out, blockEnd, b64ChecksumBytes[:], newline, armorEnd, e.blockType, armorEndOfLine)
}
return writeSlices(e.out, newline, armorEnd, e.blockType, armorEndOfLine)
}
// Encode returns a WriteCloser which will encode the data written to it in
// OpenPGP armor.
func Encode(out io.Writer, blockType string, headers map[string]string) (w io.WriteCloser, err error) {
func encode(out io.Writer, blockType string, headers map[string]string, checksum bool) (w io.WriteCloser, err error) {
bType := []byte(blockType)
err = writeSlices(out, armorStart, bType, armorEndOfLineOut)
if err != nil {
return
}
for k, v := range headers {
err = writeSlices(out, []byte(k), armorHeaderSep, []byte(v), newline)
keys := make([]string, len(headers))
i := 0
for k := range headers {
keys[i] = k
i++
}
sort.Strings(keys)
for _, k := range keys {
err = writeSlices(out, []byte(k), armorHeaderSep, []byte(headers[k]), newline)
if err != nil {
return
}
@@ -151,11 +180,27 @@ func Encode(out io.Writer, blockType string, headers map[string]string) (w io.Wr
}
e := &encoding{
out: out,
breaker: newLineBreaker(out, 64),
crc: crc24Init,
blockType: bType,
out: out,
breaker: newLineBreaker(out, 64),
blockType: bType,
crc: crc24Init,
crcEnabled: checksum,
}
e.b64 = base64.NewEncoder(base64.StdEncoding, e.breaker)
return e, nil
}
// Encode returns a WriteCloser which will encode the data written to it in
// OpenPGP armor.
func Encode(out io.Writer, blockType string, headers map[string]string) (w io.WriteCloser, err error) {
return encode(out, blockType, headers, true)
}
// EncodeWithChecksumOption returns a WriteCloser which will encode the data written to it in
// OpenPGP armor and provides the option to include a checksum.
// When forming ASCII Armor, the CRC24 footer SHOULD NOT be generated,
// unless interoperability with implementations that require the CRC24 footer
// to be present is a concern.
func EncodeWithChecksumOption(out io.Writer, blockType string, headers map[string]string, doChecksum bool) (w io.WriteCloser, err error) {
return encode(out, blockType, headers, doChecksum)
}

View File

@@ -30,8 +30,12 @@ func writeCanonical(cw io.Writer, buf []byte, s *int) (int, error) {
if c == '\r' {
*s = 1
} else if c == '\n' {
cw.Write(buf[start:i])
cw.Write(newline)
if _, err := cw.Write(buf[start:i]); err != nil {
return 0, err
}
if _, err := cw.Write(newline); err != nil {
return 0, err
}
start = i + 1
}
case 1:
@@ -39,7 +43,9 @@ func writeCanonical(cw io.Writer, buf []byte, s *int) (int, error) {
}
}
cw.Write(buf[start:])
if _, err := cw.Write(buf[start:]); err != nil {
return 0, err
}
return len(buf), nil
}

View File

@@ -163,13 +163,9 @@ func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLead
if _, err := param.Write([]byte("Anonymous Sender ")); err != nil {
return nil, err
}
// For v5 keys, the 20 leftmost octets of the fingerprint are used.
if _, err := param.Write(fingerprint[:20]); err != nil {
if _, err := param.Write(fingerprint[:]); err != nil {
return nil, err
}
if param.Len()-len(curveOID) != 45 {
return nil, errors.New("ecdh: malformed KDF Param")
}
// MB = Hash ( 00 || 00 || 00 || 01 || ZB || Param );
h := pub.KDF.Hash.New()

View File

@@ -0,0 +1,115 @@
// Package ed25519 implements the ed25519 signature algorithm for OpenPGP
// as defined in the Open PGP crypto refresh.
package ed25519
import (
"crypto/subtle"
"io"
"github.com/ProtonMail/go-crypto/openpgp/errors"
ed25519lib "github.com/cloudflare/circl/sign/ed25519"
)
const (
// PublicKeySize is the size, in bytes, of public keys in this package.
PublicKeySize = ed25519lib.PublicKeySize
// SeedSize is the size, in bytes, of private key seeds.
// The private key representation used by RFC 8032.
SeedSize = ed25519lib.SeedSize
// SignatureSize is the size, in bytes, of signatures generated and verified by this package.
SignatureSize = ed25519lib.SignatureSize
)
type PublicKey struct {
// Point represents the elliptic curve point of the public key.
Point []byte
}
type PrivateKey struct {
PublicKey
// Key the private key representation by RFC 8032,
// encoded as seed | pub key point.
Key []byte
}
// NewPublicKey creates a new empty ed25519 public key.
func NewPublicKey() *PublicKey {
return &PublicKey{}
}
// NewPrivateKey creates a new empty private key referencing the public key.
func NewPrivateKey(key PublicKey) *PrivateKey {
return &PrivateKey{
PublicKey: key,
}
}
// Seed returns the ed25519 private key secret seed.
// The private key representation by RFC 8032.
func (pk *PrivateKey) Seed() []byte {
return pk.Key[:SeedSize]
}
// MarshalByteSecret returns the underlying 32 byte seed of the private key.
func (pk *PrivateKey) MarshalByteSecret() []byte {
return pk.Seed()
}
// UnmarshalByteSecret computes the private key from the secret seed
// and stores it in the private key object.
func (sk *PrivateKey) UnmarshalByteSecret(seed []byte) error {
sk.Key = ed25519lib.NewKeyFromSeed(seed)
return nil
}
// GenerateKey generates a fresh private key with the provided randomness source.
func GenerateKey(rand io.Reader) (*PrivateKey, error) {
publicKey, privateKey, err := ed25519lib.GenerateKey(rand)
if err != nil {
return nil, err
}
privateKeyOut := new(PrivateKey)
privateKeyOut.PublicKey.Point = publicKey[:]
privateKeyOut.Key = privateKey[:]
return privateKeyOut, nil
}
// Sign signs a message with the ed25519 algorithm.
// priv MUST be a valid key! Check this with Validate() before use.
func Sign(priv *PrivateKey, message []byte) ([]byte, error) {
return ed25519lib.Sign(priv.Key, message), nil
}
// Verify verifies an ed25519 signature.
func Verify(pub *PublicKey, message []byte, signature []byte) bool {
return ed25519lib.Verify(pub.Point, message, signature)
}
// Validate checks if the ed25519 private key is valid.
func Validate(priv *PrivateKey) error {
expectedPrivateKey := ed25519lib.NewKeyFromSeed(priv.Seed())
if subtle.ConstantTimeCompare(priv.Key, expectedPrivateKey) == 0 {
return errors.KeyInvalidError("ed25519: invalid ed25519 secret")
}
if subtle.ConstantTimeCompare(priv.PublicKey.Point, expectedPrivateKey[SeedSize:]) == 0 {
return errors.KeyInvalidError("ed25519: invalid ed25519 public key")
}
return nil
}
// ENCODING/DECODING signature:
// WriteSignature encodes and writes an ed25519 signature to writer.
func WriteSignature(writer io.Writer, signature []byte) error {
_, err := writer.Write(signature)
return err
}
// ReadSignature decodes an ed25519 signature from a reader.
func ReadSignature(reader io.Reader) ([]byte, error) {
signature := make([]byte, SignatureSize)
if _, err := io.ReadFull(reader, signature); err != nil {
return nil, err
}
return signature, nil
}

View File

@@ -0,0 +1,119 @@
// Package ed448 implements the ed448 signature algorithm for OpenPGP
// as defined in the Open PGP crypto refresh.
package ed448
import (
"crypto/subtle"
"io"
"github.com/ProtonMail/go-crypto/openpgp/errors"
ed448lib "github.com/cloudflare/circl/sign/ed448"
)
const (
// PublicKeySize is the size, in bytes, of public keys in this package.
PublicKeySize = ed448lib.PublicKeySize
// SeedSize is the size, in bytes, of private key seeds.
// The private key representation used by RFC 8032.
SeedSize = ed448lib.SeedSize
// SignatureSize is the size, in bytes, of signatures generated and verified by this package.
SignatureSize = ed448lib.SignatureSize
)
type PublicKey struct {
// Point represents the elliptic curve point of the public key.
Point []byte
}
type PrivateKey struct {
PublicKey
// Key the private key representation by RFC 8032,
// encoded as seed | public key point.
Key []byte
}
// NewPublicKey creates a new empty ed448 public key.
func NewPublicKey() *PublicKey {
return &PublicKey{}
}
// NewPrivateKey creates a new empty private key referencing the public key.
func NewPrivateKey(key PublicKey) *PrivateKey {
return &PrivateKey{
PublicKey: key,
}
}
// Seed returns the ed448 private key secret seed.
// The private key representation by RFC 8032.
func (pk *PrivateKey) Seed() []byte {
return pk.Key[:SeedSize]
}
// MarshalByteSecret returns the underlying seed of the private key.
func (pk *PrivateKey) MarshalByteSecret() []byte {
return pk.Seed()
}
// UnmarshalByteSecret computes the private key from the secret seed
// and stores it in the private key object.
func (sk *PrivateKey) UnmarshalByteSecret(seed []byte) error {
sk.Key = ed448lib.NewKeyFromSeed(seed)
return nil
}
// GenerateKey generates a fresh private key with the provided randomness source.
func GenerateKey(rand io.Reader) (*PrivateKey, error) {
publicKey, privateKey, err := ed448lib.GenerateKey(rand)
if err != nil {
return nil, err
}
privateKeyOut := new(PrivateKey)
privateKeyOut.PublicKey.Point = publicKey[:]
privateKeyOut.Key = privateKey[:]
return privateKeyOut, nil
}
// Sign signs a message with the ed448 algorithm.
// priv MUST be a valid key! Check this with Validate() before use.
func Sign(priv *PrivateKey, message []byte) ([]byte, error) {
// Ed448 is used with the empty string as a context string.
// See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-08#section-13.7
return ed448lib.Sign(priv.Key, message, ""), nil
}
// Verify verifies a ed448 signature
func Verify(pub *PublicKey, message []byte, signature []byte) bool {
// Ed448 is used with the empty string as a context string.
// See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-08#section-13.7
return ed448lib.Verify(pub.Point, message, signature, "")
}
// Validate checks if the ed448 private key is valid
func Validate(priv *PrivateKey) error {
expectedPrivateKey := ed448lib.NewKeyFromSeed(priv.Seed())
if subtle.ConstantTimeCompare(priv.Key, expectedPrivateKey) == 0 {
return errors.KeyInvalidError("ed448: invalid ed448 secret")
}
if subtle.ConstantTimeCompare(priv.PublicKey.Point, expectedPrivateKey[SeedSize:]) == 0 {
return errors.KeyInvalidError("ed448: invalid ed448 public key")
}
return nil
}
// ENCODING/DECODING signature:
// WriteSignature encodes and writes an ed448 signature to writer.
func WriteSignature(writer io.Writer, signature []byte) error {
_, err := writer.Write(signature)
return err
}
// ReadSignature decodes an ed448 signature from a reader.
func ReadSignature(reader io.Reader) ([]byte, error) {
signature := make([]byte, SignatureSize)
if _, err := io.ReadFull(reader, signature); err != nil {
return nil, err
}
return signature, nil
}

View File

@@ -6,9 +6,22 @@
package errors // import "github.com/ProtonMail/go-crypto/openpgp/errors"
import (
"fmt"
"strconv"
)
var (
// ErrDecryptSessionKeyParsing is a generic error message for parsing errors in decrypted data
// to reduce the risk of oracle attacks.
ErrDecryptSessionKeyParsing = DecryptWithSessionKeyError("parsing error")
// ErrAEADTagVerification is returned if one of the tag verifications in SEIPDv2 fails
ErrAEADTagVerification error = DecryptWithSessionKeyError("AEAD tag verification failed")
// ErrMDCHashMismatch
ErrMDCHashMismatch error = SignatureError("MDC hash mismatch")
// ErrMDCMissing
ErrMDCMissing error = SignatureError("MDC packet not found")
)
// A StructuralError is returned when OpenPGP data is found to be syntactically
// invalid.
type StructuralError string
@@ -17,6 +30,34 @@ func (s StructuralError) Error() string {
return "openpgp: invalid data: " + string(s)
}
// A DecryptWithSessionKeyError is returned when a failure occurs when reading from symmetrically decrypted data or
// an authentication tag verification fails.
// Such an error indicates that the supplied session key is likely wrong or the data got corrupted.
type DecryptWithSessionKeyError string
func (s DecryptWithSessionKeyError) Error() string {
return "openpgp: decryption with session key failed: " + string(s)
}
// HandleSensitiveParsingError handles parsing errors when reading data from potentially decrypted data.
// The function makes parsing errors generic to reduce the risk of oracle attacks in SEIPDv1.
func HandleSensitiveParsingError(err error, decrypted bool) error {
if !decrypted {
// Data was not encrypted so we return the inner error.
return err
}
// The data is read from a stream that decrypts using a session key;
// therefore, we need to handle parsing errors appropriately.
// This is essential to mitigate the risk of oracle attacks.
if decError, ok := err.(*DecryptWithSessionKeyError); ok {
return decError
}
if decError, ok := err.(DecryptWithSessionKeyError); ok {
return decError
}
return ErrDecryptSessionKeyParsing
}
// UnsupportedError indicates that, although the OpenPGP data is valid, it
// makes use of currently unimplemented features.
type UnsupportedError string
@@ -41,9 +82,6 @@ func (b SignatureError) Error() string {
return "openpgp: invalid signature: " + string(b)
}
var ErrMDCHashMismatch error = SignatureError("MDC hash mismatch")
var ErrMDCMissing error = SignatureError("MDC packet not found")
type signatureExpiredError int
func (se signatureExpiredError) Error() string {
@@ -58,6 +96,14 @@ func (ke keyExpiredError) Error() string {
return "openpgp: key expired"
}
var ErrSignatureOlderThanKey error = signatureOlderThanKeyError(0)
type signatureOlderThanKeyError int
func (ske signatureOlderThanKeyError) Error() string {
return "openpgp: signature is older than the key"
}
var ErrKeyExpired error = keyExpiredError(0)
type keyIncorrectError int
@@ -92,12 +138,24 @@ func (keyRevokedError) Error() string {
var ErrKeyRevoked error = keyRevokedError(0)
type WeakAlgorithmError string
func (e WeakAlgorithmError) Error() string {
return "openpgp: weak algorithms are rejected: " + string(e)
}
type UnknownPacketTypeError uint8
func (upte UnknownPacketTypeError) Error() string {
return "openpgp: unknown packet type: " + strconv.Itoa(int(upte))
}
type CriticalUnknownPacketTypeError uint8
func (upte CriticalUnknownPacketTypeError) Error() string {
return "openpgp: unknown critical packet type: " + strconv.Itoa(int(upte))
}
// AEADError indicates that there is a problem when initializing or using a
// AEAD instance, configuration struct, nonces or index values.
type AEADError string
@@ -114,3 +172,39 @@ type ErrDummyPrivateKey string
func (dke ErrDummyPrivateKey) Error() string {
return "openpgp: s2k GNU dummy key: " + string(dke)
}
// ErrMalformedMessage results when the packet sequence is incorrect
type ErrMalformedMessage string
func (dke ErrMalformedMessage) Error() string {
return "openpgp: malformed message " + string(dke)
}
type messageTooLargeError int
func (e messageTooLargeError) Error() string {
return "openpgp: decompressed message size exceeds provided limit"
}
// ErrMessageTooLarge is returned if the read data from
// a compressed packet exceeds the provided limit.
var ErrMessageTooLarge error = messageTooLargeError(0)
// ErrEncryptionKeySelection is returned if encryption key selection fails (v2 API).
type ErrEncryptionKeySelection struct {
PrimaryKeyId string
PrimaryKeyErr error
EncSelectionKeyId *string
EncSelectionErr error
}
func (eks ErrEncryptionKeySelection) Error() string {
prefix := fmt.Sprintf("openpgp: key selection for primary key %s:", eks.PrimaryKeyId)
if eks.PrimaryKeyErr != nil {
return fmt.Sprintf("%s invalid primary key: %s", prefix, eks.PrimaryKeyErr)
}
if eks.EncSelectionKeyId != nil {
return fmt.Sprintf("%s invalid encryption key %s: %s", prefix, *eks.EncSelectionKeyId, eks.EncSelectionErr)
}
return fmt.Sprintf("%s no encryption key: %s", prefix, eks.EncSelectionErr)
}

View File

@@ -51,24 +51,14 @@ func (sk CipherFunction) Id() uint8 {
return uint8(sk)
}
var keySizeByID = map[uint8]int{
TripleDES.Id(): 24,
CAST5.Id(): cast5.KeySize,
AES128.Id(): 16,
AES192.Id(): 24,
AES256.Id(): 32,
}
// KeySize returns the key size, in bytes, of cipher.
func (cipher CipherFunction) KeySize() int {
switch cipher {
case TripleDES:
return 24
case CAST5:
return cast5.KeySize
case AES128:
return 16
case AES192:
case AES192, TripleDES:
return 24
case AES256:
return 32

View File

@@ -4,11 +4,14 @@ package ecc
import (
"bytes"
"crypto/elliptic"
"github.com/ProtonMail/go-crypto/bitcurves"
"github.com/ProtonMail/go-crypto/brainpool"
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
)
const Curve25519GenName = "Curve25519"
type CurveInfo struct {
GenName string
Oid *encoding.OID
@@ -42,19 +45,19 @@ var Curves = []CurveInfo{
},
{
// Curve25519
GenName: "Curve25519",
GenName: Curve25519GenName,
Oid: encoding.NewOID([]byte{0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01}),
Curve: NewCurve25519(),
},
{
// X448
// x448
GenName: "Curve448",
Oid: encoding.NewOID([]byte{0x2B, 0x65, 0x6F}),
Curve: NewX448(),
},
{
// Ed25519
GenName: "Curve25519",
GenName: Curve25519GenName,
Oid: encoding.NewOID([]byte{0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01}),
Curve: NewEd25519(),
},

View File

@@ -2,6 +2,7 @@
package ecc
import (
"bytes"
"crypto/subtle"
"io"
@@ -90,7 +91,14 @@ func (c *ed25519) GenerateEdDSA(rand io.Reader) (pub, priv []byte, err error) {
}
func getEd25519Sk(publicKey, privateKey []byte) ed25519lib.PrivateKey {
return append(privateKey, publicKey...)
privateKeyCap, privateKeyLen, publicKeyLen := cap(privateKey), len(privateKey), len(publicKey)
if privateKeyCap >= privateKeyLen+publicKeyLen &&
bytes.Equal(privateKey[privateKeyLen:privateKeyLen+publicKeyLen], publicKey) {
return privateKey[:privateKeyLen+publicKeyLen]
}
return append(privateKey[:privateKeyLen:privateKeyLen], publicKey...)
}
func (c *ed25519) Sign(publicKey, privateKey, message []byte) (sig []byte, err error) {

View File

@@ -2,6 +2,7 @@
package ecc
import (
"bytes"
"crypto/subtle"
"io"
@@ -84,7 +85,14 @@ func (c *ed448) GenerateEdDSA(rand io.Reader) (pub, priv []byte, err error) {
}
func getEd448Sk(publicKey, privateKey []byte) ed448lib.PrivateKey {
return append(privateKey, publicKey...)
privateKeyCap, privateKeyLen, publicKeyLen := cap(privateKey), len(privateKey), len(publicKey)
if privateKeyCap >= privateKeyLen+publicKeyLen &&
bytes.Equal(privateKey[privateKeyLen:privateKeyLen+publicKeyLen], publicKey) {
return privateKey[:privateKeyLen+publicKeyLen]
}
return append(privateKey[:privateKeyLen:privateKeyLen], publicKey...)
}
func (c *ed448) Sign(publicKey, privateKey, message []byte) (sig []byte, err error) {

View File

@@ -15,11 +15,15 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/ecdh"
"github.com/ProtonMail/go-crypto/openpgp/ecdsa"
"github.com/ProtonMail/go-crypto/openpgp/ed25519"
"github.com/ProtonMail/go-crypto/openpgp/ed448"
"github.com/ProtonMail/go-crypto/openpgp/eddsa"
"github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
"github.com/ProtonMail/go-crypto/openpgp/internal/ecc"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/ProtonMail/go-crypto/openpgp/x25519"
"github.com/ProtonMail/go-crypto/openpgp/x448"
)
// NewEntity returns an Entity that contains a fresh RSA/RSA keypair with a
@@ -36,8 +40,10 @@ func NewEntity(name, comment, email string, config *packet.Config) (*Entity, err
return nil, err
}
primary := packet.NewSignerPrivateKey(creationTime, primaryPrivRaw)
if config != nil && config.V5Keys {
primary.UpgradeToV5()
if config.V6() {
if err := primary.UpgradeToV6(); err != nil {
return nil, err
}
}
e := &Entity{
@@ -45,9 +51,25 @@ func NewEntity(name, comment, email string, config *packet.Config) (*Entity, err
PrivateKey: primary,
Identities: make(map[string]*Identity),
Subkeys: []Subkey{},
Signatures: []*packet.Signature{},
}
err = e.addUserId(name, comment, email, config, creationTime, keyLifetimeSecs)
if config.V6() {
// In v6 keys algorithm preferences should be stored in direct key signatures
selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypeDirectSignature, config)
err = writeKeyProperties(selfSignature, creationTime, keyLifetimeSecs, config)
if err != nil {
return nil, err
}
err = selfSignature.SignDirectKeyBinding(&primary.PublicKey, primary, config)
if err != nil {
return nil, err
}
e.Signatures = append(e.Signatures, selfSignature)
e.SelfSignature = selfSignature
}
err = e.addUserId(name, comment, email, config, creationTime, keyLifetimeSecs, !config.V6())
if err != nil {
return nil, err
}
@@ -65,32 +87,19 @@ func NewEntity(name, comment, email string, config *packet.Config) (*Entity, err
func (t *Entity) AddUserId(name, comment, email string, config *packet.Config) error {
creationTime := config.Now()
keyLifetimeSecs := config.KeyLifetime()
return t.addUserId(name, comment, email, config, creationTime, keyLifetimeSecs)
return t.addUserId(name, comment, email, config, creationTime, keyLifetimeSecs, !config.V6())
}
func (t *Entity) addUserId(name, comment, email string, config *packet.Config, creationTime time.Time, keyLifetimeSecs uint32) error {
uid := packet.NewUserId(name, comment, email)
if uid == nil {
return errors.InvalidArgumentError("user id field contained invalid characters")
}
func writeKeyProperties(selfSignature *packet.Signature, creationTime time.Time, keyLifetimeSecs uint32, config *packet.Config) error {
advertiseAead := config.AEAD() != nil
if _, ok := t.Identities[uid.Id]; ok {
return errors.InvalidArgumentError("user id exist")
}
primary := t.PrivateKey
isPrimaryId := len(t.Identities) == 0
selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypePositiveCert, config)
selfSignature.CreationTime = creationTime
selfSignature.KeyLifetimeSecs = &keyLifetimeSecs
selfSignature.IsPrimaryId = &isPrimaryId
selfSignature.FlagsValid = true
selfSignature.FlagSign = true
selfSignature.FlagCertify = true
selfSignature.SEIPDv1 = true // true by default, see 5.8 vs. 5.14
selfSignature.SEIPDv2 = config.AEAD() != nil
selfSignature.SEIPDv2 = advertiseAead
// Set the PreferredHash for the SelfSignature from the packet.Config.
// If it is not the must-implement algorithm from rfc4880bis, append that.
@@ -119,18 +128,44 @@ func (t *Entity) addUserId(name, comment, email string, config *packet.Config, c
selfSignature.PreferredCompression = append(selfSignature.PreferredCompression, uint8(config.Compression()))
}
// And for DefaultMode.
modes := []uint8{uint8(config.AEAD().Mode())}
if config.AEAD().Mode() != packet.AEADModeOCB {
modes = append(modes, uint8(packet.AEADModeOCB))
}
if advertiseAead {
// Get the preferred AEAD mode from the packet.Config.
// If it is not the must-implement algorithm from rfc9580, append that.
modes := []uint8{uint8(config.AEAD().Mode())}
if config.AEAD().Mode() != packet.AEADModeOCB {
modes = append(modes, uint8(packet.AEADModeOCB))
}
// For preferred (AES256, GCM), we'll generate (AES256, GCM), (AES256, OCB), (AES128, GCM), (AES128, OCB)
for _, cipher := range selfSignature.PreferredSymmetric {
for _, mode := range modes {
selfSignature.PreferredCipherSuites = append(selfSignature.PreferredCipherSuites, [2]uint8{cipher, mode})
// For preferred (AES256, GCM), we'll generate (AES256, GCM), (AES256, OCB), (AES128, GCM), (AES128, OCB)
for _, cipher := range selfSignature.PreferredSymmetric {
for _, mode := range modes {
selfSignature.PreferredCipherSuites = append(selfSignature.PreferredCipherSuites, [2]uint8{cipher, mode})
}
}
}
return nil
}
func (t *Entity) addUserId(name, comment, email string, config *packet.Config, creationTime time.Time, keyLifetimeSecs uint32, writeProperties bool) error {
uid := packet.NewUserId(name, comment, email)
if uid == nil {
return errors.InvalidArgumentError("user id field contained invalid characters")
}
if _, ok := t.Identities[uid.Id]; ok {
return errors.InvalidArgumentError("user id exist")
}
primary := t.PrivateKey
isPrimaryId := len(t.Identities) == 0
selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypePositiveCert, config)
if writeProperties {
err := writeKeyProperties(selfSignature, creationTime, keyLifetimeSecs, config)
if err != nil {
return err
}
}
selfSignature.IsPrimaryId = &isPrimaryId
// User ID binding signature
err := selfSignature.SignUserId(uid.Id, &primary.PublicKey, primary, config)
@@ -158,8 +193,10 @@ func (e *Entity) AddSigningSubkey(config *packet.Config) error {
}
sub := packet.NewSignerPrivateKey(creationTime, subPrivRaw)
sub.IsSubkey = true
if config != nil && config.V5Keys {
sub.UpgradeToV5()
if config.V6() {
if err := sub.UpgradeToV6(); err != nil {
return err
}
}
subkey := Subkey{
@@ -203,8 +240,10 @@ func (e *Entity) addEncryptionSubkey(config *packet.Config, creationTime time.Ti
}
sub := packet.NewDecrypterPrivateKey(creationTime, subPrivRaw)
sub.IsSubkey = true
if config != nil && config.V5Keys {
sub.UpgradeToV5()
if config.V6() {
if err := sub.UpgradeToV6(); err != nil {
return err
}
}
subkey := Subkey{
@@ -242,6 +281,11 @@ func newSigner(config *packet.Config) (signer interface{}, err error) {
}
return rsa.GenerateKey(config.Random(), bits)
case packet.PubKeyAlgoEdDSA:
if config.V6() {
// Implementations MUST NOT accept or generate v6 key material
// using the deprecated OIDs.
return nil, errors.InvalidArgumentError("EdDSALegacy cannot be used for v6 keys")
}
curve := ecc.FindEdDSAByGenName(string(config.CurveName()))
if curve == nil {
return nil, errors.InvalidArgumentError("unsupported curve")
@@ -263,6 +307,18 @@ func newSigner(config *packet.Config) (signer interface{}, err error) {
return nil, err
}
return priv, nil
case packet.PubKeyAlgoEd25519:
priv, err := ed25519.GenerateKey(config.Random())
if err != nil {
return nil, err
}
return priv, nil
case packet.PubKeyAlgoEd448:
priv, err := ed448.GenerateKey(config.Random())
if err != nil {
return nil, err
}
return priv, nil
default:
return nil, errors.InvalidArgumentError("unsupported public key algorithm")
}
@@ -285,6 +341,13 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) {
case packet.PubKeyAlgoEdDSA, packet.PubKeyAlgoECDSA:
fallthrough // When passing EdDSA or ECDSA, we generate an ECDH subkey
case packet.PubKeyAlgoECDH:
if config.V6() &&
(config.CurveName() == packet.Curve25519 ||
config.CurveName() == packet.Curve448) {
// Implementations MUST NOT accept or generate v6 key material
// using the deprecated OIDs.
return nil, errors.InvalidArgumentError("ECDH with Curve25519/448 legacy cannot be used for v6 keys")
}
var kdf = ecdh.KDF{
Hash: algorithm.SHA512,
Cipher: algorithm.AES256,
@@ -294,6 +357,10 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) {
return nil, errors.InvalidArgumentError("unsupported curve")
}
return ecdh.GenerateKey(config.Random(), curve, kdf)
case packet.PubKeyAlgoEd25519, packet.PubKeyAlgoX25519: // When passing Ed25519, we generate an x25519 subkey
return x25519.GenerateKey(config.Random())
case packet.PubKeyAlgoEd448, packet.PubKeyAlgoX448: // When passing Ed448, we generate an x448 subkey
return x448.GenerateKey(config.Random())
default:
return nil, errors.InvalidArgumentError("unsupported public key algorithm")
}
@@ -302,7 +369,7 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) {
var bigOne = big.NewInt(1)
// generateRSAKeyWithPrimes generates a multi-prime RSA keypair of the
// given bit size, using the given random source and prepopulated primes.
// given bit size, using the given random source and pre-populated primes.
func generateRSAKeyWithPrimes(random io.Reader, nprimes int, bits int, prepopulatedPrimes []*big.Int) (*rsa.PrivateKey, error) {
priv := new(rsa.PrivateKey)
priv.E = 65537

View File

@@ -6,6 +6,7 @@ package openpgp
import (
goerrors "errors"
"fmt"
"io"
"time"
@@ -24,11 +25,13 @@ var PrivateKeyType = "PGP PRIVATE KEY BLOCK"
// (which must be a signing key), one or more identities claimed by that key,
// and zero or more subkeys, which may be encryption keys.
type Entity struct {
PrimaryKey *packet.PublicKey
PrivateKey *packet.PrivateKey
Identities map[string]*Identity // indexed by Identity.Name
Revocations []*packet.Signature
Subkeys []Subkey
PrimaryKey *packet.PublicKey
PrivateKey *packet.PrivateKey
Identities map[string]*Identity // indexed by Identity.Name
Revocations []*packet.Signature
Subkeys []Subkey
SelfSignature *packet.Signature // Direct-key self signature of the PrimaryKey (contains primary key properties in v6)
Signatures []*packet.Signature // all (potentially unverified) self-signatures, revocations, and third-party signatures
}
// An Identity represents an identity claimed by an Entity and zero or more
@@ -120,12 +123,12 @@ func shouldPreferIdentity(existingId, potentialNewId *Identity) bool {
// given Entity.
func (e *Entity) EncryptionKey(now time.Time) (Key, bool) {
// Fail to find any encryption key if the...
i := e.PrimaryIdentity()
if e.PrimaryKey.KeyExpired(i.SelfSignature, now) || // primary key has expired
i.SelfSignature == nil || // user ID has no self-signature
i.SelfSignature.SigExpired(now) || // user ID self-signature has expired
primarySelfSignature, primaryIdentity := e.PrimarySelfSignature()
if primarySelfSignature == nil || // no self-signature found
e.PrimaryKey.KeyExpired(primarySelfSignature, now) || // primary key has expired
e.Revoked(now) || // primary key has been revoked
i.Revoked(now) { // user ID has been revoked
primarySelfSignature.SigExpired(now) || // user ID or or direct self-signature has expired
(primaryIdentity != nil && primaryIdentity.Revoked(now)) { // user ID has been revoked (for v4 keys)
return Key{}, false
}
@@ -152,9 +155,9 @@ func (e *Entity) EncryptionKey(now time.Time) (Key, bool) {
// If we don't have any subkeys for encryption and the primary key
// is marked as OK to encrypt with, then we can use it.
if i.SelfSignature.FlagsValid && i.SelfSignature.FlagEncryptCommunications &&
if primarySelfSignature.FlagsValid && primarySelfSignature.FlagEncryptCommunications &&
e.PrimaryKey.PubKeyAlgo.CanEncrypt() {
return Key{e, e.PrimaryKey, e.PrivateKey, i.SelfSignature, e.Revocations}, true
return Key{e, e.PrimaryKey, e.PrivateKey, primarySelfSignature, e.Revocations}, true
}
return Key{}, false
@@ -186,12 +189,12 @@ func (e *Entity) SigningKeyById(now time.Time, id uint64) (Key, bool) {
func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key, bool) {
// Fail to find any signing key if the...
i := e.PrimaryIdentity()
if e.PrimaryKey.KeyExpired(i.SelfSignature, now) || // primary key has expired
i.SelfSignature == nil || // user ID has no self-signature
i.SelfSignature.SigExpired(now) || // user ID self-signature has expired
primarySelfSignature, primaryIdentity := e.PrimarySelfSignature()
if primarySelfSignature == nil || // no self-signature found
e.PrimaryKey.KeyExpired(primarySelfSignature, now) || // primary key has expired
e.Revoked(now) || // primary key has been revoked
i.Revoked(now) { // user ID has been revoked
primarySelfSignature.SigExpired(now) || // user ID or direct self-signature has expired
(primaryIdentity != nil && primaryIdentity.Revoked(now)) { // user ID has been revoked (for v4 keys)
return Key{}, false
}
@@ -220,12 +223,12 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key,
// If we don't have any subkeys for signing and the primary key
// is marked as OK to sign with, then we can use it.
if i.SelfSignature.FlagsValid &&
(flags&packet.KeyFlagCertify == 0 || i.SelfSignature.FlagCertify) &&
(flags&packet.KeyFlagSign == 0 || i.SelfSignature.FlagSign) &&
if primarySelfSignature.FlagsValid &&
(flags&packet.KeyFlagCertify == 0 || primarySelfSignature.FlagCertify) &&
(flags&packet.KeyFlagSign == 0 || primarySelfSignature.FlagSign) &&
e.PrimaryKey.PubKeyAlgo.CanSign() &&
(id == 0 || e.PrimaryKey.KeyId == id) {
return Key{e, e.PrimaryKey, e.PrivateKey, i.SelfSignature, e.Revocations}, true
return Key{e, e.PrimaryKey, e.PrivateKey, primarySelfSignature, e.Revocations}, true
}
// No keys with a valid Signing Flag or no keys matched the id passed in
@@ -259,7 +262,7 @@ func (e *Entity) EncryptPrivateKeys(passphrase []byte, config *packet.Config) er
var keysToEncrypt []*packet.PrivateKey
// Add entity private key to encrypt.
if e.PrivateKey != nil && !e.PrivateKey.Dummy() && !e.PrivateKey.Encrypted {
keysToEncrypt = append(keysToEncrypt, e.PrivateKey)
keysToEncrypt = append(keysToEncrypt, e.PrivateKey)
}
// Add subkeys to encrypt.
@@ -271,7 +274,7 @@ func (e *Entity) EncryptPrivateKeys(passphrase []byte, config *packet.Config) er
return packet.EncryptPrivateKeys(keysToEncrypt, passphrase, config)
}
// DecryptPrivateKeys decrypts all encrypted keys in the entitiy with the given passphrase.
// DecryptPrivateKeys decrypts all encrypted keys in the entity with the given passphrase.
// Avoids recomputation of similar s2k key derivations. Public keys and dummy keys are ignored,
// and don't cause an error to be returned.
func (e *Entity) DecryptPrivateKeys(passphrase []byte) error {
@@ -284,7 +287,7 @@ func (e *Entity) DecryptPrivateKeys(passphrase []byte) error {
// Add subkeys to decrypt.
for _, sub := range e.Subkeys {
if sub.PrivateKey != nil && !sub.PrivateKey.Dummy() && sub.PrivateKey.Encrypted {
keysToDecrypt = append(keysToDecrypt, sub.PrivateKey)
keysToDecrypt = append(keysToDecrypt, sub.PrivateKey)
}
}
return packet.DecryptPrivateKeys(keysToDecrypt, passphrase)
@@ -318,8 +321,7 @@ type EntityList []*Entity
func (el EntityList) KeysById(id uint64) (keys []Key) {
for _, e := range el {
if e.PrimaryKey.KeyId == id {
ident := e.PrimaryIdentity()
selfSig := ident.SelfSignature
selfSig, _ := e.PrimarySelfSignature()
keys = append(keys, Key{e, e.PrimaryKey, e.PrivateKey, selfSig, e.Revocations})
}
@@ -441,7 +443,6 @@ func readToNextPublicKey(packets *packet.Reader) (err error) {
return
} else if err != nil {
if _, ok := err.(errors.UnsupportedError); ok {
err = nil
continue
}
return
@@ -479,6 +480,7 @@ func ReadEntity(packets *packet.Reader) (*Entity, error) {
}
var revocations []*packet.Signature
var directSignatures []*packet.Signature
EachPacket:
for {
p, err := packets.Next()
@@ -497,9 +499,7 @@ EachPacket:
if pkt.SigType == packet.SigTypeKeyRevocation {
revocations = append(revocations, pkt)
} else if pkt.SigType == packet.SigTypeDirectSignature {
// TODO: RFC4880 5.2.1 permits signatures
// directly on keys (eg. to bind additional
// revocation keys).
directSignatures = append(directSignatures, pkt)
}
// Else, ignoring the signature as it does not follow anything
// we would know to attach it to.
@@ -522,12 +522,39 @@ EachPacket:
return nil, err
}
default:
// we ignore unknown packets
// we ignore unknown packets.
}
}
if len(e.Identities) == 0 {
return nil, errors.StructuralError("entity without any identities")
if len(e.Identities) == 0 && e.PrimaryKey.Version < 6 {
return nil, errors.StructuralError(fmt.Sprintf("v%d entity without any identities", e.PrimaryKey.Version))
}
// An implementation MUST ensure that a valid direct-key signature is present before using a v6 key.
if e.PrimaryKey.Version == 6 {
if len(directSignatures) == 0 {
return nil, errors.StructuralError("v6 entity without a valid direct-key signature")
}
// Select main direct key signature.
var mainDirectKeySelfSignature *packet.Signature
for _, directSignature := range directSignatures {
if directSignature.SigType == packet.SigTypeDirectSignature &&
directSignature.CheckKeyIdOrFingerprint(e.PrimaryKey) &&
(mainDirectKeySelfSignature == nil ||
directSignature.CreationTime.After(mainDirectKeySelfSignature.CreationTime)) {
mainDirectKeySelfSignature = directSignature
}
}
if mainDirectKeySelfSignature == nil {
return nil, errors.StructuralError("no valid direct-key self-signature for v6 primary key found")
}
// Check that the main self-signature is valid.
err = e.PrimaryKey.VerifyDirectKeySignature(mainDirectKeySelfSignature)
if err != nil {
return nil, errors.StructuralError("invalid direct-key self-signature for v6 primary key")
}
e.SelfSignature = mainDirectKeySelfSignature
e.Signatures = directSignatures
}
for _, revocation := range revocations {
@@ -672,6 +699,12 @@ func (e *Entity) serializePrivate(w io.Writer, config *packet.Config, reSign boo
return err
}
}
for _, directSignature := range e.Signatures {
err := directSignature.Serialize(w)
if err != nil {
return err
}
}
for _, ident := range e.Identities {
err = ident.UserId.Serialize(w)
if err != nil {
@@ -738,6 +771,12 @@ func (e *Entity) Serialize(w io.Writer) error {
return err
}
}
for _, directSignature := range e.Signatures {
err := directSignature.Serialize(w)
if err != nil {
return err
}
}
for _, ident := range e.Identities {
err = ident.UserId.Serialize(w)
if err != nil {
@@ -840,3 +879,23 @@ func (e *Entity) RevokeSubkey(sk *Subkey, reason packet.ReasonForRevocation, rea
sk.Revocations = append(sk.Revocations, revSig)
return nil
}
func (e *Entity) primaryDirectSignature() *packet.Signature {
return e.SelfSignature
}
// PrimarySelfSignature searches the entity for the self-signature that stores key preferences.
// For V4 keys, returns the self-signature of the primary identity, and the identity.
// For V6 keys, returns the latest valid direct-key self-signature, and no identity (nil).
// This self-signature is to be used to check the key expiration,
// algorithm preferences, and so on.
func (e *Entity) PrimarySelfSignature() (*packet.Signature, *Identity) {
if e.PrimaryKey.Version == 6 {
return e.primaryDirectSignature(), nil
}
primaryIdentity := e.PrimaryIdentity()
if primaryIdentity == nil {
return nil, nil
}
return primaryIdentity.SelfSignature, primaryIdentity
}

View File

@@ -37,7 +37,7 @@ func (conf *AEADConfig) Mode() AEADMode {
// ChunkSizeByte returns the byte indicating the chunk size. The effective
// chunk size is computed with the formula uint64(1) << (chunkSizeByte + 6)
// limit to 16 = 4 MiB
// limit chunkSizeByte to 16 which equals to 2^22 = 4 MiB
// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-5.13.2
func (conf *AEADConfig) ChunkSizeByte() byte {
if conf == nil || conf.ChunkSize == 0 {
@@ -49,8 +49,8 @@ func (conf *AEADConfig) ChunkSizeByte() byte {
switch {
case exponent < 6:
exponent = 6
case exponent > 16:
exponent = 16
case exponent > 22:
exponent = 22
}
return byte(exponent - 6)

View File

@@ -3,7 +3,6 @@
package packet
import (
"bytes"
"crypto/cipher"
"encoding/binary"
"io"
@@ -15,12 +14,11 @@ import (
type aeadCrypter struct {
aead cipher.AEAD
chunkSize int
initialNonce []byte
nonce []byte
associatedData []byte // Chunk-independent associated data
chunkIndex []byte // Chunk counter
packetTag packetType // SEIP packet (v2) or AEAD Encrypted Data packet
bytesProcessed int // Amount of plaintext bytes encrypted/decrypted
buffer bytes.Buffer // Buffered bytes across chunks
}
// computeNonce takes the incremental index and computes an eXclusive OR with
@@ -28,12 +26,12 @@ type aeadCrypter struct {
// 5.16.1 and 5.16.2). It returns the resulting nonce.
func (wo *aeadCrypter) computeNextNonce() (nonce []byte) {
if wo.packetTag == packetTypeSymmetricallyEncryptedIntegrityProtected {
return append(wo.initialNonce, wo.chunkIndex...)
return wo.nonce
}
nonce = make([]byte, len(wo.initialNonce))
copy(nonce, wo.initialNonce)
offset := len(wo.initialNonce) - 8
nonce = make([]byte, len(wo.nonce))
copy(nonce, wo.nonce)
offset := len(wo.nonce) - 8
for i := 0; i < 8; i++ {
nonce[i+offset] ^= wo.chunkIndex[i]
}
@@ -62,8 +60,9 @@ func (wo *aeadCrypter) incrementIndex() error {
type aeadDecrypter struct {
aeadCrypter // Embedded ciphertext opener
reader io.Reader // 'reader' is a partialLengthReader
chunkBytes []byte
peekedBytes []byte // Used to detect last chunk
eof bool
buffer []byte // Buffered decrypted bytes
}
// Read decrypts bytes and reads them into dst. It decrypts when necessary and
@@ -71,51 +70,45 @@ type aeadDecrypter struct {
// and an error.
func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) {
// Return buffered plaintext bytes from previous calls
if ar.buffer.Len() > 0 {
return ar.buffer.Read(dst)
}
// Return EOF if we've previously validated the final tag
if ar.eof {
return 0, io.EOF
if len(ar.buffer) > 0 {
n = copy(dst, ar.buffer)
ar.buffer = ar.buffer[n:]
return
}
// Read a chunk
tagLen := ar.aead.Overhead()
cipherChunkBuf := new(bytes.Buffer)
_, errRead := io.CopyN(cipherChunkBuf, ar.reader, int64(ar.chunkSize+tagLen))
cipherChunk := cipherChunkBuf.Bytes()
if errRead != nil && errRead != io.EOF {
copy(ar.chunkBytes, ar.peekedBytes) // Copy bytes peeked in previous chunk or in initialization
bytesRead, errRead := io.ReadFull(ar.reader, ar.chunkBytes[tagLen:])
if errRead != nil && errRead != io.EOF && errRead != io.ErrUnexpectedEOF {
return 0, errRead
}
decrypted, errChunk := ar.openChunk(cipherChunk)
if errChunk != nil {
return 0, errChunk
}
// Return decrypted bytes, buffering if necessary
if len(dst) < len(decrypted) {
n = copy(dst, decrypted[:len(dst)])
ar.buffer.Write(decrypted[len(dst):])
} else {
n = copy(dst, decrypted)
}
if bytesRead > 0 {
ar.peekedBytes = ar.chunkBytes[bytesRead:bytesRead+tagLen]
// Check final authentication tag
if errRead == io.EOF {
errChunk := ar.validateFinalTag(ar.peekedBytes)
decrypted, errChunk := ar.openChunk(ar.chunkBytes[:bytesRead])
if errChunk != nil {
return n, errChunk
return 0, errChunk
}
ar.eof = true // Mark EOF for when we've returned all buffered data
// Return decrypted bytes, buffering if necessary
n = copy(dst, decrypted)
ar.buffer = decrypted[n:]
return
}
return
return 0, io.EOF
}
// Close is noOp. The final authentication tag of the stream was already
// checked in the last Read call. In the future, this function could be used to
// wipe the reader and peeked, decrypted bytes, if necessary.
// Close checks the final authentication tag of the stream.
// In the future, this function could also be used to wipe the reader
// and peeked & decrypted bytes, if necessary.
func (ar *aeadDecrypter) Close() (err error) {
errChunk := ar.validateFinalTag(ar.peekedBytes)
if errChunk != nil {
return errChunk
}
return nil
}
@@ -123,22 +116,15 @@ func (ar *aeadDecrypter) Close() (err error) {
// the underlying plaintext and an error. It accesses peeked bytes from next
// chunk, to identify the last chunk and decrypt/validate accordingly.
func (ar *aeadDecrypter) openChunk(data []byte) ([]byte, error) {
tagLen := ar.aead.Overhead()
// Restore carried bytes from last call
chunkExtra := append(ar.peekedBytes, data...)
// 'chunk' contains encrypted bytes, followed by an authentication tag.
chunk := chunkExtra[:len(chunkExtra)-tagLen]
ar.peekedBytes = chunkExtra[len(chunkExtra)-tagLen:]
adata := ar.associatedData
if ar.aeadCrypter.packetTag == packetTypeAEADEncrypted {
adata = append(ar.associatedData, ar.chunkIndex...)
}
nonce := ar.computeNextNonce()
plainChunk, err := ar.aead.Open(nil, nonce, chunk, adata)
plainChunk, err := ar.aead.Open(data[:0:len(data)], nonce, data, adata)
if err != nil {
return nil, err
return nil, errors.ErrAEADTagVerification
}
ar.bytesProcessed += len(plainChunk)
if err = ar.aeadCrypter.incrementIndex(); err != nil {
@@ -163,9 +149,8 @@ func (ar *aeadDecrypter) validateFinalTag(tag []byte) error {
// ... and total number of encrypted octets
adata = append(adata, amountBytes...)
nonce := ar.computeNextNonce()
_, err := ar.aead.Open(nil, nonce, tag, adata)
if err != nil {
return err
if _, err := ar.aead.Open(nil, nonce, tag, adata); err != nil {
return errors.ErrAEADTagVerification
}
return nil
}
@@ -175,27 +160,29 @@ func (ar *aeadDecrypter) validateFinalTag(tag []byte) error {
type aeadEncrypter struct {
aeadCrypter // Embedded plaintext sealer
writer io.WriteCloser // 'writer' is a partialLengthWriter
chunkBytes []byte
offset int
}
// Write encrypts and writes bytes. It encrypts when necessary and buffers extra
// plaintext bytes for next call. When the stream is finished, Close() MUST be
// called to append the final tag.
func (aw *aeadEncrypter) Write(plaintextBytes []byte) (n int, err error) {
// Append plaintextBytes to existing buffered bytes
n, err = aw.buffer.Write(plaintextBytes)
if err != nil {
return n, err
}
// Encrypt and write chunks
for aw.buffer.Len() >= aw.chunkSize {
plainChunk := aw.buffer.Next(aw.chunkSize)
encryptedChunk, err := aw.sealChunk(plainChunk)
if err != nil {
return n, err
}
_, err = aw.writer.Write(encryptedChunk)
if err != nil {
return n, err
for n != len(plaintextBytes) {
copied := copy(aw.chunkBytes[aw.offset:aw.chunkSize], plaintextBytes[n:])
n += copied
aw.offset += copied
if aw.offset == aw.chunkSize {
encryptedChunk, err := aw.sealChunk(aw.chunkBytes[:aw.offset])
if err != nil {
return n, err
}
_, err = aw.writer.Write(encryptedChunk)
if err != nil {
return n, err
}
aw.offset = 0
}
}
return
@@ -207,9 +194,8 @@ func (aw *aeadEncrypter) Write(plaintextBytes []byte) (n int, err error) {
func (aw *aeadEncrypter) Close() (err error) {
// Encrypt and write a chunk if there's buffered data left, or if we haven't
// written any chunks yet.
if aw.buffer.Len() > 0 || aw.bytesProcessed == 0 {
plainChunk := aw.buffer.Bytes()
lastEncryptedChunk, err := aw.sealChunk(plainChunk)
if aw.offset > 0 || aw.bytesProcessed == 0 {
lastEncryptedChunk, err := aw.sealChunk(aw.chunkBytes[:aw.offset])
if err != nil {
return err
}
@@ -255,7 +241,7 @@ func (aw *aeadEncrypter) sealChunk(data []byte) ([]byte, error) {
}
nonce := aw.computeNextNonce()
encrypted := aw.aead.Seal(nil, nonce, data, adata)
encrypted := aw.aead.Seal(data[:0], nonce, data, adata)
aw.bytesProcessed += len(data)
if err := aw.aeadCrypter.incrementIndex(); err != nil {
return nil, err

View File

@@ -65,24 +65,28 @@ func (ae *AEADEncrypted) decrypt(key []byte) (io.ReadCloser, error) {
blockCipher := ae.cipher.new(key)
aead := ae.mode.new(blockCipher)
// Carry the first tagLen bytes
chunkSize := decodeAEADChunkSize(ae.chunkSizeByte)
tagLen := ae.mode.TagLength()
peekedBytes := make([]byte, tagLen)
chunkBytes := make([]byte, chunkSize+tagLen*2)
peekedBytes := chunkBytes[chunkSize+tagLen:]
n, err := io.ReadFull(ae.Contents, peekedBytes)
if n < tagLen || (err != nil && err != io.EOF) {
return nil, errors.AEADError("Not enough data to decrypt:" + err.Error())
}
chunkSize := decodeAEADChunkSize(ae.chunkSizeByte)
return &aeadDecrypter{
aeadCrypter: aeadCrypter{
aead: aead,
chunkSize: chunkSize,
initialNonce: ae.initialNonce,
nonce: ae.initialNonce,
associatedData: ae.associatedData(),
chunkIndex: make([]byte, 8),
packetTag: packetTypeAEADEncrypted,
},
reader: ae.Contents,
peekedBytes: peekedBytes}, nil
chunkBytes: chunkBytes,
peekedBytes: peekedBytes,
}, nil
}
// associatedData for chunks: tag, version, cipher, mode, chunk size byte

View File

@@ -8,9 +8,10 @@ import (
"compress/bzip2"
"compress/flate"
"compress/zlib"
"github.com/ProtonMail/go-crypto/openpgp/errors"
"io"
"strconv"
"github.com/ProtonMail/go-crypto/openpgp/errors"
)
// Compressed represents a compressed OpenPGP packet. The decompressed contents
@@ -39,6 +40,37 @@ type CompressionConfig struct {
Level int
}
// decompressionReader ensures that the whole compression packet is read.
type decompressionReader struct {
compressed io.Reader
decompressed io.ReadCloser
readAll bool
}
func newDecompressionReader(r io.Reader, decompressor io.ReadCloser) *decompressionReader {
return &decompressionReader{
compressed: r,
decompressed: decompressor,
}
}
func (dr *decompressionReader) Read(data []byte) (n int, err error) {
if dr.readAll {
return 0, io.EOF
}
n, err = dr.decompressed.Read(data)
if err == io.EOF {
dr.readAll = true
// Close the decompressor.
if errDec := dr.decompressed.Close(); errDec != nil {
return n, errDec
}
// Consume all remaining data from the compressed packet.
consumeAll(dr.compressed)
}
return n, err
}
func (c *Compressed) parse(r io.Reader) error {
var buf [1]byte
_, err := readFull(r, buf[:])
@@ -50,11 +82,15 @@ func (c *Compressed) parse(r io.Reader) error {
case 0:
c.Body = r
case 1:
c.Body = flate.NewReader(r)
c.Body = newDecompressionReader(r, flate.NewReader(r))
case 2:
c.Body, err = zlib.NewReader(r)
decompressor, err := zlib.NewReader(r)
if err != nil {
return err
}
c.Body = newDecompressionReader(r, decompressor)
case 3:
c.Body = bzip2.NewReader(r)
c.Body = newDecompressionReader(r, io.NopCloser(bzip2.NewReader(r)))
default:
err = errors.UnsupportedError("unknown compression algorithm: " + strconv.Itoa(int(buf[0])))
}
@@ -62,6 +98,16 @@ func (c *Compressed) parse(r io.Reader) error {
return err
}
// LimitedBodyReader wraps the provided body reader with a limiter that restricts
// the number of bytes read to the specified limit.
// If limit is nil, the reader is unbounded.
func (c *Compressed) LimitedBodyReader(limit *int64) io.Reader {
if limit == nil {
return c.Body
}
return &LimitReader{R: c.Body, N: *limit}
}
// compressedWriterCloser represents the serialized compression stream
// header and the compressor. Its Close() method ensures that both the
// compressor and serialized stream header are closed. Its Write()
@@ -123,3 +169,24 @@ func SerializeCompressed(w io.WriteCloser, algo CompressionAlgo, cc *Compression
return
}
// LimitReader is an io.Reader that fails with MessageToLarge if read bytes exceed N.
type LimitReader struct {
R io.Reader // underlying reader
N int64 // max bytes allowed
}
func (l *LimitReader) Read(p []byte) (int, error) {
if l.N <= 0 {
return 0, errors.ErrMessageTooLarge
}
n, err := l.R.Read(p)
l.N -= int64(n)
if err == nil && l.N <= 0 {
err = errors.ErrMessageTooLarge
}
return n, err
}

View File

@@ -14,6 +14,34 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/s2k"
)
var (
defaultRejectPublicKeyAlgorithms = map[PublicKeyAlgorithm]bool{
PubKeyAlgoElGamal: true,
PubKeyAlgoDSA: true,
}
defaultRejectHashAlgorithms = map[crypto.Hash]bool{
crypto.MD5: true,
crypto.RIPEMD160: true,
}
defaultRejectMessageHashAlgorithms = map[crypto.Hash]bool{
crypto.SHA1: true,
crypto.MD5: true,
crypto.RIPEMD160: true,
}
defaultRejectCurves = map[Curve]bool{
CurveSecP256k1: true,
}
)
// A global feature flag to indicate v5 support.
// Can be set via a build tag, e.g.: `go build -tags v5 ./...`
// If the build tag is missing config_v5.go will set it to true.
//
// Disables parsing of v5 keys and v5 signatures.
// These are non-standard entities, which in the crypto-refresh have been superseded
// by v6 keys, v6 signatures and SEIPDv2 encrypted data, respectively.
var V5Disabled = false
// Config collects a number of parameters along with sensible defaults.
// A nil *Config is valid and results in all default values.
type Config struct {
@@ -73,9 +101,16 @@ type Config struct {
// **Note: using this option may break compatibility with other OpenPGP
// implementations, as well as future versions of this library.**
AEADConfig *AEADConfig
// V5Keys configures version 5 key generation. If false, this package still
// supports version 5 keys, but produces version 4 keys.
V5Keys bool
// V6Keys configures version 6 key generation. If false, this package still
// supports version 6 keys, but produces version 4 keys.
V6Keys bool
// Minimum RSA key size allowed for key generation and message signing, verification and encryption.
MinRSABits uint16
// Reject insecure algorithms, only works with v2 api
RejectPublicKeyAlgorithms map[PublicKeyAlgorithm]bool
RejectHashAlgorithms map[crypto.Hash]bool
RejectMessageHashAlgorithms map[crypto.Hash]bool
RejectCurves map[Curve]bool
// "The validity period of the key. This is the number of seconds after
// the key creation time that the key expires. If this is not present
// or has a value of zero, the key never expires. This is found only on
@@ -104,12 +139,50 @@ type Config struct {
// might be no other way than to tolerate the missing MDC. Setting this flag, allows this
// mode of operation. It should be considered a measure of last resort.
InsecureAllowUnauthenticatedMessages bool
// InsecureAllowDecryptionWithSigningKeys allows decryption with keys marked as signing keys in the v2 API.
// This setting is potentially insecure, but it is needed as some libraries
// ignored key flags when selecting a key for encryption.
// Not relevant for the v1 API, as all keys were allowed in decryption.
InsecureAllowDecryptionWithSigningKeys bool
// KnownNotations is a map of Notation Data names to bools, which controls
// the notation names that are allowed to be present in critical Notation Data
// signature subpackets.
KnownNotations map[string]bool
// SignatureNotations is a list of Notations to be added to any signatures.
SignatureNotations []*Notation
// CheckIntendedRecipients controls, whether the OpenPGP Intended Recipient Fingerprint feature
// should be enabled for encryption and decryption.
// (See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-12.html#name-intended-recipient-fingerpr).
// When the flag is set, encryption produces Intended Recipient Fingerprint signature sub-packets and decryption
// checks whether the key it was encrypted to is one of the included fingerprints in the signature.
// If the flag is disabled, no Intended Recipient Fingerprint sub-packets are created or checked.
// The default behavior, when the config or flag is nil, is to enable the feature.
CheckIntendedRecipients *bool
// CacheSessionKey controls if decryption should return the session key used for decryption.
// If the flag is set, the session key is cached in the message details struct.
CacheSessionKey bool
// CheckPacketSequence is a flag that controls if the pgp message reader should strictly check
// that the packet sequence conforms with the grammar mandated by rfc4880.
// The default behavior, when the config or flag is nil, is to check the packet sequence.
CheckPacketSequence *bool
// NonDeterministicSignaturesViaNotation is a flag to enable randomization of signatures.
// If true, a salt notation is used to randomize signatures generated by v4 and v5 keys
// (v6 signatures are always non-deterministic, by design).
// This protects EdDSA signatures from potentially leaking the secret key in case of faults (i.e. bitflips) which, in principle, could occur
// during the signing computation. It is added to signatures of any algo for simplicity, and as it may also serve as protection in case of
// weaknesses in the hash algo, potentially hindering e.g. some chosen-prefix attacks.
// The default behavior, when the config or flag is nil, is to enable the feature.
NonDeterministicSignaturesViaNotation *bool
// InsecureAllowAllKeyFlagsWhenMissing determines how a key without valid key flags is handled.
// When set to true, a key without flags is treated as if all flags are enabled.
// This behavior is consistent with GPG.
InsecureAllowAllKeyFlagsWhenMissing bool
// MaxDecompressedMessageSize specifies the maximum number of bytes that can be
// read from a compressed packet. This serves as an upper limit to prevent
// excessively large decompressed messages.
MaxDecompressedMessageSize *int64
}
func (c *Config) Random() io.Reader {
@@ -197,7 +270,7 @@ func (c *Config) S2K() *s2k.Config {
return nil
}
// for backwards compatibility
if c != nil && c.S2KCount > 0 && c.S2KConfig == nil {
if c.S2KCount > 0 && c.S2KConfig == nil {
return &s2k.Config{
S2KCount: c.S2KCount,
}
@@ -233,6 +306,13 @@ func (c *Config) AllowUnauthenticatedMessages() bool {
return c.InsecureAllowUnauthenticatedMessages
}
func (c *Config) AllowDecryptionWithSigningKeys() bool {
if c == nil {
return false
}
return c.InsecureAllowDecryptionWithSigningKeys
}
func (c *Config) KnownNotation(notationName string) bool {
if c == nil {
return false
@@ -246,3 +326,109 @@ func (c *Config) Notations() []*Notation {
}
return c.SignatureNotations
}
func (c *Config) V6() bool {
if c == nil {
return false
}
return c.V6Keys
}
func (c *Config) IntendedRecipients() bool {
if c == nil || c.CheckIntendedRecipients == nil {
return true
}
return *c.CheckIntendedRecipients
}
func (c *Config) RetrieveSessionKey() bool {
if c == nil {
return false
}
return c.CacheSessionKey
}
func (c *Config) MinimumRSABits() uint16 {
if c == nil || c.MinRSABits == 0 {
return 2047
}
return c.MinRSABits
}
func (c *Config) RejectPublicKeyAlgorithm(alg PublicKeyAlgorithm) bool {
var rejectedAlgorithms map[PublicKeyAlgorithm]bool
if c == nil || c.RejectPublicKeyAlgorithms == nil {
// Default
rejectedAlgorithms = defaultRejectPublicKeyAlgorithms
} else {
rejectedAlgorithms = c.RejectPublicKeyAlgorithms
}
return rejectedAlgorithms[alg]
}
func (c *Config) RejectHashAlgorithm(hash crypto.Hash) bool {
var rejectedAlgorithms map[crypto.Hash]bool
if c == nil || c.RejectHashAlgorithms == nil {
// Default
rejectedAlgorithms = defaultRejectHashAlgorithms
} else {
rejectedAlgorithms = c.RejectHashAlgorithms
}
return rejectedAlgorithms[hash]
}
func (c *Config) RejectMessageHashAlgorithm(hash crypto.Hash) bool {
var rejectedAlgorithms map[crypto.Hash]bool
if c == nil || c.RejectMessageHashAlgorithms == nil {
// Default
rejectedAlgorithms = defaultRejectMessageHashAlgorithms
} else {
rejectedAlgorithms = c.RejectMessageHashAlgorithms
}
return rejectedAlgorithms[hash]
}
func (c *Config) RejectCurve(curve Curve) bool {
var rejectedCurve map[Curve]bool
if c == nil || c.RejectCurves == nil {
// Default
rejectedCurve = defaultRejectCurves
} else {
rejectedCurve = c.RejectCurves
}
return rejectedCurve[curve]
}
func (c *Config) StrictPacketSequence() bool {
if c == nil || c.CheckPacketSequence == nil {
return true
}
return *c.CheckPacketSequence
}
func (c *Config) RandomizeSignaturesViaNotation() bool {
if c == nil || c.NonDeterministicSignaturesViaNotation == nil {
return true
}
return *c.NonDeterministicSignaturesViaNotation
}
func (c *Config) AllowAllKeyFlagsWhenMissing() bool {
if c == nil {
return false
}
return c.InsecureAllowAllKeyFlagsWhenMissing
}
func (c *Config) DecompressedMessageSizeLimit() *int64 {
if c == nil {
return nil
}
return c.MaxDecompressedMessageSize
}
// BoolPointer is a helper function to set a boolean pointer in the Config.
// e.g., config.CheckPacketSequence = BoolPointer(true)
func BoolPointer(value bool) *bool {
return &value
}

View File

@@ -0,0 +1,7 @@
//go:build !v5
package packet
func init() {
V5Disabled = true
}

View File

@@ -5,9 +5,11 @@
package packet
import (
"bytes"
"crypto"
"crypto/rsa"
"encoding/binary"
"encoding/hex"
"io"
"math/big"
"strconv"
@@ -16,32 +18,85 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/elgamal"
"github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
"github.com/ProtonMail/go-crypto/openpgp/x25519"
"github.com/ProtonMail/go-crypto/openpgp/x448"
)
const encryptedKeyVersion = 3
// EncryptedKey represents a public-key encrypted session key. See RFC 4880,
// section 5.1.
type EncryptedKey struct {
KeyId uint64
Algo PublicKeyAlgorithm
CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet
Key []byte // only valid after a successful Decrypt
Version int
KeyId uint64
KeyVersion int // v6
KeyFingerprint []byte // v6
Algo PublicKeyAlgorithm
CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet
Key []byte // only valid after a successful Decrypt
encryptedMPI1, encryptedMPI2 encoding.Field
ephemeralPublicX25519 *x25519.PublicKey // used for x25519
ephemeralPublicX448 *x448.PublicKey // used for x448
encryptedSession []byte // used for x25519 and x448
}
func (e *EncryptedKey) parse(r io.Reader) (err error) {
var buf [10]byte
_, err = readFull(r, buf[:])
var buf [8]byte
_, err = readFull(r, buf[:versionSize])
if err != nil {
return
}
if buf[0] != encryptedKeyVersion {
e.Version = int(buf[0])
if e.Version != 3 && e.Version != 6 {
return errors.UnsupportedError("unknown EncryptedKey version " + strconv.Itoa(int(buf[0])))
}
e.KeyId = binary.BigEndian.Uint64(buf[1:9])
e.Algo = PublicKeyAlgorithm(buf[9])
if e.Version == 6 {
//Read a one-octet size of the following two fields.
if _, err = readFull(r, buf[:1]); err != nil {
return
}
// The size may also be zero, and the key version and
// fingerprint omitted for an "anonymous recipient"
if buf[0] != 0 {
// non-anonymous case
_, err = readFull(r, buf[:versionSize])
if err != nil {
return
}
e.KeyVersion = int(buf[0])
if e.KeyVersion != 4 && e.KeyVersion != 6 {
return errors.UnsupportedError("unknown public key version " + strconv.Itoa(e.KeyVersion))
}
var fingerprint []byte
if e.KeyVersion == 6 {
fingerprint = make([]byte, fingerprintSizeV6)
} else if e.KeyVersion == 4 {
fingerprint = make([]byte, fingerprintSize)
}
_, err = readFull(r, fingerprint)
if err != nil {
return
}
e.KeyFingerprint = fingerprint
if e.KeyVersion == 6 {
e.KeyId = binary.BigEndian.Uint64(e.KeyFingerprint[:keyIdSize])
} else if e.KeyVersion == 4 {
e.KeyId = binary.BigEndian.Uint64(e.KeyFingerprint[fingerprintSize-keyIdSize : fingerprintSize])
}
}
} else {
_, err = readFull(r, buf[:8])
if err != nil {
return
}
e.KeyId = binary.BigEndian.Uint64(buf[:keyIdSize])
}
_, err = readFull(r, buf[:1])
if err != nil {
return
}
e.Algo = PublicKeyAlgorithm(buf[0])
var cipherFunction byte
switch e.Algo {
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly:
e.encryptedMPI1 = new(encoding.MPI)
@@ -68,26 +123,39 @@ func (e *EncryptedKey) parse(r io.Reader) (err error) {
if _, err = e.encryptedMPI2.ReadFrom(r); err != nil {
return
}
case PubKeyAlgoX25519:
e.ephemeralPublicX25519, e.encryptedSession, cipherFunction, err = x25519.DecodeFields(r, e.Version == 6)
if err != nil {
return
}
case PubKeyAlgoX448:
e.ephemeralPublicX448, e.encryptedSession, cipherFunction, err = x448.DecodeFields(r, e.Version == 6)
if err != nil {
return
}
}
if e.Version < 6 {
switch e.Algo {
case PubKeyAlgoX25519, PubKeyAlgoX448:
e.CipherFunc = CipherFunction(cipherFunction)
// Check for validiy is in the Decrypt method
}
}
_, err = consumeAll(r)
return
}
func checksumKeyMaterial(key []byte) uint16 {
var checksum uint16
for _, v := range key {
checksum += uint16(v)
}
return checksum
}
// Decrypt decrypts an encrypted session key with the given private key. The
// private key must have been decrypted first.
// If config is nil, sensible defaults will be used.
func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error {
if e.KeyId != 0 && e.KeyId != priv.KeyId {
if e.Version < 6 && e.KeyId != 0 && e.KeyId != priv.KeyId {
return errors.InvalidArgumentError("cannot decrypt encrypted session key for key id " + strconv.FormatUint(e.KeyId, 16) + " with private key id " + strconv.FormatUint(priv.KeyId, 16))
}
if e.Version == 6 && e.KeyVersion != 0 && !bytes.Equal(e.KeyFingerprint, priv.Fingerprint) {
return errors.InvalidArgumentError("cannot decrypt encrypted session key for key fingerprint " + hex.EncodeToString(e.KeyFingerprint) + " with private key fingerprint " + hex.EncodeToString(priv.Fingerprint))
}
if e.Algo != priv.PubKeyAlgo {
return errors.InvalidArgumentError("cannot decrypt encrypted session key of type " + strconv.Itoa(int(e.Algo)) + " with private key of type " + strconv.Itoa(int(priv.PubKeyAlgo)))
}
@@ -113,52 +181,116 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error {
vsG := e.encryptedMPI1.Bytes()
m := e.encryptedMPI2.Bytes()
oid := priv.PublicKey.oid.EncodedBytes()
b, err = ecdh.Decrypt(priv.PrivateKey.(*ecdh.PrivateKey), vsG, m, oid, priv.PublicKey.Fingerprint[:])
fp := priv.PublicKey.Fingerprint[:]
if priv.PublicKey.Version == 5 {
// For v5 the, the fingerprint must be restricted to 20 bytes
fp = fp[:20]
}
b, err = ecdh.Decrypt(priv.PrivateKey.(*ecdh.PrivateKey), vsG, m, oid, fp)
case PubKeyAlgoX25519:
b, err = x25519.Decrypt(priv.PrivateKey.(*x25519.PrivateKey), e.ephemeralPublicX25519, e.encryptedSession)
case PubKeyAlgoX448:
b, err = x448.Decrypt(priv.PrivateKey.(*x448.PrivateKey), e.ephemeralPublicX448, e.encryptedSession)
default:
err = errors.InvalidArgumentError("cannot decrypt encrypted session key with private key of type " + strconv.Itoa(int(priv.PubKeyAlgo)))
}
if err != nil {
return err
}
e.CipherFunc = CipherFunction(b[0])
if !e.CipherFunc.IsSupported() {
return errors.UnsupportedError("unsupported encryption function")
var key []byte
switch priv.PubKeyAlgo {
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH:
keyOffset := 0
if e.Version < 6 {
e.CipherFunc = CipherFunction(b[0])
keyOffset = 1
if !e.CipherFunc.IsSupported() {
return errors.UnsupportedError("unsupported encryption function")
}
}
key, err = decodeChecksumKey(b[keyOffset:])
if err != nil {
return err
}
case PubKeyAlgoX25519, PubKeyAlgoX448:
if e.Version < 6 {
switch e.CipherFunc {
case CipherAES128, CipherAES192, CipherAES256:
break
default:
return errors.StructuralError("v3 PKESK mandates AES as cipher function for x25519 and x448")
}
}
key = b[:]
default:
return errors.UnsupportedError("unsupported algorithm for decryption")
}
e.Key = b[1 : len(b)-2]
expectedChecksum := uint16(b[len(b)-2])<<8 | uint16(b[len(b)-1])
checksum := checksumKeyMaterial(e.Key)
if checksum != expectedChecksum {
return errors.StructuralError("EncryptedKey checksum incorrect")
}
e.Key = key
return nil
}
// Serialize writes the encrypted key packet, e, to w.
func (e *EncryptedKey) Serialize(w io.Writer) error {
var mpiLen int
var encodedLength int
switch e.Algo {
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly:
mpiLen = int(e.encryptedMPI1.EncodedLength())
encodedLength = int(e.encryptedMPI1.EncodedLength())
case PubKeyAlgoElGamal:
mpiLen = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength())
encodedLength = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength())
case PubKeyAlgoECDH:
mpiLen = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength())
encodedLength = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength())
case PubKeyAlgoX25519:
encodedLength = x25519.EncodedFieldsLength(e.encryptedSession, e.Version == 6)
case PubKeyAlgoX448:
encodedLength = x448.EncodedFieldsLength(e.encryptedSession, e.Version == 6)
default:
return errors.InvalidArgumentError("don't know how to serialize encrypted key type " + strconv.Itoa(int(e.Algo)))
}
err := serializeHeader(w, packetTypeEncryptedKey, 1 /* version */ +8 /* key id */ +1 /* algo */ +mpiLen)
packetLen := versionSize /* version */ + keyIdSize /* key id */ + algorithmSize /* algo */ + encodedLength
if e.Version == 6 {
packetLen = versionSize /* version */ + algorithmSize /* algo */ + encodedLength + keyVersionSize /* key version */
if e.KeyVersion == 6 {
packetLen += fingerprintSizeV6
} else if e.KeyVersion == 4 {
packetLen += fingerprintSize
}
}
err := serializeHeader(w, packetTypeEncryptedKey, packetLen)
if err != nil {
return err
}
w.Write([]byte{encryptedKeyVersion})
binary.Write(w, binary.BigEndian, e.KeyId)
w.Write([]byte{byte(e.Algo)})
_, err = w.Write([]byte{byte(e.Version)})
if err != nil {
return err
}
if e.Version == 6 {
_, err = w.Write([]byte{byte(e.KeyVersion)})
if err != nil {
return err
}
// The key version number may also be zero,
// and the fingerprint omitted
if e.KeyVersion != 0 {
_, err = w.Write(e.KeyFingerprint)
if err != nil {
return err
}
}
} else {
// Write KeyID
err = binary.Write(w, binary.BigEndian, e.KeyId)
if err != nil {
return err
}
}
_, err = w.Write([]byte{byte(e.Algo)})
if err != nil {
return err
}
switch e.Algo {
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly:
@@ -176,34 +308,115 @@ func (e *EncryptedKey) Serialize(w io.Writer) error {
}
_, err := w.Write(e.encryptedMPI2.EncodedBytes())
return err
case PubKeyAlgoX25519:
err := x25519.EncodeFields(w, e.ephemeralPublicX25519, e.encryptedSession, byte(e.CipherFunc), e.Version == 6)
return err
case PubKeyAlgoX448:
err := x448.EncodeFields(w, e.ephemeralPublicX448, e.encryptedSession, byte(e.CipherFunc), e.Version == 6)
return err
default:
panic("internal error")
}
}
// SerializeEncryptedKey serializes an encrypted key packet to w that contains
// SerializeEncryptedKeyAEAD serializes an encrypted key packet to w that contains
// key, encrypted to pub.
// If aeadSupported is set, PKESK v6 is used, otherwise v3.
// Note: aeadSupported MUST match the value passed to SerializeSymmetricallyEncrypted.
// If config is nil, sensible defaults will be used.
func SerializeEncryptedKey(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, key []byte, config *Config) error {
var buf [10]byte
buf[0] = encryptedKeyVersion
binary.BigEndian.PutUint64(buf[1:9], pub.KeyId)
buf[9] = byte(pub.PubKeyAlgo)
func SerializeEncryptedKeyAEAD(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, aeadSupported bool, key []byte, config *Config) error {
return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, aeadSupported, key, false, config)
}
keyBlock := make([]byte, 1 /* cipher type */ +len(key)+2 /* checksum */)
keyBlock[0] = byte(cipherFunc)
copy(keyBlock[1:], key)
checksum := checksumKeyMaterial(key)
keyBlock[1+len(key)] = byte(checksum >> 8)
keyBlock[1+len(key)+1] = byte(checksum)
// SerializeEncryptedKeyAEADwithHiddenOption serializes an encrypted key packet to w that contains
// key, encrypted to pub.
// Offers the hidden flag option to indicated if the PKESK packet should include a wildcard KeyID.
// If aeadSupported is set, PKESK v6 is used, otherwise v3.
// Note: aeadSupported MUST match the value passed to SerializeSymmetricallyEncrypted.
// If config is nil, sensible defaults will be used.
func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, aeadSupported bool, key []byte, hidden bool, config *Config) error {
var buf [36]byte // max possible header size is v6
lenHeaderWritten := versionSize
version := 3
if aeadSupported {
version = 6
}
// An implementation MUST NOT generate ElGamal v6 PKESKs.
if version == 6 && pub.PubKeyAlgo == PubKeyAlgoElGamal {
return errors.InvalidArgumentError("ElGamal v6 PKESK are not allowed")
}
// In v3 PKESKs, for x25519 and x448, mandate using AES
if version == 3 && (pub.PubKeyAlgo == PubKeyAlgoX25519 || pub.PubKeyAlgo == PubKeyAlgoX448) {
switch cipherFunc {
case CipherAES128, CipherAES192, CipherAES256:
break
default:
return errors.InvalidArgumentError("v3 PKESK mandates AES for x25519 and x448")
}
}
buf[0] = byte(version)
// If hidden is set, the key should be hidden
// An implementation MAY accept or use a Key ID of all zeros,
// or a key version of zero and no key fingerprint, to hide the intended decryption key.
// See Section 5.1.8. in the open pgp crypto refresh
if version == 6 {
if !hidden {
// A one-octet size of the following two fields.
buf[1] = byte(keyVersionSize + len(pub.Fingerprint))
// A one octet key version number.
buf[2] = byte(pub.Version)
lenHeaderWritten += keyVersionSize + 1
// The fingerprint of the public key
copy(buf[lenHeaderWritten:lenHeaderWritten+len(pub.Fingerprint)], pub.Fingerprint)
lenHeaderWritten += len(pub.Fingerprint)
} else {
// The size may also be zero, and the key version
// and fingerprint omitted for an "anonymous recipient"
buf[1] = 0
lenHeaderWritten += 1
}
} else {
if !hidden {
binary.BigEndian.PutUint64(buf[versionSize:(versionSize+keyIdSize)], pub.KeyId)
}
lenHeaderWritten += keyIdSize
}
buf[lenHeaderWritten] = byte(pub.PubKeyAlgo)
lenHeaderWritten += algorithmSize
var keyBlock []byte
switch pub.PubKeyAlgo {
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH:
lenKeyBlock := len(key) + 2
if version < 6 {
lenKeyBlock += 1 // cipher type included
}
keyBlock = make([]byte, lenKeyBlock)
keyOffset := 0
if version < 6 {
keyBlock[0] = byte(cipherFunc)
keyOffset = 1
}
encodeChecksumKey(keyBlock[keyOffset:], key)
case PubKeyAlgoX25519, PubKeyAlgoX448:
// algorithm is added in plaintext below
keyBlock = key
}
switch pub.PubKeyAlgo {
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly:
return serializeEncryptedKeyRSA(w, config.Random(), buf, pub.PublicKey.(*rsa.PublicKey), keyBlock)
return serializeEncryptedKeyRSA(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*rsa.PublicKey), keyBlock)
case PubKeyAlgoElGamal:
return serializeEncryptedKeyElGamal(w, config.Random(), buf, pub.PublicKey.(*elgamal.PublicKey), keyBlock)
return serializeEncryptedKeyElGamal(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*elgamal.PublicKey), keyBlock)
case PubKeyAlgoECDH:
return serializeEncryptedKeyECDH(w, config.Random(), buf, pub.PublicKey.(*ecdh.PublicKey), keyBlock, pub.oid, pub.Fingerprint)
return serializeEncryptedKeyECDH(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*ecdh.PublicKey), keyBlock, pub.oid, pub.Fingerprint)
case PubKeyAlgoX25519:
return serializeEncryptedKeyX25519(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x25519.PublicKey), keyBlock, byte(cipherFunc), version)
case PubKeyAlgoX448:
return serializeEncryptedKeyX448(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x448.PublicKey), keyBlock, byte(cipherFunc), version)
case PubKeyAlgoDSA, PubKeyAlgoRSASignOnly:
return errors.InvalidArgumentError("cannot encrypt to public key of type " + strconv.Itoa(int(pub.PubKeyAlgo)))
}
@@ -211,14 +424,32 @@ func SerializeEncryptedKey(w io.Writer, pub *PublicKey, cipherFunc CipherFunctio
return errors.UnsupportedError("encrypting a key to public key of type " + strconv.Itoa(int(pub.PubKeyAlgo)))
}
func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header [10]byte, pub *rsa.PublicKey, keyBlock []byte) error {
// SerializeEncryptedKey serializes an encrypted key packet to w that contains
// key, encrypted to pub.
// PKESKv6 is used if config.AEAD() is not nil.
// If config is nil, sensible defaults will be used.
// Deprecated: Use SerializeEncryptedKeyAEAD instead.
func SerializeEncryptedKey(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, key []byte, config *Config) error {
return SerializeEncryptedKeyAEAD(w, pub, cipherFunc, config.AEAD() != nil, key, config)
}
// SerializeEncryptedKeyWithHiddenOption serializes an encrypted key packet to w that contains
// key, encrypted to pub. PKESKv6 is used if config.AEAD() is not nil.
// The hidden option controls if the packet should be anonymous, i.e., omit key metadata.
// If config is nil, sensible defaults will be used.
// Deprecated: Use SerializeEncryptedKeyAEADwithHiddenOption instead.
func SerializeEncryptedKeyWithHiddenOption(w io.Writer, pub *PublicKey, cipherFunc CipherFunction, key []byte, hidden bool, config *Config) error {
return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, config.AEAD() != nil, key, hidden, config)
}
func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header []byte, pub *rsa.PublicKey, keyBlock []byte) error {
cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock)
if err != nil {
return errors.InvalidArgumentError("RSA encryption failed: " + err.Error())
}
cipherMPI := encoding.NewMPI(cipherText)
packetLen := 10 /* header length */ + int(cipherMPI.EncodedLength())
packetLen := len(header) /* header length */ + int(cipherMPI.EncodedLength())
err = serializeHeader(w, packetTypeEncryptedKey, packetLen)
if err != nil {
@@ -232,13 +463,13 @@ func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header [10]byte, pub
return err
}
func serializeEncryptedKeyElGamal(w io.Writer, rand io.Reader, header [10]byte, pub *elgamal.PublicKey, keyBlock []byte) error {
func serializeEncryptedKeyElGamal(w io.Writer, rand io.Reader, header []byte, pub *elgamal.PublicKey, keyBlock []byte) error {
c1, c2, err := elgamal.Encrypt(rand, pub, keyBlock)
if err != nil {
return errors.InvalidArgumentError("ElGamal encryption failed: " + err.Error())
}
packetLen := 10 /* header length */
packetLen := len(header) /* header length */
packetLen += 2 /* mpi size */ + (c1.BitLen()+7)/8
packetLen += 2 /* mpi size */ + (c2.BitLen()+7)/8
@@ -257,7 +488,7 @@ func serializeEncryptedKeyElGamal(w io.Writer, rand io.Reader, header [10]byte,
return err
}
func serializeEncryptedKeyECDH(w io.Writer, rand io.Reader, header [10]byte, pub *ecdh.PublicKey, keyBlock []byte, oid encoding.Field, fingerprint []byte) error {
func serializeEncryptedKeyECDH(w io.Writer, rand io.Reader, header []byte, pub *ecdh.PublicKey, keyBlock []byte, oid encoding.Field, fingerprint []byte) error {
vsG, c, err := ecdh.Encrypt(rand, pub, keyBlock, oid.EncodedBytes(), fingerprint)
if err != nil {
return errors.InvalidArgumentError("ECDH encryption failed: " + err.Error())
@@ -266,7 +497,7 @@ func serializeEncryptedKeyECDH(w io.Writer, rand io.Reader, header [10]byte, pub
g := encoding.NewMPI(vsG)
m := encoding.NewOID(c)
packetLen := 10 /* header length */
packetLen := len(header) /* header length */
packetLen += int(g.EncodedLength()) + int(m.EncodedLength())
err = serializeHeader(w, packetTypeEncryptedKey, packetLen)
@@ -284,3 +515,70 @@ func serializeEncryptedKeyECDH(w io.Writer, rand io.Reader, header [10]byte, pub
_, err = w.Write(m.EncodedBytes())
return err
}
func serializeEncryptedKeyX25519(w io.Writer, rand io.Reader, header []byte, pub *x25519.PublicKey, keyBlock []byte, cipherFunc byte, version int) error {
ephemeralPublicX25519, ciphertext, err := x25519.Encrypt(rand, pub, keyBlock)
if err != nil {
return errors.InvalidArgumentError("x25519 encryption failed: " + err.Error())
}
packetLen := len(header) /* header length */
packetLen += x25519.EncodedFieldsLength(ciphertext, version == 6)
err = serializeHeader(w, packetTypeEncryptedKey, packetLen)
if err != nil {
return err
}
_, err = w.Write(header[:])
if err != nil {
return err
}
return x25519.EncodeFields(w, ephemeralPublicX25519, ciphertext, cipherFunc, version == 6)
}
func serializeEncryptedKeyX448(w io.Writer, rand io.Reader, header []byte, pub *x448.PublicKey, keyBlock []byte, cipherFunc byte, version int) error {
ephemeralPublicX448, ciphertext, err := x448.Encrypt(rand, pub, keyBlock)
if err != nil {
return errors.InvalidArgumentError("x448 encryption failed: " + err.Error())
}
packetLen := len(header) /* header length */
packetLen += x448.EncodedFieldsLength(ciphertext, version == 6)
err = serializeHeader(w, packetTypeEncryptedKey, packetLen)
if err != nil {
return err
}
_, err = w.Write(header[:])
if err != nil {
return err
}
return x448.EncodeFields(w, ephemeralPublicX448, ciphertext, cipherFunc, version == 6)
}
func checksumKeyMaterial(key []byte) uint16 {
var checksum uint16
for _, v := range key {
checksum += uint16(v)
}
return checksum
}
func decodeChecksumKey(msg []byte) (key []byte, err error) {
key = msg[:len(msg)-2]
expectedChecksum := uint16(msg[len(msg)-2])<<8 | uint16(msg[len(msg)-1])
checksum := checksumKeyMaterial(key)
if checksum != expectedChecksum {
err = errors.StructuralError("session key checksum is incorrect")
}
return
}
func encodeChecksumKey(buffer []byte, key []byte) {
copy(buffer, key)
checksum := checksumKeyMaterial(key)
buffer[len(key)] = byte(checksum >> 8)
buffer[len(key)+1] = byte(checksum)
}

View File

@@ -58,9 +58,9 @@ func (l *LiteralData) parse(r io.Reader) (err error) {
// on completion. The fileName is truncated to 255 bytes.
func SerializeLiteral(w io.WriteCloser, isBinary bool, fileName string, time uint32) (plaintext io.WriteCloser, err error) {
var buf [4]byte
buf[0] = 't'
if isBinary {
buf[0] = 'b'
buf[0] = 'b'
if !isBinary {
buf[0] = 'u'
}
if len(fileName) > 255 {
fileName = fileName[:255]

View File

@@ -0,0 +1,33 @@
package packet
import (
"io"
"github.com/ProtonMail/go-crypto/openpgp/errors"
)
type Marker struct{}
const markerString = "PGP"
// parse just checks if the packet contains "PGP".
func (m *Marker) parse(reader io.Reader) error {
var buffer [3]byte
if _, err := io.ReadFull(reader, buffer[:]); err != nil {
return err
}
if string(buffer[:]) != markerString {
return errors.StructuralError("invalid marker packet")
}
return nil
}
// SerializeMarker writes a marker packet to writer.
func SerializeMarker(writer io.Writer) error {
err := serializeHeader(writer, packetTypeMarker, len(markerString))
if err != nil {
return err
}
_, err = writer.Write([]byte(markerString))
return err
}

View File

@@ -7,34 +7,37 @@ package packet
import (
"crypto"
"encoding/binary"
"github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
"io"
"strconv"
"github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
)
// OnePassSignature represents a one-pass signature packet. See RFC 4880,
// section 5.4.
type OnePassSignature struct {
SigType SignatureType
Hash crypto.Hash
PubKeyAlgo PublicKeyAlgorithm
KeyId uint64
IsLast bool
Version int
SigType SignatureType
Hash crypto.Hash
PubKeyAlgo PublicKeyAlgorithm
KeyId uint64
IsLast bool
Salt []byte // v6 only
KeyFingerprint []byte // v6 only
}
const onePassSignatureVersion = 3
func (ops *OnePassSignature) parse(r io.Reader) (err error) {
var buf [13]byte
_, err = readFull(r, buf[:])
var buf [8]byte
// Read: version | signature type | hash algorithm | public-key algorithm
_, err = readFull(r, buf[:4])
if err != nil {
return
}
if buf[0] != onePassSignatureVersion {
err = errors.UnsupportedError("one-pass-signature packet version " + strconv.Itoa(int(buf[0])))
if buf[0] != 3 && buf[0] != 6 {
return errors.UnsupportedError("one-pass-signature packet version " + strconv.Itoa(int(buf[0])))
}
ops.Version = int(buf[0])
var ok bool
ops.Hash, ok = algorithm.HashIdToHashWithSha1(buf[2])
@@ -44,15 +47,69 @@ func (ops *OnePassSignature) parse(r io.Reader) (err error) {
ops.SigType = SignatureType(buf[1])
ops.PubKeyAlgo = PublicKeyAlgorithm(buf[3])
ops.KeyId = binary.BigEndian.Uint64(buf[4:12])
ops.IsLast = buf[12] != 0
if ops.Version == 6 {
// Only for v6, a variable-length field containing the salt
_, err = readFull(r, buf[:1])
if err != nil {
return
}
saltLength := int(buf[0])
var expectedSaltLength int
expectedSaltLength, err = SaltLengthForHash(ops.Hash)
if err != nil {
return
}
if saltLength != expectedSaltLength {
err = errors.StructuralError("unexpected salt size for the given hash algorithm")
return
}
salt := make([]byte, expectedSaltLength)
_, err = readFull(r, salt)
if err != nil {
return
}
ops.Salt = salt
// Only for v6 packets, 32 octets of the fingerprint of the signing key.
fingerprint := make([]byte, 32)
_, err = readFull(r, fingerprint)
if err != nil {
return
}
ops.KeyFingerprint = fingerprint
ops.KeyId = binary.BigEndian.Uint64(ops.KeyFingerprint[:8])
} else {
_, err = readFull(r, buf[:8])
if err != nil {
return
}
ops.KeyId = binary.BigEndian.Uint64(buf[:8])
}
_, err = readFull(r, buf[:1])
if err != nil {
return
}
ops.IsLast = buf[0] != 0
return
}
// Serialize marshals the given OnePassSignature to w.
func (ops *OnePassSignature) Serialize(w io.Writer) error {
var buf [13]byte
buf[0] = onePassSignatureVersion
//v3 length 1+1+1+1+8+1 =
packetLength := 13
if ops.Version == 6 {
// v6 length 1+1+1+1+1+len(salt)+32+1 =
packetLength = 38 + len(ops.Salt)
}
if err := serializeHeader(w, packetTypeOnePassSignature, packetLength); err != nil {
return err
}
var buf [8]byte
buf[0] = byte(ops.Version)
buf[1] = uint8(ops.SigType)
var ok bool
buf[2], ok = algorithm.HashToHashIdWithSha1(ops.Hash)
@@ -60,14 +117,41 @@ func (ops *OnePassSignature) Serialize(w io.Writer) error {
return errors.UnsupportedError("hash type: " + strconv.Itoa(int(ops.Hash)))
}
buf[3] = uint8(ops.PubKeyAlgo)
binary.BigEndian.PutUint64(buf[4:12], ops.KeyId)
if ops.IsLast {
buf[12] = 1
}
if err := serializeHeader(w, packetTypeOnePassSignature, len(buf)); err != nil {
_, err := w.Write(buf[:4])
if err != nil {
return err
}
_, err := w.Write(buf[:])
if ops.Version == 6 {
// write salt for v6 signatures
_, err := w.Write([]byte{uint8(len(ops.Salt))})
if err != nil {
return err
}
_, err = w.Write(ops.Salt)
if err != nil {
return err
}
// write fingerprint v6 signatures
_, err = w.Write(ops.KeyFingerprint)
if err != nil {
return err
}
} else {
binary.BigEndian.PutUint64(buf[:8], ops.KeyId)
_, err := w.Write(buf[:8])
if err != nil {
return err
}
}
isLast := []byte{byte(0)}
if ops.IsLast {
isLast[0] = 1
}
_, err = w.Write(isLast)
return err
}

View File

@@ -7,7 +7,6 @@ package packet
import (
"bytes"
"io"
"io/ioutil"
"github.com/ProtonMail/go-crypto/openpgp/errors"
)
@@ -26,7 +25,7 @@ type OpaquePacket struct {
}
func (op *OpaquePacket) parse(r io.Reader) (err error) {
op.Contents, err = ioutil.ReadAll(r)
op.Contents, err = io.ReadAll(r)
return
}

View File

@@ -311,12 +311,15 @@ const (
packetTypePrivateSubkey packetType = 7
packetTypeCompressed packetType = 8
packetTypeSymmetricallyEncrypted packetType = 9
packetTypeMarker packetType = 10
packetTypeLiteralData packetType = 11
packetTypeTrust packetType = 12
packetTypeUserId packetType = 13
packetTypePublicSubkey packetType = 14
packetTypeUserAttribute packetType = 17
packetTypeSymmetricallyEncryptedIntegrityProtected packetType = 18
packetTypeAEADEncrypted packetType = 20
packetPadding packetType = 21
)
// EncryptedDataPacket holds encrypted data. It is currently implemented by
@@ -328,7 +331,7 @@ type EncryptedDataPacket interface {
// Read reads a single OpenPGP packet from the given io.Reader. If there is an
// error parsing a packet, the whole packet is consumed from the input.
func Read(r io.Reader) (p Packet, err error) {
tag, _, contents, err := readHeader(r)
tag, len, contents, err := readHeader(r)
if err != nil {
return
}
@@ -367,8 +370,93 @@ func Read(r io.Reader) (p Packet, err error) {
p = se
case packetTypeAEADEncrypted:
p = new(AEADEncrypted)
default:
case packetPadding:
p = Padding(len)
case packetTypeMarker:
p = new(Marker)
case packetTypeTrust:
// Not implemented, just consume
err = errors.UnknownPacketTypeError(tag)
default:
// Packet Tags from 0 to 39 are critical.
// Packet Tags from 40 to 63 are non-critical.
if tag < 40 {
err = errors.CriticalUnknownPacketTypeError(tag)
} else {
err = errors.UnknownPacketTypeError(tag)
}
}
if p != nil {
err = p.parse(contents)
}
if err != nil {
consumeAll(contents)
}
return
}
// ReadWithCheck reads a single OpenPGP message packet from the given io.Reader. If there is an
// error parsing a packet, the whole packet is consumed from the input.
// ReadWithCheck additionally checks if the OpenPGP message packet sequence adheres
// to the packet composition rules in rfc4880, if not throws an error.
func ReadWithCheck(r io.Reader, sequence *SequenceVerifier) (p Packet, msgErr error, err error) {
tag, len, contents, err := readHeader(r)
if err != nil {
return
}
switch tag {
case packetTypeEncryptedKey:
msgErr = sequence.Next(ESKSymbol)
p = new(EncryptedKey)
case packetTypeSignature:
msgErr = sequence.Next(SigSymbol)
p = new(Signature)
case packetTypeSymmetricKeyEncrypted:
msgErr = sequence.Next(ESKSymbol)
p = new(SymmetricKeyEncrypted)
case packetTypeOnePassSignature:
msgErr = sequence.Next(OPSSymbol)
p = new(OnePassSignature)
case packetTypeCompressed:
msgErr = sequence.Next(CompSymbol)
p = new(Compressed)
case packetTypeSymmetricallyEncrypted:
msgErr = sequence.Next(EncSymbol)
p = new(SymmetricallyEncrypted)
case packetTypeLiteralData:
msgErr = sequence.Next(LDSymbol)
p = new(LiteralData)
case packetTypeSymmetricallyEncryptedIntegrityProtected:
msgErr = sequence.Next(EncSymbol)
se := new(SymmetricallyEncrypted)
se.IntegrityProtected = true
p = se
case packetTypeAEADEncrypted:
msgErr = sequence.Next(EncSymbol)
p = new(AEADEncrypted)
case packetPadding:
p = Padding(len)
case packetTypeMarker:
p = new(Marker)
case packetTypeTrust:
// Not implemented, just consume
err = errors.UnknownPacketTypeError(tag)
case packetTypePrivateKey,
packetTypePrivateSubkey,
packetTypePublicKey,
packetTypePublicSubkey,
packetTypeUserId,
packetTypeUserAttribute:
msgErr = sequence.Next(UnknownSymbol)
consumeAll(contents)
default:
// Packet Tags from 0 to 39 are critical.
// Packet Tags from 40 to 63 are non-critical.
if tag < 40 {
err = errors.CriticalUnknownPacketTypeError(tag)
} else {
err = errors.UnknownPacketTypeError(tag)
}
}
if p != nil {
err = p.parse(contents)
@@ -385,17 +473,17 @@ type SignatureType uint8
const (
SigTypeBinary SignatureType = 0x00
SigTypeText = 0x01
SigTypeGenericCert = 0x10
SigTypePersonaCert = 0x11
SigTypeCasualCert = 0x12
SigTypePositiveCert = 0x13
SigTypeSubkeyBinding = 0x18
SigTypePrimaryKeyBinding = 0x19
SigTypeDirectSignature = 0x1F
SigTypeKeyRevocation = 0x20
SigTypeSubkeyRevocation = 0x28
SigTypeCertificationRevocation = 0x30
SigTypeText SignatureType = 0x01
SigTypeGenericCert SignatureType = 0x10
SigTypePersonaCert SignatureType = 0x11
SigTypeCasualCert SignatureType = 0x12
SigTypePositiveCert SignatureType = 0x13
SigTypeSubkeyBinding SignatureType = 0x18
SigTypePrimaryKeyBinding SignatureType = 0x19
SigTypeDirectSignature SignatureType = 0x1F
SigTypeKeyRevocation SignatureType = 0x20
SigTypeSubkeyRevocation SignatureType = 0x28
SigTypeCertificationRevocation SignatureType = 0x30
)
// PublicKeyAlgorithm represents the different public key system specified for
@@ -412,6 +500,11 @@ const (
PubKeyAlgoECDSA PublicKeyAlgorithm = 19
// https://www.ietf.org/archive/id/draft-koch-eddsa-for-openpgp-04.txt
PubKeyAlgoEdDSA PublicKeyAlgorithm = 22
// https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh
PubKeyAlgoX25519 PublicKeyAlgorithm = 25
PubKeyAlgoX448 PublicKeyAlgorithm = 26
PubKeyAlgoEd25519 PublicKeyAlgorithm = 27
PubKeyAlgoEd448 PublicKeyAlgorithm = 28
// Deprecated in RFC 4880, Section 13.5. Use key flags instead.
PubKeyAlgoRSAEncryptOnly PublicKeyAlgorithm = 2
@@ -422,7 +515,7 @@ const (
// key of the given type.
func (pka PublicKeyAlgorithm) CanEncrypt() bool {
switch pka {
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH:
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448:
return true
}
return false
@@ -432,7 +525,7 @@ func (pka PublicKeyAlgorithm) CanEncrypt() bool {
// sign a message.
func (pka PublicKeyAlgorithm) CanSign() bool {
switch pka {
case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA:
case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448:
return true
}
return false
@@ -512,6 +605,11 @@ func (mode AEADMode) TagLength() int {
return algorithm.AEADMode(mode).TagLength()
}
// IsSupported returns true if the aead mode is supported from the library
func (mode AEADMode) IsSupported() bool {
return algorithm.AEADMode(mode).TagLength() > 0
}
// new returns a fresh instance of the given mode.
func (mode AEADMode) new(block cipher.Block) cipher.AEAD {
return algorithm.AEADMode(mode).New(block)
@@ -526,8 +624,17 @@ const (
KeySuperseded ReasonForRevocation = 1
KeyCompromised ReasonForRevocation = 2
KeyRetired ReasonForRevocation = 3
UserIDNotValid ReasonForRevocation = 32
Unknown ReasonForRevocation = 200
)
func NewReasonForRevocation(value byte) ReasonForRevocation {
if value < 4 || value == 32 {
return ReasonForRevocation(value)
}
return Unknown
}
// Curve is a mapping to supported ECC curves for key generation.
// See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#name-curve-specific-wire-formats
type Curve string
@@ -549,3 +656,20 @@ type TrustLevel uint8
// TrustAmount represents a trust amount per RFC4880 5.2.3.13
type TrustAmount uint8
const (
// versionSize is the length in bytes of the version value.
versionSize = 1
// algorithmSize is the length in bytes of the key algorithm value.
algorithmSize = 1
// keyVersionSize is the length in bytes of the key version value
keyVersionSize = 1
// keyIdSize is the length in bytes of the key identifier value.
keyIdSize = 8
// timestampSize is the length in bytes of encoded timestamps.
timestampSize = 4
// fingerprintSizeV6 is the length in bytes of the key fingerprint in v6.
fingerprintSizeV6 = 32
// fingerprintSize is the length in bytes of the key fingerprint.
fingerprintSize = 20
)

View File

@@ -0,0 +1,222 @@
package packet
// This file implements the pushdown automata (PDA) from PGPainless (Paul Schaub)
// to verify pgp packet sequences. See Paul's blogpost for more details:
// https://blog.jabberhead.tk/2022/10/26/implementing-packet-sequence-validation-using-pushdown-automata/
import (
"fmt"
"github.com/ProtonMail/go-crypto/openpgp/errors"
)
func NewErrMalformedMessage(from State, input InputSymbol, stackSymbol StackSymbol) errors.ErrMalformedMessage {
return errors.ErrMalformedMessage(fmt.Sprintf("state %d, input symbol %d, stack symbol %d ", from, input, stackSymbol))
}
// InputSymbol defines the input alphabet of the PDA
type InputSymbol uint8
const (
LDSymbol InputSymbol = iota
SigSymbol
OPSSymbol
CompSymbol
ESKSymbol
EncSymbol
EOSSymbol
UnknownSymbol
)
// StackSymbol defines the stack alphabet of the PDA
type StackSymbol int8
const (
MsgStackSymbol StackSymbol = iota
OpsStackSymbol
KeyStackSymbol
EndStackSymbol
EmptyStackSymbol
)
// State defines the states of the PDA
type State int8
const (
OpenPGPMessage State = iota
ESKMessage
LiteralMessage
CompressedMessage
EncryptedMessage
ValidMessage
)
// transition represents a state transition in the PDA
type transition func(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error)
// SequenceVerifier is a pushdown automata to verify
// PGP messages packet sequences according to rfc4880.
type SequenceVerifier struct {
stack []StackSymbol
state State
}
// Next performs a state transition with the given input symbol.
// If the transition fails a ErrMalformedMessage is returned.
func (sv *SequenceVerifier) Next(input InputSymbol) error {
for {
stackSymbol := sv.popStack()
transitionFunc := getTransition(sv.state)
nextState, newStackSymbols, redo, err := transitionFunc(input, stackSymbol)
if err != nil {
return err
}
if redo {
sv.pushStack(stackSymbol)
}
for _, newStackSymbol := range newStackSymbols {
sv.pushStack(newStackSymbol)
}
sv.state = nextState
if !redo {
break
}
}
return nil
}
// Valid returns true if RDA is in a valid state.
func (sv *SequenceVerifier) Valid() bool {
return sv.state == ValidMessage && len(sv.stack) == 0
}
func (sv *SequenceVerifier) AssertValid() error {
if !sv.Valid() {
return errors.ErrMalformedMessage("invalid message")
}
return nil
}
func NewSequenceVerifier() *SequenceVerifier {
return &SequenceVerifier{
stack: []StackSymbol{EndStackSymbol, MsgStackSymbol},
state: OpenPGPMessage,
}
}
func (sv *SequenceVerifier) popStack() StackSymbol {
if len(sv.stack) == 0 {
return EmptyStackSymbol
}
elemIndex := len(sv.stack) - 1
stackSymbol := sv.stack[elemIndex]
sv.stack = sv.stack[:elemIndex]
return stackSymbol
}
func (sv *SequenceVerifier) pushStack(stackSymbol StackSymbol) {
sv.stack = append(sv.stack, stackSymbol)
}
func getTransition(from State) transition {
switch from {
case OpenPGPMessage:
return fromOpenPGPMessage
case LiteralMessage:
return fromLiteralMessage
case CompressedMessage:
return fromCompressedMessage
case EncryptedMessage:
return fromEncryptedMessage
case ESKMessage:
return fromESKMessage
case ValidMessage:
return fromValidMessage
}
return nil
}
// fromOpenPGPMessage is the transition for the state OpenPGPMessage.
func fromOpenPGPMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) {
if stackSymbol != MsgStackSymbol {
return 0, nil, false, NewErrMalformedMessage(OpenPGPMessage, input, stackSymbol)
}
switch input {
case LDSymbol:
return LiteralMessage, nil, false, nil
case SigSymbol:
return OpenPGPMessage, []StackSymbol{MsgStackSymbol}, false, nil
case OPSSymbol:
return OpenPGPMessage, []StackSymbol{OpsStackSymbol, MsgStackSymbol}, false, nil
case CompSymbol:
return CompressedMessage, nil, false, nil
case ESKSymbol:
return ESKMessage, []StackSymbol{KeyStackSymbol}, false, nil
case EncSymbol:
return EncryptedMessage, nil, false, nil
}
return 0, nil, false, NewErrMalformedMessage(OpenPGPMessage, input, stackSymbol)
}
// fromESKMessage is the transition for the state ESKMessage.
func fromESKMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) {
if stackSymbol != KeyStackSymbol {
return 0, nil, false, NewErrMalformedMessage(ESKMessage, input, stackSymbol)
}
switch input {
case ESKSymbol:
return ESKMessage, []StackSymbol{KeyStackSymbol}, false, nil
case EncSymbol:
return EncryptedMessage, nil, false, nil
}
return 0, nil, false, NewErrMalformedMessage(ESKMessage, input, stackSymbol)
}
// fromLiteralMessage is the transition for the state LiteralMessage.
func fromLiteralMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) {
switch input {
case SigSymbol:
if stackSymbol == OpsStackSymbol {
return LiteralMessage, nil, false, nil
}
case EOSSymbol:
if stackSymbol == EndStackSymbol {
return ValidMessage, nil, false, nil
}
}
return 0, nil, false, NewErrMalformedMessage(LiteralMessage, input, stackSymbol)
}
// fromLiteralMessage is the transition for the state CompressedMessage.
func fromCompressedMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) {
switch input {
case SigSymbol:
if stackSymbol == OpsStackSymbol {
return CompressedMessage, nil, false, nil
}
case EOSSymbol:
if stackSymbol == EndStackSymbol {
return ValidMessage, nil, false, nil
}
}
return OpenPGPMessage, []StackSymbol{MsgStackSymbol}, true, nil
}
// fromEncryptedMessage is the transition for the state EncryptedMessage.
func fromEncryptedMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) {
switch input {
case SigSymbol:
if stackSymbol == OpsStackSymbol {
return EncryptedMessage, nil, false, nil
}
case EOSSymbol:
if stackSymbol == EndStackSymbol {
return ValidMessage, nil, false, nil
}
}
return OpenPGPMessage, []StackSymbol{MsgStackSymbol}, true, nil
}
// fromValidMessage is the transition for the state ValidMessage.
func fromValidMessage(input InputSymbol, stackSymbol StackSymbol) (State, []StackSymbol, bool, error) {
return 0, nil, false, NewErrMalformedMessage(ValidMessage, input, stackSymbol)
}

View File

@@ -0,0 +1,24 @@
package packet
import (
"io"
"github.com/ProtonMail/go-crypto/openpgp/errors"
)
// UnsupportedPackage represents a OpenPGP packet with a known packet type
// but with unsupported content.
type UnsupportedPacket struct {
IncompletePacket Packet
Error errors.UnsupportedError
}
// Implements the Packet interface
func (up *UnsupportedPacket) parse(read io.Reader) error {
err := up.IncompletePacket.parse(read)
if castedErr, ok := err.(errors.UnsupportedError); ok {
up.Error = castedErr
return nil
}
return err
}

View File

@@ -0,0 +1,26 @@
package packet
import (
"io"
)
// Padding type represents a Padding Packet (Tag 21).
// The padding type is represented by the length of its padding.
// see https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#name-padding-packet-tag-21
type Padding int
// parse just ignores the padding content.
func (pad Padding) parse(reader io.Reader) error {
_, err := io.CopyN(io.Discard, reader, int64(pad))
return err
}
// SerializePadding writes the padding to writer.
func (pad Padding) SerializePadding(writer io.Writer, rand io.Reader) error {
err := serializeHeader(writer, packetPadding, int(pad))
if err != nil {
return err
}
_, err = io.CopyN(writer, rand, int64(pad))
return err
}

View File

@@ -9,22 +9,28 @@ import (
"crypto"
"crypto/cipher"
"crypto/dsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/subtle"
"fmt"
"io"
"io/ioutil"
"math/big"
"strconv"
"time"
"github.com/ProtonMail/go-crypto/openpgp/ecdh"
"github.com/ProtonMail/go-crypto/openpgp/ecdsa"
"github.com/ProtonMail/go-crypto/openpgp/ed25519"
"github.com/ProtonMail/go-crypto/openpgp/ed448"
"github.com/ProtonMail/go-crypto/openpgp/eddsa"
"github.com/ProtonMail/go-crypto/openpgp/elgamal"
"github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
"github.com/ProtonMail/go-crypto/openpgp/s2k"
"github.com/ProtonMail/go-crypto/openpgp/x25519"
"github.com/ProtonMail/go-crypto/openpgp/x448"
"golang.org/x/crypto/hkdf"
)
// PrivateKey represents a possibly encrypted private key. See RFC 4880,
@@ -35,14 +41,14 @@ type PrivateKey struct {
encryptedData []byte
cipher CipherFunction
s2k func(out, in []byte)
// An *{rsa|dsa|elgamal|ecdh|ecdsa|ed25519}.PrivateKey or
aead AEADMode // only relevant if S2KAEAD is enabled
// An *{rsa|dsa|elgamal|ecdh|ecdsa|ed25519|ed448}.PrivateKey or
// crypto.Signer/crypto.Decrypter (Decryptor RSA only).
PrivateKey interface{}
sha1Checksum bool
iv []byte
PrivateKey interface{}
iv []byte
// Type of encryption of the S2K packet
// Allowed values are 0 (Not encrypted), 254 (SHA1), or
// Allowed values are 0 (Not encrypted), 253 (AEAD), 254 (SHA1), or
// 255 (2-byte checksum)
s2kType S2KType
// Full parameters of the S2K packet
@@ -55,6 +61,8 @@ type S2KType uint8
const (
// S2KNON unencrypt
S2KNON S2KType = 0
// S2KAEAD use authenticated encryption
S2KAEAD S2KType = 253
// S2KSHA1 sha1 sum check
S2KSHA1 S2KType = 254
// S2KCHECKSUM sum check
@@ -103,6 +111,34 @@ func NewECDHPrivateKey(creationTime time.Time, priv *ecdh.PrivateKey) *PrivateKe
return pk
}
func NewX25519PrivateKey(creationTime time.Time, priv *x25519.PrivateKey) *PrivateKey {
pk := new(PrivateKey)
pk.PublicKey = *NewX25519PublicKey(creationTime, &priv.PublicKey)
pk.PrivateKey = priv
return pk
}
func NewX448PrivateKey(creationTime time.Time, priv *x448.PrivateKey) *PrivateKey {
pk := new(PrivateKey)
pk.PublicKey = *NewX448PublicKey(creationTime, &priv.PublicKey)
pk.PrivateKey = priv
return pk
}
func NewEd25519PrivateKey(creationTime time.Time, priv *ed25519.PrivateKey) *PrivateKey {
pk := new(PrivateKey)
pk.PublicKey = *NewEd25519PublicKey(creationTime, &priv.PublicKey)
pk.PrivateKey = priv
return pk
}
func NewEd448PrivateKey(creationTime time.Time, priv *ed448.PrivateKey) *PrivateKey {
pk := new(PrivateKey)
pk.PublicKey = *NewEd448PublicKey(creationTime, &priv.PublicKey)
pk.PrivateKey = priv
return pk
}
// NewSignerPrivateKey creates a PrivateKey from a crypto.Signer that
// implements RSA, ECDSA or EdDSA.
func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey {
@@ -122,6 +158,14 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey
pk.PublicKey = *NewEdDSAPublicKey(creationTime, &pubkey.PublicKey)
case eddsa.PrivateKey:
pk.PublicKey = *NewEdDSAPublicKey(creationTime, &pubkey.PublicKey)
case *ed25519.PrivateKey:
pk.PublicKey = *NewEd25519PublicKey(creationTime, &pubkey.PublicKey)
case ed25519.PrivateKey:
pk.PublicKey = *NewEd25519PublicKey(creationTime, &pubkey.PublicKey)
case *ed448.PrivateKey:
pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey)
case ed448.PrivateKey:
pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey)
default:
panic("openpgp: unknown signer type in NewSignerPrivateKey")
}
@@ -129,7 +173,7 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey
return pk
}
// NewDecrypterPrivateKey creates a PrivateKey from a *{rsa|elgamal|ecdh}.PrivateKey.
// NewDecrypterPrivateKey creates a PrivateKey from a *{rsa|elgamal|ecdh|x25519|x448}.PrivateKey.
func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *PrivateKey {
pk := new(PrivateKey)
switch priv := decrypter.(type) {
@@ -139,6 +183,10 @@ func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *Priv
pk.PublicKey = *NewElGamalPublicKey(creationTime, &priv.PublicKey)
case *ecdh.PrivateKey:
pk.PublicKey = *NewECDHPublicKey(creationTime, &priv.PublicKey)
case *x25519.PrivateKey:
pk.PublicKey = *NewX25519PublicKey(creationTime, &priv.PublicKey)
case *x448.PrivateKey:
pk.PublicKey = *NewX448PublicKey(creationTime, &priv.PublicKey)
default:
panic("openpgp: unknown decrypter type in NewDecrypterPrivateKey")
}
@@ -152,6 +200,11 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
return
}
v5 := pk.PublicKey.Version == 5
v6 := pk.PublicKey.Version == 6
if V5Disabled && v5 {
return errors.UnsupportedError("support for parsing v5 entities is disabled; build with `-tags v5` if needed")
}
var buf [1]byte
_, err = readFull(r, buf[:])
@@ -160,7 +213,7 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
}
pk.s2kType = S2KType(buf[0])
var optCount [1]byte
if v5 {
if v5 || (v6 && pk.s2kType != S2KNON) {
if _, err = readFull(r, optCount[:]); err != nil {
return
}
@@ -170,9 +223,9 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
case S2KNON:
pk.s2k = nil
pk.Encrypted = false
case S2KSHA1, S2KCHECKSUM:
if v5 && pk.s2kType == S2KCHECKSUM {
return errors.StructuralError("wrong s2k identifier for version 5")
case S2KSHA1, S2KCHECKSUM, S2KAEAD:
if (v5 || v6) && pk.s2kType == S2KCHECKSUM {
return errors.StructuralError(fmt.Sprintf("wrong s2k identifier for version %d", pk.Version))
}
_, err = readFull(r, buf[:])
if err != nil {
@@ -182,6 +235,29 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
if pk.cipher != 0 && !pk.cipher.IsSupported() {
return errors.UnsupportedError("unsupported cipher function in private key")
}
// [Optional] If string-to-key usage octet was 253,
// a one-octet AEAD algorithm.
if pk.s2kType == S2KAEAD {
_, err = readFull(r, buf[:])
if err != nil {
return
}
pk.aead = AEADMode(buf[0])
if !pk.aead.IsSupported() {
return errors.UnsupportedError("unsupported aead mode in private key")
}
}
// [Optional] Only for a version 6 packet,
// and if string-to-key usage octet was 255, 254, or 253,
// an one-octet count of the following field.
if v6 {
_, err = readFull(r, buf[:])
if err != nil {
return
}
}
pk.s2kParams, err = s2k.ParseIntoParams(r)
if err != nil {
return
@@ -189,28 +265,43 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
if pk.s2kParams.Dummy() {
return
}
if pk.s2kParams.Mode() == s2k.Argon2S2K && pk.s2kType != S2KAEAD {
return errors.StructuralError("using Argon2 S2K without AEAD is not allowed")
}
if pk.s2kParams.Mode() == s2k.SimpleS2K && pk.Version == 6 {
return errors.StructuralError("using Simple S2K with version 6 keys is not allowed")
}
pk.s2k, err = pk.s2kParams.Function()
if err != nil {
return
}
pk.Encrypted = true
if pk.s2kType == S2KSHA1 {
pk.sha1Checksum = true
}
default:
return errors.UnsupportedError("deprecated s2k function in private key")
}
if pk.Encrypted {
blockSize := pk.cipher.blockSize()
if blockSize == 0 {
var ivSize int
// If the S2K usage octet was 253, the IV is of the size expected by the AEAD mode,
// unless it's a version 5 key, in which case it's the size of the symmetric cipher's block size.
// For all other S2K modes, it's always the block size.
if !v5 && pk.s2kType == S2KAEAD {
ivSize = pk.aead.IvLength()
} else {
ivSize = pk.cipher.blockSize()
}
if ivSize == 0 {
return errors.UnsupportedError("unsupported cipher in private key: " + strconv.Itoa(int(pk.cipher)))
}
pk.iv = make([]byte, blockSize)
pk.iv = make([]byte, ivSize)
_, err = readFull(r, pk.iv)
if err != nil {
return
}
if v5 && pk.s2kType == S2KAEAD {
pk.iv = pk.iv[:pk.aead.IvLength()]
}
}
var privateKeyData []byte
@@ -230,7 +321,7 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
return
}
} else {
privateKeyData, err = ioutil.ReadAll(r)
privateKeyData, err = io.ReadAll(r)
if err != nil {
return
}
@@ -239,16 +330,22 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
if len(privateKeyData) < 2 {
return errors.StructuralError("truncated private key data")
}
var sum uint16
for i := 0; i < len(privateKeyData)-2; i++ {
sum += uint16(privateKeyData[i])
if pk.Version != 6 {
// checksum
var sum uint16
for i := 0; i < len(privateKeyData)-2; i++ {
sum += uint16(privateKeyData[i])
}
if privateKeyData[len(privateKeyData)-2] != uint8(sum>>8) ||
privateKeyData[len(privateKeyData)-1] != uint8(sum) {
return errors.StructuralError("private key checksum failure")
}
privateKeyData = privateKeyData[:len(privateKeyData)-2]
return pk.parsePrivateKey(privateKeyData)
} else {
// No checksum
return pk.parsePrivateKey(privateKeyData)
}
if privateKeyData[len(privateKeyData)-2] != uint8(sum>>8) ||
privateKeyData[len(privateKeyData)-1] != uint8(sum) {
return errors.StructuralError("private key checksum failure")
}
privateKeyData = privateKeyData[:len(privateKeyData)-2]
return pk.parsePrivateKey(privateKeyData)
}
pk.encryptedData = privateKeyData
@@ -280,18 +377,59 @@ func (pk *PrivateKey) Serialize(w io.Writer) (err error) {
optional := bytes.NewBuffer(nil)
if pk.Encrypted || pk.Dummy() {
optional.Write([]byte{uint8(pk.cipher)})
if err := pk.s2kParams.Serialize(optional); err != nil {
// [Optional] If string-to-key usage octet was 255, 254, or 253,
// a one-octet symmetric encryption algorithm.
if _, err = optional.Write([]byte{uint8(pk.cipher)}); err != nil {
return
}
// [Optional] If string-to-key usage octet was 253,
// a one-octet AEAD algorithm.
if pk.s2kType == S2KAEAD {
if _, err = optional.Write([]byte{uint8(pk.aead)}); err != nil {
return
}
}
s2kBuffer := bytes.NewBuffer(nil)
if err := pk.s2kParams.Serialize(s2kBuffer); err != nil {
return err
}
// [Optional] Only for a version 6 packet, and if string-to-key
// usage octet was 255, 254, or 253, an one-octet
// count of the following field.
if pk.Version == 6 {
if _, err = optional.Write([]byte{uint8(s2kBuffer.Len())}); err != nil {
return
}
}
// [Optional] If string-to-key usage octet was 255, 254, or 253,
// a string-to-key (S2K) specifier. The length of the string-to-key specifier
// depends on its type
if _, err = io.Copy(optional, s2kBuffer); err != nil {
return
}
// IV
if pk.Encrypted {
optional.Write(pk.iv)
if _, err = optional.Write(pk.iv); err != nil {
return
}
if pk.Version == 5 && pk.s2kType == S2KAEAD {
// Add padding for version 5
padding := make([]byte, pk.cipher.blockSize()-len(pk.iv))
if _, err = optional.Write(padding); err != nil {
return
}
}
}
}
if pk.Version == 5 {
if pk.Version == 5 || (pk.Version == 6 && pk.s2kType != S2KNON) {
contents.Write([]byte{uint8(optional.Len())})
}
io.Copy(contents, optional)
if _, err := io.Copy(contents, optional); err != nil {
return err
}
if !pk.Dummy() {
l := 0
@@ -303,8 +441,10 @@ func (pk *PrivateKey) Serialize(w io.Writer) (err error) {
return err
}
l = buf.Len()
checksum := mod64kHash(buf.Bytes())
buf.Write([]byte{byte(checksum >> 8), byte(checksum)})
if pk.Version != 6 {
checksum := mod64kHash(buf.Bytes())
buf.Write([]byte{byte(checksum >> 8), byte(checksum)})
}
priv = buf.Bytes()
} else {
priv, l = pk.encryptedData, len(pk.encryptedData)
@@ -370,6 +510,26 @@ func serializeECDHPrivateKey(w io.Writer, priv *ecdh.PrivateKey) error {
return err
}
func serializeX25519PrivateKey(w io.Writer, priv *x25519.PrivateKey) error {
_, err := w.Write(priv.Secret)
return err
}
func serializeX448PrivateKey(w io.Writer, priv *x448.PrivateKey) error {
_, err := w.Write(priv.Secret)
return err
}
func serializeEd25519PrivateKey(w io.Writer, priv *ed25519.PrivateKey) error {
_, err := w.Write(priv.MarshalByteSecret())
return err
}
func serializeEd448PrivateKey(w io.Writer, priv *ed448.PrivateKey) error {
_, err := w.Write(priv.MarshalByteSecret())
return err
}
// decrypt decrypts an encrypted private key using a decryption key.
func (pk *PrivateKey) decrypt(decryptionKey []byte) error {
if pk.Dummy() {
@@ -378,37 +538,51 @@ func (pk *PrivateKey) decrypt(decryptionKey []byte) error {
if !pk.Encrypted {
return nil
}
block := pk.cipher.new(decryptionKey)
cfb := cipher.NewCFBDecrypter(block, pk.iv)
data := make([]byte, len(pk.encryptedData))
cfb.XORKeyStream(data, pk.encryptedData)
if pk.sha1Checksum {
if len(data) < sha1.Size {
return errors.StructuralError("truncated private key data")
var data []byte
switch pk.s2kType {
case S2KAEAD:
aead := pk.aead.new(block)
additionalData, err := pk.additionalData()
if err != nil {
return err
}
h := sha1.New()
h.Write(data[:len(data)-sha1.Size])
sum := h.Sum(nil)
if !bytes.Equal(sum, data[len(data)-sha1.Size:]) {
return errors.StructuralError("private key checksum failure")
// Decrypt the encrypted key material with aead
data, err = aead.Open(nil, pk.iv, pk.encryptedData, additionalData)
if err != nil {
return err
}
data = data[:len(data)-sha1.Size]
} else {
if len(data) < 2 {
return errors.StructuralError("truncated private key data")
case S2KSHA1, S2KCHECKSUM:
cfb := cipher.NewCFBDecrypter(block, pk.iv)
data = make([]byte, len(pk.encryptedData))
cfb.XORKeyStream(data, pk.encryptedData)
if pk.s2kType == S2KSHA1 {
if len(data) < sha1.Size {
return errors.StructuralError("truncated private key data")
}
h := sha1.New()
h.Write(data[:len(data)-sha1.Size])
sum := h.Sum(nil)
if !bytes.Equal(sum, data[len(data)-sha1.Size:]) {
return errors.StructuralError("private key checksum failure")
}
data = data[:len(data)-sha1.Size]
} else {
if len(data) < 2 {
return errors.StructuralError("truncated private key data")
}
var sum uint16
for i := 0; i < len(data)-2; i++ {
sum += uint16(data[i])
}
if data[len(data)-2] != uint8(sum>>8) ||
data[len(data)-1] != uint8(sum) {
return errors.StructuralError("private key checksum failure")
}
data = data[:len(data)-2]
}
var sum uint16
for i := 0; i < len(data)-2; i++ {
sum += uint16(data[i])
}
if data[len(data)-2] != uint8(sum>>8) ||
data[len(data)-1] != uint8(sum) {
return errors.StructuralError("private key checksum failure")
}
data = data[:len(data)-2]
default:
return errors.InvalidArgumentError("invalid s2k type")
}
err := pk.parsePrivateKey(data)
@@ -424,7 +598,6 @@ func (pk *PrivateKey) decrypt(decryptionKey []byte) error {
pk.s2k = nil
pk.Encrypted = false
pk.encryptedData = nil
return nil
}
@@ -440,6 +613,9 @@ func (pk *PrivateKey) decryptWithCache(passphrase []byte, keyCache *s2k.Cache) e
if err != nil {
return err
}
if pk.s2kType == S2KAEAD {
key = pk.applyHKDF(key)
}
return pk.decrypt(key)
}
@@ -454,11 +630,14 @@ func (pk *PrivateKey) Decrypt(passphrase []byte) error {
key := make([]byte, pk.cipher.KeySize())
pk.s2k(key, passphrase)
if pk.s2kType == S2KAEAD {
key = pk.applyHKDF(key)
}
return pk.decrypt(key)
}
// DecryptPrivateKeys decrypts all encrypted keys with the given config and passphrase.
// Avoids recomputation of similar s2k key derivations.
// Avoids recomputation of similar s2k key derivations.
func DecryptPrivateKeys(keys []*PrivateKey, passphrase []byte) error {
// Create a cache to avoid recomputation of key derviations for the same passphrase.
s2kCache := &s2k.Cache{}
@@ -474,7 +653,7 @@ func DecryptPrivateKeys(keys []*PrivateKey, passphrase []byte) error {
}
// encrypt encrypts an unencrypted private key.
func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, cipherFunction CipherFunction) error {
func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, s2kType S2KType, cipherFunction CipherFunction, rand io.Reader) error {
if pk.Dummy() {
return errors.ErrDummyPrivateKey("dummy key found")
}
@@ -485,7 +664,15 @@ func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, cipherFunction Cip
if len(key) != cipherFunction.KeySize() {
return errors.InvalidArgumentError("supplied encryption key has the wrong size")
}
if params.Mode() == s2k.Argon2S2K && s2kType != S2KAEAD {
return errors.InvalidArgumentError("using Argon2 S2K without AEAD is not allowed")
}
if params.Mode() != s2k.Argon2S2K && params.Mode() != s2k.IteratedSaltedS2K &&
params.Mode() != s2k.SaltedS2K { // only allowed for high-entropy passphrases
return errors.InvalidArgumentError("insecure S2K mode")
}
priv := bytes.NewBuffer(nil)
err := pk.serializePrivateKey(priv)
if err != nil {
@@ -497,35 +684,53 @@ func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, cipherFunction Cip
pk.s2k, err = pk.s2kParams.Function()
if err != nil {
return err
}
}
privateKeyBytes := priv.Bytes()
pk.sha1Checksum = true
pk.s2kType = s2kType
block := pk.cipher.new(key)
pk.iv = make([]byte, pk.cipher.blockSize())
_, err = rand.Read(pk.iv)
if err != nil {
return err
}
cfb := cipher.NewCFBEncrypter(block, pk.iv)
if pk.sha1Checksum {
pk.s2kType = S2KSHA1
h := sha1.New()
h.Write(privateKeyBytes)
sum := h.Sum(nil)
privateKeyBytes = append(privateKeyBytes, sum...)
} else {
pk.s2kType = S2KCHECKSUM
var sum uint16
for _, b := range privateKeyBytes {
sum += uint16(b)
switch s2kType {
case S2KAEAD:
if pk.aead == 0 {
return errors.StructuralError("aead mode is not set on key")
}
priv.Write([]byte{uint8(sum >> 8), uint8(sum)})
aead := pk.aead.new(block)
additionalData, err := pk.additionalData()
if err != nil {
return err
}
pk.iv = make([]byte, aead.NonceSize())
_, err = io.ReadFull(rand, pk.iv)
if err != nil {
return err
}
// Decrypt the encrypted key material with aead
pk.encryptedData = aead.Seal(nil, pk.iv, privateKeyBytes, additionalData)
case S2KSHA1, S2KCHECKSUM:
pk.iv = make([]byte, pk.cipher.blockSize())
_, err = io.ReadFull(rand, pk.iv)
if err != nil {
return err
}
cfb := cipher.NewCFBEncrypter(block, pk.iv)
if s2kType == S2KSHA1 {
h := sha1.New()
h.Write(privateKeyBytes)
sum := h.Sum(nil)
privateKeyBytes = append(privateKeyBytes, sum...)
} else {
var sum uint16
for _, b := range privateKeyBytes {
sum += uint16(b)
}
privateKeyBytes = append(privateKeyBytes, []byte{uint8(sum >> 8), uint8(sum)}...)
}
pk.encryptedData = make([]byte, len(privateKeyBytes))
cfb.XORKeyStream(pk.encryptedData, privateKeyBytes)
default:
return errors.InvalidArgumentError("invalid s2k type for encryption")
}
pk.encryptedData = make([]byte, len(privateKeyBytes))
cfb.XORKeyStream(pk.encryptedData, privateKeyBytes)
pk.Encrypted = true
pk.PrivateKey = nil
return err
@@ -544,8 +749,15 @@ func (pk *PrivateKey) EncryptWithConfig(passphrase []byte, config *Config) error
return err
}
s2k(key, passphrase)
s2kType := S2KSHA1
if config.AEAD() != nil {
s2kType = S2KAEAD
pk.aead = config.AEAD().Mode()
pk.cipher = config.Cipher()
key = pk.applyHKDF(key)
}
// Encrypt the private key with the derived encryption key.
return pk.encrypt(key, params, config.Cipher())
return pk.encrypt(key, params, s2kType, config.Cipher(), config.Random())
}
// EncryptPrivateKeys encrypts all unencrypted keys with the given config and passphrase.
@@ -564,7 +776,16 @@ func EncryptPrivateKeys(keys []*PrivateKey, passphrase []byte, config *Config) e
s2k(encryptionKey, passphrase)
for _, key := range keys {
if key != nil && !key.Dummy() && !key.Encrypted {
err = key.encrypt(encryptionKey, params, config.Cipher())
s2kType := S2KSHA1
if config.AEAD() != nil {
s2kType = S2KAEAD
key.aead = config.AEAD().Mode()
key.cipher = config.Cipher()
derivedKey := key.applyHKDF(encryptionKey)
err = key.encrypt(derivedKey, params, s2kType, config.Cipher(), config.Random())
} else {
err = key.encrypt(encryptionKey, params, s2kType, config.Cipher(), config.Random())
}
if err != nil {
return err
}
@@ -581,7 +802,7 @@ func (pk *PrivateKey) Encrypt(passphrase []byte) error {
S2KMode: s2k.IteratedSaltedS2K,
S2KCount: 65536,
Hash: crypto.SHA256,
} ,
},
DefaultCipher: CipherAES256,
}
return pk.EncryptWithConfig(passphrase, config)
@@ -601,6 +822,14 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) {
err = serializeEdDSAPrivateKey(w, priv)
case *ecdh.PrivateKey:
err = serializeECDHPrivateKey(w, priv)
case *x25519.PrivateKey:
err = serializeX25519PrivateKey(w, priv)
case *x448.PrivateKey:
err = serializeX448PrivateKey(w, priv)
case *ed25519.PrivateKey:
err = serializeEd25519PrivateKey(w, priv)
case *ed448.PrivateKey:
err = serializeEd448PrivateKey(w, priv)
default:
err = errors.InvalidArgumentError("unknown private key type")
}
@@ -621,8 +850,18 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) {
return pk.parseECDHPrivateKey(data)
case PubKeyAlgoEdDSA:
return pk.parseEdDSAPrivateKey(data)
case PubKeyAlgoX25519:
return pk.parseX25519PrivateKey(data)
case PubKeyAlgoX448:
return pk.parseX448PrivateKey(data)
case PubKeyAlgoEd25519:
return pk.parseEd25519PrivateKey(data)
case PubKeyAlgoEd448:
return pk.parseEd448PrivateKey(data)
default:
err = errors.StructuralError("unknown private key type")
return
}
panic("impossible")
}
func (pk *PrivateKey) parseRSAPrivateKey(data []byte) (err error) {
@@ -743,6 +982,86 @@ func (pk *PrivateKey) parseECDHPrivateKey(data []byte) (err error) {
return nil
}
func (pk *PrivateKey) parseX25519PrivateKey(data []byte) (err error) {
publicKey := pk.PublicKey.PublicKey.(*x25519.PublicKey)
privateKey := x25519.NewPrivateKey(*publicKey)
privateKey.PublicKey = *publicKey
privateKey.Secret = make([]byte, x25519.KeySize)
if len(data) != x25519.KeySize {
err = errors.StructuralError("wrong x25519 key size")
return err
}
subtle.ConstantTimeCopy(1, privateKey.Secret, data)
if err = x25519.Validate(privateKey); err != nil {
return err
}
pk.PrivateKey = privateKey
return nil
}
func (pk *PrivateKey) parseX448PrivateKey(data []byte) (err error) {
publicKey := pk.PublicKey.PublicKey.(*x448.PublicKey)
privateKey := x448.NewPrivateKey(*publicKey)
privateKey.PublicKey = *publicKey
privateKey.Secret = make([]byte, x448.KeySize)
if len(data) != x448.KeySize {
err = errors.StructuralError("wrong x448 key size")
return err
}
subtle.ConstantTimeCopy(1, privateKey.Secret, data)
if err = x448.Validate(privateKey); err != nil {
return err
}
pk.PrivateKey = privateKey
return nil
}
func (pk *PrivateKey) parseEd25519PrivateKey(data []byte) (err error) {
publicKey := pk.PublicKey.PublicKey.(*ed25519.PublicKey)
privateKey := ed25519.NewPrivateKey(*publicKey)
privateKey.PublicKey = *publicKey
if len(data) != ed25519.SeedSize {
err = errors.StructuralError("wrong ed25519 key size")
return err
}
err = privateKey.UnmarshalByteSecret(data)
if err != nil {
return err
}
err = ed25519.Validate(privateKey)
if err != nil {
return err
}
pk.PrivateKey = privateKey
return nil
}
func (pk *PrivateKey) parseEd448PrivateKey(data []byte) (err error) {
publicKey := pk.PublicKey.PublicKey.(*ed448.PublicKey)
privateKey := ed448.NewPrivateKey(*publicKey)
privateKey.PublicKey = *publicKey
if len(data) != ed448.SeedSize {
err = errors.StructuralError("wrong ed448 key size")
return err
}
err = privateKey.UnmarshalByteSecret(data)
if err != nil {
return err
}
err = ed448.Validate(privateKey)
if err != nil {
return err
}
pk.PrivateKey = privateKey
return nil
}
func (pk *PrivateKey) parseEdDSAPrivateKey(data []byte) (err error) {
eddsaPub := pk.PublicKey.PublicKey.(*eddsa.PublicKey)
eddsaPriv := eddsa.NewPrivateKey(*eddsaPub)
@@ -767,6 +1086,41 @@ func (pk *PrivateKey) parseEdDSAPrivateKey(data []byte) (err error) {
return nil
}
func (pk *PrivateKey) additionalData() ([]byte, error) {
additionalData := bytes.NewBuffer(nil)
// Write additional data prefix based on packet type
var packetByte byte
if pk.PublicKey.IsSubkey {
packetByte = 0xc7
} else {
packetByte = 0xc5
}
// Write public key to additional data
_, err := additionalData.Write([]byte{packetByte})
if err != nil {
return nil, err
}
err = pk.PublicKey.serializeWithoutHeaders(additionalData)
if err != nil {
return nil, err
}
return additionalData.Bytes(), nil
}
func (pk *PrivateKey) applyHKDF(inputKey []byte) []byte {
var packetByte byte
if pk.PublicKey.IsSubkey {
packetByte = 0xc7
} else {
packetByte = 0xc5
}
associatedData := []byte{packetByte, byte(pk.Version), byte(pk.cipher), byte(pk.aead)}
hkdfReader := hkdf.New(sha256.New, inputKey, []byte{}, associatedData)
encryptionKey := make([]byte, pk.cipher.KeySize())
_, _ = readFull(hkdfReader, encryptionKey)
return encryptionKey
}
func validateDSAParameters(priv *dsa.PrivateKey) error {
p := priv.P // group prime
q := priv.Q // subgroup order

View File

@@ -5,7 +5,6 @@
package packet
import (
"crypto"
"crypto/dsa"
"crypto/rsa"
"crypto/sha1"
@@ -21,23 +20,24 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/ecdh"
"github.com/ProtonMail/go-crypto/openpgp/ecdsa"
"github.com/ProtonMail/go-crypto/openpgp/ed25519"
"github.com/ProtonMail/go-crypto/openpgp/ed448"
"github.com/ProtonMail/go-crypto/openpgp/eddsa"
"github.com/ProtonMail/go-crypto/openpgp/elgamal"
"github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
"github.com/ProtonMail/go-crypto/openpgp/internal/ecc"
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
"github.com/ProtonMail/go-crypto/openpgp/x25519"
"github.com/ProtonMail/go-crypto/openpgp/x448"
)
type kdfHashFunction byte
type kdfAlgorithm byte
// PublicKey represents an OpenPGP public key. See RFC 4880, section 5.5.2.
type PublicKey struct {
Version int
CreationTime time.Time
PubKeyAlgo PublicKeyAlgorithm
PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey
PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey, *x25519.PublicKey, *x448.PublicKey, *ed25519.PublicKey, *ed448.PublicKey
Fingerprint []byte
KeyId uint64
IsSubkey bool
@@ -61,11 +61,19 @@ func (pk *PublicKey) UpgradeToV5() {
pk.setFingerprintAndKeyId()
}
// UpgradeToV6 updates the version of the key to v6, and updates all necessary
// fields.
func (pk *PublicKey) UpgradeToV6() error {
pk.Version = 6
pk.setFingerprintAndKeyId()
return pk.checkV6Compatibility()
}
// signingKey provides a convenient abstraction over signature verification
// for v3 and v4 public keys.
type signingKey interface {
SerializeForHash(io.Writer) error
SerializeSignaturePrefix(io.Writer)
SerializeSignaturePrefix(io.Writer) error
serializeWithoutHeaders(io.Writer) error
}
@@ -174,6 +182,54 @@ func NewEdDSAPublicKey(creationTime time.Time, pub *eddsa.PublicKey) *PublicKey
return pk
}
func NewX25519PublicKey(creationTime time.Time, pub *x25519.PublicKey) *PublicKey {
pk := &PublicKey{
Version: 4,
CreationTime: creationTime,
PubKeyAlgo: PubKeyAlgoX25519,
PublicKey: pub,
}
pk.setFingerprintAndKeyId()
return pk
}
func NewX448PublicKey(creationTime time.Time, pub *x448.PublicKey) *PublicKey {
pk := &PublicKey{
Version: 4,
CreationTime: creationTime,
PubKeyAlgo: PubKeyAlgoX448,
PublicKey: pub,
}
pk.setFingerprintAndKeyId()
return pk
}
func NewEd25519PublicKey(creationTime time.Time, pub *ed25519.PublicKey) *PublicKey {
pk := &PublicKey{
Version: 4,
CreationTime: creationTime,
PubKeyAlgo: PubKeyAlgoEd25519,
PublicKey: pub,
}
pk.setFingerprintAndKeyId()
return pk
}
func NewEd448PublicKey(creationTime time.Time, pub *ed448.PublicKey) *PublicKey {
pk := &PublicKey{
Version: 4,
CreationTime: creationTime,
PubKeyAlgo: PubKeyAlgoEd448,
PublicKey: pub,
}
pk.setFingerprintAndKeyId()
return pk
}
func (pk *PublicKey) parse(r io.Reader) (err error) {
// RFC 4880, section 5.5.2
var buf [6]byte
@@ -181,12 +237,19 @@ func (pk *PublicKey) parse(r io.Reader) (err error) {
if err != nil {
return
}
if buf[0] != 4 && buf[0] != 5 {
pk.Version = int(buf[0])
if pk.Version != 4 && pk.Version != 5 && pk.Version != 6 {
return errors.UnsupportedError("public key version " + strconv.Itoa(int(buf[0])))
}
pk.Version = int(buf[0])
if pk.Version == 5 {
if V5Disabled && pk.Version == 5 {
return errors.UnsupportedError("support for parsing v5 entities is disabled; build with `-tags v5` if needed")
}
if pk.Version >= 5 {
// Read the four-octet scalar octet count
// The count is not used in this implementation
var n [4]byte
_, err = readFull(r, n[:])
if err != nil {
@@ -195,6 +258,7 @@ func (pk *PublicKey) parse(r io.Reader) (err error) {
}
pk.CreationTime = time.Unix(int64(uint32(buf[1])<<24|uint32(buf[2])<<16|uint32(buf[3])<<8|uint32(buf[4])), 0)
pk.PubKeyAlgo = PublicKeyAlgorithm(buf[5])
// Ignore four-ocet length
switch pk.PubKeyAlgo {
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly:
err = pk.parseRSA(r)
@@ -208,6 +272,14 @@ func (pk *PublicKey) parse(r io.Reader) (err error) {
err = pk.parseECDH(r)
case PubKeyAlgoEdDSA:
err = pk.parseEdDSA(r)
case PubKeyAlgoX25519:
err = pk.parseX25519(r)
case PubKeyAlgoX448:
err = pk.parseX448(r)
case PubKeyAlgoEd25519:
err = pk.parseEd25519(r)
case PubKeyAlgoEd448:
err = pk.parseEd448(r)
default:
err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo)))
}
@@ -221,21 +293,44 @@ func (pk *PublicKey) parse(r io.Reader) (err error) {
func (pk *PublicKey) setFingerprintAndKeyId() {
// RFC 4880, section 12.2
if pk.Version == 5 {
if pk.Version >= 5 {
fingerprint := sha256.New()
pk.SerializeForHash(fingerprint)
if err := pk.SerializeForHash(fingerprint); err != nil {
// Should not happen for a hash.
panic(err)
}
pk.Fingerprint = make([]byte, 32)
copy(pk.Fingerprint, fingerprint.Sum(nil))
pk.KeyId = binary.BigEndian.Uint64(pk.Fingerprint[:8])
} else {
fingerprint := sha1.New()
pk.SerializeForHash(fingerprint)
if err := pk.SerializeForHash(fingerprint); err != nil {
// Should not happen for a hash.
panic(err)
}
pk.Fingerprint = make([]byte, 20)
copy(pk.Fingerprint, fingerprint.Sum(nil))
pk.KeyId = binary.BigEndian.Uint64(pk.Fingerprint[12:20])
}
}
func (pk *PublicKey) checkV6Compatibility() error {
// Implementations MUST NOT accept or generate version 6 key material using the deprecated OIDs.
switch pk.PubKeyAlgo {
case PubKeyAlgoECDH:
curveInfo := ecc.FindByOid(pk.oid)
if curveInfo == nil {
return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
}
if curveInfo.GenName == ecc.Curve25519GenName {
return errors.StructuralError("cannot generate v6 key with deprecated OID: Curve25519Legacy")
}
case PubKeyAlgoEdDSA:
return errors.StructuralError("cannot generate v6 key with deprecated algorithm: EdDSALegacy")
}
return nil
}
// parseRSA parses RSA public key material from the given Reader. See RFC 4880,
// section 5.5.2.
func (pk *PublicKey) parseRSA(r io.Reader) (err error) {
@@ -324,16 +419,17 @@ func (pk *PublicKey) parseECDSA(r io.Reader) (err error) {
if _, err = pk.oid.ReadFrom(r); err != nil {
return
}
pk.p = new(encoding.MPI)
if _, err = pk.p.ReadFrom(r); err != nil {
return
}
curveInfo := ecc.FindByOid(pk.oid)
if curveInfo == nil {
return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
}
pk.p = new(encoding.MPI)
if _, err = pk.p.ReadFrom(r); err != nil {
return
}
c, ok := curveInfo.Curve.(ecc.ECDSACurve)
if !ok {
return errors.UnsupportedError(fmt.Sprintf("unsupported oid: %x", pk.oid))
@@ -353,6 +449,17 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) {
if _, err = pk.oid.ReadFrom(r); err != nil {
return
}
curveInfo := ecc.FindByOid(pk.oid)
if curveInfo == nil {
return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
}
if pk.Version == 6 && curveInfo.GenName == ecc.Curve25519GenName {
// Implementations MUST NOT accept or generate version 6 key material using the deprecated OIDs.
return errors.StructuralError("cannot read v6 key with deprecated OID: Curve25519Legacy")
}
pk.p = new(encoding.MPI)
if _, err = pk.p.ReadFrom(r); err != nil {
return
@@ -362,12 +469,6 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) {
return
}
curveInfo := ecc.FindByOid(pk.oid)
if curveInfo == nil {
return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
}
c, ok := curveInfo.Curve.(ecc.ECDHCurve)
if !ok {
return errors.UnsupportedError(fmt.Sprintf("unsupported oid: %x", pk.oid))
@@ -396,10 +497,16 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) {
}
func (pk *PublicKey) parseEdDSA(r io.Reader) (err error) {
if pk.Version == 6 {
// Implementations MUST NOT accept or generate version 6 key material using the deprecated OIDs.
return errors.StructuralError("cannot generate v6 key with deprecated algorithm: EdDSALegacy")
}
pk.oid = new(encoding.OID)
if _, err = pk.oid.ReadFrom(r); err != nil {
return
}
curveInfo := ecc.FindByOid(pk.oid)
if curveInfo == nil {
return errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
@@ -435,75 +542,145 @@ func (pk *PublicKey) parseEdDSA(r io.Reader) (err error) {
return
}
func (pk *PublicKey) parseX25519(r io.Reader) (err error) {
point := make([]byte, x25519.KeySize)
_, err = io.ReadFull(r, point)
if err != nil {
return
}
pub := &x25519.PublicKey{
Point: point,
}
pk.PublicKey = pub
return
}
func (pk *PublicKey) parseX448(r io.Reader) (err error) {
point := make([]byte, x448.KeySize)
_, err = io.ReadFull(r, point)
if err != nil {
return
}
pub := &x448.PublicKey{
Point: point,
}
pk.PublicKey = pub
return
}
func (pk *PublicKey) parseEd25519(r io.Reader) (err error) {
point := make([]byte, ed25519.PublicKeySize)
_, err = io.ReadFull(r, point)
if err != nil {
return
}
pub := &ed25519.PublicKey{
Point: point,
}
pk.PublicKey = pub
return
}
func (pk *PublicKey) parseEd448(r io.Reader) (err error) {
point := make([]byte, ed448.PublicKeySize)
_, err = io.ReadFull(r, point)
if err != nil {
return
}
pub := &ed448.PublicKey{
Point: point,
}
pk.PublicKey = pub
return
}
// SerializeForHash serializes the PublicKey to w with the special packet
// header format needed for hashing.
func (pk *PublicKey) SerializeForHash(w io.Writer) error {
pk.SerializeSignaturePrefix(w)
if err := pk.SerializeSignaturePrefix(w); err != nil {
return err
}
return pk.serializeWithoutHeaders(w)
}
// SerializeSignaturePrefix writes the prefix for this public key to the given Writer.
// The prefix is used when calculating a signature over this public key. See
// RFC 4880, section 5.2.4.
func (pk *PublicKey) SerializeSignaturePrefix(w io.Writer) {
func (pk *PublicKey) SerializeSignaturePrefix(w io.Writer) error {
var pLength = pk.algorithmSpecificByteCount()
if pk.Version == 5 {
pLength += 10 // version, timestamp (4), algorithm, key octet count (4).
w.Write([]byte{
0x9A,
// version, timestamp, algorithm
pLength += versionSize + timestampSize + algorithmSize
if pk.Version >= 5 {
// key octet count (4).
pLength += 4
_, err := w.Write([]byte{
// When a v4 signature is made over a key, the hash data starts with the octet 0x99, followed by a two-octet length
// of the key, and then the body of the key packet. When a v6 signature is made over a key, the hash data starts
// with the salt, then octet 0x9B, followed by a four-octet length of the key, and then the body of the key packet.
0x95 + byte(pk.Version),
byte(pLength >> 24),
byte(pLength >> 16),
byte(pLength >> 8),
byte(pLength),
})
return
return err
}
pLength += 6
w.Write([]byte{0x99, byte(pLength >> 8), byte(pLength)})
if _, err := w.Write([]byte{0x99, byte(pLength >> 8), byte(pLength)}); err != nil {
return err
}
return nil
}
func (pk *PublicKey) Serialize(w io.Writer) (err error) {
length := 6 // 6 byte header
length := uint32(versionSize + timestampSize + algorithmSize) // 6 byte header
length += pk.algorithmSpecificByteCount()
if pk.Version == 5 {
if pk.Version >= 5 {
length += 4 // octet key count
}
packetType := packetTypePublicKey
if pk.IsSubkey {
packetType = packetTypePublicSubkey
}
err = serializeHeader(w, packetType, length)
err = serializeHeader(w, packetType, int(length))
if err != nil {
return
}
return pk.serializeWithoutHeaders(w)
}
func (pk *PublicKey) algorithmSpecificByteCount() int {
length := 0
func (pk *PublicKey) algorithmSpecificByteCount() uint32 {
length := uint32(0)
switch pk.PubKeyAlgo {
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly:
length += int(pk.n.EncodedLength())
length += int(pk.e.EncodedLength())
length += uint32(pk.n.EncodedLength())
length += uint32(pk.e.EncodedLength())
case PubKeyAlgoDSA:
length += int(pk.p.EncodedLength())
length += int(pk.q.EncodedLength())
length += int(pk.g.EncodedLength())
length += int(pk.y.EncodedLength())
length += uint32(pk.p.EncodedLength())
length += uint32(pk.q.EncodedLength())
length += uint32(pk.g.EncodedLength())
length += uint32(pk.y.EncodedLength())
case PubKeyAlgoElGamal:
length += int(pk.p.EncodedLength())
length += int(pk.g.EncodedLength())
length += int(pk.y.EncodedLength())
length += uint32(pk.p.EncodedLength())
length += uint32(pk.g.EncodedLength())
length += uint32(pk.y.EncodedLength())
case PubKeyAlgoECDSA:
length += int(pk.oid.EncodedLength())
length += int(pk.p.EncodedLength())
length += uint32(pk.oid.EncodedLength())
length += uint32(pk.p.EncodedLength())
case PubKeyAlgoECDH:
length += int(pk.oid.EncodedLength())
length += int(pk.p.EncodedLength())
length += int(pk.kdf.EncodedLength())
length += uint32(pk.oid.EncodedLength())
length += uint32(pk.p.EncodedLength())
length += uint32(pk.kdf.EncodedLength())
case PubKeyAlgoEdDSA:
length += int(pk.oid.EncodedLength())
length += int(pk.p.EncodedLength())
length += uint32(pk.oid.EncodedLength())
length += uint32(pk.p.EncodedLength())
case PubKeyAlgoX25519:
length += x25519.KeySize
case PubKeyAlgoX448:
length += x448.KeySize
case PubKeyAlgoEd25519:
length += ed25519.PublicKeySize
case PubKeyAlgoEd448:
length += ed448.PublicKeySize
default:
panic("unknown public key algorithm")
}
@@ -522,7 +699,7 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) {
return
}
if pk.Version == 5 {
if pk.Version >= 5 {
n := pk.algorithmSpecificByteCount()
if _, err = w.Write([]byte{
byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n),
@@ -580,6 +757,22 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) {
}
_, err = w.Write(pk.p.EncodedBytes())
return
case PubKeyAlgoX25519:
publicKey := pk.PublicKey.(*x25519.PublicKey)
_, err = w.Write(publicKey.Point)
return
case PubKeyAlgoX448:
publicKey := pk.PublicKey.(*x448.PublicKey)
_, err = w.Write(publicKey.Point)
return
case PubKeyAlgoEd25519:
publicKey := pk.PublicKey.(*ed25519.PublicKey)
_, err = w.Write(publicKey.Point)
return
case PubKeyAlgoEd448:
publicKey := pk.PublicKey.(*ed448.PublicKey)
_, err = w.Write(publicKey.Point)
return
}
return errors.InvalidArgumentError("bad public-key algorithm")
}
@@ -589,6 +782,20 @@ func (pk *PublicKey) CanSign() bool {
return pk.PubKeyAlgo != PubKeyAlgoRSAEncryptOnly && pk.PubKeyAlgo != PubKeyAlgoElGamal && pk.PubKeyAlgo != PubKeyAlgoECDH
}
// VerifyHashTag returns nil iff sig appears to be a plausible signature of the data
// hashed into signed, based solely on its HashTag. signed is mutated by this call.
func VerifyHashTag(signed hash.Hash, sig *Signature) (err error) {
if sig.Version == 5 && (sig.SigType == 0x00 || sig.SigType == 0x01) {
sig.AddMetadataToHashSuffix()
}
signed.Write(sig.HashSuffix)
hashBytes := signed.Sum(nil)
if hashBytes[0] != sig.HashTag[0] || hashBytes[1] != sig.HashTag[1] {
return errors.SignatureError("hash tag doesn't match")
}
return nil
}
// VerifySignature returns nil iff sig is a valid signature, made by this
// public key, of the data hashed into signed. signed is mutated by this call.
func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err error) {
@@ -600,7 +807,8 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro
}
signed.Write(sig.HashSuffix)
hashBytes := signed.Sum(nil)
if sig.Version == 5 && (hashBytes[0] != sig.HashTag[0] || hashBytes[1] != sig.HashTag[1]) {
// see discussion https://github.com/ProtonMail/go-crypto/issues/107
if sig.Version >= 5 && (hashBytes[0] != sig.HashTag[0] || hashBytes[1] != sig.HashTag[1]) {
return errors.SignatureError("hash tag doesn't match")
}
@@ -639,6 +847,18 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro
return errors.SignatureError("EdDSA verification failure")
}
return nil
case PubKeyAlgoEd25519:
ed25519PublicKey := pk.PublicKey.(*ed25519.PublicKey)
if !ed25519.Verify(ed25519PublicKey, hashBytes, sig.EdSig) {
return errors.SignatureError("Ed25519 verification failure")
}
return nil
case PubKeyAlgoEd448:
ed448PublicKey := pk.PublicKey.(*ed448.PublicKey)
if !ed448.Verify(ed448PublicKey, hashBytes, sig.EdSig) {
return errors.SignatureError("ed448 verification failure")
}
return nil
default:
return errors.SignatureError("Unsupported public key algorithm used in signature")
}
@@ -646,11 +866,8 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro
// keySignatureHash returns a Hash of the message that needs to be signed for
// pk to assert a subkey relationship to signed.
func keySignatureHash(pk, signed signingKey, hashFunc crypto.Hash) (h hash.Hash, err error) {
if !hashFunc.Available() {
return nil, errors.UnsupportedError("hash function")
}
h = hashFunc.New()
func keySignatureHash(pk, signed signingKey, hashFunc hash.Hash) (h hash.Hash, err error) {
h = hashFunc
// RFC 4880, section 5.2.4
err = pk.SerializeForHash(h)
@@ -662,10 +879,28 @@ func keySignatureHash(pk, signed signingKey, hashFunc crypto.Hash) (h hash.Hash,
return
}
// VerifyKeyHashTag returns nil iff sig appears to be a plausible signature over this
// primary key and subkey, based solely on its HashTag.
func (pk *PublicKey) VerifyKeyHashTag(signed *PublicKey, sig *Signature) error {
preparedHash, err := sig.PrepareVerify()
if err != nil {
return err
}
h, err := keySignatureHash(pk, signed, preparedHash)
if err != nil {
return err
}
return VerifyHashTag(h, sig)
}
// VerifyKeySignature returns nil iff sig is a valid signature, made by this
// public key, of signed.
func (pk *PublicKey) VerifyKeySignature(signed *PublicKey, sig *Signature) error {
h, err := keySignatureHash(pk, signed, sig.Hash)
preparedHash, err := sig.PrepareVerify()
if err != nil {
return err
}
h, err := keySignatureHash(pk, signed, preparedHash)
if err != nil {
return err
}
@@ -679,10 +914,14 @@ func (pk *PublicKey) VerifyKeySignature(signed *PublicKey, sig *Signature) error
if sig.EmbeddedSignature == nil {
return errors.StructuralError("signing subkey is missing cross-signature")
}
preparedHashEmbedded, err := sig.EmbeddedSignature.PrepareVerify()
if err != nil {
return err
}
// Verify the cross-signature. This is calculated over the same
// data as the main signature, so we cannot just recursively
// call signed.VerifyKeySignature(...)
if h, err = keySignatureHash(pk, signed, sig.EmbeddedSignature.Hash); err != nil {
if h, err = keySignatureHash(pk, signed, preparedHashEmbedded); err != nil {
return errors.StructuralError("error while hashing for cross-signature: " + err.Error())
}
if err := signed.VerifySignature(h, sig.EmbeddedSignature); err != nil {
@@ -693,32 +932,44 @@ func (pk *PublicKey) VerifyKeySignature(signed *PublicKey, sig *Signature) error
return nil
}
func keyRevocationHash(pk signingKey, hashFunc crypto.Hash) (h hash.Hash, err error) {
if !hashFunc.Available() {
return nil, errors.UnsupportedError("hash function")
func keyRevocationHash(pk signingKey, hashFunc hash.Hash) (err error) {
return pk.SerializeForHash(hashFunc)
}
// VerifyRevocationHashTag returns nil iff sig appears to be a plausible signature
// over this public key, based solely on its HashTag.
func (pk *PublicKey) VerifyRevocationHashTag(sig *Signature) (err error) {
preparedHash, err := sig.PrepareVerify()
if err != nil {
return err
}
h = hashFunc.New()
// RFC 4880, section 5.2.4
err = pk.SerializeForHash(h)
return
if err = keyRevocationHash(pk, preparedHash); err != nil {
return err
}
return VerifyHashTag(preparedHash, sig)
}
// VerifyRevocationSignature returns nil iff sig is a valid signature, made by this
// public key.
func (pk *PublicKey) VerifyRevocationSignature(sig *Signature) (err error) {
h, err := keyRevocationHash(pk, sig.Hash)
preparedHash, err := sig.PrepareVerify()
if err != nil {
return err
}
return pk.VerifySignature(h, sig)
if err = keyRevocationHash(pk, preparedHash); err != nil {
return err
}
return pk.VerifySignature(preparedHash, sig)
}
// VerifySubkeyRevocationSignature returns nil iff sig is a valid subkey revocation signature,
// made by this public key, of signed.
func (pk *PublicKey) VerifySubkeyRevocationSignature(sig *Signature, signed *PublicKey) (err error) {
h, err := keySignatureHash(pk, signed, sig.Hash)
preparedHash, err := sig.PrepareVerify()
if err != nil {
return err
}
h, err := keySignatureHash(pk, signed, preparedHash)
if err != nil {
return err
}
@@ -727,15 +978,15 @@ func (pk *PublicKey) VerifySubkeyRevocationSignature(sig *Signature, signed *Pub
// userIdSignatureHash returns a Hash of the message that needs to be signed
// to assert that pk is a valid key for id.
func userIdSignatureHash(id string, pk *PublicKey, hashFunc crypto.Hash) (h hash.Hash, err error) {
if !hashFunc.Available() {
return nil, errors.UnsupportedError("hash function")
}
h = hashFunc.New()
func userIdSignatureHash(id string, pk *PublicKey, h hash.Hash) (err error) {
// RFC 4880, section 5.2.4
pk.SerializeSignaturePrefix(h)
pk.serializeWithoutHeaders(h)
if err := pk.SerializeSignaturePrefix(h); err != nil {
return err
}
if err := pk.serializeWithoutHeaders(h); err != nil {
return err
}
var buf [5]byte
buf[0] = 0xb4
@@ -746,28 +997,68 @@ func userIdSignatureHash(id string, pk *PublicKey, hashFunc crypto.Hash) (h hash
h.Write(buf[:])
h.Write([]byte(id))
return
return nil
}
// directKeySignatureHash returns a Hash of the message that needs to be signed.
func directKeySignatureHash(pk *PublicKey, h hash.Hash) (err error) {
return pk.SerializeForHash(h)
}
// VerifyUserIdHashTag returns nil iff sig appears to be a plausible signature over this
// public key and UserId, based solely on its HashTag
func (pk *PublicKey) VerifyUserIdHashTag(id string, sig *Signature) (err error) {
preparedHash, err := sig.PrepareVerify()
if err != nil {
return err
}
err = userIdSignatureHash(id, pk, preparedHash)
if err != nil {
return err
}
return VerifyHashTag(preparedHash, sig)
}
// VerifyUserIdSignature returns nil iff sig is a valid signature, made by this
// public key, that id is the identity of pub.
func (pk *PublicKey) VerifyUserIdSignature(id string, pub *PublicKey, sig *Signature) (err error) {
h, err := userIdSignatureHash(id, pub, sig.Hash)
h, err := sig.PrepareVerify()
if err != nil {
return err
}
if err := userIdSignatureHash(id, pub, h); err != nil {
return err
}
return pk.VerifySignature(h, sig)
}
// VerifyDirectKeySignature returns nil iff sig is a valid signature, made by this
// public key.
func (pk *PublicKey) VerifyDirectKeySignature(sig *Signature) (err error) {
h, err := sig.PrepareVerify()
if err != nil {
return err
}
if err := directKeySignatureHash(pk, h); err != nil {
return err
}
return pk.VerifySignature(h, sig)
}
// KeyIdString returns the public key's fingerprint in capital hex
// (e.g. "6C7EE1B8621CC013").
func (pk *PublicKey) KeyIdString() string {
return fmt.Sprintf("%X", pk.Fingerprint[12:20])
return fmt.Sprintf("%016X", pk.KeyId)
}
// KeyIdShortString returns the short form of public key's fingerprint
// in capital hex, as shown by gpg --list-keys (e.g. "621CC013").
// This function will return the full key id for v5 and v6 keys
// since the short key id is undefined for them.
func (pk *PublicKey) KeyIdShortString() string {
if pk.Version >= 5 {
return pk.KeyIdString()
}
return fmt.Sprintf("%X", pk.Fingerprint[16:20])
}
@@ -786,21 +1077,49 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) {
bitLength = pk.p.BitLength()
case PubKeyAlgoEdDSA:
bitLength = pk.p.BitLength()
case PubKeyAlgoX25519:
bitLength = x25519.KeySize * 8
case PubKeyAlgoX448:
bitLength = x448.KeySize * 8
case PubKeyAlgoEd25519:
bitLength = ed25519.PublicKeySize * 8
case PubKeyAlgoEd448:
bitLength = ed448.PublicKeySize * 8
default:
err = errors.InvalidArgumentError("bad public-key algorithm")
}
return
}
// Curve returns the used elliptic curve of this public key.
// Returns an error if no elliptic curve is used.
func (pk *PublicKey) Curve() (curve Curve, err error) {
switch pk.PubKeyAlgo {
case PubKeyAlgoECDSA, PubKeyAlgoECDH, PubKeyAlgoEdDSA:
curveInfo := ecc.FindByOid(pk.oid)
if curveInfo == nil {
return "", errors.UnsupportedError(fmt.Sprintf("unknown oid: %x", pk.oid))
}
curve = Curve(curveInfo.GenName)
case PubKeyAlgoEd25519, PubKeyAlgoX25519:
curve = Curve25519
case PubKeyAlgoEd448, PubKeyAlgoX448:
curve = Curve448
default:
err = errors.InvalidArgumentError("public key does not operate with an elliptic curve")
}
return
}
// KeyExpired returns whether sig is a self-signature of a key that has
// expired or is created in the future.
func (pk *PublicKey) KeyExpired(sig *Signature, currentTime time.Time) bool {
if pk.CreationTime.After(currentTime) {
if pk.CreationTime.Unix() > currentTime.Unix() {
return true
}
if sig.KeyLifetimeSecs == nil || *sig.KeyLifetimeSecs == 0 {
return false
}
expiry := pk.CreationTime.Add(time.Duration(*sig.KeyLifetimeSecs) * time.Second)
return currentTime.After(expiry)
return currentTime.Unix() > expiry.Unix()
}

View File

@@ -10,6 +10,12 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/errors"
)
type PacketReader interface {
Next() (p Packet, err error)
Push(reader io.Reader) (err error)
Unread(p Packet)
}
// Reader reads packets from an io.Reader and allows packets to be 'unread' so
// that they result from the next call to Next.
type Reader struct {
@@ -26,37 +32,81 @@ type Reader struct {
const maxReaders = 32
// Next returns the most recently unread Packet, or reads another packet from
// the top-most io.Reader. Unknown packet types are skipped.
// the top-most io.Reader. Unknown/unsupported/Marker packet types are skipped.
func (r *Reader) Next() (p Packet, err error) {
for {
p, err := r.read()
if err == io.EOF {
break
} else if err != nil {
if _, ok := err.(errors.UnknownPacketTypeError); ok {
continue
}
if _, ok := err.(errors.UnsupportedError); ok {
switch p.(type) {
case *SymmetricallyEncrypted, *AEADEncrypted, *Compressed, *LiteralData:
return nil, err
}
continue
}
return nil, err
} else {
//A marker packet MUST be ignored when received
switch p.(type) {
case *Marker:
continue
}
return p, nil
}
}
return nil, io.EOF
}
// Next returns the most recently unread Packet, or reads another packet from
// the top-most io.Reader. Unknown/Marker packet types are skipped while unsupported
// packets are returned as UnsupportedPacket type.
func (r *Reader) NextWithUnsupported() (p Packet, err error) {
for {
p, err = r.read()
if err == io.EOF {
break
} else if err != nil {
if _, ok := err.(errors.UnknownPacketTypeError); ok {
continue
}
if casteErr, ok := err.(errors.UnsupportedError); ok {
return &UnsupportedPacket{
IncompletePacket: p,
Error: casteErr,
}, nil
}
return
} else {
//A marker packet MUST be ignored when received
switch p.(type) {
case *Marker:
continue
}
return
}
}
return nil, io.EOF
}
func (r *Reader) read() (p Packet, err error) {
if len(r.q) > 0 {
p = r.q[len(r.q)-1]
r.q = r.q[:len(r.q)-1]
return
}
for len(r.readers) > 0 {
p, err = Read(r.readers[len(r.readers)-1])
if err == nil {
return
}
if err == io.EOF {
r.readers = r.readers[:len(r.readers)-1]
continue
}
// TODO: Add strict mode that rejects unknown packets, instead of ignoring them.
if _, ok := err.(errors.UnknownPacketTypeError); ok {
continue
}
if _, ok := err.(errors.UnsupportedError); ok {
switch p.(type) {
case *SymmetricallyEncrypted, *AEADEncrypted, *Compressed, *LiteralData:
return nil, err
}
continue
}
return nil, err
return p, err
}
return nil, io.EOF
}
@@ -84,3 +134,76 @@ func NewReader(r io.Reader) *Reader {
readers: []io.Reader{r},
}
}
// CheckReader is similar to Reader but additionally
// uses the pushdown automata to verify the read packet sequence.
type CheckReader struct {
Reader
verifier *SequenceVerifier
fullyRead bool
}
// Next returns the most recently unread Packet, or reads another packet from
// the top-most io.Reader. Unknown packet types are skipped.
// If the read packet sequence does not conform to the packet composition
// rules in rfc4880, it returns an error.
func (r *CheckReader) Next() (p Packet, err error) {
if r.fullyRead {
return nil, io.EOF
}
if len(r.q) > 0 {
p = r.q[len(r.q)-1]
r.q = r.q[:len(r.q)-1]
return
}
var errMsg error
for len(r.readers) > 0 {
p, errMsg, err = ReadWithCheck(r.readers[len(r.readers)-1], r.verifier)
if errMsg != nil {
err = errMsg
return
}
if err == nil {
return
}
if err == io.EOF {
r.readers = r.readers[:len(r.readers)-1]
continue
}
//A marker packet MUST be ignored when received
switch p.(type) {
case *Marker:
continue
}
if _, ok := err.(errors.UnknownPacketTypeError); ok {
continue
}
if _, ok := err.(errors.UnsupportedError); ok {
switch p.(type) {
case *SymmetricallyEncrypted, *AEADEncrypted, *Compressed, *LiteralData:
return nil, err
}
continue
}
return nil, err
}
if errMsg = r.verifier.Next(EOSSymbol); errMsg != nil {
return nil, errMsg
}
if errMsg = r.verifier.AssertValid(); errMsg != nil {
return nil, errMsg
}
r.fullyRead = true
return nil, io.EOF
}
func NewCheckReader(r io.Reader) *CheckReader {
return &CheckReader{
Reader: Reader{
q: nil,
readers: []io.Reader{r},
},
verifier: NewSequenceVerifier(),
fullyRead: false,
}
}

View File

@@ -0,0 +1,15 @@
package packet
// Recipient type represents a Intended Recipient Fingerprint subpacket
// See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#name-intended-recipient-fingerpr
type Recipient struct {
KeyVersion int
Fingerprint []byte
}
func (r *Recipient) Serialize() []byte {
packet := make([]byte, len(r.Fingerprint)+1)
packet[0] = byte(r.KeyVersion)
copy(packet[1:], r.Fingerprint)
return packet
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,13 @@ package packet
import (
"bytes"
"crypto/cipher"
"crypto/sha256"
"io"
"strconv"
"github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/s2k"
"golang.org/x/crypto/hkdf"
)
// This is the largest session key that we'll support. Since at most 256-bit cipher
@@ -39,10 +41,21 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error {
return err
}
ske.Version = int(buf[0])
if ske.Version != 4 && ske.Version != 5 {
if ske.Version != 4 && ske.Version != 5 && ske.Version != 6 {
return errors.UnsupportedError("unknown SymmetricKeyEncrypted version")
}
if V5Disabled && ske.Version == 5 {
return errors.UnsupportedError("support for parsing v5 entities is disabled; build with `-tags v5` if needed")
}
if ske.Version > 5 {
// Scalar octet count
if _, err := readFull(r, buf[:]); err != nil {
return err
}
}
// Cipher function
if _, err := readFull(r, buf[:]); err != nil {
return err
@@ -52,7 +65,7 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error {
return errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(buf[0])))
}
if ske.Version == 5 {
if ske.Version >= 5 {
// AEAD mode
if _, err := readFull(r, buf[:]); err != nil {
return errors.StructuralError("cannot read AEAD octet from packet")
@@ -60,6 +73,13 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error {
ske.Mode = AEADMode(buf[0])
}
if ske.Version > 5 {
// Scalar octet count
if _, err := readFull(r, buf[:]); err != nil {
return err
}
}
var err error
if ske.s2k, err = s2k.Parse(r); err != nil {
if _, ok := err.(errors.ErrDummyPrivateKey); ok {
@@ -68,7 +88,7 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error {
return err
}
if ske.Version == 5 {
if ske.Version >= 5 {
// AEAD IV
iv := make([]byte, ske.Mode.IvLength())
_, err := readFull(r, iv)
@@ -109,8 +129,8 @@ func (ske *SymmetricKeyEncrypted) Decrypt(passphrase []byte) ([]byte, CipherFunc
case 4:
plaintextKey, cipherFunc, err := ske.decryptV4(key)
return plaintextKey, cipherFunc, err
case 5:
plaintextKey, err := ske.decryptV5(key)
case 5, 6:
plaintextKey, err := ske.aeadDecrypt(ske.Version, key)
return plaintextKey, CipherFunction(0), err
}
err := errors.UnsupportedError("unknown SymmetricKeyEncrypted version")
@@ -136,9 +156,9 @@ func (ske *SymmetricKeyEncrypted) decryptV4(key []byte) ([]byte, CipherFunction,
return plaintextKey, cipherFunc, nil
}
func (ske *SymmetricKeyEncrypted) decryptV5(key []byte) ([]byte, error) {
adata := []byte{0xc3, byte(5), byte(ske.CipherFunc), byte(ske.Mode)}
aead := getEncryptedKeyAeadInstance(ske.CipherFunc, ske.Mode, key, adata)
func (ske *SymmetricKeyEncrypted) aeadDecrypt(version int, key []byte) ([]byte, error) {
adata := []byte{0xc3, byte(version), byte(ske.CipherFunc), byte(ske.Mode)}
aead := getEncryptedKeyAeadInstance(ske.CipherFunc, ske.Mode, key, adata, version)
plaintextKey, err := aead.Open(nil, ske.iv, ske.encryptedKey, adata)
if err != nil {
@@ -175,10 +195,22 @@ func SerializeSymmetricKeyEncrypted(w io.Writer, passphrase []byte, config *Conf
// the given passphrase. The returned session key must be passed to
// SerializeSymmetricallyEncrypted.
// If config is nil, sensible defaults will be used.
// Deprecated: Use SerializeSymmetricKeyEncryptedAEADReuseKey instead.
func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, passphrase []byte, config *Config) (err error) {
return SerializeSymmetricKeyEncryptedAEADReuseKey(w, sessionKey, passphrase, config.AEAD() != nil, config)
}
// SerializeSymmetricKeyEncryptedAEADReuseKey serializes a symmetric key packet to w.
// The packet contains the given session key, encrypted by a key derived from
// the given passphrase. The returned session key must be passed to
// SerializeSymmetricallyEncrypted.
// If aeadSupported is set, SKESK v6 is used, otherwise v4.
// Note: aeadSupported MUST match the value passed to SerializeSymmetricallyEncrypted.
// If config is nil, sensible defaults will be used.
func SerializeSymmetricKeyEncryptedAEADReuseKey(w io.Writer, sessionKey []byte, passphrase []byte, aeadSupported bool, config *Config) (err error) {
var version int
if config.AEAD() != nil {
version = 5
if aeadSupported {
version = 6
} else {
version = 4
}
@@ -203,11 +235,15 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass
switch version {
case 4:
packetLength = 2 /* header */ + len(s2kBytes) + 1 /* cipher type */ + keySize
case 5:
case 5, 6:
ivLen := config.AEAD().Mode().IvLength()
tagLen := config.AEAD().Mode().TagLength()
packetLength = 3 + len(s2kBytes) + ivLen + keySize + tagLen
}
if version > 5 {
packetLength += 2 // additional octet count fields
}
err = serializeHeader(w, packetTypeSymmetricKeyEncrypted, packetLength)
if err != nil {
return
@@ -216,13 +252,22 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass
// Symmetric Key Encrypted Version
buf := []byte{byte(version)}
if version > 5 {
// Scalar octet count
buf = append(buf, byte(3+len(s2kBytes)+config.AEAD().Mode().IvLength()))
}
// Cipher function
buf = append(buf, byte(cipherFunc))
if version == 5 {
if version >= 5 {
// AEAD mode
buf = append(buf, byte(config.AEAD().Mode()))
}
if version > 5 {
// Scalar octet count
buf = append(buf, byte(len(s2kBytes)))
}
_, err = w.Write(buf)
if err != nil {
return
@@ -243,10 +288,10 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass
if err != nil {
return
}
case 5:
case 5, 6:
mode := config.AEAD().Mode()
adata := []byte{0xc3, byte(5), byte(cipherFunc), byte(mode)}
aead := getEncryptedKeyAeadInstance(cipherFunc, mode, keyEncryptingKey, adata)
adata := []byte{0xc3, byte(version), byte(cipherFunc), byte(mode)}
aead := getEncryptedKeyAeadInstance(cipherFunc, mode, keyEncryptingKey, adata, version)
// Sample iv using random reader
iv := make([]byte, config.AEAD().Mode().IvLength())
@@ -270,7 +315,17 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass
return
}
func getEncryptedKeyAeadInstance(c CipherFunction, mode AEADMode, inputKey, associatedData []byte) (aead cipher.AEAD) {
blockCipher := c.new(inputKey)
func getEncryptedKeyAeadInstance(c CipherFunction, mode AEADMode, inputKey, associatedData []byte, version int) (aead cipher.AEAD) {
var blockCipher cipher.Block
if version > 5 {
hkdfReader := hkdf.New(sha256.New, inputKey, []byte{}, associatedData)
encryptionKey := make([]byte, c.KeySize())
_, _ = readFull(hkdfReader, encryptionKey)
blockCipher = c.new(encryptionKey)
} else {
blockCipher = c.new(inputKey)
}
return mode.new(blockCipher)
}

View File

@@ -74,6 +74,10 @@ func (se *SymmetricallyEncrypted) Decrypt(c CipherFunction, key []byte) (io.Read
// SerializeSymmetricallyEncrypted serializes a symmetrically encrypted packet
// to w and returns a WriteCloser to which the to-be-encrypted packets can be
// written.
// If aeadSupported is set to true, SEIPDv2 is used with the indicated CipherSuite.
// Otherwise, SEIPDv1 is used with the indicated CipherFunction.
// Note: aeadSupported MUST match the value passed to SerializeEncryptedKeyAEAD
// and/or SerializeSymmetricKeyEncryptedAEADReuseKey.
// If config is nil, sensible defaults will be used.
func SerializeSymmetricallyEncrypted(w io.Writer, c CipherFunction, aeadSupported bool, cipherSuite CipherSuite, key []byte, config *Config) (Contents io.WriteCloser, err error) {
writeCloser := noOpCloser{w}

View File

@@ -7,7 +7,9 @@ package packet
import (
"crypto/cipher"
"crypto/sha256"
"fmt"
"io"
"strconv"
"github.com/ProtonMail/go-crypto/openpgp/errors"
"golang.org/x/crypto/hkdf"
@@ -25,19 +27,19 @@ func (se *SymmetricallyEncrypted) parseAead(r io.Reader) error {
se.Cipher = CipherFunction(headerData[0])
// cipherFunc must have block size 16 to use AEAD
if se.Cipher.blockSize() != 16 {
return errors.UnsupportedError("invalid aead cipher: " + string(se.Cipher))
return errors.UnsupportedError("invalid aead cipher: " + strconv.Itoa(int(se.Cipher)))
}
// Mode
se.Mode = AEADMode(headerData[1])
if se.Mode.TagLength() == 0 {
return errors.UnsupportedError("unknown aead mode: " + string(se.Mode))
return errors.UnsupportedError("unknown aead mode: " + strconv.Itoa(int(se.Mode)))
}
// Chunk size
se.ChunkSizeByte = headerData[2]
if se.ChunkSizeByte > 16 {
return errors.UnsupportedError("invalid aead chunk size byte: " + string(se.ChunkSizeByte))
return errors.UnsupportedError("invalid aead chunk size byte: " + strconv.Itoa(int(se.ChunkSizeByte)))
}
// Salt
@@ -62,11 +64,16 @@ func (se *SymmetricallyEncrypted) associatedData() []byte {
// decryptAead decrypts a V2 SEIPD packet (AEAD) as specified in
// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-5.13.2
func (se *SymmetricallyEncrypted) decryptAead(inputKey []byte) (io.ReadCloser, error) {
aead, nonce := getSymmetricallyEncryptedAeadInstance(se.Cipher, se.Mode, inputKey, se.Salt[:], se.associatedData())
if se.Cipher.KeySize() != len(inputKey) {
return nil, errors.StructuralError(fmt.Sprintf("invalid session key length for cipher: got %d bytes, but expected %d bytes", len(inputKey), se.Cipher.KeySize()))
}
aead, nonce := getSymmetricallyEncryptedAeadInstance(se.Cipher, se.Mode, inputKey, se.Salt[:], se.associatedData())
// Carry the first tagLen bytes
chunkSize := decodeAEADChunkSize(se.ChunkSizeByte)
tagLen := se.Mode.TagLength()
peekedBytes := make([]byte, tagLen)
chunkBytes := make([]byte, chunkSize+tagLen*2)
peekedBytes := chunkBytes[chunkSize+tagLen:]
n, err := io.ReadFull(se.Contents, peekedBytes)
if n < tagLen || (err != nil && err != io.EOF) {
return nil, errors.StructuralError("not enough data to decrypt:" + err.Error())
@@ -76,12 +83,13 @@ func (se *SymmetricallyEncrypted) decryptAead(inputKey []byte) (io.ReadCloser, e
aeadCrypter: aeadCrypter{
aead: aead,
chunkSize: decodeAEADChunkSize(se.ChunkSizeByte),
initialNonce: nonce,
nonce: nonce,
associatedData: se.associatedData(),
chunkIndex: make([]byte, 8),
chunkIndex: nonce[len(nonce)-8:],
packetTag: packetTypeSymmetricallyEncryptedIntegrityProtected,
},
reader: se.Contents,
chunkBytes: chunkBytes,
peekedBytes: peekedBytes,
}, nil
}
@@ -115,7 +123,7 @@ func serializeSymmetricallyEncryptedAead(ciphertext io.WriteCloser, cipherSuite
// Random salt
salt := make([]byte, aeadSaltSize)
if _, err := rand.Read(salt); err != nil {
if _, err := io.ReadFull(rand, salt); err != nil {
return nil, err
}
@@ -125,16 +133,20 @@ func serializeSymmetricallyEncryptedAead(ciphertext io.WriteCloser, cipherSuite
aead, nonce := getSymmetricallyEncryptedAeadInstance(cipherSuite.Cipher, cipherSuite.Mode, inputKey, salt, prefix)
chunkSize := decodeAEADChunkSize(chunkSizeByte)
tagLen := aead.Overhead()
chunkBytes := make([]byte, chunkSize+tagLen)
return &aeadEncrypter{
aeadCrypter: aeadCrypter{
aead: aead,
chunkSize: decodeAEADChunkSize(chunkSizeByte),
chunkSize: chunkSize,
associatedData: prefix,
chunkIndex: make([]byte, 8),
initialNonce: nonce,
nonce: nonce,
chunkIndex: nonce[len(nonce)-8:],
packetTag: packetTypeSymmetricallyEncryptedIntegrityProtected,
},
writer: ciphertext,
writer: ciphertext,
chunkBytes: chunkBytes,
}, nil
}
@@ -144,10 +156,10 @@ func getSymmetricallyEncryptedAeadInstance(c CipherFunction, mode AEADMode, inpu
encryptionKey := make([]byte, c.KeySize())
_, _ = readFull(hkdfReader, encryptionKey)
// Last 64 bits of nonce are the counter
nonce = make([]byte, mode.IvLength()-8)
nonce = make([]byte, mode.IvLength())
_, _ = readFull(hkdfReader, nonce)
// Last 64 bits of nonce are the counter
_, _ = readFull(hkdfReader, nonce[:len(nonce)-8])
blockCipher := c.new(encryptionKey)
aead = mode.new(blockCipher)

View File

@@ -148,7 +148,7 @@ const mdcPacketTagByte = byte(0x80) | 0x40 | 19
func (ser *seMDCReader) Close() error {
if ser.error {
return errors.ErrMDCMissing
return errors.ErrMDCHashMismatch
}
for !ser.eof {
@@ -159,7 +159,7 @@ func (ser *seMDCReader) Close() error {
break
}
if err != nil {
return errors.ErrMDCMissing
return errors.ErrMDCHashMismatch
}
}
@@ -172,7 +172,7 @@ func (ser *seMDCReader) Close() error {
// The hash already includes the MDC header, but we still check its value
// to confirm encryption correctness
if ser.trailer[0] != mdcPacketTagByte || ser.trailer[1] != sha1.Size {
return errors.ErrMDCMissing
return errors.ErrMDCHashMismatch
}
return nil
}
@@ -237,9 +237,9 @@ func serializeSymmetricallyEncryptedMdc(ciphertext io.WriteCloser, c CipherFunct
block := c.new(key)
blockSize := block.BlockSize()
iv := make([]byte, blockSize)
_, err = config.Random().Read(iv)
_, err = io.ReadFull(config.Random(), iv)
if err != nil {
return
return nil, err
}
s, prefix := NewOCFBEncrypter(block, iv, OCFBNoResync)
_, err = ciphertext.Write(prefix)

View File

@@ -9,7 +9,6 @@ import (
"image"
"image/jpeg"
"io"
"io/ioutil"
)
const UserAttrImageSubpacket = 1
@@ -63,7 +62,7 @@ func NewUserAttribute(contents ...*OpaqueSubpacket) *UserAttribute {
func (uat *UserAttribute) parse(r io.Reader) (err error) {
// RFC 4880, section 5.13
b, err := ioutil.ReadAll(r)
b, err := io.ReadAll(r)
if err != nil {
return
}

View File

@@ -6,7 +6,6 @@ package packet
import (
"io"
"io/ioutil"
"strings"
)
@@ -66,7 +65,7 @@ func NewUserId(name, comment, email string) *UserId {
func (uid *UserId) parse(r io.Reader) (err error) {
// RFC 4880, section 5.11
b, err := ioutil.ReadAll(r)
b, err := io.ReadAll(r)
if err != nil {
return
}

View File

@@ -46,6 +46,7 @@ type MessageDetails struct {
DecryptedWith Key // the private key used to decrypt the message, if any.
IsSigned bool // true if the message is signed.
SignedByKeyId uint64 // the key id of the signer, if any.
SignedByFingerprint []byte // the key fingerprint of the signer, if any.
SignedBy *Key // the key of the signer, if available.
LiteralData *packet.LiteralData // the metadata of the contents
UnverifiedBody io.Reader // the contents of the message.
@@ -117,7 +118,7 @@ ParsePackets:
// This packet contains the decryption key encrypted to a public key.
md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId)
switch p.Algo {
case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH:
case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448:
break
default:
continue
@@ -232,7 +233,7 @@ FindKey:
}
mdFinal, sensitiveParsingErr := readSignedMessage(packets, md, keyring, config)
if sensitiveParsingErr != nil {
return nil, errors.StructuralError("parsing error")
return nil, errors.HandleSensitiveParsingError(sensitiveParsingErr, md.decrypted != nil)
}
return mdFinal, nil
}
@@ -258,7 +259,7 @@ FindLiteralData:
}
switch p := p.(type) {
case *packet.Compressed:
if err := packets.Push(p.Body); err != nil {
if err := packets.Push(p.LimitedBodyReader(config.DecompressedMessageSizeLimit())); err != nil {
return nil, err
}
case *packet.OnePassSignature:
@@ -270,13 +271,17 @@ FindLiteralData:
prevLast = true
}
h, wrappedHash, err = hashForSignature(p.Hash, p.SigType)
h, wrappedHash, err = hashForSignature(p.Hash, p.SigType, p.Salt)
if err != nil {
md.SignatureError = err
}
md.IsSigned = true
if p.Version == 6 {
md.SignedByFingerprint = p.KeyFingerprint
}
md.SignedByKeyId = p.KeyId
if keyring != nil {
keys := keyring.KeysByIdUsage(p.KeyId, packet.KeyFlagSign)
if len(keys) > 0 {
@@ -292,7 +297,7 @@ FindLiteralData:
if md.IsSigned && md.SignatureError == nil {
md.UnverifiedBody = &signatureCheckReader{packets, h, wrappedHash, md, config}
} else if md.decrypted != nil {
md.UnverifiedBody = checkReader{md}
md.UnverifiedBody = &checkReader{md, false}
} else {
md.UnverifiedBody = md.LiteralData.Body
}
@@ -300,12 +305,22 @@ FindLiteralData:
return md, nil
}
func wrapHashForSignature(hashFunc hash.Hash, sigType packet.SignatureType) (hash.Hash, error) {
switch sigType {
case packet.SigTypeBinary:
return hashFunc, nil
case packet.SigTypeText:
return NewCanonicalTextHash(hashFunc), nil
}
return nil, errors.UnsupportedError("unsupported signature type: " + strconv.Itoa(int(sigType)))
}
// hashForSignature returns a pair of hashes that can be used to verify a
// signature. The signature may specify that the contents of the signed message
// should be preprocessed (i.e. to normalize line endings). Thus this function
// returns two hashes. The second should be used to hash the message itself and
// performs any needed preprocessing.
func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType) (hash.Hash, hash.Hash, error) {
func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType, sigSalt []byte) (hash.Hash, hash.Hash, error) {
if _, ok := algorithm.HashToHashIdWithSha1(hashFunc); !ok {
return nil, nil, errors.UnsupportedError("unsupported hash function")
}
@@ -313,14 +328,19 @@ func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType) (hash.
return nil, nil, errors.UnsupportedError("hash not available: " + strconv.Itoa(int(hashFunc)))
}
h := hashFunc.New()
if sigSalt != nil {
h.Write(sigSalt)
}
wrappedHash, err := wrapHashForSignature(h, sigType)
if err != nil {
return nil, nil, err
}
switch sigType {
case packet.SigTypeBinary:
return h, h, nil
return h, wrappedHash, nil
case packet.SigTypeText:
return h, NewCanonicalTextHash(h), nil
return h, wrappedHash, nil
}
return nil, nil, errors.UnsupportedError("unsupported signature type: " + strconv.Itoa(int(sigType)))
}
@@ -328,21 +348,27 @@ func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType) (hash.
// it closes the ReadCloser from any SymmetricallyEncrypted packet to trigger
// MDC checks.
type checkReader struct {
md *MessageDetails
md *MessageDetails
checked bool
}
func (cr checkReader) Read(buf []byte) (int, error) {
func (cr *checkReader) Read(buf []byte) (int, error) {
n, sensitiveParsingError := cr.md.LiteralData.Body.Read(buf)
if sensitiveParsingError == io.EOF {
if cr.checked {
// Only check once
return n, io.EOF
}
mdcErr := cr.md.decrypted.Close()
if mdcErr != nil {
return n, mdcErr
}
cr.checked = true
return n, io.EOF
}
if sensitiveParsingError != nil {
return n, errors.StructuralError("parsing error")
return n, errors.HandleSensitiveParsingError(sensitiveParsingError, true)
}
return n, nil
@@ -366,6 +392,7 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) {
scr.wrappedHash.Write(buf[:n])
}
readsDecryptedData := scr.md.decrypted != nil
if sensitiveParsingError == io.EOF {
var p packet.Packet
var readError error
@@ -384,7 +411,7 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) {
key := scr.md.SignedBy
signatureError := key.PublicKey.VerifySignature(scr.h, sig)
if signatureError == nil {
signatureError = checkSignatureDetails(key, sig, scr.config)
signatureError = checkMessageSignatureDetails(key, sig, scr.config)
}
scr.md.Signature = sig
scr.md.SignatureError = signatureError
@@ -408,16 +435,15 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) {
// unsigned hash of its own. In order to check this we need to
// close that Reader.
if scr.md.decrypted != nil {
mdcErr := scr.md.decrypted.Close()
if mdcErr != nil {
return n, mdcErr
if sensitiveParsingError := scr.md.decrypted.Close(); sensitiveParsingError != nil {
return n, errors.HandleSensitiveParsingError(sensitiveParsingError, true)
}
}
return n, io.EOF
}
if sensitiveParsingError != nil {
return n, errors.StructuralError("parsing error")
return n, errors.HandleSensitiveParsingError(sensitiveParsingError, readsDecryptedData)
}
return n, nil
@@ -428,14 +454,13 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) {
// if any, and a possible signature verification error.
// If the signer isn't known, ErrUnknownIssuer is returned.
func VerifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) {
var expectedHashes []crypto.Hash
return verifyDetachedSignature(keyring, signed, signature, expectedHashes, config)
return verifyDetachedSignature(keyring, signed, signature, nil, false, config)
}
// VerifyDetachedSignatureAndHash performs the same actions as
// VerifyDetachedSignature and checks that the expected hash functions were used.
func VerifyDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) {
return verifyDetachedSignature(keyring, signed, signature, expectedHashes, config)
return verifyDetachedSignature(keyring, signed, signature, expectedHashes, true, config)
}
// CheckDetachedSignature takes a signed file and a detached signature and
@@ -443,25 +468,24 @@ func VerifyDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader
// signature verification error. If the signer isn't known,
// ErrUnknownIssuer is returned.
func CheckDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (signer *Entity, err error) {
var expectedHashes []crypto.Hash
return CheckDetachedSignatureAndHash(keyring, signed, signature, expectedHashes, config)
_, signer, err = verifyDetachedSignature(keyring, signed, signature, nil, false, config)
return
}
// CheckDetachedSignatureAndHash performs the same actions as
// CheckDetachedSignature and checks that the expected hash functions were used.
func CheckDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (signer *Entity, err error) {
_, signer, err = verifyDetachedSignature(keyring, signed, signature, expectedHashes, config)
_, signer, err = verifyDetachedSignature(keyring, signed, signature, expectedHashes, true, config)
return
}
func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) {
func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, checkHashes bool, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) {
var issuerKeyId uint64
var hashFunc crypto.Hash
var sigType packet.SignatureType
var keys []Key
var p packet.Packet
expectedHashesLen := len(expectedHashes)
packets := packet.NewReader(signature)
for {
p, err = packets.Next()
@@ -483,16 +507,19 @@ func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expec
issuerKeyId = *sig.IssuerKeyId
hashFunc = sig.Hash
sigType = sig.SigType
for i, expectedHash := range expectedHashes {
if hashFunc == expectedHash {
break
if checkHashes {
matchFound := false
// check for hashes
for _, expectedHash := range expectedHashes {
if hashFunc == expectedHash {
matchFound = true
break
}
}
if i+1 == expectedHashesLen {
return nil, nil, errors.StructuralError("hash algorithm mismatch with cleartext message headers")
if !matchFound {
return nil, nil, errors.StructuralError("hash algorithm or salt mismatch with cleartext message headers")
}
}
keys = keyring.KeysByIdUsage(issuerKeyId, packet.KeyFlagSign)
if len(keys) > 0 {
break
@@ -503,7 +530,11 @@ func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expec
panic("unreachable")
}
h, wrappedHash, err := hashForSignature(hashFunc, sigType)
h, err := sig.PrepareVerify()
if err != nil {
return nil, nil, err
}
wrappedHash, err := wrapHashForSignature(h, sigType)
if err != nil {
return nil, nil, err
}
@@ -515,7 +546,7 @@ func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expec
for _, key := range keys {
err = key.PublicKey.VerifySignature(h, sig)
if err == nil {
return sig, key.Entity, checkSignatureDetails(&key, sig, config)
return sig, key.Entity, checkMessageSignatureDetails(&key, sig, config)
}
}
@@ -533,7 +564,7 @@ func CheckArmoredDetachedSignature(keyring KeyRing, signed, signature io.Reader,
return CheckDetachedSignature(keyring, signed, body, config)
}
// checkSignatureDetails returns an error if:
// checkMessageSignatureDetails returns an error if:
// - The signature (or one of the binding signatures mentioned below)
// has a unknown critical notation data subpacket
// - The primary key of the signing entity is revoked
@@ -551,15 +582,11 @@ func CheckArmoredDetachedSignature(keyring KeyRing, signed, signature io.Reader,
// NOTE: The order of these checks is important, as the caller may choose to
// ignore ErrSignatureExpired or ErrKeyExpired errors, but should never
// ignore any other errors.
//
// TODO: Also return an error if:
// - The primary key is expired according to a direct-key signature
// - (For V5 keys only:) The direct-key signature (exists and) is expired
func checkSignatureDetails(key *Key, signature *packet.Signature, config *packet.Config) error {
func checkMessageSignatureDetails(key *Key, signature *packet.Signature, config *packet.Config) error {
now := config.Now()
primaryIdentity := key.Entity.PrimaryIdentity()
primarySelfSignature, primaryIdentity := key.Entity.PrimarySelfSignature()
signedBySubKey := key.PublicKey != key.Entity.PrimaryKey
sigsToCheck := []*packet.Signature{signature, primaryIdentity.SelfSignature}
sigsToCheck := []*packet.Signature{signature, primarySelfSignature}
if signedBySubKey {
sigsToCheck = append(sigsToCheck, key.SelfSignature, key.SelfSignature.EmbeddedSignature)
}
@@ -572,10 +599,10 @@ func checkSignatureDetails(key *Key, signature *packet.Signature, config *packet
}
if key.Entity.Revoked(now) || // primary key is revoked
(signedBySubKey && key.Revoked(now)) || // subkey is revoked
primaryIdentity.Revoked(now) { // primary identity is revoked
(primaryIdentity != nil && primaryIdentity.Revoked(now)) { // primary identity is revoked for v4
return errors.ErrKeyRevoked
}
if key.Entity.PrimaryKey.KeyExpired(primaryIdentity.SelfSignature, now) { // primary key is expired
if key.Entity.PrimaryKey.KeyExpired(primarySelfSignature, now) { // primary key is expired
return errors.ErrKeyExpired
}
if signedBySubKey {

View File

@@ -26,6 +26,8 @@ const testKeys1And2PrivateHex = "9501d8044d3c5c10010400b1d13382944bd5aba23a43129
const dsaElGamalTestKeysHex = "9501e1044dfcb16a110400aa3e5c1a1f43dd28c2ffae8abf5cfce555ee874134d8ba0a0f7b868ce2214beddc74e5e1e21ded354a95d18acdaf69e5e342371a71fbb9093162e0c5f3427de413a7f2c157d83f5cd2f9d791256dc4f6f0e13f13c3302af27f2384075ab3021dff7a050e14854bbde0a1094174855fc02f0bae8e00a340d94a1f22b32e48485700a0cec672ac21258fb95f61de2ce1af74b2c4fa3e6703ff698edc9be22c02ae4d916e4fa223f819d46582c0516235848a77b577ea49018dcd5e9e15cff9dbb4663a1ae6dd7580fa40946d40c05f72814b0f88481207e6c0832c3bded4853ebba0a7e3bd8e8c66df33d5a537cd4acf946d1080e7a3dcea679cb2b11a72a33a2b6a9dc85f466ad2ddf4c3db6283fa645343286971e3dd700703fc0c4e290d45767f370831a90187e74e9972aae5bff488eeff7d620af0362bfb95c1a6c3413ab5d15a2e4139e5d07a54d72583914661ed6a87cce810be28a0aa8879a2dd39e52fb6fe800f4f181ac7e328f740cde3d09a05cecf9483e4cca4253e60d4429ffd679d9996a520012aad119878c941e3cf151459873bdfc2a9563472fe0303027a728f9feb3b864260a1babe83925ce794710cfd642ee4ae0e5b9d74cee49e9c67b6cd0ea5dfbb582132195a121356a1513e1bca73e5b80c58c7ccb4164453412f456c47616d616c2054657374204b65792031886204131102002205024dfcb16a021b03060b090807030206150802090a0b0416020301021e01021780000a091033af447ccd759b09fadd00a0b8fd6f5a790bad7e9f2dbb7632046dc4493588db009c087c6a9ba9f7f49fab221587a74788c00db4889ab00200009d0157044dfcb16a1004008dec3f9291205255ccff8c532318133a6840739dd68b03ba942676f9038612071447bf07d00d559c5c0875724ea16a4c774f80d8338b55fca691a0522e530e604215b467bbc9ccfd483a1da99d7bc2648b4318fdbd27766fc8bfad3fddb37c62b8ae7ccfe9577e9b8d1e77c1d417ed2c2ef02d52f4da11600d85d3229607943700030503ff506c94c87c8cab778e963b76cf63770f0a79bf48fb49d3b4e52234620fc9f7657f9f8d56c96a2b7c7826ae6b57ebb2221a3fe154b03b6637cea7e6d98e3e45d87cf8dc432f723d3d71f89c5192ac8d7290684d2c25ce55846a80c9a7823f6acd9bb29fa6cd71f20bc90eccfca20451d0c976e460e672b000df49466408d527affe0303027a728f9feb3b864260abd761730327bca2aaa4ea0525c175e92bf240682a0e83b226f97ecb2e935b62c9a133858ce31b271fa8eb41f6a1b3cd72a63025ce1a75ee4180dcc284884904181102000905024dfcb16a021b0c000a091033af447ccd759b09dd0b009e3c3e7296092c81bee5a19929462caaf2fff3ae26009e218c437a2340e7ea628149af1ec98ec091a43992b00200009501e1044dfcb1be1104009f61faa61aa43df75d128cbe53de528c4aec49ce9360c992e70c77072ad5623de0a3a6212771b66b39a30dad6781799e92608316900518ec01184a85d872365b7d2ba4bacfb5882ea3c2473d3750dc6178cc1cf82147fb58caa28b28e9f12f6d1efcb0534abed644156c91cca4ab78834268495160b2400bc422beb37d237c2300a0cac94911b6d493bda1e1fbc6feeca7cb7421d34b03fe22cec6ccb39675bb7b94a335c2b7be888fd3906a1125f33301d8aa6ec6ee6878f46f73961c8d57a3e9544d8ef2a2cbfd4d52da665b1266928cfe4cb347a58c412815f3b2d2369dec04b41ac9a71cc9547426d5ab941cccf3b18575637ccfb42df1a802df3cfe0a999f9e7109331170e3a221991bf868543960f8c816c28097e503fe319db10fb98049f3a57d7c80c420da66d56f3644371631fad3f0ff4040a19a4fedc2d07727a1b27576f75a4d28c47d8246f27071e12d7a8de62aad216ddbae6aa02efd6b8a3e2818cda48526549791ab277e447b3a36c57cefe9b592f5eab73959743fcc8e83cbefec03a329b55018b53eec196765ae40ef9e20521a603c551efe0303020950d53a146bf9c66034d00c23130cce95576a2ff78016ca471276e8227fb30b1ffbd92e61804fb0c3eff9e30b1a826ee8f3e4730b4d86273ca977b4164453412f456c47616d616c2054657374204b65792032886204131102002205024dfcb1be021b03060b090807030206150802090a0b0416020301021e01021780000a0910a86bf526325b21b22bd9009e34511620415c974750a20df5cb56b182f3b48e6600a0a9466cb1a1305a84953445f77d461593f1d42bc1b00200009d0157044dfcb1be1004009565a951da1ee87119d600c077198f1c1bceb0f7aa54552489298e41ff788fa8f0d43a69871f0f6f77ebdfb14a4260cf9fbeb65d5844b4272a1904dd95136d06c3da745dc46327dd44a0f16f60135914368c8039a34033862261806bb2c5ce1152e2840254697872c85441ccb7321431d75a747a4bfb1d2c66362b51ce76311700030503fc0ea76601c196768070b7365a200e6ddb09307f262d5f39eec467b5f5784e22abdf1aa49226f59ab37cb49969d8f5230ea65caf56015abda62604544ed526c5c522bf92bed178a078789f6c807b6d34885688024a5bed9e9f8c58d11d4b82487b44c5f470c5606806a0443b79cadb45e0f897a561a53f724e5349b9267c75ca17fe0303020950d53a146bf9c660bc5f4ce8f072465e2d2466434320c1e712272fafc20e342fe7608101580fa1a1a367e60486a7cd1246b7ef5586cf5e10b32762b710a30144f12dd17dd4884904181102000905024dfcb1be021b0c000a0910a86bf526325b21b2904c00a0b2b66b4b39ccffda1d10f3ea8d58f827e30a8b8e009f4255b2d8112a184e40cde43a34e8655ca7809370b0020000"
const ed25519wX25519Key = "c54b0663877fe31b00000020f94da7bb48d60a61e567706a6587d0331999bb9d891a08242ead84543df895a3001972817b12be707e8d5f586ce61361201d344eb266a2c82fde6835762b65b0b7c2b1061f1b0a00000042058263877fe3030b090705150a0e080c021600029b03021e09222106cb186c4f0609a697e4d52dfa6c722b0c1f1e27c18a56708f6525ec27bad9acc905270902070200000000ad2820103e2d7d227ec0e6d7ce4471db36bfc97083253690271498a7ef0576c07faae14585b3b903b0127ec4fda2f023045a2ec76bcb4f9571a9651e14aee1137a1d668442c88f951e33c4ffd33fb9a17d511eed758fc6d9cc50cb5fd793b2039d5804c74b0663877fe319000000208693248367f9e5015db922f8f48095dda784987f2d5985b12fbad16caf5e4435004d600a4f794d44775c57a26e0feefed558e9afffd6ad0d582d57fb2ba2dcedb8c29b06181b0a0000002c050263877fe322a106cb186c4f0609a697e4d52dfa6c722b0c1f1e27c18a56708f6525ec27bad9acc9021b0c00000000defa20a6e9186d9d5935fc8fe56314cdb527486a5a5120f9b762a235a729f039010a56b89c658568341fbef3b894e9834ad9bc72afae2f4c9c47a43855e65f1cb0a3f77bbc5f61085c1f8249fe4e7ca59af5f0bcee9398e0fa8d76e522e1d8ab42bb0d"
const signedMessageHex = "a3019bc0cbccc0c4b8d8b74ee2108fe16ec6d3ca490cbe362d3f8333d3f352531472538b8b13d353b97232f352158c20943157c71c16064626063656269052062e4e01987e9b6fccff4b7df3a34c534b23e679cbec3bc0f8f6e64dfb4b55fe3f8efa9ce110ddb5cd79faf1d753c51aecfa669f7e7aa043436596cccc3359cb7dd6bbe9ecaa69e5989d9e57209571edc0b2fa7f57b9b79a64ee6e99ce1371395fee92fec2796f7b15a77c386ff668ee27f6d38f0baa6c438b561657377bf6acff3c5947befd7bf4c196252f1d6e5c524d0300"
const signedTextMessageHex = "a3019bc0cbccc8c4b8d8b74ee2108fe16ec6d36a250cbece0c178233d3f352531472538b8b13d35379b97232f352158ca0b4312f57c71c1646462606365626906a062e4e019811591798ff99bf8afee860b0d8a8c2a85c3387e3bcf0bb3b17987f2bbcfab2aa526d930cbfd3d98757184df3995c9f3e7790e36e3e9779f06089d4c64e9e47dd6202cb6e9bc73c5d11bb59fbaf89d22d8dc7cf199ddf17af96e77c5f65f9bbed56f427bd8db7af37f6c9984bf9385efaf5f184f986fb3e6adb0ecfe35bbf92d16a7aa2a344fb0bc52fb7624f0200"
@@ -160,18 +162,78 @@ TcIYl5/Uyoi+FOvPLcNw4hOv2nwUzSSVAw==
=IiS2
-----END PGP PRIVATE KEY BLOCK-----`
// Generated with the above private key
const v5PrivKeyMsg = `-----BEGIN PGP MESSAGE-----
Version: OpenPGP.js v4.10.7
Comment: https://openpgpjs.org
// See OpenPGP crypto refresh Section A.3.
const v6PrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
xA0DAQoWGTR7yYckZAIByxF1B21zZy50eHRfbIGSdGVzdMJ3BQEWCgAGBQJf
bIGSACMiIQUZNHvJhyRkAl+Z3z7C4AAO2YhIkuH3s+pMlACRWVabVDQvAP9G
y29VPonFXqi2zKkpZrvyvZxg+n5e8Nt9wNbuxeCd3QD/TtO2s+JvjrE4Siwv
UQdl5MlBka1QSNbMq2Bz7XwNPg4=
=6lbM
xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB
exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ
BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6
2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh
RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe
7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/
LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG
GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6
2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE
M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr
k0mXubZvyl4GBg==
-----END PGP PRIVATE KEY BLOCK-----`
// See OpenPGP crypto refresh merge request:
// https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/304
const v6PrivKeyMsg = `-----BEGIN PGP MESSAGE-----
wV0GIQYSyD8ecG9jCP4VGkF3Q6HwM3kOk+mXhIjR2zeNqZMIhRmHzxjV8bU/gXzO
WgBM85PMiVi93AZfJfhK9QmxfdNnZBjeo1VDeVZheQHgaVf7yopqR6W1FT6NOrfS
aQIHAgZhZBZTW+CwcW1g4FKlbExAf56zaw76/prQoN+bAzxpohup69LA7JW/Vp0l
yZnuSj3hcFj0DfqLTGgr4/u717J+sPWbtQBfgMfG9AOIwwrUBqsFE9zW+f1zdlYo
bhF30A+IitsxxA==
-----END PGP MESSAGE-----`
// See OpenPGP crypto refresh merge request:
// https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/305
const v6PrivKeyInlineSignMsg = `-----BEGIN PGP MESSAGE-----
wV0GIQYSyD8ecG9jCP4VGkF3Q6HwM3kOk+mXhIjR2zeNqZMIhRmHzxjV8bU/gXzO
WgBM85PMiVi93AZfJfhK9QmxfdNnZBjeo1VDeVZheQHgaVf7yopqR6W1FT6NOrfS
aQIHAgZhZBZTW+CwcW1g4FKlbExAf56zaw76/prQoN+bAzxpohup69LA7JW/Vp0l
yZnuSj3hcFj0DfqLTGgr4/u717J+sPWbtQBfgMfG9AOIwwrUBqsFE9zW+f1zdlYo
bhF30A+IitsxxA==
-----END PGP MESSAGE-----`
// See https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/274
// decryption password: "correct horse battery staple"
const v6ArgonSealedPrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
xYIGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laP9JgkC
FARdb9ccngltHraRe25uHuyuAQQVtKipJ0+r5jL4dacGWSAheCWPpITYiyfyIOPS
3gIDyg8f7strd1OB4+LZsUhcIjOMpVHgmiY/IutJkulneoBYwrEGHxsKAAAAQgWC
Y4d/4wMLCQcFFQoOCAwCFgACmwMCHgkiIQbLGGxPBgmml+TVLfpscisMHx4nwYpW
cI9lJewnutmsyQUnCQIHAgAAAACtKCAQPi19In7A5tfORHHbNr/JcIMlNpAnFJin
7wV2wH+q4UWFs7kDsBJ+xP2i8CMEWi7Ha8tPlXGpZR4UruETeh1mhELIj5UeM8T/
0z+5oX1RHu11j8bZzFDLX9eTsgOdWATHggZjh3/jGQAAACCGkySDZ/nlAV25Ivj0
gJXdp4SYfy1ZhbEvutFsr15ENf0mCQIUBA5hhGgp2oaavg6mFUXcFMwBBBUuE8qf
9Ock+xwusd+GAglBr5LVyr/lup3xxQvHXFSjjA2haXfoN6xUGRdDEHI6+uevKjVR
v5oAxgu7eJpaXNjCmwYYGwoAAAAsBYJjh3/jApsMIiEGyxhsTwYJppfk1S36bHIr
DB8eJ8GKVnCPZSXsJ7rZrMkAAAAABAEgpukYbZ1ZNfyP5WMUzbUnSGpaUSD5t2Ki
Nacp8DkBClZRa2c3AMQzSDXa9jGhYzxjzVb5scHDzTkjyRZWRdTq8U6L4da+/+Kt
ruh8m7Xo2ehSSFyWRSuTSZe5tm/KXgYG
-----END PGP PRIVATE KEY BLOCK-----`
const v4Key25519 = `-----BEGIN PGP PRIVATE KEY BLOCK-----
xUkEZB3qzRto01j2k2pwN5ux9w70stPinAdXULLr20CRW7U7h2GSeACch0M+
qzQg8yjFQ8VBvu3uwgKH9senoHmj72lLSCLTmhFKzQR0ZXN0wogEEBsIAD4F
gmQd6s0ECwkHCAmQIf45+TuC+xMDFQgKBBYAAgECGQECmwMCHgEWIQSWEzMi
jJUHvyIbVKIh/jn5O4L7EwAAUhaHNlgudvxARdPPETUzVgjuWi+YIz8w1xIb
lHQMvIrbe2sGCQIethpWofd0x7DHuv/ciHg+EoxJ/Td6h4pWtIoKx0kEZB3q
zRm4CyA7quliq7yx08AoOqHTuuCgvpkSdEhpp3pEyejQOgBo0p6ywIiLPllY
0t+jpNspHpAGfXID6oqjpYuJw3AfVRBlwnQEGBsIACoFgmQd6s0JkCH+Ofk7
gvsTApsMFiEElhMzIoyVB78iG1SiIf45+TuC+xMAAGgQuN9G73446ykvJ/mL
sCZ7zGFId2gBd1EnG0FTC4npfOKpck0X8dngByrCxU8LDSfvjsEp/xDAiKsQ
aU71tdtNBQ==
=e7jT
-----END PGP PRIVATE KEY BLOCK-----`
const keyWithExpiredCrossSig = `-----BEGIN PGP PUBLIC KEY BLOCK-----
xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv
@@ -272,3 +334,124 @@ AtNTq6ihLMD5v1d82ZC7tNatdlDMGWnIdvEMCv2GZcuIqDQ9rXWs49e7tq1NncLY
hz3tYjKhoFTKEIq3y3Pp
=h/aX
-----END PGP PUBLIC KEY BLOCK-----`
const keyv5Test = `-----BEGIN PGP PRIVATE KEY BLOCK-----
Comment: Bob's OpenPGP Transferable Secret Key
lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv
/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz
/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/
5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3
X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv
9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0
qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb
SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb
vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM
cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK
3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z
Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs
hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ
bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4
i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI
1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP
fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6
fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E
LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx
+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL
hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN
WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/
MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC
mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC
YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E
he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8
zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P
NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT
t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w
ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC
F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U
2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX
yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe
doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3
BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl
sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN
4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+
L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG
ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad
BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD
bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar
29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2
WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB
leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te
g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj
Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn
JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx
IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp
SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h
OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np
Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c
+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0
tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o
BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny
zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK
clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl
zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr
gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ
aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5
fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/
ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5
HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf
SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd
5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ
E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM
GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY
vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ
26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP
eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX
c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief
rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0
JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg
71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH
s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd
NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91
6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7
xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=
=miES
-----END PGP PRIVATE KEY BLOCK-----
`
const certv5Test = `-----BEGIN PGP PRIVATE KEY BLOCK-----
lGEFXJH05BYAAAAtCSsGAQQB2kcPAQEHQFhZlVcVVtwf+21xNQPX+ecMJJBL0MPd
fj75iux+my8QAAAAAAAiAQCHZ1SnSUmWqxEsoI6facIVZQu6mph3cBFzzTvcm5lA
Ng5ctBhlbW1hLmdvbGRtYW5AZXhhbXBsZS5uZXSIlgUTFggASCIhBRk0e8mHJGQC
X5nfPsLgAA7ZiEiS4fez6kyUAJFZVptUBQJckfTkAhsDBQsJCAcCAyICAQYVCgkI
CwIEFgIDAQIeBwIXgAAA9cAA/jiR3yMsZMeEQ40u6uzEoXa6UXeV/S3wwJAXRJy9
M8s0AP9vuL/7AyTfFXwwzSjDnYmzS0qAhbLDQ643N+MXGBJ2BZxmBVyR9OQSAAAA
MgorBgEEAZdVAQUBAQdA+nysrzml2UCweAqtpDuncSPlvrcBWKU0yfU0YvYWWAoD
AQgHAAAAAAAiAP9OdAPppjU1WwpqjIItkxr+VPQRT8Zm/Riw7U3F6v3OiBFHiHoF
GBYIACwiIQUZNHvJhyRkAl+Z3z7C4AAO2YhIkuH3s+pMlACRWVabVAUCXJH05AIb
DAAAOSQBAP4BOOIR/sGLNMOfeb5fPs/02QMieoiSjIBnijhob2U5AQC+RtOHCHx7
TcIYl5/Uyoi+FOvPLcNw4hOv2nwUzSSVAw==
=IiS2
-----END PGP PRIVATE KEY BLOCK-----
`
const msgv5Test = `-----BEGIN PGP MESSAGE-----
wcDMA3wvqk35PDeyAQv+PcQiLsoYTH30nJYQh3j3cJaO2+jErtVCrIQRIU0+
rmgMddERYST4A9mA0DQIiTI4FQ0Lp440D3BWCgpq3LlNWewGzduaWwym5rN6
cwHz5ccDqOcqbd9X0GXXGy/ZH/ljSgzuVMIytMAXKdF/vrRrVgH/+I7cxvm9
HwnhjMN5dF0j4aEt996H2T7cbtzSr2GN9SWGW8Gyu7I8Zx73hgrGUI7gDiJB
Afaff+P6hfkkHSGOItr94dde8J/7AUF4VEwwxdVVPvsNEFyvv6gRIbYtOCa2
6RE6h1V/QTxW2O7zZgzWALrE2ui0oaYr9QuqQSssd9CdgExLfdPbI+3/ZAnE
v31Idzpk3/6ILiakYHtXkElPXvf46mCNpobty8ysT34irF+fy3C1p3oGwAsx
5VDV9OSFU6z5U+UPbSPYAy9rkc5ZssuIKxCER2oTvZ2L8Q5cfUvEUiJtRGGn
CJlHrVDdp3FssKv2tlKgLkvxJLyoOjuEkj44H1qRk+D02FzmmUT/0sAHAYYx
lTir6mjHeLpcGjn4waUuWIAJyph8SxUexP60bic0L0NBa6Qp5SxxijKsPIDb
FPHxWwfJSDZRrgUyYT7089YFB/ZM4FHyH9TZcnxn0f0xIB7NS6YNDsxzN2zT
EVEYf+De4qT/dQTsdww78Chtcv9JY9r2kDm77dk2MUGHL2j7n8jasbLtgA7h
pn2DMIWLrGamMLWRmlwslolKr1sMV5x8w+5Ias6C33iBMl9phkg42an0gYmc
byVJHvLO/XErtC+GNIJeMg==
=liRq
-----END PGP MESSAGE-----
`

View File

@@ -87,10 +87,10 @@ func decodeCount(c uint8) int {
// encodeMemory converts the Argon2 "memory" in the range parallelism*8 to
// 2**31, inclusive, to an encoded memory. The return value is the
// octet that is actually stored in the GPG file. encodeMemory panics
// if is not in the above range
// if is not in the above range
// See OpenPGP crypto refresh Section 3.7.1.4.
func encodeMemory(memory uint32, parallelism uint8) uint8 {
if memory < (8 * uint32(parallelism)) || memory > uint32(2147483648) {
if memory < (8*uint32(parallelism)) || memory > uint32(2147483648) {
panic("Memory argument memory is outside the required range")
}
@@ -199,8 +199,8 @@ func Generate(rand io.Reader, c *Config) (*Params, error) {
}
params = &Params{
mode: SaltedS2K,
hashId: hashId,
mode: SaltedS2K,
hashId: hashId,
}
} else { // Enforce IteratedSaltedS2K method otherwise
hashId, ok := algorithm.HashToHashId(c.hash())
@@ -211,7 +211,7 @@ func Generate(rand io.Reader, c *Config) (*Params, error) {
c.S2KMode = IteratedSaltedS2K
}
params = &Params{
mode: IteratedSaltedS2K,
mode: IteratedSaltedS2K,
hashId: hashId,
countByte: c.EncodedCount(),
}
@@ -283,6 +283,9 @@ func ParseIntoParams(r io.Reader) (params *Params, err error) {
params.passes = buf[Argon2SaltSize]
params.parallelism = buf[Argon2SaltSize+1]
params.memoryExp = buf[Argon2SaltSize+2]
if err := validateArgon2Params(params); err != nil {
return nil, err
}
return params, nil
case GnuS2K:
// This is a GNU extension. See
@@ -300,15 +303,22 @@ func ParseIntoParams(r io.Reader) (params *Params, err error) {
return nil, errors.UnsupportedError("S2K function")
}
func (params *Params) Mode() Mode {
return params.mode
}
func (params *Params) Dummy() bool {
return params != nil && params.mode == GnuS2K
}
func (params *Params) salt() []byte {
switch params.mode {
case SaltedS2K, IteratedSaltedS2K: return params.saltBytes[:8]
case Argon2S2K: return params.saltBytes[:Argon2SaltSize]
default: return nil
case SaltedS2K, IteratedSaltedS2K:
return params.saltBytes[:8]
case Argon2S2K:
return params.saltBytes[:Argon2SaltSize]
default:
return nil
}
}
@@ -405,3 +415,22 @@ func Serialize(w io.Writer, key []byte, rand io.Reader, passphrase []byte, c *Co
f(key, passphrase)
return nil
}
// validateArgon2Params checks that the argon2 parameters are valid according to RFC9580.
func validateArgon2Params(params *Params) error {
// The number of passes t and the degree of parallelism p MUST be non-zero.
if params.parallelism == 0 {
return errors.StructuralError("invalid argon2 params: parallelism is 0")
}
if params.passes == 0 {
return errors.StructuralError("invalid argon2 params: iterations is 0")
}
// The encoded memory size MUST be a value from 3+ceil(log2(p)) to 31,
// such that the decoded memory size m is a value from 8*p to 2^31.
if params.memoryExp > 31 || decodeMemory(params.memoryExp) < 8*uint32(params.parallelism) {
return errors.StructuralError("invalid argon2 params: memory is out of bounds")
}
return nil
}

View File

@@ -5,7 +5,7 @@ package s2k
// the same parameters.
type Cache map[Params][]byte
// GetOrComputeDerivedKey tries to retrieve the key
// GetOrComputeDerivedKey tries to retrieve the key
// for the given s2k parameters from the cache.
// If there is no hit, it derives the key with the s2k function from the passphrase,
// updates the cache, and returns the key.

View File

@@ -50,9 +50,9 @@ type Config struct {
type Argon2Config struct {
NumberOfPasses uint8
DegreeOfParallelism uint8
// The memory parameter for Argon2 specifies desired memory usage in kibibytes.
// Memory specifies the desired Argon2 memory usage in kibibytes.
// For example memory=64*1024 sets the memory cost to ~64 MB.
Memory uint32
Memory uint32
}
func (c *Config) Mode() Mode {
@@ -115,7 +115,7 @@ func (c *Argon2Config) EncodedMemory() uint8 {
}
memory := c.Memory
lowerBound := uint32(c.Parallelism())*8
lowerBound := uint32(c.Parallelism()) * 8
upperBound := uint32(2147483648)
switch {

View File

@@ -76,7 +76,11 @@ func detachSign(w io.Writer, signer *Entity, message io.Reader, sigType packet.S
sig := createSignaturePacket(signingKey.PublicKey, sigType, config)
h, wrappedHash, err := hashForSignature(sig.Hash, sig.SigType)
h, err := sig.PrepareSign(config)
if err != nil {
return
}
wrappedHash, err := wrapHashForSignature(h, sig.SigType)
if err != nil {
return
}
@@ -249,40 +253,32 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit
}
var hash crypto.Hash
for _, hashId := range candidateHashes {
if h, ok := algorithm.HashIdToHash(hashId); ok && h.Available() {
hash = h
break
}
}
// If the hash specified by config is a candidate, we'll use that.
if configuredHash := config.Hash(); configuredHash.Available() {
for _, hashId := range candidateHashes {
if h, ok := algorithm.HashIdToHash(hashId); ok && h == configuredHash {
hash = h
break
}
}
}
if hash == 0 {
hashId := candidateHashes[0]
name, ok := algorithm.HashIdToString(hashId)
if !ok {
name = "#" + strconv.Itoa(int(hashId))
}
return nil, errors.InvalidArgumentError("cannot encrypt because no candidate hash functions are compiled in. (Wanted " + name + " in this case.)")
}
var salt []byte
if signer != nil {
if hash, err = selectHash(candidateHashes, config.Hash(), signer); err != nil {
return nil, err
}
var opsVersion = 3
if signer.Version == 6 {
opsVersion = signer.Version
}
ops := &packet.OnePassSignature{
Version: opsVersion,
SigType: sigType,
Hash: hash,
PubKeyAlgo: signer.PubKeyAlgo,
KeyId: signer.KeyId,
IsLast: true,
}
if opsVersion == 6 {
ops.KeyFingerprint = signer.Fingerprint
salt, err = packet.SignatureSaltForHash(hash, config.Random())
if err != nil {
return nil, err
}
ops.Salt = salt
}
if err := ops.Serialize(payload); err != nil {
return nil, err
}
@@ -310,19 +306,19 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit
}
if signer != nil {
h, wrappedHash, err := hashForSignature(hash, sigType)
h, wrappedHash, err := hashForSignature(hash, sigType, salt)
if err != nil {
return nil, err
}
metadata := &packet.LiteralData{
Format: 't',
Format: 'u',
FileName: hints.FileName,
Time: epochSeconds,
}
if hints.IsBinary {
metadata.Format = 'b'
}
return signatureWriter{payload, literalData, hash, wrappedHash, h, signer, sigType, config, metadata}, nil
return signatureWriter{payload, literalData, hash, wrappedHash, h, salt, signer, sigType, config, metadata}, nil
}
return literalData, nil
}
@@ -380,15 +376,19 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En
return nil, errors.InvalidArgumentError("cannot encrypt a message to key id " + strconv.FormatUint(to[i].PrimaryKey.KeyId, 16) + " because it has no valid encryption keys")
}
sig := to[i].PrimaryIdentity().SelfSignature
if !sig.SEIPDv2 {
primarySelfSignature, _ := to[i].PrimarySelfSignature()
if primarySelfSignature == nil {
return nil, errors.InvalidArgumentError("entity without a self-signature")
}
if !primarySelfSignature.SEIPDv2 {
aeadSupported = false
}
candidateCiphers = intersectPreferences(candidateCiphers, sig.PreferredSymmetric)
candidateHashes = intersectPreferences(candidateHashes, sig.PreferredHash)
candidateCipherSuites = intersectCipherSuites(candidateCipherSuites, sig.PreferredCipherSuites)
candidateCompression = intersectPreferences(candidateCompression, sig.PreferredCompression)
candidateCiphers = intersectPreferences(candidateCiphers, primarySelfSignature.PreferredSymmetric)
candidateHashes = intersectPreferences(candidateHashes, primarySelfSignature.PreferredHash)
candidateCipherSuites = intersectCipherSuites(candidateCipherSuites, primarySelfSignature.PreferredCipherSuites)
candidateCompression = intersectPreferences(candidateCompression, primarySelfSignature.PreferredCompression)
}
// In the event that the intersection of supported algorithms is empty we use the ones
@@ -422,13 +422,19 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En
}
}
symKey := make([]byte, cipher.KeySize())
var symKey []byte
if aeadSupported {
symKey = make([]byte, aeadCipherSuite.Cipher.KeySize())
} else {
symKey = make([]byte, cipher.KeySize())
}
if _, err := io.ReadFull(config.Random(), symKey); err != nil {
return nil, err
}
for _, key := range encryptKeys {
if err := packet.SerializeEncryptedKey(keyWriter, key.PublicKey, cipher, symKey, config); err != nil {
if err := packet.SerializeEncryptedKeyAEAD(keyWriter, key.PublicKey, cipher, aeadSupported, symKey, config); err != nil {
return nil, err
}
}
@@ -465,13 +471,17 @@ func Sign(output io.Writer, signed *Entity, hints *FileHints, config *packet.Con
hashToHashId(crypto.SHA3_512),
}
defaultHashes := candidateHashes[0:1]
preferredHashes := signed.PrimaryIdentity().SelfSignature.PreferredHash
primarySelfSignature, _ := signed.PrimarySelfSignature()
if primarySelfSignature == nil {
return nil, errors.StructuralError("signed entity has no self-signature")
}
preferredHashes := primarySelfSignature.PreferredHash
if len(preferredHashes) == 0 {
preferredHashes = defaultHashes
}
candidateHashes = intersectPreferences(candidateHashes, preferredHashes)
if len(candidateHashes) == 0 {
return nil, errors.InvalidArgumentError("cannot sign because signing key shares no common algorithms with candidate hashes")
return nil, errors.StructuralError("cannot sign because signing key shares no common algorithms with candidate hashes")
}
return writeAndSign(noOpCloser{output}, candidateHashes, signed, hints, packet.SigTypeBinary, config)
@@ -486,6 +496,7 @@ type signatureWriter struct {
hashType crypto.Hash
wrappedHash hash.Hash
h hash.Hash
salt []byte // v6 only
signer *packet.PrivateKey
sigType packet.SignatureType
config *packet.Config
@@ -509,6 +520,10 @@ func (s signatureWriter) Close() error {
sig.Hash = s.hashType
sig.Metadata = s.metadata
if err := sig.SetSalt(s.salt); err != nil {
return err
}
if err := sig.Sign(s.h, s.signer, s.config); err != nil {
return err
}
@@ -521,13 +536,34 @@ func (s signatureWriter) Close() error {
return s.encryptedData.Close()
}
func selectHashForSigningKey(config *packet.Config, signer *packet.PublicKey) crypto.Hash {
acceptableHashes := acceptableHashesToWrite(signer)
hash, ok := algorithm.HashToHashId(config.Hash())
if !ok {
return config.Hash()
}
for _, acceptableHashes := range acceptableHashes {
if acceptableHashes == hash {
return config.Hash()
}
}
if len(acceptableHashes) > 0 {
defaultAcceptedHash, ok := algorithm.HashIdToHash(acceptableHashes[0])
if ok {
return defaultAcceptedHash
}
}
return config.Hash()
}
func createSignaturePacket(signer *packet.PublicKey, sigType packet.SignatureType, config *packet.Config) *packet.Signature {
sigLifetimeSecs := config.SigLifetime()
hash := selectHashForSigningKey(config, signer)
return &packet.Signature{
Version: signer.Version,
SigType: sigType,
PubKeyAlgo: signer.PubKeyAlgo,
Hash: config.Hash(),
Hash: hash,
CreationTime: config.Now(),
IssuerKeyId: &signer.KeyId,
IssuerFingerprint: signer.Fingerprint,
@@ -581,3 +617,74 @@ func handleCompression(compressed io.WriteCloser, candidateCompression []uint8,
}
return data, nil
}
// selectHash selects the preferred hash given the candidateHashes and the configuredHash
func selectHash(candidateHashes []byte, configuredHash crypto.Hash, signer *packet.PrivateKey) (hash crypto.Hash, err error) {
acceptableHashes := acceptableHashesToWrite(&signer.PublicKey)
candidateHashes = intersectPreferences(acceptableHashes, candidateHashes)
for _, hashId := range candidateHashes {
if h, ok := algorithm.HashIdToHash(hashId); ok && h.Available() {
hash = h
break
}
}
// If the hash specified by config is a candidate, we'll use that.
if configuredHash.Available() {
for _, hashId := range candidateHashes {
if h, ok := algorithm.HashIdToHash(hashId); ok && h == configuredHash {
hash = h
break
}
}
}
if hash == 0 {
if len(acceptableHashes) > 0 {
if h, ok := algorithm.HashIdToHash(acceptableHashes[0]); ok {
hash = h
} else {
return 0, errors.UnsupportedError("no candidate hash functions are compiled in.")
}
} else {
return 0, errors.UnsupportedError("no candidate hash functions are compiled in.")
}
}
return
}
func acceptableHashesToWrite(singingKey *packet.PublicKey) []uint8 {
switch singingKey.PubKeyAlgo {
case packet.PubKeyAlgoEd448:
return []uint8{
hashToHashId(crypto.SHA512),
hashToHashId(crypto.SHA3_512),
}
case packet.PubKeyAlgoECDSA, packet.PubKeyAlgoEdDSA:
if curve, err := singingKey.Curve(); err == nil {
if curve == packet.Curve448 ||
curve == packet.CurveNistP521 ||
curve == packet.CurveBrainpoolP512 {
return []uint8{
hashToHashId(crypto.SHA512),
hashToHashId(crypto.SHA3_512),
}
} else if curve == packet.CurveBrainpoolP384 ||
curve == packet.CurveNistP384 {
return []uint8{
hashToHashId(crypto.SHA384),
hashToHashId(crypto.SHA512),
hashToHashId(crypto.SHA3_512),
}
}
}
}
return []uint8{
hashToHashId(crypto.SHA256),
hashToHashId(crypto.SHA384),
hashToHashId(crypto.SHA512),
hashToHashId(crypto.SHA3_256),
hashToHashId(crypto.SHA3_512),
}
}

View File

@@ -0,0 +1,221 @@
package x25519
import (
"crypto/sha256"
"crypto/subtle"
"io"
"github.com/ProtonMail/go-crypto/openpgp/aes/keywrap"
"github.com/ProtonMail/go-crypto/openpgp/errors"
x25519lib "github.com/cloudflare/circl/dh/x25519"
"golang.org/x/crypto/hkdf"
)
const (
hkdfInfo = "OpenPGP X25519"
aes128KeySize = 16
// The size of a public or private key in bytes.
KeySize = x25519lib.Size
)
type PublicKey struct {
// Point represents the encoded elliptic curve point of the public key.
Point []byte
}
type PrivateKey struct {
PublicKey
// Secret represents the secret of the private key.
Secret []byte
}
// NewPrivateKey creates a new empty private key including the public key.
func NewPrivateKey(key PublicKey) *PrivateKey {
return &PrivateKey{
PublicKey: key,
}
}
// Validate validates that the provided public key matches the private key.
func Validate(pk *PrivateKey) (err error) {
var expectedPublicKey, privateKey x25519lib.Key
subtle.ConstantTimeCopy(1, privateKey[:], pk.Secret)
x25519lib.KeyGen(&expectedPublicKey, &privateKey)
if subtle.ConstantTimeCompare(expectedPublicKey[:], pk.PublicKey.Point) == 0 {
return errors.KeyInvalidError("x25519: invalid key")
}
return nil
}
// GenerateKey generates a new x25519 key pair.
func GenerateKey(rand io.Reader) (*PrivateKey, error) {
var privateKey, publicKey x25519lib.Key
privateKeyOut := new(PrivateKey)
err := generateKey(rand, &privateKey, &publicKey)
if err != nil {
return nil, err
}
privateKeyOut.PublicKey.Point = publicKey[:]
privateKeyOut.Secret = privateKey[:]
return privateKeyOut, nil
}
func generateKey(rand io.Reader, privateKey *x25519lib.Key, publicKey *x25519lib.Key) error {
maxRounds := 10
isZero := true
for round := 0; isZero; round++ {
if round == maxRounds {
return errors.InvalidArgumentError("x25519: zero keys only, randomness source might be corrupt")
}
_, err := io.ReadFull(rand, privateKey[:])
if err != nil {
return err
}
isZero = constantTimeIsZero(privateKey[:])
}
x25519lib.KeyGen(publicKey, privateKey)
return nil
}
// Encrypt encrypts a sessionKey with x25519 according to
// the OpenPGP crypto refresh specification section 5.1.6. The function assumes that the
// sessionKey has the correct format and padding according to the specification.
func Encrypt(rand io.Reader, publicKey *PublicKey, sessionKey []byte) (ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, err error) {
var ephemeralPrivate, ephemeralPublic, staticPublic, shared x25519lib.Key
// Check that the input static public key has 32 bytes
if len(publicKey.Point) != KeySize {
err = errors.KeyInvalidError("x25519: the public key has the wrong size")
return
}
copy(staticPublic[:], publicKey.Point)
// Generate ephemeral keyPair
err = generateKey(rand, &ephemeralPrivate, &ephemeralPublic)
if err != nil {
return
}
// Compute shared key
ok := x25519lib.Shared(&shared, &ephemeralPrivate, &staticPublic)
if !ok {
err = errors.KeyInvalidError("x25519: the public key is a low order point")
return
}
// Derive the encryption key from the shared secret
encryptionKey := applyHKDF(ephemeralPublic[:], publicKey.Point[:], shared[:])
ephemeralPublicKey = &PublicKey{
Point: ephemeralPublic[:],
}
// Encrypt the sessionKey with aes key wrapping
encryptedSessionKey, err = keywrap.Wrap(encryptionKey, sessionKey)
return
}
// Decrypt decrypts a session key stored in ciphertext with the provided x25519
// private key and ephemeral public key.
func Decrypt(privateKey *PrivateKey, ephemeralPublicKey *PublicKey, ciphertext []byte) (encodedSessionKey []byte, err error) {
var ephemeralPublic, staticPrivate, shared x25519lib.Key
// Check that the input ephemeral public key has 32 bytes
if len(ephemeralPublicKey.Point) != KeySize {
err = errors.KeyInvalidError("x25519: the public key has the wrong size")
return
}
copy(ephemeralPublic[:], ephemeralPublicKey.Point)
subtle.ConstantTimeCopy(1, staticPrivate[:], privateKey.Secret)
// Compute shared key
ok := x25519lib.Shared(&shared, &staticPrivate, &ephemeralPublic)
if !ok {
err = errors.KeyInvalidError("x25519: the ephemeral public key is a low order point")
return
}
// Derive the encryption key from the shared secret
encryptionKey := applyHKDF(ephemeralPublicKey.Point[:], privateKey.PublicKey.Point[:], shared[:])
// Decrypt the session key with aes key wrapping
encodedSessionKey, err = keywrap.Unwrap(encryptionKey, ciphertext)
return
}
func applyHKDF(ephemeralPublicKey []byte, publicKey []byte, sharedSecret []byte) []byte {
inputKey := make([]byte, 3*KeySize)
// ephemeral public key | recipient public key | shared secret
subtle.ConstantTimeCopy(1, inputKey[:KeySize], ephemeralPublicKey)
subtle.ConstantTimeCopy(1, inputKey[KeySize:2*KeySize], publicKey)
subtle.ConstantTimeCopy(1, inputKey[2*KeySize:], sharedSecret)
hkdfReader := hkdf.New(sha256.New, inputKey, []byte{}, []byte(hkdfInfo))
encryptionKey := make([]byte, aes128KeySize)
_, _ = io.ReadFull(hkdfReader, encryptionKey)
return encryptionKey
}
func constantTimeIsZero(bytes []byte) bool {
isZero := byte(0)
for _, b := range bytes {
isZero |= b
}
return isZero == 0
}
// ENCODING/DECODING ciphertexts:
// EncodeFieldsLength returns the length of the ciphertext encoding
// given the encrypted session key.
func EncodedFieldsLength(encryptedSessionKey []byte, v6 bool) int {
lenCipherFunction := 0
if !v6 {
lenCipherFunction = 1
}
return KeySize + 1 + len(encryptedSessionKey) + lenCipherFunction
}
// EncodeField encodes x25519 session key encryption fields as
// ephemeral x25519 public key | follow byte length | cipherFunction (v3 only) | encryptedSessionKey
// and writes it to writer.
func EncodeFields(writer io.Writer, ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, cipherFunction byte, v6 bool) (err error) {
lenAlgorithm := 0
if !v6 {
lenAlgorithm = 1
}
if _, err = writer.Write(ephemeralPublicKey.Point); err != nil {
return err
}
if _, err = writer.Write([]byte{byte(len(encryptedSessionKey) + lenAlgorithm)}); err != nil {
return err
}
if !v6 {
if _, err = writer.Write([]byte{cipherFunction}); err != nil {
return err
}
}
_, err = writer.Write(encryptedSessionKey)
return err
}
// DecodeField decodes a x25519 session key encryption as
// ephemeral x25519 public key | follow byte length | cipherFunction (v3 only) | encryptedSessionKey.
func DecodeFields(reader io.Reader, v6 bool) (ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, cipherFunction byte, err error) {
var buf [1]byte
ephemeralPublicKey = &PublicKey{
Point: make([]byte, KeySize),
}
// 32 octets representing an ephemeral x25519 public key.
if _, err = io.ReadFull(reader, ephemeralPublicKey.Point); err != nil {
return nil, nil, 0, err
}
// A one-octet size of the following fields.
if _, err = io.ReadFull(reader, buf[:]); err != nil {
return nil, nil, 0, err
}
followingLen := buf[0]
// The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet).
if !v6 {
if _, err = io.ReadFull(reader, buf[:]); err != nil {
return nil, nil, 0, err
}
cipherFunction = buf[0]
followingLen -= 1
}
// The encrypted session key.
encryptedSessionKey = make([]byte, followingLen)
if _, err = io.ReadFull(reader, encryptedSessionKey); err != nil {
return nil, nil, 0, err
}
return ephemeralPublicKey, encryptedSessionKey, cipherFunction, nil
}

View File

@@ -0,0 +1,229 @@
package x448
import (
"crypto/sha512"
"crypto/subtle"
"io"
"github.com/ProtonMail/go-crypto/openpgp/aes/keywrap"
"github.com/ProtonMail/go-crypto/openpgp/errors"
x448lib "github.com/cloudflare/circl/dh/x448"
"golang.org/x/crypto/hkdf"
)
const (
hkdfInfo = "OpenPGP X448"
aes256KeySize = 32
// The size of a public or private key in bytes.
KeySize = x448lib.Size
)
type PublicKey struct {
// Point represents the encoded elliptic curve point of the public key.
Point []byte
}
type PrivateKey struct {
PublicKey
// Secret represents the secret of the private key.
Secret []byte
}
// NewPrivateKey creates a new empty private key including the public key.
func NewPrivateKey(key PublicKey) *PrivateKey {
return &PrivateKey{
PublicKey: key,
}
}
// Validate validates that the provided public key matches
// the private key.
func Validate(pk *PrivateKey) (err error) {
var expectedPublicKey, privateKey x448lib.Key
subtle.ConstantTimeCopy(1, privateKey[:], pk.Secret)
x448lib.KeyGen(&expectedPublicKey, &privateKey)
if subtle.ConstantTimeCompare(expectedPublicKey[:], pk.PublicKey.Point) == 0 {
return errors.KeyInvalidError("x448: invalid key")
}
return nil
}
// GenerateKey generates a new x448 key pair.
func GenerateKey(rand io.Reader) (*PrivateKey, error) {
var privateKey, publicKey x448lib.Key
privateKeyOut := new(PrivateKey)
err := generateKey(rand, &privateKey, &publicKey)
if err != nil {
return nil, err
}
privateKeyOut.PublicKey.Point = publicKey[:]
privateKeyOut.Secret = privateKey[:]
return privateKeyOut, nil
}
func generateKey(rand io.Reader, privateKey *x448lib.Key, publicKey *x448lib.Key) error {
maxRounds := 10
isZero := true
for round := 0; isZero; round++ {
if round == maxRounds {
return errors.InvalidArgumentError("x448: zero keys only, randomness source might be corrupt")
}
_, err := io.ReadFull(rand, privateKey[:])
if err != nil {
return err
}
isZero = constantTimeIsZero(privateKey[:])
}
x448lib.KeyGen(publicKey, privateKey)
return nil
}
// Encrypt encrypts a sessionKey with x448 according to
// the OpenPGP crypto refresh specification section 5.1.7. The function assumes that the
// sessionKey has the correct format and padding according to the specification.
func Encrypt(rand io.Reader, publicKey *PublicKey, sessionKey []byte) (ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, err error) {
var ephemeralPrivate, ephemeralPublic, staticPublic, shared x448lib.Key
// Check that the input static public key has 56 bytes.
if len(publicKey.Point) != KeySize {
err = errors.KeyInvalidError("x448: the public key has the wrong size")
return nil, nil, err
}
copy(staticPublic[:], publicKey.Point)
// Generate ephemeral keyPair.
if err = generateKey(rand, &ephemeralPrivate, &ephemeralPublic); err != nil {
return nil, nil, err
}
// Compute shared key.
ok := x448lib.Shared(&shared, &ephemeralPrivate, &staticPublic)
if !ok {
err = errors.KeyInvalidError("x448: the public key is a low order point")
return nil, nil, err
}
// Derive the encryption key from the shared secret.
encryptionKey := applyHKDF(ephemeralPublic[:], publicKey.Point[:], shared[:])
ephemeralPublicKey = &PublicKey{
Point: ephemeralPublic[:],
}
// Encrypt the sessionKey with aes key wrapping.
encryptedSessionKey, err = keywrap.Wrap(encryptionKey, sessionKey)
if err != nil {
return nil, nil, err
}
return ephemeralPublicKey, encryptedSessionKey, nil
}
// Decrypt decrypts a session key stored in ciphertext with the provided x448
// private key and ephemeral public key.
func Decrypt(privateKey *PrivateKey, ephemeralPublicKey *PublicKey, ciphertext []byte) (encodedSessionKey []byte, err error) {
var ephemeralPublic, staticPrivate, shared x448lib.Key
// Check that the input ephemeral public key has 56 bytes.
if len(ephemeralPublicKey.Point) != KeySize {
err = errors.KeyInvalidError("x448: the public key has the wrong size")
return nil, err
}
copy(ephemeralPublic[:], ephemeralPublicKey.Point)
subtle.ConstantTimeCopy(1, staticPrivate[:], privateKey.Secret)
// Compute shared key.
ok := x448lib.Shared(&shared, &staticPrivate, &ephemeralPublic)
if !ok {
err = errors.KeyInvalidError("x448: the ephemeral public key is a low order point")
return nil, err
}
// Derive the encryption key from the shared secret.
encryptionKey := applyHKDF(ephemeralPublicKey.Point[:], privateKey.PublicKey.Point[:], shared[:])
// Decrypt the session key with aes key wrapping.
encodedSessionKey, err = keywrap.Unwrap(encryptionKey, ciphertext)
if err != nil {
return nil, err
}
return encodedSessionKey, nil
}
func applyHKDF(ephemeralPublicKey []byte, publicKey []byte, sharedSecret []byte) []byte {
inputKey := make([]byte, 3*KeySize)
// ephemeral public key | recipient public key | shared secret.
subtle.ConstantTimeCopy(1, inputKey[:KeySize], ephemeralPublicKey)
subtle.ConstantTimeCopy(1, inputKey[KeySize:2*KeySize], publicKey)
subtle.ConstantTimeCopy(1, inputKey[2*KeySize:], sharedSecret)
hkdfReader := hkdf.New(sha512.New, inputKey, []byte{}, []byte(hkdfInfo))
encryptionKey := make([]byte, aes256KeySize)
_, _ = io.ReadFull(hkdfReader, encryptionKey)
return encryptionKey
}
func constantTimeIsZero(bytes []byte) bool {
isZero := byte(0)
for _, b := range bytes {
isZero |= b
}
return isZero == 0
}
// ENCODING/DECODING ciphertexts:
// EncodeFieldsLength returns the length of the ciphertext encoding
// given the encrypted session key.
func EncodedFieldsLength(encryptedSessionKey []byte, v6 bool) int {
lenCipherFunction := 0
if !v6 {
lenCipherFunction = 1
}
return KeySize + 1 + len(encryptedSessionKey) + lenCipherFunction
}
// EncodeField encodes x448 session key encryption fields as
// ephemeral x448 public key | follow byte length | cipherFunction (v3 only) | encryptedSessionKey
// and writes it to writer.
func EncodeFields(writer io.Writer, ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, cipherFunction byte, v6 bool) (err error) {
lenAlgorithm := 0
if !v6 {
lenAlgorithm = 1
}
if _, err = writer.Write(ephemeralPublicKey.Point); err != nil {
return err
}
if _, err = writer.Write([]byte{byte(len(encryptedSessionKey) + lenAlgorithm)}); err != nil {
return err
}
if !v6 {
if _, err = writer.Write([]byte{cipherFunction}); err != nil {
return err
}
}
if _, err = writer.Write(encryptedSessionKey); err != nil {
return err
}
return nil
}
// DecodeField decodes a x448 session key encryption as
// ephemeral x448 public key | follow byte length | cipherFunction (v3 only) | encryptedSessionKey.
func DecodeFields(reader io.Reader, v6 bool) (ephemeralPublicKey *PublicKey, encryptedSessionKey []byte, cipherFunction byte, err error) {
var buf [1]byte
ephemeralPublicKey = &PublicKey{
Point: make([]byte, KeySize),
}
// 56 octets representing an ephemeral x448 public key.
if _, err = io.ReadFull(reader, ephemeralPublicKey.Point); err != nil {
return nil, nil, 0, err
}
// A one-octet size of the following fields.
if _, err = io.ReadFull(reader, buf[:]); err != nil {
return nil, nil, 0, err
}
followingLen := buf[0]
// The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet).
if !v6 {
if _, err = io.ReadFull(reader, buf[:]); err != nil {
return nil, nil, 0, err
}
cipherFunction = buf[0]
followingLen -= 1
}
// The encrypted session key.
encryptedSessionKey = make([]byte, followingLen)
if _, err = io.ReadFull(reader, encryptedSessionKey); err != nil {
return nil, nil, 0, err
}
return ephemeralPublicKey, encryptedSessionKey, cipherFunction, nil
}

View File

@@ -11,3 +11,7 @@ insert_final_newline = true
indent_style = space
indent_size = 2
insert_final_newline = false
[*.yml]
indent_style = space
indent_size = 2

View File

@@ -23,3 +23,6 @@ _models/
_examples/
*.min.*
build/
cmd/chromad/static/chroma.wasm
cmd/chromad/static/wasm_exec.js

View File

@@ -1,7 +1,5 @@
run:
tests: true
skip-dirs:
- _examples
output:
print-issued-lines: false
@@ -9,8 +7,6 @@ output:
linters:
enable-all: true
disable:
- maligned
- megacheck
- lll
- gocyclo
- dupl
@@ -18,9 +14,7 @@ linters:
- funlen
- godox
- wsl
- gomnd
- gocognit
- goerr113
- nolintlint
- testpackage
- godot
@@ -28,12 +22,10 @@ linters:
- paralleltest
- nlreturn
- cyclop
- exhaustivestruct
- gci
- gofumpt
- errorlint
- exhaustive
- ifshort
- wrapcheck
- stylecheck
- thelper
@@ -46,13 +38,18 @@ linters:
- ireturn
- maintidx
- govet
- nosnakecase
- testableexamples
- musttag
- depguard
- goconst
- perfsprint
- mnd
- predeclared
- recvcheck
- tenv
- err113
linters-settings:
govet:
check-shadowing: true
gocyclo:
min-complexity: 10
dupl:
@@ -67,6 +64,8 @@ linters-settings:
issues:
exclude-dirs:
- _examples
max-per-linter: 0
max-same: 0
exclude-use-default: false

View File

@@ -4,34 +4,31 @@ release:
owner: alecthomas
name: chroma
brews:
-
install: bin.install "chroma"
- install: bin.install "chroma"
env:
- CGO_ENABLED=0
builds:
- goos:
- linux
- darwin
- windows
goarch:
- arm64
- amd64
- "386"
goarm:
- "6"
dir: ./cmd/chroma
main: .
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
binary: chroma
- goos:
- linux
- darwin
- windows
goarch:
- arm64
- amd64
- "386"
goarm:
- "6"
dir: ./cmd/chroma
main: .
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
binary: chroma
archives:
-
format: tar.gz
name_template: '{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{
.Arm }}{{ end }}'
- format: tar.gz
name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
files:
- COPYING
- README*
snapshot:
name_template: SNAPSHOT-{{ .Commit }}
checksum:
name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt'
name_template: "{{ .ProjectName }}-{{ .Version }}-checksums.txt"

11
vendor/github.com/alecthomas/chroma/v2/AGENTS.md generated vendored Normal file
View File

@@ -0,0 +1,11 @@
Chroma is a syntax highlighting library, tool and web playground for Go. It is based on Pygments and includes importers for it, so most of the same concepts from Pygments apply to Chroma.
This project is written in Go, uses Hermit to manage tooling, and Just for helper commands. Helper scripts are in ./scripts.
Language definitions are XML files defined in ./lexers/embedded/*.xml.
Styles/themes are defined in ./styles/*.xml.
The CLI can be run with `chroma`.
The web playground can be run with `chromad --csrf-key=moo`. It blocks, so should generally be run in the background. It also does not hot reload, so has to be manually restarted. The playground has two modes - for local development it uses the server itself to render, while for production running `just chromad` will compile ./cmd/libchromawasm into a WASM module that is bundled into `chromad`.

64
vendor/github.com/alecthomas/chroma/v2/Dockerfile generated vendored Normal file
View File

@@ -0,0 +1,64 @@
# Multi-stage Dockerfile for chromad Go application using Hermit-managed tools
# Build stage
FROM ubuntu:24.04 AS builder
# Install system dependencies
RUN apt-get update && apt-get install -y \
curl \
git \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy the entire project (including bin directory with Hermit tools)
COPY . .
# Make Hermit tools executable and add to PATH
ENV PATH="/app/bin:${PATH}"
# Set Go environment variables for static compilation
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
# Build the application using just
RUN just chromad
# Runtime stage
FROM alpine:3.23 AS runtime
# Install ca-certificates for HTTPS requests
RUN apk --no-cache add ca-certificates curl
# Create a non-root user
RUN addgroup -g 1001 chromad && \
adduser -D -s /bin/sh -u 1001 -G chromad chromad
# Set working directory
WORKDIR /app
# Copy the binary from build stage
COPY --from=builder /app/build/chromad /app/chromad
# Change ownership to non-root user
RUN chown chromad:chromad /app/chromad
# Switch to non-root user
USER chromad
# Expose port (default is 8080, but can be overridden via PORT env var)
EXPOSE 8080
# Set default environment variables
ENV PORT=8080
ENV CHROMA_CSRF_KEY="testtest"
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -fsSL http://127.0.0.1:8080/ > /dev/null
# Run the application
CMD ["sh", "-c", "./chromad --csrf-key=$CHROMA_CSRF_KEY --bind=0.0.0.0:$PORT"]

55
vendor/github.com/alecthomas/chroma/v2/Justfile generated vendored Normal file
View File

@@ -0,0 +1,55 @@
set positional-arguments := true
set shell := ["bash", "-c"]
version := `git describe --tags --dirty --always`
export GOOS := env("GOOS", "linux")
export GOARCH := env("GOARCH", "amd64")
_help:
@just -l
# Generate README.md from lexer definitions
readme:
#!/usr/bin/env bash
GOOS= GOARCH= ./table.py
# Generate tokentype_string.go
tokentype-string:
go generate
# Format JavaScript files
format-js:
biome format --write cmd/chromad/static/index.js cmd/chromad/static/chroma.js
# Build chromad binary
chromad: wasm-exec chroma-wasm
#!/usr/bin/env bash
rm -rf build
mk cmd/chromad/static/index.min.js : cmd/chromad/static/{index,chroma}.js -- \
esbuild --platform=browser --format=esm --bundle cmd/chromad/static/index.js --minify --external:./wasm_exec.js --outfile=cmd/chromad/static/index.min.js
mk cmd/chromad/static/index.min.css : cmd/chromad/static/index.css -- \
esbuild --bundle cmd/chromad/static/index.css --minify --outfile=cmd/chromad/static/index.min.css
cd cmd/chromad && CGOENABLED=0 go build -ldflags="-X 'main.version={{ version }}'" -o ../../build/chromad .
# Copy wasm_exec.js from TinyGo
wasm-exec:
#!/usr/bin/env bash
tinygoroot=$(tinygo env TINYGOROOT)
mk cmd/chromad/static/wasm_exec.js : "$tinygoroot/targets/wasm_exec.js" -- \
install -m644 "$tinygoroot/targets/wasm_exec.js" cmd/chromad/static/wasm_exec.js
# Build WASM binary
chroma-wasm:
#!/usr/bin/env bash
if type tinygo > /dev/null 2>&1; then
mk cmd/chromad/static/chroma.wasm : cmd/libchromawasm/main.go -- \
tinygo build -no-debug -target wasm -o cmd/chromad/static/chroma.wasm cmd/libchromawasm/main.go
else
mk cmd/chromad/static/chroma.wasm : cmd/libchromawasm/main.go -- \
GOOS=js GOARCH=wasm go build -o cmd/chromad/static/chroma.wasm cmd/libchromawasm/main.go
fi
# Upload chromad to server
upload: chromad
scp build/chromad root@swapoff.org:
ssh root@swapoff.org 'install -m755 ./chromad /srv/http/swapoff.org/bin && service chromad restart'

View File

@@ -1,23 +0,0 @@
.PHONY: chromad upload all
VERSION ?= $(shell git describe --tags --dirty --always)
export GOOS ?= linux
export GOARCH ?= amd64
all: README.md tokentype_string.go
README.md: lexers/*/*.go
./table.py
tokentype_string.go: types.go
go generate
chromad:
rm -rf build
esbuild --bundle cmd/chromad/static/index.js --minify --outfile=cmd/chromad/static/index.min.js
esbuild --bundle cmd/chromad/static/index.css --minify --outfile=cmd/chromad/static/index.min.css
(export CGOENABLED=0 ; cd ./cmd/chromad && go build -ldflags="-X 'main.version=$(VERSION)'" -o ../../build/chromad .)
upload: build/chromad
scp build/chromad root@swapoff.org: && \
ssh root@swapoff.org 'install -m755 ./chromad /srv/http/swapoff.org/bin && service chromad restart'

View File

@@ -1,6 +1,9 @@
# Chroma — A general purpose syntax highlighter in pure Go
![Chroma](chroma.jpg)
# A general purpose syntax highlighter in pure Go
[![Go Reference](https://pkg.go.dev/badge/github.com/alecthomas/chroma/v2.svg)](https://pkg.go.dev/github.com/alecthomas/chroma/v2) [![CI](https://github.com/alecthomas/chroma/actions/workflows/ci.yml/badge.svg)](https://github.com/alecthomas/chroma/actions/workflows/ci.yml) [![Slack chat](https://img.shields.io/static/v1?logo=slack&style=flat&label=slack&color=green&message=gophers)](https://invite.slack.golangbridge.org/)
[![Golang Documentation](https://godoc.org/github.com/alecthomas/chroma?status.svg)](https://godoc.org/github.com/alecthomas/chroma) [![CI](https://github.com/alecthomas/chroma/actions/workflows/ci.yml/badge.svg)](https://github.com/alecthomas/chroma/actions/workflows/ci.yml) [![Slack chat](https://img.shields.io/static/v1?logo=slack&style=flat&label=slack&color=green&message=gophers)](https://invite.slack.golangbridge.org/)
Chroma takes source code and other structured text and converts it into syntax
highlighted HTML, ANSI-coloured text, etc.
@@ -8,75 +11,73 @@ highlighted HTML, ANSI-coloured text, etc.
Chroma is based heavily on [Pygments](http://pygments.org/), and includes
translators for Pygments lexers and styles.
<a id="markdown-table-of-contents" name="table-of-contents"></a>
## Table of Contents
<!-- TOC -->
1. [Table of Contents](#table-of-contents)
2. [Supported languages](#supported-languages)
3. [Try it](#try-it)
4. [Using the library](#using-the-library)
1. [Supported languages](#supported-languages)
2. [Try it](#try-it)
3. [Using the library](#using-the-library)
1. [Quick start](#quick-start)
2. [Identifying the language](#identifying-the-language)
3. [Formatting the output](#formatting-the-output)
4. [The HTML formatter](#the-html-formatter)
5. [More detail](#more-detail)
4. [More detail](#more-detail)
1. [Lexers](#lexers)
2. [Formatters](#formatters)
3. [Styles](#styles)
6. [Command-line interface](#command-line-interface)
7. [Testing lexers](#testing-lexers)
8. [What's missing compared to Pygments?](#whats-missing-compared-to-pygments)
5. [Command-line interface](#command-line-interface)
6. [Testing lexers](#testing-lexers)
7. [What's missing compared to Pygments?](#whats-missing-compared-to-pygments)
<!-- /TOC -->
<a id="markdown-supported-languages" name="supported-languages"></a>
## Supported languages
| Prefix | Language |
| :----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| A | ABAP, ABNF, ActionScript, ActionScript 3, Ada, Agda, AL, Alloy, Angular2, ANTLR, ApacheConf, APL, AppleScript, ArangoDB AQL, Arduino, ArmAsm, AutoHotkey, AutoIt, Awk |
| B | Ballerina, Bash, Bash Session, Batchfile, BibTeX, Bicep, BlitzBasic, BNF, BQN, Brainfuck |
| C | C, C#, C++, Caddyfile, Caddyfile Directives, Cap'n Proto, Cassandra CQL, Ceylon, CFEngine3, cfstatement, ChaiScript, Chapel, Cheetah, Clojure, CMake, COBOL, CoffeeScript, Common Lisp, Coq, Crystal, CSS, Cython |
| D | D, Dart, Dax, Diff, Django/Jinja, dns, Docker, DTD, Dylan |
| E | EBNF, Elixir, Elm, EmacsLisp, Erlang |
| F | Factor, Fennel, Fish, Forth, Fortran, FortranFixed, FSharp |
| G | GAS, GDScript, Genshi, Genshi HTML, Genshi Text, Gherkin, GLSL, Gnuplot, Go, Go HTML Template, Go Text Template, GraphQL, Groff, Groovy |
| H | Handlebars, Hare, Haskell, Haxe, HCL, Hexdump, HLB, HLSL, HolyC, HTML, HTTP, Hy |
| I | Idris, Igor, INI, Io, ISCdhcpd |
| J | J, Java, JavaScript, JSON, Julia, Jungle |
| K | Kotlin |
| L | Lighttpd configuration file, LLVM, Lua |
| M | Makefile, Mako, markdown, Mason, Mathematica, Matlab, mcfunction, Meson, Metal, MiniZinc, MLIR, Modula-2, MonkeyC, MorrowindScript, Myghty, MySQL |
| N | NASM, Natural, Newspeak, Nginx configuration file, Nim, Nix |
| O | Objective-C, OCaml, Octave, Odin, OnesEnterprise, OpenEdge ABL, OpenSCAD, Org Mode |
| P | PacmanConf, Perl, PHP, PHTML, Pig, PkgConfig, PL/pgSQL, plaintext, Plutus Core, Pony, PostgreSQL SQL dialect, PostScript, POVRay, PowerQuery, PowerShell, Prolog, PromQL, properties, Protocol Buffer, PRQL, PSL, Puppet, Python, Python 2 |
| Q | QBasic, QML |
| R | R, Racket, Ragel, Raku, react, ReasonML, reg, reStructuredText, Rexx, Ruby, Rust |
| S | SAS, Sass, Scala, Scheme, Scilab, SCSS, Sed, Sieve, Smali, Smalltalk, Smarty, Snobol, Solidity, SourcePawn, SPARQL, SQL, SquidConf, Standard ML, stas, Stylus, Svelte, Swift, SYSTEMD, systemverilog |
| T | TableGen, Tal, TASM, Tcl, Tcsh, Termcap, Terminfo, Terraform, TeX, Thrift, TOML, TradingView, Transact-SQL, Turing, Turtle, Twig, TypeScript, TypoScript, TypoScriptCssData, TypoScriptHtmlData |
| V | V, V shell, Vala, VB.net, verilog, VHDL, VHS, VimL, vue |
| W | WDTE, WebGPU Shading Language, Whiley |
| X | XML, Xorg |
| Y | YAML, YANG |
| Z | Z80 Assembly, Zed, Zig |
| Prefix | Language
| :----: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| A | ABAP, ABNF, ActionScript, ActionScript 3, Ada, Agda, AL, Alloy, Angular2, ANTLR, ApacheConf, APL, AppleScript, ArangoDB AQL, Arduino, ArmAsm, ATL, AutoHotkey, AutoIt, Awk
| B | Ballerina, Bash, Bash Session, Batchfile, Beef, BibTeX, Bicep, BlitzBasic, BNF, BQN, Brainfuck
| C | C, C#, C++, C3, Caddyfile, Caddyfile Directives, Cap'n Proto, Cassandra CQL, Ceylon, CFEngine3, cfstatement, ChaiScript, Chapel, Cheetah, Clojure, CMake, COBOL, CoffeeScript, Common Lisp, Coq, Core, Crystal, CSS, CSV, CUE, Cython
| D | D, Dart, Dax, Desktop file, Diff, Django/Jinja, dns, Docker, DTD, Dylan
| E | EBNF, Elixir, Elm, EmacsLisp, Erlang
| F | Factor, Fennel, Fish, Forth, Fortran, FortranFixed, FSharp
| G | GAS, GDScript, GDScript3, Gemtext, Genshi, Genshi HTML, Genshi Text, Gherkin, Gleam, GLSL, Gnuplot, Go, Go HTML Template, Go Template, Go Text Template, GraphQL, Groff, Groovy
| H | Handlebars, Hare, Haskell, Haxe, HCL, Hexdump, HLB, HLSL, HolyC, HTML, HTTP, Hy
| I | Idris, Igor, INI, Io, ISCdhcpd
| J | J, Janet, Java, JavaScript, JSON, JSONata, Jsonnet, Julia, Jungle
| K | Kakoune, Kotlin
| L | Lean4, Lighttpd configuration file, LLVM, lox, Lua
| M | Makefile, Mako, markdown, Markless, Mason, Materialize SQL dialect, Mathematica, Matlab, MCFunction, Meson, Metal, MiniZinc, MLIR, Modelica, Modula-2, Mojo, MonkeyC, MoonScript, MorrowindScript, Myghty, MySQL
| N | NASM, Natural, NDISASM, Newspeak, Nginx configuration file, Nim, Nix, NSIS, Nu
| O | Objective-C, ObjectPascal, OCaml, Octave, Odin, OnesEnterprise, OpenEdge ABL, OpenSCAD, Org Mode
| P | PacmanConf, Perl, PHP, PHTML, Pig, PkgConfig, PL/pgSQL, plaintext, Plutus Core, Pony, PostgreSQL SQL dialect, PostScript, POVRay, PowerQuery, PowerShell, Prolog, Promela, PromQL, properties, Protocol Buffer, Protocol Buffer Text Format, PRQL, PSL, Puppet, Python, Python 2
| Q | QBasic, QML
| R | R, Racket, Ragel, Raku, react, ReasonML, reg, Rego, reStructuredText, Rexx, RGBDS Assembly, Ring, RPGLE, RPMSpec, Ruby, Rust
| S | SAS, Sass, Scala, Scheme, Scilab, SCSS, Sed, Sieve, Smali, Smalltalk, Smarty, SNBT, Snobol, Solidity, SourcePawn, SPARQL, SQL, SquidConf, Standard ML, stas, Stylus, Svelte, Swift, SYSTEMD, systemverilog
| T | TableGen, Tal, TASM, Tcl, Tcsh, Termcap, Terminfo, Terraform, TeX, Thrift, TOML, TradingView, Transact-SQL, Turing, Turtle, Twig, TypeScript, TypoScript, TypoScriptCssData, TypoScriptHtmlData, Typst
| U | ucode
| V | V, V shell, Vala, VB.net, verilog, VHDL, VHS, VimL, vue
| W | WDTE, WebAssembly Text Format, WebGPU Shading Language, WebVTT, Whiley
| X | XML, Xorg
| Y | YAML, YANG
| Z | Z80 Assembly, Zed, Zig
_I will attempt to keep this section up to date, but an authoritative list can be
displayed with `chroma --list`._
<a id="markdown-try-it" name="try-it"></a>
## Try it
Try out various languages and styles on the [Chroma Playground](https://swapoff.org/chroma/playground/).
<a id="markdown-using-the-library" name="using-the-library"></a>
## Using the library
This is version 2 of Chroma, use the import path:
```go
import "github.com/alecthomas/chroma/v2"
```
Chroma, like Pygments, has the concepts of
[lexers](https://github.com/alecthomas/chroma/tree/master/lexers),
[formatters](https://github.com/alecthomas/chroma/tree/master/formatters) and
@@ -95,8 +96,6 @@ In all cases, if a lexer, formatter or style can not be determined, `nil` will
be returned. In this situation you may want to default to the `Fallback`
value in each respective package, which provides sane defaults.
<a id="markdown-quick-start" name="quick-start"></a>
### Quick start
A convenience function exists that can be used to simply format some source
@@ -106,8 +105,6 @@ text, without any effort:
err := quick.Highlight(os.Stdout, someSourceCode, "go", "html", "monokai")
```
<a id="markdown-identifying-the-language" name="identifying-the-language"></a>
### Identifying the language
To highlight code, you'll first have to identify what language the code is
@@ -147,8 +144,6 @@ token types into a single token:
lexer = chroma.Coalesce(lexer)
```
<a id="markdown-formatting-the-output" name="formatting-the-output"></a>
### Formatting the output
Once a language is identified you will need to pick a formatter and a style (theme).
@@ -177,8 +172,6 @@ And finally, format the tokens from the iterator:
err := formatter.Format(w, style, iterator)
```
<a id="markdown-the-html-formatter" name="the-html-formatter"></a>
### The HTML formatter
By default the `html` registered formatter generates standalone HTML with
@@ -203,12 +196,8 @@ formatter := html.New(html.WithClasses(true))
err := formatter.WriteCSS(w, style)
```
<a id="markdown-more-detail" name="more-detail"></a>
## More detail
<a id="markdown-lexers" name="lexers"></a>
### Lexers
See the [Pygments documentation](http://pygments.org/docs/lexerdevelopment/)
@@ -220,15 +209,12 @@ using the included Python 3 script `pygments2chroma_xml.py`. I use something lik
the following:
```sh
python3 _tools/pygments2chroma_xml.py \
uv run --script _tools/pygments2chroma_xml.py \
pygments.lexers.jvm.KotlinLexer \
> lexers/embedded/kotlin.xml
```
See notes in [pygments-lexers.txt](https://github.com/alecthomas/chroma/blob/master/pygments-lexers.txt)
for a list of lexers, and notes on some of the issues importing them.
<a id="markdown-formatters" name="formatters"></a>
A list of all lexers available in Pygments can be found in [pygments-lexers.txt](https://github.com/alecthomas/chroma/blob/master/pygments-lexers.txt).
### Formatters
@@ -237,8 +223,6 @@ Chroma supports HTML output, as well as terminal output in 8 colour, 256 colour,
A `noop` formatter is included that outputs the token text only, and a `tokens`
formatter outputs raw tokens. The latter is useful for debugging lexers.
<a id="markdown-styles" name="styles"></a>
### Styles
Chroma styles are defined in XML. The style entries use the
@@ -262,8 +246,6 @@ Also, token types in a style file are hierarchical. For instance, when `CommentS
For a quick overview of the available styles and how they look, check out the [Chroma Style Gallery](https://xyproto.github.io/splash/docs/).
<a id="markdown-command-line-interface" name="command-line-interface"></a>
## Command-line interface
A command-line interface to Chroma is included.
@@ -288,16 +270,19 @@ on under the hood for easy integration with [lesspipe shipping with
Debian and derivatives](https://manpages.debian.org/lesspipe#USER_DEFINED_FILTERS);
for that setup the `chroma` executable can be just symlinked to `~/.lessfilter`.
<a id="markdown-whats-missing-compared-to-pygments" name="whats-missing-compared-to-pygments"></a>
## Projects using Chroma
<a id="markdown-testing-lexers" name="testing-lexers"></a>
* [`moor`](https://github.com/walles/moor) is a full-blown pager that colorizes
its input using Chroma
* [Hugo](https://gohugo.io/) is a static site generator that [uses Chroma for syntax
highlighting code examples](https://gohugo.io/content-management/syntax-highlighting/)
## Testing lexers
If you edit some lexers and want to try it, open a shell in `cmd/chromad` and run:
```shell
go run .
go run . --csrf-key=securekey
```
A Link will be printed. Open it in your Browser. Now you can test on the Playground with your local changes.

6
vendor/github.com/alecthomas/chroma/v2/biome.json generated vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
"formatter": {
"indentStyle": "space"
}
}

BIN
vendor/github.com/alecthomas/chroma/v2/chroma.jpg generated vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -141,7 +141,7 @@ func ParseColour(colour string) Colour {
if err != nil {
return 0
}
return Colour(n + 1)
return Colour(n + 1) //nolint:gosec
}
// MustParseColour is like ParseColour except it panics if the colour is invalid.
@@ -162,13 +162,13 @@ func (c Colour) String() string { return fmt.Sprintf("#%06x", int(c-1)) }
func (c Colour) GoString() string { return fmt.Sprintf("Colour(0x%06x)", int(c-1)) }
// Red component of colour.
func (c Colour) Red() uint8 { return uint8(((c - 1) >> 16) & 0xff) }
func (c Colour) Red() uint8 { return uint8(((c - 1) >> 16) & 0xff) } //nolint:gosec
// Green component of colour.
func (c Colour) Green() uint8 { return uint8(((c - 1) >> 8) & 0xff) }
func (c Colour) Green() uint8 { return uint8(((c - 1) >> 8) & 0xff) } //nolint:gosec
// Blue component of colour.
func (c Colour) Blue() uint8 { return uint8((c - 1) & 0xff) }
func (c Colour) Blue() uint8 { return uint8((c - 1) & 0xff) } //nolint:gosec
// Colours is an orderable set of colours.
type Colours []Colour

View File

@@ -24,6 +24,15 @@ func DelegatingLexer(root Lexer, language Lexer) Lexer {
}
}
func (d *delegatingLexer) SetTracing(enable bool) {
if l, ok := d.language.(TracingLexer); ok {
l.SetTracing(enable)
}
if l, ok := d.root.(TracingLexer); ok {
l.SetTracing(enable)
}
}
func (d *delegatingLexer) AnalyseText(text string) float32 {
return d.root.AnalyseText(text)
}

View File

@@ -10,6 +10,12 @@ type Emitter interface {
Emit(groups []string, state *LexerState) Iterator
}
// ValidatingEmitter is an Emitter that can validate against a compiled rule.
type ValidatingEmitter interface {
Emitter
ValidateEmitter(rule *CompiledRule) error
}
// SerialisableEmitter is an Emitter that can be serialised and deserialised to/from JSON.
type SerialisableEmitter interface {
Emitter
@@ -30,6 +36,8 @@ type byGroupsEmitter struct {
Emitters
}
var _ ValidatingEmitter = (*byGroupsEmitter)(nil)
// ByGroups emits a token for each matching group in the rule's regex.
func ByGroups(emitters ...Emitter) Emitter {
return &byGroupsEmitter{Emitters: emitters}
@@ -37,6 +45,13 @@ func ByGroups(emitters ...Emitter) Emitter {
func (b *byGroupsEmitter) EmitterKind() string { return "bygroups" }
func (b *byGroupsEmitter) ValidateEmitter(rule *CompiledRule) error {
if len(rule.Regexp.GetGroupNumbers())-1 != len(b.Emitters) {
return fmt.Errorf("number of groups %d does not match number of emitters %d", len(rule.Regexp.GetGroupNumbers())-1, len(b.Emitters))
}
return nil
}
func (b *byGroupsEmitter) Emit(groups []string, state *LexerState) Iterator {
iterators := make([]Iterator, 0, len(groups)-1)
if len(b.Emitters) != len(groups)-1 {

View File

@@ -5,7 +5,9 @@ import (
"html"
"io"
"sort"
"strconv"
"strings"
"sync"
"github.com/alecthomas/chroma/v2"
)
@@ -32,6 +34,9 @@ func WithCustomCSS(css map[chroma.TokenType]string) Option {
}
}
// WithCSSComments adds prefixe comments to the css classes. Defaults to true.
func WithCSSComments(b bool) Option { return func(f *Formatter) { f.writeCSSComments = b } }
// TabWidth sets the number of characters for a tab. Defaults to 8.
func TabWidth(width int) Option { return func(f *Formatter) { f.tabWidth = width } }
@@ -129,9 +134,11 @@ func BaseLineNumber(n int) Option {
// New HTML formatter.
func New(options ...Option) *Formatter {
f := &Formatter{
baseLineNumber: 1,
preWrapper: defaultPreWrapper,
baseLineNumber: 1,
preWrapper: defaultPreWrapper,
writeCSSComments: true,
}
f.styleCache = newStyleCache(f)
for _, option := range options {
option(f)
}
@@ -188,11 +195,13 @@ var (
// Formatter that generates HTML.
type Formatter struct {
styleCache *styleCache
standalone bool
prefix string
Classes bool // Exported field to detect when classes are being used
allClasses bool
customCSS map[chroma.TokenType]string
writeCSSComments bool
preWrapper PreWrapper
inlineCode bool
preventSurroundingPre bool
@@ -220,12 +229,7 @@ func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Ite
//
// OTOH we need to be super careful about correct escaping...
func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.Token) (err error) { // nolint: gocyclo
css := f.styleToCSS(style)
if !f.Classes {
for t, style := range css {
css[t] = compressStyle(style)
}
}
css := f.styleCache.get(style, true)
if f.standalone {
fmt.Fprint(w, "<html>\n")
if f.Classes {
@@ -243,7 +247,7 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
wrapInTable := f.lineNumbers && f.lineNumbersInTable
lines := chroma.SplitTokensIntoLines(tokens)
lineDigits := len(fmt.Sprintf("%d", f.baseLineNumber+len(lines)-1))
lineDigits := len(strconv.Itoa(f.baseLineNumber + len(lines) - 1))
highlightIndex := 0
if wrapInTable {
@@ -251,7 +255,7 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
fmt.Fprintf(w, "<div%s>\n", f.styleAttr(css, chroma.PreWrapper))
fmt.Fprintf(w, "<table%s><tr>", f.styleAttr(css, chroma.LineTable))
fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD))
fmt.Fprintf(w, f.preWrapper.Start(false, f.styleAttr(css, chroma.PreWrapper)))
fmt.Fprintf(w, "%s", f.preWrapper.Start(false, f.styleAttr(css, chroma.PreWrapper)))
for index := range lines {
line := f.baseLineNumber + index
highlight, next := f.shouldHighlight(highlightIndex, line)
@@ -273,7 +277,7 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD, "width:100%"))
}
fmt.Fprintf(w, f.preWrapper.Start(true, f.styleAttr(css, chroma.PreWrapper)))
fmt.Fprintf(w, "%s", f.preWrapper.Start(true, f.styleAttr(css, chroma.PreWrapper)))
highlightIndex = 0
for index, tokens := range lines {
@@ -323,7 +327,7 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
fmt.Fprint(w, `</span>`) // End of Line
}
}
fmt.Fprintf(w, f.preWrapper.End(true))
fmt.Fprintf(w, "%s", f.preWrapper.End(true))
if wrapInTable {
fmt.Fprint(w, "</td></tr></table>\n")
@@ -417,21 +421,37 @@ func (f *Formatter) tabWidthStyle() string {
return ""
}
func (f *Formatter) writeCSSRule(w io.Writer, comment string, selector string, styles string) error {
if styles == "" {
return nil
}
if f.writeCSSComments && comment != "" {
if _, err := fmt.Fprintf(w, "/* %s */ ", comment); err != nil {
return err
}
}
if _, err := fmt.Fprintf(w, "%s { %s }\n", selector, styles); err != nil {
return err
}
return nil
}
// WriteCSS writes CSS style definitions (without any surrounding HTML).
func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
css := f.styleToCSS(style)
css := f.styleCache.get(style, false)
// Special-case background as it is mapped to the outer ".chroma" class.
if _, err := fmt.Fprintf(w, "/* %s */ .%sbg { %s }\n", chroma.Background, f.prefix, css[chroma.Background]); err != nil {
if err := f.writeCSSRule(w, chroma.Background.String(), fmt.Sprintf(".%sbg", f.prefix), css[chroma.Background]); err != nil {
return err
}
// Special-case PreWrapper as it is the ".chroma" class.
if _, err := fmt.Fprintf(w, "/* %s */ .%schroma { %s }\n", chroma.PreWrapper, f.prefix, css[chroma.PreWrapper]); err != nil {
if err := f.writeCSSRule(w, chroma.PreWrapper.String(), fmt.Sprintf(".%schroma", f.prefix), css[chroma.PreWrapper]); err != nil {
return err
}
// Special-case code column of table to expand width.
if f.lineNumbers && f.lineNumbersInTable {
if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s:last-child { width: 100%%; }",
chroma.LineTableTD, f.prefix, f.class(chroma.LineTableTD)); err != nil {
selector := fmt.Sprintf(".%schroma .%s:last-child", f.prefix, f.class(chroma.LineTableTD))
if err := f.writeCSSRule(w, chroma.LineTableTD.String(), selector, "width: 100%;"); err != nil {
return err
}
}
@@ -439,7 +459,11 @@ func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
if f.lineNumbers || f.lineNumbersInTable {
targetedLineCSS := StyleEntryToCSS(style.Get(chroma.LineHighlight))
for _, tt := range []chroma.TokenType{chroma.LineNumbers, chroma.LineNumbersTable} {
fmt.Fprintf(w, "/* %s targeted by URL anchor */ .%schroma .%s:target { %s }\n", tt, f.prefix, f.class(tt), targetedLineCSS)
comment := fmt.Sprintf("%s targeted by URL anchor", tt)
selector := fmt.Sprintf(".%schroma .%s:target", f.prefix, f.class(tt))
if err := f.writeCSSRule(w, comment, selector, targetedLineCSS); err != nil {
return err
}
}
}
tts := []int{}
@@ -457,8 +481,7 @@ func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
if class == "" {
continue
}
styles := css[tt]
if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s { %s }\n", tt, f.prefix, class, styles); err != nil {
if err := f.writeCSSRule(w, tt.String(), fmt.Sprintf(".%schroma .%s", f.prefix, class), css[tt]); err != nil {
return err
}
}
@@ -505,6 +528,7 @@ func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string
}
classes[chroma.Background] += `;` + f.tabWidthStyle()
classes[chroma.PreWrapper] += classes[chroma.Background]
classes[chroma.PreWrapper] += ` -webkit-text-size-adjust: none;`
// Make PreWrapper a grid to show highlight style with full width.
if len(f.highlightRanges) > 0 && f.customCSS[chroma.PreWrapper] == `` {
classes[chroma.PreWrapper] += `display: grid;`
@@ -562,3 +586,63 @@ func compressStyle(s string) string {
}
return strings.Join(out, ";")
}
const styleCacheLimit = 32
type styleCacheEntry struct {
style *chroma.Style
compressed bool
cache map[chroma.TokenType]string
}
type styleCache struct {
mu sync.Mutex
// LRU cache of compiled (and possibly compressed) styles. This is a slice
// because the cache size is small, and a slice is sufficiently fast for
// small N.
cache []styleCacheEntry
f *Formatter
}
func newStyleCache(f *Formatter) *styleCache {
return &styleCache{f: f}
}
func (l *styleCache) get(style *chroma.Style, compress bool) map[chroma.TokenType]string {
l.mu.Lock()
defer l.mu.Unlock()
// Look for an existing entry.
for i := len(l.cache) - 1; i >= 0; i-- {
entry := l.cache[i]
if entry.style == style && entry.compressed == compress {
// Top of the cache, no need to adjust the order.
if i == len(l.cache)-1 {
return entry.cache
}
// Move this entry to the end of the LRU
copy(l.cache[i:], l.cache[i+1:])
l.cache[len(l.cache)-1] = entry
return entry.cache
}
}
// No entry, create one.
cached := l.f.styleToCSS(style)
if !l.f.Classes {
for t, style := range cached {
cached[t] = compressStyle(style)
}
}
if compress {
for t, style := range cached {
cached[t] = compressStyle(style)
}
}
// Evict the oldest entry.
if len(l.cache) >= styleCacheLimit {
l.cache = l.cache[0:copy(l.cache, l.cache[1:])]
}
l.cache = append(l.cache, styleCacheEntry{style: style, cache: cached, compressed: compress})
return cached
}

View File

@@ -10,11 +10,15 @@ import (
// JSON formatter outputs the raw token structures as JSON.
var JSON = Register("json", chroma.FormatterFunc(func(w io.Writer, s *chroma.Style, it chroma.Iterator) error {
fmt.Fprintln(w, "[")
if _, err := fmt.Fprintln(w, "["); err != nil {
return err
}
i := 0
for t := it(); t != chroma.EOF; t = it() {
if i > 0 {
fmt.Fprintln(w, ",")
if _, err := fmt.Fprintln(w, ","); err != nil {
return err
}
}
i++
bytes, err := json.Marshal(t)
@@ -25,7 +29,11 @@ var JSON = Register("json", chroma.FormatterFunc(func(w io.Writer, s *chroma.Sty
return err
}
}
fmt.Fprintln(w)
fmt.Fprintln(w, "]")
if _, err := fmt.Fprintln(w); err != nil {
return err
}
if _, err := fmt.Fprintln(w, "]"); err != nil {
return err
}
return nil
}))

View File

@@ -1,7 +1,6 @@
package formatters
import (
"fmt"
"io"
"math"
@@ -257,13 +256,7 @@ func (c *indexedTTYFormatter) Format(w io.Writer, style *chroma.Style, it chroma
}
}
if clr != "" {
fmt.Fprint(w, clr)
}
fmt.Fprint(w, token.Value)
if clr != "" {
fmt.Fprintf(w, "\033[0m")
}
writeToken(w, clr, token.Value)
}
return nil
}

View File

@@ -3,6 +3,7 @@ package formatters
import (
"fmt"
"io"
"regexp"
"github.com/alecthomas/chroma/v2"
)
@@ -10,33 +11,66 @@ import (
// TTY16m is a true-colour terminal formatter.
var TTY16m = Register("terminal16m", chroma.FormatterFunc(trueColourFormatter))
var crOrCrLf = regexp.MustCompile(`\r?\n`)
// Print the text with the given formatting, resetting the formatting at the end
// of each line and resuming it on the next line.
//
// This way, a pager (like https://github.com/walles/moar for example) can show
// any line in the output by itself, and it will get the right formatting.
func writeToken(w io.Writer, formatting string, text string) {
if formatting == "" {
fmt.Fprint(w, text)
return
}
newlineIndices := crOrCrLf.FindAllStringIndex(text, -1)
afterLastNewline := 0
for _, indices := range newlineIndices {
newlineStart, afterNewline := indices[0], indices[1]
fmt.Fprint(w, formatting)
fmt.Fprint(w, text[afterLastNewline:newlineStart])
fmt.Fprint(w, "\033[0m")
fmt.Fprint(w, text[newlineStart:afterNewline])
afterLastNewline = afterNewline
}
if afterLastNewline < len(text) {
// Print whatever is left after the last newline
fmt.Fprint(w, formatting)
fmt.Fprint(w, text[afterLastNewline:])
fmt.Fprint(w, "\033[0m")
}
}
func trueColourFormatter(w io.Writer, style *chroma.Style, it chroma.Iterator) error {
style = clearBackground(style)
for token := it(); token != chroma.EOF; token = it() {
entry := style.Get(token.Type)
if !entry.IsZero() {
out := ""
if entry.Bold == chroma.Yes {
out += "\033[1m"
}
if entry.Underline == chroma.Yes {
out += "\033[4m"
}
if entry.Italic == chroma.Yes {
out += "\033[3m"
}
if entry.Colour.IsSet() {
out += fmt.Sprintf("\033[38;2;%d;%d;%dm", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())
}
if entry.Background.IsSet() {
out += fmt.Sprintf("\033[48;2;%d;%d;%dm", entry.Background.Red(), entry.Background.Green(), entry.Background.Blue())
}
fmt.Fprint(w, out)
if entry.IsZero() {
fmt.Fprint(w, token.Value)
continue
}
fmt.Fprint(w, token.Value)
if !entry.IsZero() {
fmt.Fprint(w, "\033[0m")
formatting := ""
if entry.Bold == chroma.Yes {
formatting += "\033[1m"
}
if entry.Underline == chroma.Yes {
formatting += "\033[4m"
}
if entry.Italic == chroma.Yes {
formatting += "\033[3m"
}
if entry.Colour.IsSet() {
formatting += fmt.Sprintf("\033[38;2;%d;%d;%dm", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())
}
if entry.Background.IsSet() {
formatting += fmt.Sprintf("\033[48;2;%d;%d;%dm", entry.Background.Red(), entry.Background.Green(), entry.Background.Blue())
}
writeToken(w, formatting, token.Value)
}
return nil
}

View File

@@ -18,6 +18,17 @@ func (i Iterator) Tokens() []Token {
return out
}
// Stdlib converts a Chroma iterator to a Go 1.23-compatible iterator.
func (i Iterator) Stdlib() func(yield func(Token) bool) {
return func(yield func(Token) bool) {
for t := i(); t != EOF; t = i() {
if !yield(t) {
return
}
}
}
}
// Concaterator concatenates tokens from a series of iterators.
func Concaterator(iterators ...Iterator) Iterator {
return func() Token {
@@ -47,6 +58,7 @@ func Literator(tokens ...Token) Iterator {
// SplitTokensIntoLines splits tokens containing newlines in two.
func SplitTokensIntoLines(tokens []Token) (out [][]Token) {
var line []Token // nolint: prealloc
tokenLoop:
for _, token := range tokens {
for strings.Contains(token.Value, "\n") {
parts := strings.SplitAfterN(token.Value, "\n", 2)
@@ -59,6 +71,11 @@ func SplitTokensIntoLines(tokens []Token) (out [][]Token) {
line = append(line, clone)
out = append(out, line)
line = nil
// If the tail token is empty, don't emit it.
if len(token.Value) == 0 {
continue tokenLoop
}
}
line = append(line, token)
}
@@ -72,5 +89,5 @@ func SplitTokensIntoLines(tokens []Token) (out [][]Token) {
out = out[:len(out)-1]
}
}
return
return out
}

View File

@@ -130,6 +130,23 @@ type Lexer interface {
AnalyseText(text string) float32
}
// Trace is the trace of a tokenisation process.
type Trace struct {
Lexer string `json:"lexer"`
State string `json:"state"`
Rule int `json:"rule"`
Pattern string `json:"pattern"`
Pos int `json:"pos"`
Length int `json:"length"`
Elapsed float64 `json:"elapsedMs"` // Elapsed time spent matching for this rule.
}
// TracingLexer is a Lexer that can trace its tokenisation process.
type TracingLexer interface {
Lexer
SetTracing(enable bool)
}
// Lexers is a slice of lexers sortable by name.
type Lexers []Lexer

View File

@@ -4,52 +4,82 @@ import (
. "github.com/alecthomas/chroma/v2" // nolint
)
// Matcher token stub for docs, or
// Named matcher: @name, or
// Path matcher: /foo, or
// Wildcard path matcher: *
// nolint: gosec
var caddyfileMatcherTokenRegexp = `(\[\<matcher\>\]|@[^\s]+|/[^\s]+|\*)`
// Comment at start of line, or
// Comment preceded by whitespace
var caddyfileCommentRegexp = `(^|\s+)#.*\n`
// caddyfileCommon are the rules common to both of the lexer variants
func caddyfileCommonRules() Rules {
return Rules{
"site_block_common": {
Include("site_body"),
// Any other directive
{`[^\s#]+`, Keyword, Push("directive")},
Include("base"),
},
"site_body": {
// Import keyword
{`(import)(\s+)([^\s]+)`, ByGroups(Keyword, Text, NameVariableMagic), nil},
{`\b(import|invoke)\b( [^\s#]+)`, ByGroups(Keyword, Text), Push("subdirective")},
// Matcher definition
{`@[^\s]+(?=\s)`, NameDecorator, Push("matcher")},
// Matcher token stub for docs
{`\[\<matcher\>\]`, NameDecorator, Push("matcher")},
// These cannot have matchers but may have things that look like
// matchers in their arguments, so we just parse as a subdirective.
{`try_files`, Keyword, Push("subdirective")},
{`\b(try_files|tls|log|bind)\b`, Keyword, Push("subdirective")},
// These are special, they can nest more directives
{`handle_errors|handle|route|handle_path|not`, Keyword, Push("nested_directive")},
// Any other directive
{`[^\s#]+`, Keyword, Push("directive")},
Include("base"),
{`\b(handle_errors|handle_path|handle_response|replace_status|handle|route)\b`, Keyword, Push("nested_directive")},
// uri directive has special syntax
{`\b(uri)\b`, Keyword, Push("uri_directive")},
},
"matcher": {
{`\{`, Punctuation, Push("block")},
// Not can be one-liner
{`not`, Keyword, Push("deep_not_matcher")},
// Heredoc for CEL expression
Include("heredoc"),
// Backtick for CEL expression
{"`", StringBacktick, Push("backticks")},
// Any other same-line matcher
{`[^\s#]+`, Keyword, Push("arguments")},
// Terminators
{`\n`, Text, Pop(1)},
{`\s*\n`, Text, Pop(1)},
{`\}`, Punctuation, Pop(1)},
Include("base"),
},
"block": {
{`\}`, Punctuation, Pop(2)},
// Using double quotes doesn't stop at spaces
{`"`, StringDouble, Push("double_quotes")},
// Using backticks doesn't stop at spaces
{"`", StringBacktick, Push("backticks")},
// Not can be one-liner
{`not`, Keyword, Push("not_matcher")},
// Any other subdirective
// Directives & matcher definitions
Include("site_body"),
// Any directive
{`[^\s#]+`, Keyword, Push("subdirective")},
Include("base"),
},
"nested_block": {
{`\}`, Punctuation, Pop(2)},
// Matcher definition
{`@[^\s]+(?=\s)`, NameDecorator, Push("matcher")},
// Something that starts with literally < is probably a docs stub
{`\<[^#]+\>`, Keyword, Push("nested_directive")},
// Any other directive
{`[^\s#]+`, Keyword, Push("nested_directive")},
// Using double quotes doesn't stop at spaces
{`"`, StringDouble, Push("double_quotes")},
// Using backticks doesn't stop at spaces
{"`", StringBacktick, Push("backticks")},
// Not can be one-liner
{`not`, Keyword, Push("not_matcher")},
// Directives & matcher definitions
Include("site_body"),
// Any other subdirective
{`[^\s#]+`, Keyword, Push("directive")},
Include("base"),
},
"not_matcher": {
@@ -66,69 +96,97 @@ func caddyfileCommonRules() Rules {
},
"directive": {
{`\{(?=\s)`, Punctuation, Push("block")},
Include("matcher_token"),
Include("comments_pop_1"),
{`\n`, Text, Pop(1)},
{caddyfileMatcherTokenRegexp, NameDecorator, Push("arguments")},
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
{`\s*\n`, Text, Pop(1)},
Include("base"),
},
"nested_directive": {
{`\{(?=\s)`, Punctuation, Push("nested_block")},
Include("matcher_token"),
Include("comments_pop_1"),
{`\n`, Text, Pop(1)},
{caddyfileMatcherTokenRegexp, NameDecorator, Push("nested_arguments")},
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
{`\s*\n`, Text, Pop(1)},
Include("base"),
},
"subdirective": {
{`\{(?=\s)`, Punctuation, Push("block")},
Include("comments_pop_1"),
{`\n`, Text, Pop(1)},
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
{`\s*\n`, Text, Pop(1)},
Include("base"),
},
"arguments": {
{`\{(?=\s)`, Punctuation, Push("block")},
Include("comments_pop_2"),
{caddyfileCommentRegexp, CommentSingle, Pop(2)},
{`\\\n`, Text, nil}, // Skip escaped newlines
{`\n`, Text, Pop(2)},
{`\s*\n`, Text, Pop(2)},
Include("base"),
},
"nested_arguments": {
{`\{(?=\s)`, Punctuation, Push("nested_block")},
{caddyfileCommentRegexp, CommentSingle, Pop(2)},
{`\\\n`, Text, nil}, // Skip escaped newlines
{`\s*\n`, Text, Pop(2)},
Include("base"),
},
"deep_subdirective": {
{`\{(?=\s)`, Punctuation, Push("block")},
Include("comments_pop_3"),
{`\n`, Text, Pop(3)},
{caddyfileCommentRegexp, CommentSingle, Pop(3)},
{`\s*\n`, Text, Pop(3)},
Include("base"),
},
"matcher_token": {
{`@[^\s]+`, NameDecorator, Push("arguments")}, // Named matcher
{`/[^\s]+`, NameDecorator, Push("arguments")}, // Path matcher
{`\*`, NameDecorator, Push("arguments")}, // Wildcard path matcher
{`\[\<matcher\>\]`, NameDecorator, Push("arguments")}, // Matcher token stub for docs
"uri_directive": {
{`\{(?=\s)`, Punctuation, Push("block")},
{caddyfileMatcherTokenRegexp, NameDecorator, nil},
{`(strip_prefix|strip_suffix|replace|path_regexp)`, NameConstant, Push("arguments")},
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
{`\s*\n`, Text, Pop(1)},
Include("base"),
},
"comments": {
{`^#.*\n`, CommentSingle, nil}, // Comment at start of line
{`\s+#.*\n`, CommentSingle, nil}, // Comment preceded by whitespace
"double_quotes": {
Include("placeholder"),
{`\\"`, StringDouble, nil},
{`[^"]`, StringDouble, nil},
{`"`, StringDouble, Pop(1)},
},
"comments_pop_1": {
{`^#.*\n`, CommentSingle, Pop(1)}, // Comment at start of line
{`\s+#.*\n`, CommentSingle, Pop(1)}, // Comment preceded by whitespace
"backticks": {
Include("placeholder"),
{"\\\\`", StringBacktick, nil},
{"[^`]", StringBacktick, nil},
{"`", StringBacktick, Pop(1)},
},
"comments_pop_2": {
{`^#.*\n`, CommentSingle, Pop(2)}, // Comment at start of line
{`\s+#.*\n`, CommentSingle, Pop(2)}, // Comment preceded by whitespace
"optional": {
// Docs syntax for showing optional parts with [ ]
{`\[`, Punctuation, Push("optional")},
Include("name_constants"),
{`\|`, Punctuation, nil},
{`[^\[\]\|]+`, String, nil},
{`\]`, Punctuation, Pop(1)},
},
"comments_pop_3": {
{`^#.*\n`, CommentSingle, Pop(3)}, // Comment at start of line
{`\s+#.*\n`, CommentSingle, Pop(3)}, // Comment preceded by whitespace
"heredoc": {
{`(<<([a-zA-Z0-9_-]+))(\n(.*|\n)*)(\s*)(\2)`, ByGroups(StringHeredoc, nil, String, String, String, StringHeredoc), nil},
},
"name_constants": {
{`\b(most_recently_modified|largest_size|smallest_size|first_exist|internal|disable_redirects|ignore_loaded_certs|disable_certs|private_ranges|first|last|before|after|on|off)\b(\||(?=\]|\s|$))`, ByGroups(NameConstant, Punctuation), nil},
},
"placeholder": {
// Placeholder with dots, colon for default value, brackets for args[0:]
{`\{[\w+.\[\]\:\$-]+\}`, StringEscape, nil},
// Handle opening brackets with no matching closing one
{`\{[^\}\s]*\b`, String, nil},
},
"base": {
Include("comments"),
{`(on|off|first|last|before|after|internal|strip_prefix|strip_suffix|replace)\b`, NameConstant, nil},
{`(https?://)?([a-z0-9.-]+)(:)([0-9]+)`, ByGroups(Name, Name, Punctuation, LiteralNumberInteger), nil},
{`[a-z-]+/[a-z-+]+`, LiteralString, nil},
{`[0-9]+[km]?\b`, LiteralNumberInteger, nil},
{`\{[\w+.\$-]+\}`, LiteralStringEscape, nil}, // Placeholder
{`\[(?=[^#{}$]+\])`, Punctuation, nil},
{`\]|\|`, Punctuation, nil},
{`[^\s#{}$\]]+`, LiteralString, nil},
{caddyfileCommentRegexp, CommentSingle, nil},
{`\[\<matcher\>\]`, NameDecorator, nil},
Include("name_constants"),
Include("heredoc"),
{`(https?://)?([a-z0-9.-]+)(:)([0-9]+)([^\s]*)`, ByGroups(Name, Name, Punctuation, NumberInteger, Name), nil},
{`\[`, Punctuation, Push("optional")},
{"`", StringBacktick, Push("backticks")},
{`"`, StringDouble, Push("double_quotes")},
Include("placeholder"),
{`[a-z-]+/[a-z-+]+`, String, nil},
{`[0-9]+([smhdk]|ns|us|µs|ms)?\b`, NumberInteger, nil},
{`[^\s\n#\{]+`, String, nil},
{`/[^\s#]*`, Name, nil},
{`\s+`, Text, nil},
},
@@ -149,27 +207,29 @@ var Caddyfile = Register(MustNewLexer(
func caddyfileRules() Rules {
return Rules{
"root": {
Include("comments"),
{caddyfileCommentRegexp, CommentSingle, nil},
// Global options block
{`^\s*(\{)\s*$`, ByGroups(Punctuation), Push("globals")},
// Top level import
{`(import)(\s+)([^\s]+)`, ByGroups(Keyword, Text, NameVariableMagic), nil},
// Snippets
{`(\([^\s#]+\))(\s*)(\{)`, ByGroups(NameVariableAnonymous, Text, Punctuation), Push("snippet")},
{`(&?\([^\s#]+\))(\s*)(\{)`, ByGroups(NameVariableAnonymous, Text, Punctuation), Push("snippet")},
// Site label
{`[^#{(\s,]+`, GenericHeading, Push("label")},
// Site label with placeholder
{`\{[\w+.\$-]+\}`, LiteralStringEscape, Push("label")},
{`\{[\w+.\[\]\:\$-]+\}`, StringEscape, Push("label")},
{`\s+`, Text, nil},
},
"globals": {
{`\}`, Punctuation, Pop(1)},
{`[^\s#]+`, Keyword, Push("directive")},
// Global options are parsed as subdirectives (no matcher)
{`[^\s#]+`, Keyword, Push("subdirective")},
Include("base"),
},
"snippet": {
{`\}`, Punctuation, Pop(1)},
// Matcher definition
{`@[^\s]+(?=\s)`, NameDecorator, Push("matcher")},
// Any directive
Include("site_body"),
// Any other directive
{`[^\s#]+`, Keyword, Push("directive")},
Include("base"),
},
@@ -179,7 +239,7 @@ func caddyfileRules() Rules {
{`,\s*\n?`, Text, nil},
{` `, Text, nil},
// Site label with placeholder
{`\{[\w+.\$-]+\}`, LiteralStringEscape, nil},
Include("placeholder"),
// Site label
{`[^#{(\s,]+`, GenericHeading, nil},
// Comment after non-block label (hack because comments end in \n)

View File

@@ -6,103 +6,104 @@
<rules>
<state name="attr">
<rule pattern="&#34;.*?&#34;">
<token type="LiteralString"/>
<pop depth="1"/>
<token type="LiteralString" />
<pop depth="1" />
</rule>
<rule pattern="&#39;.*?&#39;">
<token type="LiteralString"/>
<pop depth="1"/>
<token type="LiteralString" />
<pop depth="1" />
</rule>
<rule pattern="[^\s&gt;]+">
<token type="LiteralString"/>
<pop depth="1"/>
<token type="LiteralString" />
<pop depth="1" />
</rule>
</state>
<state name="root">
<rule pattern="[^{([*#]+">
<token type="Other"/>
<token type="Other" />
</rule>
<rule pattern="(\{\{)(\s*)">
<bygroups>
<token type="CommentPreproc"/>
<token type="Text"/>
<token type="CommentPreproc" />
<token type="Text" />
</bygroups>
<push state="ngExpression"/>
<push state="ngExpression" />
</rule>
<rule pattern="([([]+)([\w:.-]+)([\])]+)(\s*)(=)(\s*)">
<bygroups>
<token type="Punctuation"/>
<token type="NameAttribute"/>
<token type="Punctuation"/>
<token type="Text"/>
<token type="Operator"/>
<token type="Text"/>
<token type="Punctuation" />
<token type="NameAttribute" />
<token type="Punctuation" />
<token type="Text" />
<token type="Operator" />
<token type="Text" />
</bygroups>
<push state="attr"/>
<push state="attr" />
</rule>
<rule pattern="([([]+)([\w:.-]+)([\])]+)(\s*)">
<bygroups>
<token type="Punctuation"/>
<token type="NameAttribute"/>
<token type="Punctuation"/>
<token type="Text"/>
<token type="Punctuation" />
<token type="NameAttribute" />
<token type="Punctuation" />
<token type="TextWhitespace" />
</bygroups>
</rule>
<rule pattern="([*#])([\w:.-]+)(\s*)(=)(\s*)">
<bygroups>
<token type="Punctuation"/>
<token type="NameAttribute"/>
<token type="Punctuation"/>
<token type="Operator"/>
<token type="Punctuation" />
<token type="NameAttribute" />
<token type="Punctuation" />
<token type="Operator" />
<token type="TextWhitespace" />
</bygroups>
<push state="attr"/>
<push state="attr" />
</rule>
<rule pattern="([*#])([\w:.-]+)(\s*)">
<bygroups>
<token type="Punctuation"/>
<token type="NameAttribute"/>
<token type="Punctuation"/>
<token type="Punctuation" />
<token type="NameAttribute" />
<token type="Punctuation" />
</bygroups>
</rule>
</state>
<state name="ngExpression">
<rule pattern="\s+(\|\s+)?">
<token type="Text"/>
<token type="Text" />
</rule>
<rule pattern="\}\}">
<token type="CommentPreproc"/>
<pop depth="1"/>
<token type="CommentPreproc" />
<pop depth="1" />
</rule>
<rule pattern=":?(true|false)">
<token type="LiteralStringBoolean"/>
<token type="LiteralStringBoolean" />
</rule>
<rule pattern=":?&#34;(\\\\|\\&#34;|[^&#34;])*&#34;">
<token type="LiteralStringDouble"/>
<token type="LiteralStringDouble" />
</rule>
<rule pattern=":?&#39;(\\\\|\\&#39;|[^&#39;])*&#39;">
<token type="LiteralStringSingle"/>
<token type="LiteralStringSingle" />
</rule>
<rule pattern="[0-9](\.[0-9]*)?(eE[+-][0-9])?[flFLdD]?|0[xX][0-9a-fA-F]+[Ll]?">
<token type="LiteralNumber"/>
<token type="LiteralNumber" />
</rule>
<rule pattern="[a-zA-Z][\w-]*(\(.*\))?">
<token type="NameVariable"/>
<token type="NameVariable" />
</rule>
<rule pattern="\.[\w-]+(\(.*\))?">
<token type="NameVariable"/>
<token type="NameVariable" />
</rule>
<rule pattern="(\?)(\s*)([^}\s]+)(\s*)(:)(\s*)([^}\s]+)(\s*)">
<bygroups>
<token type="Operator"/>
<token type="Text"/>
<token type="LiteralString"/>
<token type="Text"/>
<token type="Operator"/>
<token type="Text"/>
<token type="LiteralString"/>
<token type="Text"/>
<token type="Operator" />
<token type="Text" />
<token type="LiteralString" />
<token type="Text" />
<token type="Operator" />
<token type="Text" />
<token type="LiteralString" />
<token type="Text" />
</bygroups>
</rule>
</state>
</rules>
</lexer>
</lexer>

View File

@@ -8,123 +8,144 @@
<rules>
<state name="root">
<rule pattern="\s+">
<token type="Text"/>
<token type="Text" />
</rule>
<rule pattern="¬\n">
<token type="LiteralStringEscape"/>
<token type="LiteralStringEscape" />
</rule>
<rule pattern="&#39;s\s+">
<token type="Text"/>
<token type="Text" />
</rule>
<rule pattern="(--|#).*?$">
<token type="Comment"/>
<token type="Comment" />
</rule>
<rule pattern="\(\*">
<token type="CommentMultiline"/>
<push state="comment"/>
<token type="CommentMultiline" />
<push state="comment" />
</rule>
<rule pattern="[(){}!,.:]">
<token type="Punctuation"/>
<token type="Punctuation" />
</rule>
<rule pattern="(«)([^»]+)(»)">
<bygroups>
<token type="Text"/>
<token type="NameBuiltin"/>
<token type="Text"/>
<token type="Text" />
<token type="NameBuiltin" />
<token type="Text" />
</bygroups>
</rule>
<rule pattern="\b((?:considering|ignoring)\s*)(application responses|case|diacriticals|hyphens|numeric strings|punctuation|white space)">
<rule
pattern="\b((?:considering|ignoring)\s*)(application responses|case|diacriticals|hyphens|numeric strings|punctuation|white space)"
>
<bygroups>
<token type="Keyword"/>
<token type="NameBuiltin"/>
<token type="Keyword" />
<token type="NameBuiltin" />
</bygroups>
</rule>
<rule pattern="(-|\*|\+|&amp;|≠|&gt;=?|&lt;=?|=|≥|≤|/|÷|\^)">
<token type="Operator"/>
<token type="Operator" />
</rule>
<rule pattern="\b(and|or|is equal|equals|(is )?equal to|is not|isn&#39;t|isn&#39;t equal( to)?|is not equal( to)?|doesn&#39;t equal|does not equal|(is )?greater than|comes after|is not less than or equal( to)?|isn&#39;t less than or equal( to)?|(is )?less than|comes before|is not greater than or equal( to)?|isn&#39;t greater than or equal( to)?|(is )?greater than or equal( to)?|is not less than|isn&#39;t less than|does not come before|doesn&#39;t come before|(is )?less than or equal( to)?|is not greater than|isn&#39;t greater than|does not come after|doesn&#39;t come after|starts? with|begins? with|ends? with|contains?|does not contain|doesn&#39;t contain|is in|is contained by|is not in|is not contained by|isn&#39;t contained by|div|mod|not|(a )?(ref( to)?|reference to)|is|does)\b">
<token type="OperatorWord"/>
<rule
pattern="\b(and|or|is equal|equals|(is )?equal to|is not|isn&#39;t|isn&#39;t equal( to)?|is not equal( to)?|doesn&#39;t equal|does not equal|(is )?greater than|comes after|is not less than or equal( to)?|isn&#39;t less than or equal( to)?|(is )?less than|comes before|is not greater than or equal( to)?|isn&#39;t greater than or equal( to)?|(is )?greater than or equal( to)?|is not less than|isn&#39;t less than|does not come before|doesn&#39;t come before|(is )?less than or equal( to)?|is not greater than|isn&#39;t greater than|does not come after|doesn&#39;t come after|starts? with|begins? with|ends? with|contains?|does not contain|doesn&#39;t contain|is in|is contained by|is not in|is not contained by|isn&#39;t contained by|div|mod|not|(a )?(ref( to)?|reference to)|is|does)\b"
>
<token type="OperatorWord" />
</rule>
<rule pattern="^(\s*(?:on|end)\s+)(zoomed|write to file|will zoom|will show|will select tab view item|will resize( sub views)?|will resign active|will quit|will pop up|will open|will move|will miniaturize|will hide|will finish launching|will display outline cell|will display item cell|will display cell|will display browser cell|will dismiss|will close|will become active|was miniaturized|was hidden|update toolbar item|update parameters|update menu item|shown|should zoom|should selection change|should select tab view item|should select row|should select item|should select column|should quit( after last window closed)?|should open( untitled)?|should expand item|should end editing|should collapse item|should close|should begin editing|selection changing|selection changed|selected tab view item|scroll wheel|rows changed|right mouse up|right mouse dragged|right mouse down|resized( sub views)?|resigned main|resigned key|resigned active|read from file|prepare table drop|prepare table drag|prepare outline drop|prepare outline drag|prepare drop|plugin loaded|parameters updated|panel ended|opened|open untitled|number of rows|number of items|number of browser rows|moved|mouse up|mouse moved|mouse exited|mouse entered|mouse dragged|mouse down|miniaturized|load data representation|launched|keyboard up|keyboard down|items changed|item value changed|item value|item expandable|idle|exposed|end editing|drop|drag( (entered|exited|updated))?|double clicked|document nib name|dialog ended|deminiaturized|data representation|conclude drop|column resized|column moved|column clicked|closed|clicked toolbar item|clicked|choose menu item|child of item|changed|change item value|change cell value|cell value changed|cell value|bounds changed|begin editing|became main|became key|awake from nib|alert ended|activated|action|accept table drop|accept outline drop)">
<bygroups>
<token type="Keyword"/>
<token type="NameFunction"/>
</bygroups>
<rule
pattern="^(\s*(?:on|end)\s+)(zoomed|write to file|will zoom|will show|will select tab view item|will resize( sub views)?|will resign active|will quit|will pop up|will open|will move|will miniaturize|will hide|will finish launching|will display outline cell|will display item cell|will display cell|will display browser cell|will dismiss|will close|will become active|was miniaturized|was hidden|update toolbar item|update parameters|update menu item|shown|should zoom|should selection change|should select tab view item|should select row|should select item|should select column|should quit( after last window closed)?|should open( untitled)?|should expand item|should end editing|should collapse item|should close|should begin editing|selection changing|selection changed|selected tab view item|scroll wheel|rows changed|right mouse up|right mouse dragged|right mouse down|resized( sub views)?|resigned main|resigned key|resigned active|read from file|prepare table drop|prepare table drag|prepare outline drop|prepare outline drag|prepare drop|plugin loaded|parameters updated|panel ended|opened|open untitled|number of rows|number of items|number of browser rows|moved|mouse up|mouse moved|mouse exited|mouse entered|mouse dragged|mouse down|miniaturized|load data representation|launched|keyboard up|keyboard down|items changed|item value changed|item value|item expandable|idle|exposed|end editing|drop|drag( (entered|exited|updated))?|double clicked|document nib name|dialog ended|deminiaturized|data representation|conclude drop|column resized|column moved|column clicked|closed|clicked toolbar item|clicked|choose menu item|child of item|changed|change item value|change cell value|cell value changed|cell value|bounds changed|begin editing|became main|became key|awake from nib|alert ended|activated|action|accept table drop|accept outline drop)"
>
<token type="Keyword" />
</rule>
<rule pattern="^(\s*)(in|on|script|to)(\s+)">
<bygroups>
<token type="Text"/>
<token type="Keyword"/>
<token type="Text"/>
<token type="Text" />
<token type="Keyword" />
<token type="Text" />
</bygroups>
</rule>
<rule pattern="\b(as )(alias |application |boolean |class |constant |date |file |integer |list |number |POSIX file |real |record |reference |RGB color |script |text |unit types|(?:Unicode )?text|string)\b">
<rule
pattern="\b(as )(alias |application |boolean |class |constant |date |file |integer |list |number |POSIX file |real |record |reference |RGB color |script |text |unit types|(?:Unicode )?text|string)\b"
>
<bygroups>
<token type="Keyword"/>
<token type="NameClass"/>
<token type="Keyword" />
<token type="NameClass" />
</bygroups>
</rule>
<rule pattern="\b(AppleScript|current application|false|linefeed|missing value|pi|quote|result|return|space|tab|text item delimiters|true|version)\b">
<token type="NameConstant"/>
<rule
pattern="\b(AppleScript|current application|false|linefeed|missing value|pi|quote|result|return|space|tab|text item delimiters|true|version)\b"
>
<token type="NameConstant" />
</rule>
<rule pattern="\b(ASCII (character|number)|activate|beep|choose URL|choose application|choose color|choose file( name)?|choose folder|choose from list|choose remote application|clipboard info|close( access)?|copy|count|current date|delay|delete|display (alert|dialog)|do shell script|duplicate|exists|get eof|get volume settings|info for|launch|list (disks|folder)|load script|log|make|mount volume|new|offset|open( (for access|location))?|path to|print|quit|random number|read|round|run( script)?|say|scripting components|set (eof|the clipboard to|volume)|store script|summarize|system attribute|system info|the clipboard|time to GMT|write|quoted form)\b">
<token type="NameBuiltin"/>
<rule
pattern="\b(ASCII (character|number)|activate|beep|choose URL|choose application|choose color|choose file( name)?|choose folder|choose from list|choose remote application|clipboard info|close( access)?|copy|count|current date|delay|delete|display (alert|dialog)|do shell script|duplicate|exists|get eof|get volume settings|info for|launch|list (disks|folder)|load script|log|make|mount volume|new|offset|open( (for access|location))?|path to|print|quit|random number|read|round|run( script)?|say|scripting components|set (eof|the clipboard to|volume)|store script|summarize|system attribute|system info|the clipboard|time to GMT|write|quoted form)\b"
>
<token type="NameBuiltin" />
</rule>
<rule pattern="\b(considering|else|error|exit|from|if|ignoring|in|repeat|tell|then|times|to|try|until|using terms from|while|with|with timeout( of)?|with transaction|by|continue|end|its?|me|my|return|of|as)\b">
<token type="Keyword"/>
<rule
pattern="\b(considering|else|error|exit|from|if|ignoring|in|repeat|tell|then|times|to|try|until|using terms from|while|with|with timeout( of)?|with transaction|by|continue|end|its?|me|my|return|of|as)\b"
>
<token type="Keyword" />
</rule>
<rule pattern="\b(global|local|prop(erty)?|set|get)\b">
<token type="Keyword"/>
<token type="Keyword" />
</rule>
<rule pattern="\b(but|put|returning|the)\b">
<token type="NameBuiltin"/>
<token type="NameBuiltin" />
</rule>
<rule pattern="\b(attachment|attribute run|character|day|month|paragraph|word|year)s?\b">
<token type="NameBuiltin"/>
<token type="NameBuiltin" />
</rule>
<rule pattern="\b(about|above|against|apart from|around|aside from|at|below|beneath|beside|between|for|given|instead of|on|onto|out of|over|since)\b">
<token type="NameBuiltin"/>
<rule
pattern="\b(about|above|against|apart from|around|aside from|at|below|beneath|beside|between|for|given|instead of|on|onto|out of|over|since)\b"
>
<token type="NameBuiltin" />
</rule>
<rule pattern="\b(accepts arrow key|action method|active|alignment|allowed identifiers|allows branch selection|allows column reordering|allows column resizing|allows column selection|allows customization|allows editing text attributes|allows empty selection|allows mixed state|allows multiple selection|allows reordering|allows undo|alpha( value)?|alternate image|alternate increment value|alternate title|animation delay|associated file name|associated object|auto completes|auto display|auto enables items|auto repeat|auto resizes( outline column)?|auto save expanded items|auto save name|auto save table columns|auto saves configuration|auto scroll|auto sizes all columns to fit|auto sizes cells|background color|bezel state|bezel style|bezeled|border rect|border type|bordered|bounds( rotation)?|box type|button returned|button type|can choose directories|can choose files|can draw|can hide|cell( (background color|size|type))?|characters|class|click count|clicked( data)? column|clicked data item|clicked( data)? row|closeable|collating|color( (mode|panel))|command key down|configuration|content(s| (size|view( margins)?))?|context|continuous|control key down|control size|control tint|control view|controller visible|coordinate system|copies( on scroll)?|corner view|current cell|current column|current( field)? editor|current( menu)? item|current row|current tab view item|data source|default identifiers|delta (x|y|z)|destination window|directory|display mode|displayed cell|document( (edited|rect|view))?|double value|dragged column|dragged distance|dragged items|draws( cell)? background|draws grid|dynamically scrolls|echos bullets|edge|editable|edited( data)? column|edited data item|edited( data)? row|enabled|enclosing scroll view|ending page|error handling|event number|event type|excluded from windows menu|executable path|expanded|fax number|field editor|file kind|file name|file type|first responder|first visible column|flipped|floating|font( panel)?|formatter|frameworks path|frontmost|gave up|grid color|has data items|has horizontal ruler|has horizontal scroller|has parent data item|has resize indicator|has shadow|has sub menu|has vertical ruler|has vertical scroller|header cell|header view|hidden|hides when deactivated|highlights by|horizontal line scroll|horizontal page scroll|horizontal ruler view|horizontally resizable|icon image|id|identifier|ignores multiple clicks|image( (alignment|dims when disabled|frame style|scaling))?|imports graphics|increment value|indentation per level|indeterminate|index|integer value|intercell spacing|item height|key( (code|equivalent( modifier)?|window))?|knob thickness|label|last( visible)? column|leading offset|leaf|level|line scroll|loaded|localized sort|location|loop mode|main( (bunde|menu|window))?|marker follows cell|matrix mode|maximum( content)? size|maximum visible columns|menu( form representation)?|miniaturizable|miniaturized|minimized image|minimized title|minimum column width|minimum( content)? size|modal|modified|mouse down state|movie( (controller|file|rect))?|muted|name|needs display|next state|next text|number of tick marks|only tick mark values|opaque|open panel|option key down|outline table column|page scroll|pages across|pages down|palette label|pane splitter|parent data item|parent window|pasteboard|path( (names|separator))?|playing|plays every frame|plays selection only|position|preferred edge|preferred type|pressure|previous text|prompt|properties|prototype cell|pulls down|rate|released when closed|repeated|requested print time|required file type|resizable|resized column|resource path|returns records|reuses columns|rich text|roll over|row height|rulers visible|save panel|scripts path|scrollable|selectable( identifiers)?|selected cell|selected( data)? columns?|selected data items?|selected( data)? rows?|selected item identifier|selection by rect|send action on arrow key|sends action when done editing|separates columns|separator item|sequence number|services menu|shared frameworks path|shared support path|sheet|shift key down|shows alpha|shows state by|size( mode)?|smart insert delete enabled|sort case sensitivity|sort column|sort order|sort type|sorted( data rows)?|sound|source( mask)?|spell checking enabled|starting page|state|string value|sub menu|super menu|super view|tab key traverses cells|tab state|tab type|tab view|table view|tag|target( printer)?|text color|text container insert|text container origin|text returned|tick mark position|time stamp|title(d| (cell|font|height|position|rect))?|tool tip|toolbar|trailing offset|transparent|treat packages as directories|truncated labels|types|unmodified characters|update views|use sort indicator|user defaults|uses data source|uses ruler|uses threaded animation|uses title from previous column|value wraps|version|vertical( (line scroll|page scroll|ruler view))?|vertically resizable|view|visible( document rect)?|volume|width|window|windows menu|wraps|zoomable|zoomed)\b">
<token type="NameAttribute"/>
<rule
pattern="\b(accepts arrow key|action method|active|alignment|allowed identifiers|allows branch selection|allows column reordering|allows column resizing|allows column selection|allows customization|allows editing text attributes|allows empty selection|allows mixed state|allows multiple selection|allows reordering|allows undo|alpha( value)?|alternate image|alternate increment value|alternate title|animation delay|associated file name|associated object|auto completes|auto display|auto enables items|auto repeat|auto resizes( outline column)?|auto save expanded items|auto save name|auto save table columns|auto saves configuration|auto scroll|auto sizes all columns to fit|auto sizes cells|background color|bezel state|bezel style|bezeled|border rect|border type|bordered|bounds( rotation)?|box type|button returned|button type|can choose directories|can choose files|can draw|can hide|cell( (background color|size|type))?|characters|class|click count|clicked( data)? column|clicked data item|clicked( data)? row|closeable|collating|color( (mode|panel))|command key down|configuration|content(s| (size|view( margins)?))?|context|continuous|control key down|control size|control tint|control view|controller visible|coordinate system|copies( on scroll)?|corner view|current cell|current column|current( field)? editor|current( menu)? item|current row|current tab view item|data source|default identifiers|delta (x|y|z)|destination window|directory|display mode|displayed cell|document( (edited|rect|view))?|double value|dragged column|dragged distance|dragged items|draws( cell)? background|draws grid|dynamically scrolls|echos bullets|edge|editable|edited( data)? column|edited data item|edited( data)? row|enabled|enclosing scroll view|ending page|error handling|event number|event type|excluded from windows menu|executable path|expanded|fax number|field editor|file kind|file name|file type|first responder|first visible column|flipped|floating|font( panel)?|formatter|frameworks path|frontmost|gave up|grid color|has data items|has horizontal ruler|has horizontal scroller|has parent data item|has resize indicator|has shadow|has sub menu|has vertical ruler|has vertical scroller|header cell|header view|hidden|hides when deactivated|highlights by|horizontal line scroll|horizontal page scroll|horizontal ruler view|horizontally resizable|icon image|id|identifier|ignores multiple clicks|image( (alignment|dims when disabled|frame style|scaling))?|imports graphics|increment value|indentation per level|indeterminate|index|integer value|intercell spacing|item height|key( (code|equivalent( modifier)?|window))?|knob thickness|label|last( visible)? column|leading offset|leaf|level|line scroll|loaded|localized sort|location|loop mode|main( (bunde|menu|window))?|marker follows cell|matrix mode|maximum( content)? size|maximum visible columns|menu( form representation)?|miniaturizable|miniaturized|minimized image|minimized title|minimum column width|minimum( content)? size|modal|modified|mouse down state|movie( (controller|file|rect))?|muted|name|needs display|next state|next text|number of tick marks|only tick mark values|opaque|open panel|option key down|outline table column|page scroll|pages across|pages down|palette label|pane splitter|parent data item|parent window|pasteboard|path( (names|separator))?|playing|plays every frame|plays selection only|position|preferred edge|preferred type|pressure|previous text|prompt|properties|prototype cell|pulls down|rate|released when closed|repeated|requested print time|required file type|resizable|resized column|resource path|returns records|reuses columns|rich text|roll over|row height|rulers visible|save panel|scripts path|scrollable|selectable( identifiers)?|selected cell|selected( data)? columns?|selected data items?|selected( data)? rows?|selected item identifier|selection by rect|send action on arrow key|sends action when done editing|separates columns|separator item|sequence number|services menu|shared frameworks path|shared support path|sheet|shift key down|shows alpha|shows state by|size( mode)?|smart insert delete enabled|sort case sensitivity|sort column|sort order|sort type|sorted( data rows)?|sound|source( mask)?|spell checking enabled|starting page|state|string value|sub menu|super menu|super view|tab key traverses cells|tab state|tab type|tab view|table view|tag|target( printer)?|text color|text container insert|text container origin|text returned|tick mark position|time stamp|title(d| (cell|font|height|position|rect))?|tool tip|toolbar|trailing offset|transparent|treat packages as directories|truncated labels|types|unmodified characters|update views|use sort indicator|user defaults|uses data source|uses ruler|uses threaded animation|uses title from previous column|value wraps|version|vertical( (line scroll|page scroll|ruler view))?|vertically resizable|view|visible( document rect)?|volume|width|window|windows menu|wraps|zoomable|zoomed)\b"
>
<token type="NameAttribute" />
</rule>
<rule pattern="\b(action cell|alert reply|application|box|browser( cell)?|bundle|button( cell)?|cell|clip view|color well|color-panel|combo box( item)?|control|data( (cell|column|item|row|source))?|default entry|dialog reply|document|drag info|drawer|event|font(-panel)?|formatter|image( (cell|view))?|matrix|menu( item)?|item|movie( view)?|open-panel|outline view|panel|pasteboard|plugin|popup button|progress indicator|responder|save-panel|scroll view|secure text field( cell)?|slider|sound|split view|stepper|tab view( item)?|table( (column|header cell|header view|view))|text( (field( cell)?|view))?|toolbar( item)?|user-defaults|view|window)s?\b">
<token type="NameBuiltin"/>
<rule
pattern="\b(action cell|alert reply|application|box|browser( cell)?|bundle|button( cell)?|cell|clip view|color well|color-panel|combo box( item)?|control|data( (cell|column|item|row|source))?|default entry|dialog reply|document|drag info|drawer|event|font(-panel)?|formatter|image( (cell|view))?|matrix|menu( item)?|item|movie( view)?|open-panel|outline view|panel|pasteboard|plugin|popup button|progress indicator|responder|save-panel|scroll view|secure text field( cell)?|slider|sound|split view|stepper|tab view( item)?|table( (column|header cell|header view|view))|text( (field( cell)?|view))?|toolbar( item)?|user-defaults|view|window)s?\b"
>
<token type="NameBuiltin" />
</rule>
<rule pattern="\b(animate|append|call method|center|close drawer|close panel|display|display alert|display dialog|display panel|go|hide|highlight|increment|item for|load image|load movie|load nib|load panel|load sound|localized string|lock focus|log|open drawer|path for|pause|perform action|play|register|resume|scroll|select( all)?|show|size to fit|start|step back|step forward|stop|synchronize|unlock focus|update)\b">
<token type="NameBuiltin"/>
<rule
pattern="\b(animate|append|call method|center|close drawer|close panel|display|display alert|display dialog|display panel|go|hide|highlight|increment|item for|load image|load movie|load nib|load panel|load sound|localized string|lock focus|log|open drawer|path for|pause|perform action|play|register|resume|scroll|select( all)?|show|size to fit|start|step back|step forward|stop|synchronize|unlock focus|update)\b"
>
<token type="NameBuiltin" />
</rule>
<rule pattern="\b((in )?back of|(in )?front of|[0-9]+(st|nd|rd|th)|first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth|after|back|before|behind|every|front|index|last|middle|some|that|through|thru|where|whose)\b">
<token type="NameBuiltin"/>
<rule
pattern="\b((in )?back of|(in )?front of|[0-9]+(st|nd|rd|th)|first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth|after|back|before|behind|every|front|index|last|middle|some|that|through|thru|where|whose)\b"
>
<token type="NameBuiltin" />
</rule>
<rule pattern="&#34;(\\\\|\\&#34;|[^&#34;])*&#34;">
<token type="LiteralStringDouble"/>
<token type="LiteralStringDouble" />
</rule>
<rule pattern="\b([a-zA-Z]\w*)\b">
<token type="NameVariable"/>
<token type="NameVariable" />
</rule>
<rule pattern="[-+]?(\d+\.\d*|\d*\.\d+)(E[-+][0-9]+)?">
<token type="LiteralNumberFloat"/>
<token type="LiteralNumberFloat" />
</rule>
<rule pattern="[-+]?\d+">
<token type="LiteralNumberInteger"/>
<token type="LiteralNumberInteger" />
</rule>
</state>
<state name="comment">
<rule pattern="\(\*">
<token type="CommentMultiline"/>
<push/>
<token type="CommentMultiline" />
<push />
</rule>
<rule pattern="\*\)">
<token type="CommentMultiline"/>
<pop depth="1"/>
<token type="CommentMultiline" />
<pop depth="1" />
</rule>
<rule pattern="[^*(]+">
<token type="CommentMultiline"/>
<token type="CommentMultiline" />
</rule>
<rule pattern="[*(]">
<token type="CommentMultiline"/>
<token type="CommentMultiline" />
</rule>
</state>
</rules>
</lexer>
</lexer>

View File

@@ -152,7 +152,7 @@
<rule pattern="(?-i)(?:CURRENT|NEW|OLD)\b">
<token type="NameBuiltinPseudo"/>
</rule>
<rule pattern="(?:to_bool|to_number|to_string|to_array|to_list|is_null|is_bool|is_number|is_string|is_array|is_list|is_object|is_document|is_datestring|typename|json_stringify|json_parse|concat|concat_separator|char_length|lower|upper|substring|substring_bytes|left|right|trim|reverse|contains|log|log2|log10|exp|exp2|sin|cos|tan|asin|acos|atan|atan2|radians|degrees|pi|regex_test|regex_replace|like|floor|ceil|round|abs|rand|sqrt|pow|length|count|min|max|average|avg|sum|product|median|variance_population|variance_sample|variance|percentile|bit_and|bit_or|bit_xor|bit_negate|bit_test|bit_popcount|bit_shift_left|bit_shift_right|bit_construct|bit_deconstruct|bit_to_string|bit_from_string|first|last|unique|outersection|interleave|in_range|jaccard|matches|merge|merge_recursive|has|attributes|keys|values|unset|unset_recursive|keep|keep_recursive|near|within|within_rectangle|is_in_polygon|distance|fulltext|stddev_sample|stddev_population|stddev|slice|nth|position|contains_array|translate|zip|call|apply|push|append|pop|shift|unshift|remove_value|remove_values|remove_nth|replace_nth|date_now|date_timestamp|date_iso8601|date_dayofweek|date_year|date_month|date_day|date_hour|date_minute|date_second|date_millisecond|date_dayofyear|date_isoweek|date_isoweekyear|date_leapyear|date_quarter|date_days_in_month|date_trunc|date_round|date_add|date_subtract|date_diff|date_compare|date_format|date_utctolocal|date_localtoutc|date_timezone|date_timezones|fail|passthru|v8|sleep|schema_get|schema_validate|shard_id|version|noopt|noeval|not_null|first_list|first_document|parse_identifier|current_user|current_database|collection_count|pregel_result|collections|document|decode_rev|range|union|union_distinct|minus|intersection|flatten|is_same_collection|check_document|ltrim|rtrim|find_first|find_last|split|substitute|ipv4_to_number|ipv4_from_number|is_ipv4|md5|sha1|sha256|sha512|crc32|fnv64|hash|random_token|to_base64|to_hex|encode_uri_component|soundex|assert|warn|is_key|sorted|sorted_unique|count_distinct|count_unique|levenshtein_distance|levenshtein_match|regex_matches|regex_split|ngram_match|ngram_similarity|ngram_positional_similarity|uuid|tokens|exists|starts_with|phrase|min_match|bm25|tfidf|boost|analyzer|offset_info|value|cosine_similarity|decay_exp|decay_gauss|decay_linear|l1_distance|l2_distance|minhash|minhash_count|minhash_error|minhash_match|geo_point|geo_multipoint|geo_polygon|geo_multipolygon|geo_linestring|geo_multilinestring|geo_contains|geo_intersects|geo_equals|geo_distance|geo_area|geo_in_range)(?=\s*\()">
<rule pattern="(?:to_bool|to_number|to_char|to_string|to_array|to_list|is_null|is_bool|is_number|is_string|is_array|is_list|is_object|is_document|is_datestring|typename|json_stringify|json_parse|concat|concat_separator|char_length|lower|upper|substring|substring_bytes|left|right|trim|reverse|repeat|contains|log|log2|log10|exp|exp2|sin|cos|tan|asin|acos|atan|atan2|radians|degrees|pi|regex_test|regex_replace|like|floor|ceil|round|abs|rand|random|sqrt|pow|length|count|min|max|average|avg|sum|product|median|variance_population|variance_sample|variance|percentile|bit_and|bit_or|bit_xor|bit_negate|bit_test|bit_popcount|bit_shift_left|bit_shift_right|bit_construct|bit_deconstruct|bit_to_string|bit_from_string|first|last|unique|outersection|interleave|in_range|jaccard|matches|merge|merge_recursive|has|attributes|keys|values|entries|unset|unset_recursive|keep|keep_recursive|near|within|within_rectangle|is_in_polygon|distance|fulltext|stddev_sample|stddev_population|stddev|slice|nth|position|contains_array|translate|zip|call|apply|push|append|pop|shift|unshift|remove_value|remove_values|remove_nth|replace_nth|date_now|date_timestamp|date_iso8601|date_dayofweek|date_year|date_month|date_day|date_hour|date_minute|date_second|date_millisecond|date_dayofyear|date_isoweek|date_isoweekyear|date_leapyear|date_quarter|date_days_in_month|date_trunc|date_round|date_add|date_subtract|date_diff|date_compare|date_format|date_utctolocal|date_localtoutc|date_timezone|date_timezones|fail|passthru|v8|sleep|schema_get|schema_validate|shard_id|version|noopt|noeval|not_null|first_list|first_document|parse_identifier|parse_collection|parse_key|current_user|current_database|collection_count|pregel_result|collections|document|decode_rev|range|union|union_distinct|minus|intersection|flatten|is_same_collection|check_document|ltrim|rtrim|find_first|find_last|split|substitute|ipv4_to_number|ipv4_from_number|is_ipv4|md5|sha1|sha256|sha512|crc32|fnv64|hash|random_token|to_base64|to_hex|encode_uri_component|soundex|assert|warn|is_key|sorted|sorted_unique|count_distinct|count_unique|levenshtein_distance|levenshtein_match|regex_matches|regex_split|ngram_match|ngram_similarity|ngram_positional_similarity|uuid|tokens|exists|starts_with|phrase|min_match|bm25|tfidf|boost|analyzer|offset_info|value|cosine_similarity|decay_exp|decay_gauss|decay_linear|l1_distance|l2_distance|minhash|minhash_count|minhash_error|minhash_match|geo_point|geo_multipoint|geo_polygon|geo_multipolygon|geo_linestring|geo_multilinestring|geo_contains|geo_intersects|geo_equals|geo_distance|geo_area|geo_in_range)(?=\s*\()">
<token type="NameFunction"/>
</rule>
<rule pattern="&#34;">

Some files were not shown because too many files have changed in this diff Show More