chore: make deps, go mod vendor
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
decentral1se 2024-12-02 01:45:06 +01:00
parent f664599836
commit 31fa9b1a7a
Signed by: decentral1se
GPG Key ID: 03789458B3D0C410
598 changed files with 37898 additions and 18309 deletions

93
go.mod
View File

@ -1,6 +1,8 @@
module coopcloud.tech/abra
go 1.21
go 1.22.7
toolchain go1.23.1
replace github.com/urfave/cli/v3 => github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44
@ -8,38 +10,38 @@ require (
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/lipgloss v0.11.1
github.com/charmbracelet/lipgloss v1.0.0
github.com/charmbracelet/log v0.4.0
github.com/distribution/reference v0.6.0
github.com/docker/cli v27.0.3+incompatible
github.com/docker/docker v27.0.3+incompatible
github.com/docker/cli v27.3.1+incompatible
github.com/docker/docker v27.3.1+incompatible
github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.12.0
github.com/google/go-cmp v0.6.0
github.com/moby/sys/signal v0.7.0
github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.0
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.14.4
github.com/schollz/progressbar/v3 v3.17.1
github.com/urfave/cli/v3 v3.0.0-alpha9
golang.org/x/term v0.22.0
golang.org/x/term v0.26.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1
)
require (
dario.cat/mergo v1.0.0 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/x/ansi v0.1.3 // indirect
github.com/cloudflare/circl v1.3.9 // indirect
github.com/charmbracelet/x/ansi v0.5.2 // indirect
github.com/cloudflare/circl v1.5.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/cyphar/filepath-securejoin v0.3.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
@ -50,31 +52,32 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/ghodss/yaml v1.0.0 // 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/go-git/go-billy/v5 v5.6.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@ -83,41 +86,41 @@ require (
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/common v0.60.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
golang.org/x/crypto v0.29.0 // indirect
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.8.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/grpc v1.68.0 // indirect
google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
github.com/containerd/containerd v1.7.19 // indirect
github.com/containerd/containerd v1.7.24 // indirect
github.com/containers/image v3.0.2+incompatible
github.com/containers/storage v1.38.2 // indirect
github.com/decentral1se/passgen v1.0.1
@ -127,13 +130,13 @@ require (
github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.22.0
golang.org/x/sys v0.27.0
)

95
go.sum
View File

@ -26,6 +26,8 @@ coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb h1:Ws6WEwKXeaYEkfdkX6Aq
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
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.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 h1:tCv2B4qoN6RMheKDnCzIafOkWS5BB1h7hwhmo+9bVeE=
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q=
@ -81,6 +83,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
@ -130,16 +134,21 @@ github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/lipgloss v0.11.1 h1:a8KgVPHa7kOoP95vm2tQQrjD2AKhbWmfr4uJ2RW6kNk=
github.com/charmbracelet/lipgloss v0.11.1/go.mod h1:beLlcmkF7MWA+5UrKKIRo/VJ21xGXr7YJ9miWfdMRIU=
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.1.3 h1:RBh/eleNWML5R524mjUF0yVRePTwqN9tPtV+DPgO5Lw=
github.com/charmbracelet/x/ansi v0.1.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/ansi v0.5.2 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuXXrU9E=
github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
@ -158,6 +167,8 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
@ -198,6 +209,8 @@ github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTV
github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=
github.com/containerd/containerd v1.7.19 h1:/xQ4XRJ0tamDkdzrrBAUy/LE5nCcxFKdBm4EcPrSMEE=
github.com/containerd/containerd v1.7.19/go.mod h1:h4FtNYUUMB4Phr6v+xG89RYKj9XccvbNSCKjdufCrkc=
github.com/containerd/containerd v1.7.24 h1:zxszGrGjrra1yYJW/6rhm9cJ1ZQ8rkKBR48brqsa7nA=
github.com/containerd/containerd v1.7.24/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
@ -283,6 +296,8 @@ github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
@ -303,6 +318,8 @@ github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyG
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.0.3+incompatible h1:usGs0/BoBW8MWxGeEtqPMkzOY56jZ6kYlSN5BLDioCQ=
github.com/docker/cli v27.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@ -311,6 +328,8 @@ github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4Kfc
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
@ -374,6 +393,8 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D
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.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
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.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
@ -411,6 +432,8 @@ github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
@ -436,6 +459,8 @@ github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
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/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/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -515,9 +540,12 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -582,6 +610,8 @@ github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -621,6 +651,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
@ -656,11 +688,19 @@ github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vyg
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI=
github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=
github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8=
github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
@ -702,6 +742,7 @@ github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoT
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
@ -760,6 +801,8 @@ github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQ
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -775,6 +818,8 @@ github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@ -797,12 +842,15 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74=
github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI=
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U=
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4=
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
@ -821,6 +869,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@ -864,6 +914,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
@ -923,24 +975,42 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08=
go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
@ -972,6 +1042,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -984,6 +1056,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1053,6 +1127,8 @@ 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.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1073,6 +1149,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1157,6 +1235,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
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=
@ -1166,6 +1246,8 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1179,6 +1261,8 @@ 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1187,6 +1271,8 @@ golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -1283,8 +1369,12 @@ google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7Fc
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -1306,6 +1396,8 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -1321,6 +1413,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
@ -1360,6 +1454,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=

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/

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,24 @@ 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)
* [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes)
* [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 +118,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

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

@ -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 {
@ -153,7 +154,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 {

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
}
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
}
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

@ -14,6 +14,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 {
@ -103,11 +120,14 @@ type encoding struct {
breaker *lineBreaker
b64 io.WriteCloser
crc uint32
crcEnabled bool
blockType []byte
}
func (e *encoding) Write(data []byte) (n int, err error) {
if e.crcEnabled {
e.crc = crc24(e.crc, data)
}
return e.b64.Write(data)
}
@ -118,6 +138,7 @@ func (e *encoding) Close() (err error) {
}
e.breaker.Close()
if e.crcEnabled {
var checksumBytes [3]byte
checksumBytes[0] = byte(e.crc >> 16)
checksumBytes[1] = byte(e.crc >> 8)
@ -128,10 +149,10 @@ func (e *encoding) Close() (err error) {
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 {
@ -153,9 +174,25 @@ 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,
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

@ -9,6 +9,18 @@ import (
"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 +29,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 +81,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 +95,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 +137,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 +171,10 @@ 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)
}

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,7 +128,9 @@ func (t *Entity) addUserId(name, comment, email string, config *packet.Config, c
selfSignature.PreferredCompression = append(selfSignature.PreferredCompression, uint8(config.Compression()))
}
// And for DefaultMode.
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))
@ -131,6 +142,30 @@ func (t *Entity) addUserId(name, comment, email string, config *packet.Config, c
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"
@ -29,6 +30,8 @@ type Entity struct {
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
@ -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 {
@ -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

@ -88,6 +88,8 @@ func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) {
if errRead != nil && errRead != io.EOF {
return 0, errRead
}
if len(cipherChunk) > 0 {
decrypted, errChunk := ar.openChunk(cipherChunk)
if errChunk != nil {
return 0, errChunk
@ -100,6 +102,7 @@ func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) {
} else {
n = copy(dst, decrypted)
}
}
// Check final authentication tag
if errRead == io.EOF {
@ -116,6 +119,12 @@ func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) {
// checked in the last Read call. In the future, this function could be used to
// wipe the reader and peeked, decrypted bytes, if necessary.
func (ar *aeadDecrypter) Close() (err error) {
if !ar.eof {
errChunk := ar.validateFinalTag(ar.peekedBytes)
if errChunk != nil {
return errChunk
}
}
return nil
}
@ -138,7 +147,7 @@ func (ar *aeadDecrypter) openChunk(data []byte) ([]byte, error) {
nonce := ar.computeNextNonce()
plainChunk, err := ar.aead.Open(nil, nonce, chunk, adata)
if err != nil {
return nil, err
return nil, errors.ErrAEADTagVerification
}
ar.bytesProcessed += len(plainChunk)
if err = ar.aeadCrypter.incrementIndex(); err != nil {
@ -163,9 +172,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
}

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])))
}

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,40 @@ 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
}
func (c *Config) Random() io.Reader {
@ -197,7 +260,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 +296,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 +316,95 @@ 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
}
// 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 {
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
}
}
_, err = consumeAll(r)
case PubKeyAlgoX25519:
e.ephemeralPublicX25519, e.encryptedSession, cipherFunction, err = x25519.DecodeFields(r, e.Version == 6)
if err != nil {
return
}
func checksumKeyMaterial(key []byte) uint16 {
var checksum uint16
for _, v := range key {
checksum += uint16(v)
case PubKeyAlgoX448:
e.ephemeralPublicX448, e.encryptedSession, cipherFunction, err = x448.DecodeFields(r, e.Version == 6)
if err != nil {
return
}
return checksum
}
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
}
// 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
}
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")
}
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")
}
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 = 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 */)
// 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)
copy(keyBlock[1:], key)
checksum := checksumKeyMaterial(key)
keyBlock[1+len(key)] = byte(checksum >> 8)
keyBlock[1+len(key)+1] = byte(checksum)
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'
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 {
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
_, err := w.Write(buf[:4])
if err != nil {
return err
}
if err := serializeHeader(w, packetTypeOnePassSignature, len(buf)); err != nil {
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(buf[:])
_, 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
// 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,6 +330,8 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
if len(privateKeyData) < 2 {
return errors.StructuralError("truncated private key data")
}
if pk.Version != 6 {
// checksum
var sum uint16
for i := 0; i < len(privateKeyData)-2; i++ {
sum += uint16(privateKeyData[i])
@ -249,6 +342,10 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) {
}
privateKeyData = privateKeyData[:len(privateKeyData)-2]
return pk.parsePrivateKey(privateKeyData)
} else {
// No checksum
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()
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,14 +538,25 @@ func (pk *PrivateKey) decrypt(decryptionKey []byte) error {
if !pk.Encrypted {
return nil
}
block := pk.cipher.new(decryptionKey)
var data []byte
switch pk.s2kType {
case S2KAEAD:
aead := pk.aead.new(block)
additionalData, err := pk.additionalData()
if err != nil {
return err
}
// Decrypt the encrypted key material with aead
data, err = aead.Open(nil, pk.iv, pk.encryptedData, additionalData)
if err != nil {
return err
}
case S2KSHA1, S2KCHECKSUM:
cfb := cipher.NewCFBDecrypter(block, pk.iv)
data := make([]byte, len(pk.encryptedData))
data = make([]byte, len(pk.encryptedData))
cfb.XORKeyStream(data, pk.encryptedData)
if pk.sha1Checksum {
if pk.s2kType == S2KSHA1 {
if len(data) < sha1.Size {
return errors.StructuralError("truncated private key data")
}
@ -410,6 +581,9 @@ func (pk *PrivateKey) decrypt(decryptionKey []byte) error {
}
data = data[:len(data)-2]
}
default:
return errors.InvalidArgumentError("invalid s2k type")
}
err := pk.parsePrivateKey(data)
if _, ok := err.(errors.KeyInvalidError); ok {
@ -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,6 +630,9 @@ 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)
}
@ -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")
}
@ -486,6 +665,14 @@ func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, cipherFunction Cip
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 {
@ -500,32 +687,50 @@ func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, cipherFunction Cip
}
privateKeyBytes := priv.Bytes()
pk.sha1Checksum = true
pk.s2kType = s2kType
block := pk.cipher.new(key)
switch s2kType {
case S2KAEAD:
if pk.aead == 0 {
return errors.StructuralError("aead mode is not set on key")
}
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 = rand.Read(pk.iv)
_, err = io.ReadFull(rand, pk.iv)
if err != nil {
return err
}
cfb := cipher.NewCFBEncrypter(block, pk.iv)
if pk.sha1Checksum {
pk.s2kType = S2KSHA1
if 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)
}
priv.Write([]byte{uint8(sum >> 8), uint8(sum)})
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.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
}
@ -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)
}
h = hashFunc.New()
// RFC 4880, section 5.2.4
err = pk.SerializeForHash(h)
return
// 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
}
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,16 +997,51 @@ 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)
}
@ -786,21 +1072,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,24 +32,13 @@ 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) {
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
}
for {
p, err := r.read()
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.
break
} else if err != nil {
if _, ok := err.(errors.UnknownPacketTypeError); ok {
continue
}
@ -55,8 +50,63 @@ func (r *Reader) Next() (p Packet, err error) {
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 == io.EOF {
r.readers = r.readers[:len(r.readers)-1]
continue
}
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,8 +64,11 @@ 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
tagLen := se.Mode.TagLength()
peekedBytes := make([]byte, tagLen)
@ -115,7 +120,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
}

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
}
@ -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)))
}
@ -329,20 +349,26 @@ func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType) (hash.
// MDC checks.
type checkReader struct {
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 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

@ -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

@ -50,7 +50,7 @@ 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
}

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
}
@ -275,14 +279,28 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit
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 {
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 +328,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 +398,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 +444,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 +493,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 +518,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 +542,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
}

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

@ -14,16 +14,13 @@ issues:
linters:
enable:
# - dupl
- exhaustive
# - exhaustivestruct
- goconst
- godot
- godox
- gomnd
- mnd
- gomoddirectives
- goprintffuncname
# - lll
- misspell
- nakedret
- nestif
@ -34,13 +31,10 @@ linters:
# disable default linters, they are already enabled in .golangci.yml
disable:
- deadcode
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- structcheck
- typecheck
- unused
- varcheck

View File

@ -15,12 +15,10 @@ issues:
linters:
enable:
- bodyclose
- exportloopref
- gofumpt
- goimports
- gosec
- nilerr
- predeclared
- revive
- rowserrcheck
- sqlclosecheck

View File

@ -0,0 +1,5 @@
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-lib.yaml
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json

View File

@ -10,7 +10,7 @@
Style definitions for nice terminal layouts. Built with TUIs in mind.
![Lip Gloss example](https://stuff.charm.sh/lipgloss/lipgloss-example.png)
![Lip Gloss example](https://github.com/user-attachments/assets/99c5c015-551b-4897-8cd1-bcaafa0aad5a)
Lip Gloss takes an expressive, declarative approach to terminal rendering.
Users familiar with CSS will feel at home with Lip Gloss.
@ -77,11 +77,11 @@ appropriate color will be chosen at runtime.
### Complete Colors
CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color
CompleteColor specifies exact values for True Color, ANSI256, and ANSI color
profiles.
```go
lipgloss.CompleteColor{True: "#0000FF", ANSI256: "86", ANSI: "5"}
lipgloss.CompleteColor{TrueColor: "#0000FF", ANSI256: "86", ANSI: "5"}
```
Automatic color degradation will not be performed in this case and it will be
@ -89,7 +89,7 @@ based on the color specified.
### Complete Adaptive Colors
You can use CompleteColor with AdaptiveColor to specify the exact values for
You can use `CompleteColor` with `AdaptiveColor` to specify the exact values for
light and dark backgrounds without automatic color degradation.
```go
@ -402,7 +402,7 @@ block := lipgloss.Place(30, 80, lipgloss.Right, lipgloss.Bottom, fancyStyledPara
You can also style the whitespace. For details, see [the docs][docs].
### Rendering Tables
## Rendering Tables
Lip Gloss ships with a table rendering sub-package.
@ -455,114 +455,9 @@ fmt.Println(t)
For more on tables see [the docs](https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc) and [examples](https://github.com/charmbracelet/lipgloss/tree/master/examples/table).
## Rendering Trees
Lip Gloss ships with a tree rendering sub-package.
```go
import "github.com/charmbracelet/lipgloss/tree"
```
Define a new tree.
```go
t := tree.New("root", "child 1", "child 2", tree.New("child 3", "child 3.1"))
```
Print the tree.
```go
fmt.Println(t)
// root
// ├── child 1
// ├── child 2
// └── child 3
// └── child 3.1
```
### Customization
Trees can be customized via their enumeration function as well as using
`lipgloss.Style`s.
```go
style1 := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1)
style2 := lipgloss.NewStyle().Foreground(lipgloss.Color("10")).MarginRight(1)
t := tree.New().
Items(
"Glossier",
"Claires Boutique",
tree.New().
Root("Nyx").
Items("Qux", "Quux").
EnumeratorStyle(style2),
"Mac",
"Milk",
).
EnumeratorStyle(style1)
```
Print the tree:
<p align="center">
<img
width="600"
alt="Tree example"
src="https://github.com/charmbracelet/lipgloss/assets/245435/5a875269-f6d6-43fa-9916-5d8360e66964"
/>
</p>
You may also define custom enumerator implementations:
```go
t := tree.New().
Items(
"Glossier",
"Claires Boutique",
tree.New().
Root("Nyx").
Items(
"Qux",
"Quux",
),
"Mac",
"Milk",
).
Enumerator(func(tree.Data, int) (string, string) {
return "->", "->"
})
```
Print the tree.
<p align="center">
<img
width="600"
alt="Tree example"
src="https://github.com/charmbracelet/lipgloss/assets/245435/811e8b39-124f-48bb-b3dd-e015a65b1065"
/>
</p>
### Building
If you need, you can also build trees incrementally:
```go
t := tree.New("")
for i := 0; i < repeat; i++ {
t.Item("Lip Gloss")
}
```
## Rendering Lists
Lip Gloss ships with a list rendering sub-package.
Implementation-wise, lists are still trees.
The `list` package provides many common `Enumerator` implementations, as well as
some syntactic sugar.
```go
import "github.com/charmbracelet/lipgloss/list"
@ -584,15 +479,38 @@ fmt.Println(l)
// • C
```
Lists have the ability to nest.
### Customization
```go
l := list.New(
"A", list.New("Artichoke"),
"B", list.New("Baking Flour", "Bananas", "Barley", "Bean Sprouts"),
"C", list.New("Cashew Apple", "Cashews", "Coconut Milk", "Curry Paste", "Currywurst"),
"D", list.New("Dill", "Dragonfruit", "Dried Shrimp"),
"E", list.New("Eggs"),
"F", list.New("Fish Cake", "Furikake"),
"J", list.New("Jicama"),
"K", list.New("Kohlrabi"),
"L", list.New("Leeks", "Lentils", "Licorice Root"),
)
```
Print the list.
```go
fmt.Println(l)
```
<p align="center">
<img width="600" alt="image" src="https://github.com/charmbracelet/lipgloss/assets/42545625/0dc9f440-0748-4151-a3b0-7dcf29dfcdb0">
</p>
Lists can be customized via their enumeration function as well as using
`lipgloss.Style`s.
```go
enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1)
itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")).MarginRight(1)
itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")).MarginRight(1)
l := list.New(
"Glossier",
@ -609,44 +527,32 @@ l := list.New(
Print the list.
<p align="center">
<img
width="600"
alt="List example"
src="https://github.com/charmbracelet/lipgloss/assets/245435/8f5e5e0b-7bf9-4e3b-a8ba-3af10825320e"
/>
<img width="600" alt="List example" src="https://github.com/charmbracelet/lipgloss/assets/42545625/360494f1-57fb-4e13-bc19-0006efe01561">
</p>
In addition to the predefined enumerators (`Arabic`, `Alphabet`, `Roman`, `Bullet`, `Tree`),
you may also define your own custom enumerator:
```go
var DuckDuckGooseEnumerator Enumerator = func(l *List, i int) string {
if l.At(i) == "Goose" {
l := list.New("Duck", "Duck", "Duck", "Duck", "Goose", "Duck", "Duck")
func DuckDuckGooseEnumerator(l list.Items, i int) string {
if l.At(i).Value() == "Goose" {
return "Honk →"
}
return ""
}
```
Use it in a list:
```go
l := list.New("Duck", "Duck", "Duck", "Duck", "Goose", "Duck", "Duck")
l.Enumerator(DuckDuckGooseEnumerator)
l = l.Enumerator(DuckDuckGooseEnumerator)
```
Print the list:
<p align="center">
<img
width="600"
alt="image"
src="https://github.com/charmbracelet/lipgloss/assets/245435/44e37a5b-5124-4f49-a332-1756a355002e"
/>
<img width="600" alt="image" src="https://github.com/charmbracelet/lipgloss/assets/42545625/157aaf30-140d-4948-9bb4-dfba46e5b87e">
</p>
### Building
If you need, you can also build trees incrementally:
If you need, you can also build lists incrementally:
```go
l := list.New()
@ -655,6 +561,108 @@ for i := 0; i < repeat; i++ {
l.Item("Lip Gloss")
}
```
## Rendering Trees
Lip Gloss ships with a tree rendering sub-package.
```go
import "github.com/charmbracelet/lipgloss/tree"
```
Define a new tree.
```go
t := tree.Root(".").
Child("A", "B", "C")
```
Print the tree.
```go
fmt.Println(t)
// .
// ├── A
// ├── B
// └── C
```
Trees have the ability to nest.
```go
t := tree.Root(".").
Child("macOS").
Child(
tree.New().
Root("Linux").
Child("NixOS").
Child("Arch Linux (btw)").
Child("Void Linux"),
).
Child(
tree.New().
Root("BSD").
Child("FreeBSD").
Child("OpenBSD"),
)
```
Print the tree.
```go
fmt.Println(t)
```
<p align="center">
<img width="663" alt="Tree Example (simple)" src="https://github.com/user-attachments/assets/5ef14eb8-a5d4-4f94-8834-e15d1e714f89">
</p>
Trees can be customized via their enumeration function as well as using
`lipgloss.Style`s.
```go
enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1)
rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35"))
itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212"))
t := tree.
Root("⁜ Makeup").
Child(
"Glossier",
"Fenty Beauty",
tree.New().Child(
"Gloss Bomb Universal Lip Luminizer",
"Hot Cheeks Velour Blushlighter",
),
"Nyx",
"Mac",
"Milk",
).
Enumerator(tree.RoundedEnumerator).
EnumeratorStyle(enumeratorStyle).
RootStyle(rootStyle).
ItemStyle(itemStyle)
```
Print the tree.
<p align="center">
<img width="663" alt="Tree Example (makeup)" src="https://github.com/user-attachments/assets/06d12d87-744a-4c89-bd98-45de9094a97e">
</p>
The predefined enumerators for trees are `DefaultEnumerator` and `RoundedEnumerator`.
If you need, you can also build trees incrementally:
```go
t := tree.New()
for i := 0; i < repeat; i++ {
t.Child("Lip Gloss")
}
```
---
## FAQ
@ -726,6 +734,12 @@ the stylesheet-based Markdown renderer.
[glamour]: https://github.com/charmbracelet/glamour
## Contributing
See [contributing][contribute].
[contribute]: https://github.com/charmbracelet/lipgloss/contribute
## Feedback
Wed love to hear your thoughts on this project. Feel free to drop us a note!

View File

@ -306,10 +306,8 @@ func (s Style) Render(strs ...string) string {
if underline {
te = te.Underline()
}
if reverse {
if reverse {
teWhitespace = teWhitespace.Reverse()
}
te = te.Reverse()
}
if blink {
@ -355,6 +353,8 @@ func (s Style) Render(strs ...string) string {
// Potentially convert tabs to spaces
str = s.maybeConvertTabs(str)
// carriage returns can cause strange behaviour when rendering.
str = strings.ReplaceAll(str, "\r\n", "\n")
// Strip newlines in single line mode
if inline {
@ -564,14 +564,14 @@ func pad(str string, n int, style *termenv.Style) string {
return b.String()
}
func max(a, b int) int { //nolint:unparam
func max(a, b int) int { //nolint:unparam,predeclared
if a > b {
return a
}
return b
}
func min(a, b int) int {
func min(a, b int) int { //nolint:predeclared
if a < b {
return a
}

View File

@ -7,6 +7,10 @@ import (
"github.com/charmbracelet/x/ansi"
)
// HeaderRow denotes the header's row index used when rendering headers. Use
// this value when looking to customize header styles in StyleFunc.
const HeaderRow int = -1
// StyleFunc is the style function that determines the style of a Cell.
//
// It takes the row and column of the cell as an input and determines the
@ -55,6 +59,7 @@ type Table struct {
width int
height int
useManualHeight bool
offset int
// widths tracks the width of each column.
@ -84,7 +89,7 @@ func New() *Table {
// ClearRows clears the table rows.
func (t *Table) ClearRows() *Table {
t.data = nil
t.data = NewStringData()
return t
}
@ -199,6 +204,7 @@ func (t *Table) Width(w int) *Table {
// Height sets the table height.
func (t *Table) Height(h int) *Table {
t.height = h
t.useManualHeight = true
return t
}
@ -210,15 +216,13 @@ func (t *Table) Offset(o int) *Table {
// String returns the table as a string.
func (t *Table) String() string {
hasHeaders := t.headers != nil && len(t.headers) > 0
hasHeaders := len(t.headers) > 0
hasRows := t.data != nil && t.data.Rows() > 0
if !hasHeaders && !hasRows {
return ""
}
var s strings.Builder
// Add empty cells to the headers, until it's the same length as the longest
// row (only if there are at headers in the first place).
if hasHeaders {
@ -235,15 +239,15 @@ func (t *Table) String() string {
// the StyleFunc after the headers and rows. Update the widths for a final
// time.
for i, cell := range t.headers {
t.widths[i] = max(t.widths[i], lipgloss.Width(t.style(0, i).Render(cell)))
t.heights[0] = max(t.heights[0], lipgloss.Height(t.style(0, i).Render(cell)))
t.widths[i] = max(t.widths[i], lipgloss.Width(t.style(HeaderRow, i).Render(cell)))
t.heights[0] = max(t.heights[0], lipgloss.Height(t.style(HeaderRow, i).Render(cell)))
}
for r := 0; r < t.data.Rows(); r++ {
for i := 0; i < t.data.Columns(); i++ {
cell := t.data.At(r, i)
rendered := t.style(r+1, i).Render(cell)
rendered := t.style(r, i).Render(cell)
t.heights[r+btoi(hasHeaders)] = max(t.heights[r+btoi(hasHeaders)], lipgloss.Height(rendered))
t.widths[i] = max(t.widths[i], lipgloss.Width(rendered))
}
@ -342,27 +346,51 @@ func (t *Table) String() string {
}
}
var sb strings.Builder
if t.borderTop {
s.WriteString(t.constructTopBorder())
s.WriteString("\n")
sb.WriteString(t.constructTopBorder())
sb.WriteString("\n")
}
if hasHeaders {
s.WriteString(t.constructHeaders())
s.WriteString("\n")
}
for r := t.offset; r < t.data.Rows(); r++ {
s.WriteString(t.constructRow(r))
sb.WriteString(t.constructHeaders())
sb.WriteString("\n")
}
var bottom string
if t.borderBottom {
s.WriteString(t.constructBottomBorder())
bottom = t.constructBottomBorder()
}
// If there are no data rows render nothing.
if t.data.Rows() > 0 {
switch {
case t.useManualHeight:
// The height of the top border. Subtract 1 for the newline.
topHeight := lipgloss.Height(sb.String()) - 1
availableLines := t.height - (topHeight + lipgloss.Height(bottom))
// if the height is larger than the number of rows, use the number
// of rows.
if availableLines > t.data.Rows() {
availableLines = t.data.Rows()
}
sb.WriteString(t.constructRows(availableLines))
default:
for r := t.offset; r < t.data.Rows(); r++ {
sb.WriteString(t.constructRow(r, false))
}
}
}
sb.WriteString(bottom)
return lipgloss.NewStyle().
MaxHeight(t.computeHeight()).
MaxWidth(t.width).Render(s.String())
MaxWidth(t.width).
Render(sb.String())
}
// computeWidth computes the width of the table in it's current configuration.
@ -376,7 +404,7 @@ func (t *Table) computeWidth() int {
// computeHeight computes the height of the table in it's current configuration.
func (t *Table) computeHeight() int {
hasHeaders := t.headers != nil && len(t.headers) > 0
hasHeaders := len(t.headers) > 0
return sum(t.heights) - 1 + btoi(hasHeaders) +
btoi(t.borderTop) + btoi(t.borderBottom) +
btoi(t.borderHeader) + t.data.Rows()*btoi(t.borderRow)
@ -433,7 +461,7 @@ func (t *Table) constructHeaders() string {
s.WriteString(t.borderStyle.Render(t.border.Left))
}
for i, header := range t.headers {
s.WriteString(t.style(0, i).
s.WriteString(t.style(HeaderRow, i).
MaxHeight(1).
Width(t.widths[i]).
MaxWidth(t.widths[i]).
@ -466,13 +494,49 @@ func (t *Table) constructHeaders() string {
return s.String()
}
func (t *Table) constructRows(availableLines int) string {
var sb strings.Builder
// The number of rows to render after removing the offset.
offsetRowCount := t.data.Rows() - t.offset
// The number of rows to render. We always render at least one row.
rowsToRender := availableLines
rowsToRender = max(rowsToRender, 1)
// Check if we need to render an overflow row.
needsOverflow := rowsToRender < offsetRowCount
// only use the offset as the starting value if there is overflow.
rowIdx := t.offset
if !needsOverflow {
// if there is no overflow, just render to the height of the table
// check there's enough content to fill the table
rowIdx = t.data.Rows() - rowsToRender
}
for rowsToRender > 0 && rowIdx < t.data.Rows() {
// Whenever the height is too small to render all rows, the bottom row will be an overflow row (ellipsis).
isOverflow := needsOverflow && rowsToRender == 1
sb.WriteString(t.constructRow(rowIdx, isOverflow))
rowIdx++
rowsToRender--
}
return sb.String()
}
// constructRow constructs the row for the table given an index and row data
// based on the current configuration.
func (t *Table) constructRow(index int) string {
// based on the current configuration. If isOverflow is true, the row is
// rendered as an overflow row (using ellipsis).
func (t *Table) constructRow(index int, isOverflow bool) string {
var s strings.Builder
hasHeaders := t.headers != nil && len(t.headers) > 0
hasHeaders := len(t.headers) > 0
height := t.heights[index+btoi(hasHeaders)]
if isOverflow {
height = 1
}
var cells []string
left := strings.Repeat(t.borderStyle.Render(t.border.Left)+"\n", height)
@ -481,14 +545,21 @@ func (t *Table) constructRow(index int) string {
}
for c := 0; c < t.data.Columns(); c++ {
cell := t.data.At(index, c)
cellWidth := t.widths[c]
cells = append(cells, t.style(index+1, c).
Height(height).
cell := "…"
if !isOverflow {
cell = t.data.At(index, c)
}
cellStyle := t.style(index, c)
cells = append(cells, cellStyle.
// Account for the margins in the cell sizing.
Height(height-cellStyle.GetVerticalMargins()).
MaxHeight(height).
Width(t.widths[c]).
Width(t.widths[c]-cellStyle.GetHorizontalMargins()).
MaxWidth(t.widths[c]).
Render(ansi.Truncate(cell, t.widths[c]*height, "…")))
Render(ansi.Truncate(cell, cellWidth*height, "…")))
if c < t.data.Columns()-1 && t.borderColumn {
cells = append(cells, left)

View File

@ -13,7 +13,7 @@ func btoi(b bool) int {
}
// max returns the greater of two integers.
func max(a, b int) int {
func max(a, b int) int { //nolint:predeclared
if a > b {
return a
}
@ -21,7 +21,7 @@ func max(a, b int) int {
}
// min returns the greater of two integers.
func min(a, b int) int {
func min(a, b int) int { //nolint:predeclared
if a < b {
return a
}

View File

@ -1,9 +1,73 @@
package ansi
import (
"fmt"
"image/color"
)
// Colorizer is a [color.Color] interface that can be formatted as a string.
type Colorizer interface {
color.Color
fmt.Stringer
}
// HexColorizer is a [color.Color] that can be formatted as a hex string.
type HexColorizer struct{ color.Color }
var _ Colorizer = HexColorizer{}
// String returns the color as a hex string. If the color is nil, an empty
// string is returned.
func (h HexColorizer) String() string {
if h.Color == nil {
return ""
}
r, g, b, _ := h.RGBA()
// Get the lower 8 bits
r &= 0xff
g &= 0xff
b &= 0xff
return fmt.Sprintf("#%02x%02x%02x", uint8(r), uint8(g), uint8(b)) //nolint:gosec
}
// XRGBColorizer is a [color.Color] that can be formatted as an XParseColor
// rgb: string.
//
// See: https://linux.die.net/man/3/xparsecolor
type XRGBColorizer struct{ color.Color }
var _ Colorizer = XRGBColorizer{}
// String returns the color as an XParseColor rgb: string. If the color is nil,
// an empty string is returned.
func (x XRGBColorizer) String() string {
if x.Color == nil {
return ""
}
r, g, b, _ := x.RGBA()
// Get the lower 8 bits
return fmt.Sprintf("rgb:%04x/%04x/%04x", r, g, b)
}
// XRGBAColorizer is a [color.Color] that can be formatted as an XParseColor
// rgba: string.
//
// See: https://linux.die.net/man/3/xparsecolor
type XRGBAColorizer struct{ color.Color }
var _ Colorizer = XRGBAColorizer{}
// String returns the color as an XParseColor rgba: string. If the color is nil,
// an empty string is returned.
func (x XRGBAColorizer) String() string {
if x.Color == nil {
return ""
}
r, g, b, a := x.RGBA()
// Get the lower 8 bits
return fmt.Sprintf("rgba:%04x/%04x/%04x/%04x", r, g, b, a)
}
// SetForegroundColor returns a sequence that sets the default terminal
// foreground color.
//
@ -14,7 +78,16 @@ import (
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
func SetForegroundColor(c color.Color) string {
return "\x1b]10;" + colorToHexString(c) + "\x07"
var s string
switch c := c.(type) {
case Colorizer:
s = c.String()
case fmt.Stringer:
s = c.String()
default:
s = HexColorizer{c}.String()
}
return "\x1b]10;" + s + "\x07"
}
// RequestForegroundColor is a sequence that requests the current default
@ -23,6 +96,12 @@ func SetForegroundColor(c color.Color) string {
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
const RequestForegroundColor = "\x1b]10;?\x07"
// ResetForegroundColor is a sequence that resets the default terminal
// foreground color.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
const ResetForegroundColor = "\x1b]110\x07"
// SetBackgroundColor returns a sequence that sets the default terminal
// background color.
//
@ -33,7 +112,16 @@ const RequestForegroundColor = "\x1b]10;?\x07"
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
func SetBackgroundColor(c color.Color) string {
return "\x1b]11;" + colorToHexString(c) + "\x07"
var s string
switch c := c.(type) {
case Colorizer:
s = c.String()
case fmt.Stringer:
s = c.String()
default:
s = HexColorizer{c}.String()
}
return "\x1b]11;" + s + "\x07"
}
// RequestBackgroundColor is a sequence that requests the current default
@ -42,6 +130,12 @@ func SetBackgroundColor(c color.Color) string {
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
const RequestBackgroundColor = "\x1b]11;?\x07"
// ResetBackgroundColor is a sequence that resets the default terminal
// background color.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
const ResetBackgroundColor = "\x1b]111\x07"
// SetCursorColor returns a sequence that sets the terminal cursor color.
//
// OSC 12 ; color ST
@ -51,7 +145,16 @@ const RequestBackgroundColor = "\x1b]11;?\x07"
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
func SetCursorColor(c color.Color) string {
return "\x1b]12;" + colorToHexString(c) + "\x07"
var s string
switch c := c.(type) {
case Colorizer:
s = c.String()
case fmt.Stringer:
s = c.String()
default:
s = HexColorizer{c}.String()
}
return "\x1b]12;" + s + "\x07"
}
// RequestCursorColor is a sequence that requests the current terminal cursor
@ -59,3 +162,8 @@ func SetCursorColor(c color.Color) string {
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
const RequestCursorColor = "\x1b]12;?\x07"
// ResetCursorColor is a sequence that resets the terminal cursor color.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
const ResetCursorColor = "\x1b]112\x07"

View File

@ -69,4 +69,11 @@ const (
RS = 0x1E
// US is the unit separator character (Caret: ^_).
US = 0x1F
// LS0 is the locking shift 0 character.
// This is an alias for [SI].
LS0 = SI
// LS1 is the locking shift 1 character.
// This is an alias for [SO].
LS1 = SO
)

55
vendor/github.com/charmbracelet/x/ansi/charset.go generated vendored Normal file
View File

@ -0,0 +1,55 @@
package ansi
// SelectCharacterSet sets the G-set character designator to the specified
// character set.
//
// ESC Ps Pd
//
// Where Ps is the G-set character designator, and Pd is the identifier.
// For 94-character sets, the designator can be one of:
// - ( G0
// - ) G1
// - * G2
// - + G3
//
// For 96-character sets, the designator can be one of:
// - - G1
// - . G2
// - / G3
//
// Some common 94-character sets are:
// - 0 DEC Special Drawing Set
// - A United Kingdom (UK)
// - B United States (USASCII)
//
// Examples:
//
// ESC ( B Select character set G0 = United States (USASCII)
// ESC ( 0 Select character set G0 = Special Character and Line Drawing Set
// ESC ) 0 Select character set G1 = Special Character and Line Drawing Set
// ESC * A Select character set G2 = United Kingdom (UK)
//
// See: https://vt100.net/docs/vt510-rm/SCS.html
func SelectCharacterSet(gset byte, charset byte) string {
return "\x1b" + string(gset) + string(charset)
}
// SCS is an alias for SelectCharacterSet.
func SCS(gset byte, charset byte) string {
return SelectCharacterSet(gset, charset)
}
// Locking Shift 1 Right (LS1R) shifts G1 into GR character set.
const LS1R = "\x1b~"
// Locking Shift 2 (LS2) shifts G2 into GL character set.
const LS2 = "\x1bn"
// Locking Shift 2 Right (LS2R) shifts G2 into GR character set.
const LS2R = "\x1b}"
// Locking Shift 3 (LS3) shifts G3 into GL character set.
const LS3 = "\x1bo"
// Locking Shift 3 Right (LS3R) shifts G3 into GR character set.
const LS3R = "\x1b|"

View File

@ -3,8 +3,6 @@ package ansi
import (
"bytes"
"strconv"
"github.com/charmbracelet/x/ansi/parser"
)
// CsiSequence represents a control sequence introducer (CSI) sequence.
@ -23,7 +21,7 @@ type CsiSequence struct {
// This is a slice of integers, where each integer is a 32-bit integer
// containing the parameter value in the lower 31 bits and a flag in the
// most significant bit indicating whether there are more sub-parameters.
Params []int
Params []Parameter
// Cmd contains the raw command of the sequence.
// The command is a 32-bit integer containing the CSI command byte in the
@ -35,17 +33,25 @@ type CsiSequence struct {
// Is represented as:
//
// 'u' | '?' << 8
Cmd int
Cmd Command
}
var _ Sequence = CsiSequence{}
// Clone returns a deep copy of the CSI sequence.
func (s CsiSequence) Clone() Sequence {
return CsiSequence{
Params: append([]Parameter(nil), s.Params...),
Cmd: s.Cmd,
}
}
// Marker returns the marker byte of the CSI sequence.
// This is always gonna be one of the following '<' '=' '>' '?' and in the
// range of 0x3C-0x3F.
// Zero is returned if the sequence does not have a marker.
func (s CsiSequence) Marker() int {
return parser.Marker(s.Cmd)
return s.Cmd.Marker()
}
// Intermediate returns the intermediate byte of the CSI sequence.
@ -54,51 +60,22 @@ func (s CsiSequence) Marker() int {
// ',', '-', '.', '/'.
// Zero is returned if the sequence does not have an intermediate byte.
func (s CsiSequence) Intermediate() int {
return parser.Intermediate(s.Cmd)
return s.Cmd.Intermediate()
}
// Command returns the command byte of the CSI sequence.
func (s CsiSequence) Command() int {
return parser.Command(s.Cmd)
return s.Cmd.Command()
}
// Param returns the parameter at the given index.
// It returns -1 if the parameter does not exist.
func (s CsiSequence) Param(i int) int {
return parser.Param(s.Params, i)
}
// HasMore returns true if the parameter has more sub-parameters.
func (s CsiSequence) HasMore(i int) bool {
return parser.HasMore(s.Params, i)
}
// Subparams returns the sub-parameters of the given parameter.
// It returns nil if the parameter does not exist.
func (s CsiSequence) Subparams(i int) []int {
return parser.Subparams(s.Params, i)
}
// Len returns the number of parameters in the sequence.
// This will return the number of parameters in the sequence, excluding any
// sub-parameters.
func (s CsiSequence) Len() int {
return parser.Len(s.Params)
}
// Range iterates over the parameters of the sequence and calls the given
// function for each parameter.
// The function should return false to stop the iteration.
func (s CsiSequence) Range(fn func(i int, param int, hasMore bool) bool) {
parser.Range(s.Params, fn)
}
// Clone returns a copy of the CSI sequence.
func (s CsiSequence) Clone() Sequence {
return CsiSequence{
Params: append([]int(nil), s.Params...),
Cmd: s.Cmd,
// Param is a helper that returns the parameter at the given index and falls
// back to the default value if the parameter is missing. If the index is out
// of bounds, it returns the default value and false.
func (s CsiSequence) Param(i, def int) (int, bool) {
if i < 0 || i >= len(s.Params) {
return def, false
}
return s.Params[i].Param(def), true
}
// String returns a string representation of the sequence.
@ -114,23 +91,25 @@ func (s CsiSequence) buffer() *bytes.Buffer {
if m := s.Marker(); m != 0 {
b.WriteByte(byte(m))
}
s.Range(func(i, param int, hasMore bool) bool {
for i, p := range s.Params {
param := p.Param(-1)
if param >= 0 {
b.WriteString(strconv.Itoa(param))
}
if i < len(s.Params)-1 {
if hasMore {
if p.HasMore() {
b.WriteByte(':')
} else {
b.WriteByte(';')
}
}
return true
})
}
if i := s.Intermediate(); i != 0 {
b.WriteByte(byte(i))
}
b.WriteByte(byte(s.Command()))
if cmd := s.Command(); cmd != 0 {
b.WriteByte(byte(cmd))
}
return &b
}

View File

@ -1,12 +1,61 @@
package ansi
import (
"strconv"
"strings"
)
// RequestNameVersion (XTVERSION) is a control sequence that requests the
// terminal's name and version. It responds with a DSR sequence identifying the
// terminal.
//
// CSI > 0 q
// DCS > | text ST
//
// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-PC-Style-Function-Keys
const (
RequestNameVersion = "\x1b[>0q"
XTVERSION = RequestNameVersion
)
// RequestXTVersion is a control sequence that requests the terminal's XTVERSION. It responds with a DSR sequence identifying the version.
//
// CSI > Ps q
// DCS > | text ST
//
// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-PC-Style-Function-Keys
const RequestXTVersion = "\x1b[>0q"
// Deprecated: use [RequestNameVersion] instead.
const RequestXTVersion = RequestNameVersion
// PrimaryDeviceAttributes (DA1) is a control sequence that reports the
// terminal's primary device attributes.
//
// CSI c
// CSI 0 c
// CSI ? Ps ; ... c
//
// If no attributes are given, or if the attribute is 0, this function returns
// the request sequence. Otherwise, it returns the response sequence.
//
// See https://vt100.net/docs/vt510-rm/DA1.html
func PrimaryDeviceAttributes(attrs ...int) string {
if len(attrs) == 0 {
return "\x1b[c"
} else if len(attrs) == 1 && attrs[0] == 0 {
return "\x1b[0c"
}
as := make([]string, len(attrs))
for i, a := range attrs {
as[i] = strconv.Itoa(a)
}
return "\x1b[?" + strings.Join(as, ";") + "c"
}
// DA1 is an alias for [PrimaryDeviceAttributes].
func DA1(attrs ...int) string {
return PrimaryDeviceAttributes(attrs...)
}
// RequestPrimaryDeviceAttributes is a control sequence that requests the
// terminal's primary device attributes (DA1).
@ -15,3 +64,57 @@ const RequestXTVersion = "\x1b[>0q"
//
// See https://vt100.net/docs/vt510-rm/DA1.html
const RequestPrimaryDeviceAttributes = "\x1b[c"
// SecondaryDeviceAttributes (DA2) is a control sequence that reports the
// terminal's secondary device attributes.
//
// CSI > c
// CSI > 0 c
// CSI > Ps ; ... c
//
// See https://vt100.net/docs/vt510-rm/DA2.html
func SecondaryDeviceAttributes(attrs ...int) string {
if len(attrs) == 0 {
return "\x1b[>c"
}
as := make([]string, len(attrs))
for i, a := range attrs {
as[i] = strconv.Itoa(a)
}
return "\x1b[>" + strings.Join(as, ";") + "c"
}
// DA2 is an alias for [SecondaryDeviceAttributes].
func DA2(attrs ...int) string {
return SecondaryDeviceAttributes(attrs...)
}
// TertiaryDeviceAttributes (DA3) is a control sequence that reports the
// terminal's tertiary device attributes.
//
// CSI = c
// CSI = 0 c
// DCS ! | Text ST
//
// Where Text is the unit ID for the terminal.
//
// If no unit ID is given, or if the unit ID is 0, this function returns the
// request sequence. Otherwise, it returns the response sequence.
//
// See https://vt100.net/docs/vt510-rm/DA3.html
func TertiaryDeviceAttributes(unitID string) string {
switch unitID {
case "":
return "\x1b[=c"
case "0":
return "\x1b[=0c"
}
return "\x1bP!|" + unitID + "\x1b\\"
}
// DA3 is an alias for [TertiaryDeviceAttributes].
func DA3(unitID string) string {
return TertiaryDeviceAttributes(unitID)
}

View File

@ -8,7 +8,10 @@ import "strconv"
// ESC 7
//
// See: https://vt100.net/docs/vt510-rm/DECSC.html
const SaveCursor = "\x1b7"
const (
SaveCursor = "\x1b7"
DECSC = SaveCursor
)
// RestoreCursor (DECRC) is an escape sequence that restores the cursor
// position.
@ -16,10 +19,13 @@ const SaveCursor = "\x1b7"
// ESC 8
//
// See: https://vt100.net/docs/vt510-rm/DECRC.html
const RestoreCursor = "\x1b8"
const (
RestoreCursor = "\x1b8"
DECRC = RestoreCursor
)
// RequestCursorPosition (CPR) is an escape sequence that requests the current
// cursor position.
// RequestCursorPosition is an escape sequence that requests the current cursor
// position.
//
// CSI 6 n
//
@ -60,9 +66,18 @@ func CursorUp(n int) string {
return "\x1b[" + s + "A"
}
// CUU is an alias for [CursorUp].
func CUU(n int) string {
return CursorUp(n)
}
// CUU1 is a sequence for moving the cursor up one cell.
const CUU1 = "\x1b[A"
// CursorUp1 is a sequence for moving the cursor up one cell.
//
// This is equivalent to CursorUp(1).
// Deprecated: use [CUU1] instead.
const CursorUp1 = "\x1b[A"
// CursorDown (CUD) returns a sequence for moving the cursor down n cells.
@ -78,17 +93,26 @@ func CursorDown(n int) string {
return "\x1b[" + s + "B"
}
// CUD is an alias for [CursorDown].
func CUD(n int) string {
return CursorDown(n)
}
// CUD1 is a sequence for moving the cursor down one cell.
const CUD1 = "\x1b[B"
// CursorDown1 is a sequence for moving the cursor down one cell.
//
// This is equivalent to CursorDown(1).
// Deprecated: use [CUD1] instead.
const CursorDown1 = "\x1b[B"
// CursorRight (CUF) returns a sequence for moving the cursor right n cells.
// CursorForward (CUF) returns a sequence for moving the cursor right n cells.
//
// CSI n C
// # CSI n C
//
// See: https://vt100.net/docs/vt510-rm/CUF.html
func CursorRight(n int) string {
func CursorForward(n int) string {
var s string
if n > 1 {
s = strconv.Itoa(n)
@ -96,17 +120,36 @@ func CursorRight(n int) string {
return "\x1b[" + s + "C"
}
// CUF is an alias for [CursorForward].
func CUF(n int) string {
return CursorForward(n)
}
// CUF1 is a sequence for moving the cursor right one cell.
const CUF1 = "\x1b[C"
// CursorRight (CUF) returns a sequence for moving the cursor right n cells.
//
// CSI n C
//
// See: https://vt100.net/docs/vt510-rm/CUF.html
// Deprecated: use [CursorForward] instead.
func CursorRight(n int) string {
return CursorForward(n)
}
// CursorRight1 is a sequence for moving the cursor right one cell.
//
// This is equivalent to CursorRight(1).
const CursorRight1 = "\x1b[C"
// Deprecated: use [CUF1] instead.
const CursorRight1 = CUF1
// CursorLeft (CUB) returns a sequence for moving the cursor left n cells.
// CursorBackward (CUB) returns a sequence for moving the cursor left n cells.
//
// CSI n D
// # CSI n D
//
// See: https://vt100.net/docs/vt510-rm/CUB.html
func CursorLeft(n int) string {
func CursorBackward(n int) string {
var s string
if n > 1 {
s = strconv.Itoa(n)
@ -114,10 +157,29 @@ func CursorLeft(n int) string {
return "\x1b[" + s + "D"
}
// CUB is an alias for [CursorBackward].
func CUB(n int) string {
return CursorBackward(n)
}
// CUB1 is a sequence for moving the cursor left one cell.
const CUB1 = "\x1b[D"
// CursorLeft (CUB) returns a sequence for moving the cursor left n cells.
//
// CSI n D
//
// See: https://vt100.net/docs/vt510-rm/CUB.html
// Deprecated: use [CursorBackward] instead.
func CursorLeft(n int) string {
return CursorBackward(n)
}
// CursorLeft1 is a sequence for moving the cursor left one cell.
//
// This is equivalent to CursorLeft(1).
const CursorLeft1 = "\x1b[D"
// Deprecated: use [CUB1] instead.
const CursorLeft1 = CUB1
// CursorNextLine (CNL) returns a sequence for moving the cursor to the
// beginning of the next line n times.
@ -133,6 +195,11 @@ func CursorNextLine(n int) string {
return "\x1b[" + s + "E"
}
// CNL is an alias for [CursorNextLine].
func CNL(n int) string {
return CursorNextLine(n)
}
// CursorPreviousLine (CPL) returns a sequence for moving the cursor to the
// beginning of the previous line n times.
//
@ -147,25 +214,264 @@ func CursorPreviousLine(n int) string {
return "\x1b[" + s + "F"
}
// MoveCursor (CUP) returns a sequence for moving the cursor to the given row
// and column.
// CPL is an alias for [CursorPreviousLine].
func CPL(n int) string {
return CursorPreviousLine(n)
}
// CursorHorizontalAbsolute (CHA) returns a sequence for moving the cursor to
// the given column.
//
// Default is 1.
//
// CSI n G
//
// See: https://vt100.net/docs/vt510-rm/CHA.html
func CursorHorizontalAbsolute(col int) string {
var s string
if col > 0 {
s = strconv.Itoa(col)
}
return "\x1b[" + s + "G"
}
// CHA is an alias for [CursorHorizontalAbsolute].
func CHA(col int) string {
return CursorHorizontalAbsolute(col)
}
// CursorPosition (CUP) returns a sequence for setting the cursor to the
// given row and column.
//
// Default is 1,1.
//
// CSI n ; m H
//
// See: https://vt100.net/docs/vt510-rm/CUP.html
func MoveCursor(row, col int) string {
if row < 0 {
row = 0
}
if col < 0 {
col = 0
}
return "\x1b[" + strconv.Itoa(row) + ";" + strconv.Itoa(col) + "H"
func CursorPosition(col, row int) string {
if row <= 0 && col <= 0 {
return HomeCursorPosition
}
var r, c string
if row > 0 {
r = strconv.Itoa(row)
}
if col > 0 {
c = strconv.Itoa(col)
}
return "\x1b[" + r + ";" + c + "H"
}
// CUP is an alias for [CursorPosition].
func CUP(col, row int) string {
return CursorPosition(col, row)
}
// CursorHomePosition is a sequence for moving the cursor to the upper left
// corner of the scrolling region. This is equivalent to `CursorPosition(1, 1)`.
const CursorHomePosition = "\x1b[H"
// SetCursorPosition (CUP) returns a sequence for setting the cursor to the
// given row and column.
//
// CSI n ; m H
//
// See: https://vt100.net/docs/vt510-rm/CUP.html
// Deprecated: use [CursorPosition] instead.
func SetCursorPosition(col, row int) string {
if row <= 0 && col <= 0 {
return HomeCursorPosition
}
var r, c string
if row > 0 {
r = strconv.Itoa(row)
}
if col > 0 {
c = strconv.Itoa(col)
}
return "\x1b[" + r + ";" + c + "H"
}
// HomeCursorPosition is a sequence for moving the cursor to the upper left
// corner of the scrolling region. This is equivalent to `SetCursorPosition(1, 1)`.
// Deprecated: use [CursorHomePosition] instead.
const HomeCursorPosition = CursorHomePosition
// MoveCursor (CUP) returns a sequence for setting the cursor to the
// given row and column.
//
// CSI n ; m H
//
// See: https://vt100.net/docs/vt510-rm/CUP.html
//
// Deprecated: use [CursorPosition] instead.
func MoveCursor(col, row int) string {
return SetCursorPosition(col, row)
}
// CursorOrigin is a sequence for moving the cursor to the upper left corner of
// the display. This is equivalent to `SetCursorPosition(1, 1)`.
//
// Deprecated: use [CursorHomePosition] instead.
const CursorOrigin = "\x1b[1;1H"
// MoveCursorOrigin is a sequence for moving the cursor to the upper left
// corner of the screen. This is equivalent to MoveCursor(1, 1).
const MoveCursorOrigin = "\x1b[1;1H"
// corner of the display. This is equivalent to `SetCursorPosition(1, 1)`.
//
// Deprecated: use [CursorHomePosition] instead.
const MoveCursorOrigin = CursorOrigin
// CursorHorizontalForwardTab (CHT) returns a sequence for moving the cursor to
// the next tab stop n times.
//
// Default is 1.
//
// CSI n I
//
// See: https://vt100.net/docs/vt510-rm/CHT.html
func CursorHorizontalForwardTab(n int) string {
var s string
if n > 1 {
s = strconv.Itoa(n)
}
return "\x1b[" + s + "I"
}
// CHT is an alias for [CursorHorizontalForwardTab].
func CHT(n int) string {
return CursorHorizontalForwardTab(n)
}
// EraseCharacter (ECH) returns a sequence for erasing n characters and moving
// the cursor to the right. This doesn't affect other cell attributes.
//
// Default is 1.
//
// CSI n X
//
// See: https://vt100.net/docs/vt510-rm/ECH.html
func EraseCharacter(n int) string {
var s string
if n > 1 {
s = strconv.Itoa(n)
}
return "\x1b[" + s + "X"
}
// ECH is an alias for [EraseCharacter].
func ECH(n int) string {
return EraseCharacter(n)
}
// CursorBackwardTab (CBT) returns a sequence for moving the cursor to the
// previous tab stop n times.
//
// Default is 1.
//
// CSI n Z
//
// See: https://vt100.net/docs/vt510-rm/CBT.html
func CursorBackwardTab(n int) string {
var s string
if n > 1 {
s = strconv.Itoa(n)
}
return "\x1b[" + s + "Z"
}
// CBT is an alias for [CursorBackwardTab].
func CBT(n int) string {
return CursorBackwardTab(n)
}
// VerticalPositionAbsolute (VPA) returns a sequence for moving the cursor to
// the given row.
//
// Default is 1.
//
// CSI n d
//
// See: https://vt100.net/docs/vt510-rm/VPA.html
func VerticalPositionAbsolute(row int) string {
var s string
if row > 0 {
s = strconv.Itoa(row)
}
return "\x1b[" + s + "d"
}
// VPA is an alias for [VerticalPositionAbsolute].
func VPA(row int) string {
return VerticalPositionAbsolute(row)
}
// VerticalPositionRelative (VPR) returns a sequence for moving the cursor down
// n rows relative to the current position.
//
// Default is 1.
//
// CSI n e
//
// See: https://vt100.net/docs/vt510-rm/VPR.html
func VerticalPositionRelative(n int) string {
var s string
if n > 1 {
s = strconv.Itoa(n)
}
return "\x1b[" + s + "e"
}
// VPR is an alias for [VerticalPositionRelative].
func VPR(n int) string {
return VerticalPositionRelative(n)
}
// HorizontalVerticalPosition (HVP) returns a sequence for moving the cursor to
// the given row and column.
//
// Default is 1,1.
//
// CSI n ; m f
//
// This has the same effect as [CursorPosition].
//
// See: https://vt100.net/docs/vt510-rm/HVP.html
func HorizontalVerticalPosition(col, row int) string {
var r, c string
if row > 0 {
r = strconv.Itoa(row)
}
if col > 0 {
c = strconv.Itoa(col)
}
return "\x1b[" + r + ";" + c + "f"
}
// HVP is an alias for [HorizontalVerticalPosition].
func HVP(col, row int) string {
return HorizontalVerticalPosition(col, row)
}
// HorizontalVerticalHomePosition is a sequence for moving the cursor to the
// upper left corner of the scrolling region. This is equivalent to
// `HorizontalVerticalPosition(1, 1)`.
const HorizontalVerticalHomePosition = "\x1b[f"
// SaveCurrentCursorPosition (SCOSC) is a sequence for saving the current cursor
// position for SCO console mode.
//
// CSI s
//
// This acts like [DECSC], except the page number where the cursor is located
// is not saved.
//
// See: https://vt100.net/docs/vt510-rm/SCOSC.html
const (
SaveCurrentCursorPosition = "\x1b[s"
SCOSC = SaveCurrentCursorPosition
)
// SaveCursorPosition (SCP or SCOSC) is a sequence for saving the cursor
// position.
@ -176,8 +482,23 @@ const MoveCursorOrigin = "\x1b[1;1H"
// not saved.
//
// See: https://vt100.net/docs/vt510-rm/SCOSC.html
// Deprecated: use [SaveCurrentCursorPosition] instead.
const SaveCursorPosition = "\x1b[s"
// RestoreCurrentCursorPosition (SCORC) is a sequence for restoring the current
// cursor position for SCO console mode.
//
// CSI u
//
// This acts like [DECRC], except the page number where the cursor was saved is
// not restored.
//
// See: https://vt100.net/docs/vt510-rm/SCORC.html
const (
RestoreCurrentCursorPosition = "\x1b[u"
SCORC = RestoreCurrentCursorPosition
)
// RestoreCursorPosition (RCP or SCORC) is a sequence for restoring the cursor
// position.
//
@ -187,4 +508,112 @@ const SaveCursorPosition = "\x1b[s"
// cursor was saved.
//
// See: https://vt100.net/docs/vt510-rm/SCORC.html
// Deprecated: use [RestoreCurrentCursorPosition] instead.
const RestoreCursorPosition = "\x1b[u"
// SetCursorStyle (DECSCUSR) returns a sequence for changing the cursor style.
//
// Default is 1.
//
// CSI Ps SP q
//
// Where Ps is the cursor style:
//
// 0: Blinking block
// 1: Blinking block (default)
// 2: Steady block
// 3: Blinking underline
// 4: Steady underline
// 5: Blinking bar (xterm)
// 6: Steady bar (xterm)
//
// See: https://vt100.net/docs/vt510-rm/DECSCUSR.html
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-Ps-SP-q.1D81
func SetCursorStyle(style int) string {
if style < 0 {
style = 0
}
return "\x1b[" + strconv.Itoa(style) + " q"
}
// DECSCUSR is an alias for [SetCursorStyle].
func DECSCUSR(style int) string {
return SetCursorStyle(style)
}
// SetPointerShape returns a sequence for changing the mouse pointer cursor
// shape. Use "default" for the default pointer shape.
//
// OSC 22 ; Pt ST
// OSC 22 ; Pt BEL
//
// Where Pt is the pointer shape name. The name can be anything that the
// operating system can understand. Some common names are:
//
// - copy
// - crosshair
// - default
// - ew-resize
// - n-resize
// - text
// - wait
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands
func SetPointerShape(shape string) string {
return "\x1b]22;" + shape + "\x07"
}
// ReverseIndex (RI) is an escape sequence for moving the cursor up one line in
// the same column. If the cursor is at the top margin, the screen scrolls
// down.
//
// This has the same effect as [RI].
const ReverseIndex = "\x1bM"
// HorizontalPositionAbsolute (HPA) returns a sequence for moving the cursor to
// the given column. This has the same effect as [CUP].
//
// Default is 1.
//
// CSI n `
//
// See: https://vt100.net/docs/vt510-rm/HPA.html
func HorizontalPositionAbsolute(col int) string {
var s string
if col > 0 {
s = strconv.Itoa(col)
}
return "\x1b[" + s + "`"
}
// HPA is an alias for [HorizontalPositionAbsolute].
func HPA(col int) string {
return HorizontalPositionAbsolute(col)
}
// HorizontalPositionRelative (HPR) returns a sequence for moving the cursor
// right n columns relative to the current position. This has the same effect
// as [CUP].
//
// Default is 1.
//
// CSI n a
//
// See: https://vt100.net/docs/vt510-rm/HPR.html
func HorizontalPositionRelative(n int) string {
var s string
if n > 0 {
s = strconv.Itoa(n)
}
return "\x1b[" + s + "a"
}
// HPR is an alias for [HorizontalPositionRelative].
func HPR(n int) string {
return HorizontalPositionRelative(n)
}
// Index (IND) is an escape sequence for moving the cursor down one line in the
// same column. If the cursor is at the bottom margin, the screen scrolls up.
// This has the same effect as [IND].
const Index = "\x1bD"

26
vendor/github.com/charmbracelet/x/ansi/cwd.go generated vendored Normal file
View File

@ -0,0 +1,26 @@
package ansi
import (
"net/url"
"path"
)
// NotifyWorkingDirectory returns a sequence that notifies the terminal
// of the current working directory.
//
// OSC 7 ; Pt BEL
//
// Where Pt is a URL in the format "file://[host]/[path]".
// Set host to "localhost" if this is a path on the local computer.
//
// See: https://wezfurlong.org/wezterm/shell-integration.html#osc-7-escape-sequence-to-set-the-working-directory
// See: https://iterm2.com/documentation-escape-codes.html#:~:text=RemoteHost%20and%20CurrentDir%3A-,OSC%207,-%3B%20%5BPs%5D%20ST
func NotifyWorkingDirectory(host string, paths ...string) string {
path := path.Join(paths...)
u := &url.URL{
Scheme: "file",
Host: host,
Path: path,
}
return "\x1b]7;" + u.String() + "\x07"
}

View File

@ -3,8 +3,7 @@ package ansi
import (
"bytes"
"strconv"
"github.com/charmbracelet/x/ansi/parser"
"strings"
)
// DcsSequence represents a Device Control String (DCS) escape sequence.
@ -22,7 +21,7 @@ type DcsSequence struct {
// This is a slice of integers, where each integer is a 32-bit integer
// containing the parameter value in the lower 31 bits and a flag in the
// most significant bit indicating whether there are more sub-parameters.
Params []int
Params []Parameter
// Data contains the string raw data of the sequence.
// This is the data between the final byte and the escape sequence terminator.
@ -38,17 +37,31 @@ type DcsSequence struct {
// Is represented as:
//
// 'r' | '>' << 8 | '$' << 16
Cmd int
Cmd Command
}
var _ Sequence = DcsSequence{}
// Clone returns a deep copy of the DCS sequence.
func (s DcsSequence) Clone() Sequence {
return DcsSequence{
Params: append([]Parameter(nil), s.Params...),
Data: append([]byte(nil), s.Data...),
Cmd: s.Cmd,
}
}
// Split returns a slice of data split by the semicolon.
func (s DcsSequence) Split() []string {
return strings.Split(string(s.Data), ";")
}
// Marker returns the marker byte of the DCS sequence.
// This is always gonna be one of the following '<' '=' '>' '?' and in the
// range of 0x3C-0x3F.
// Zero is returned if the sequence does not have a marker.
func (s DcsSequence) Marker() int {
return parser.Marker(s.Cmd)
return s.Cmd.Marker()
}
// Intermediate returns the intermediate byte of the DCS sequence.
@ -57,52 +70,22 @@ func (s DcsSequence) Marker() int {
// ',', '-', '.', '/'.
// Zero is returned if the sequence does not have an intermediate byte.
func (s DcsSequence) Intermediate() int {
return parser.Intermediate(s.Cmd)
return s.Cmd.Intermediate()
}
// Command returns the command byte of the CSI sequence.
func (s DcsSequence) Command() int {
return parser.Command(s.Cmd)
return s.Cmd.Command()
}
// Param returns the parameter at the given index.
// It returns -1 if the parameter does not exist.
func (s DcsSequence) Param(i int) int {
return parser.Param(s.Params, i)
}
// HasMore returns true if the parameter has more sub-parameters.
func (s DcsSequence) HasMore(i int) bool {
return parser.HasMore(s.Params, i)
}
// Subparams returns the sub-parameters of the given parameter.
// It returns nil if the parameter does not exist.
func (s DcsSequence) Subparams(i int) []int {
return parser.Subparams(s.Params, i)
}
// Len returns the number of parameters in the sequence.
// This will return the number of parameters in the sequence, excluding any
// sub-parameters.
func (s DcsSequence) Len() int {
return parser.Len(s.Params)
}
// Range iterates over the parameters of the sequence and calls the given
// function for each parameter.
// The function should return false to stop the iteration.
func (s DcsSequence) Range(fn func(i int, param int, hasMore bool) bool) {
parser.Range(s.Params, fn)
}
// Clone returns a copy of the DCS sequence.
func (s DcsSequence) Clone() Sequence {
return DcsSequence{
Params: append([]int(nil), s.Params...),
Data: append([]byte(nil), s.Data...),
Cmd: s.Cmd,
// Param is a helper that returns the parameter at the given index and falls
// back to the default value if the parameter is missing. If the index is out
// of bounds, it returns the default value and false.
func (s DcsSequence) Param(i, def int) (int, bool) {
if i < 0 || i >= len(s.Params) {
return def, false
}
return s.Params[i].Param(def), true
}
// String returns a string representation of the sequence.
@ -118,23 +101,25 @@ func (s DcsSequence) buffer() *bytes.Buffer {
if m := s.Marker(); m != 0 {
b.WriteByte(byte(m))
}
s.Range(func(i, param int, hasMore bool) bool {
if param >= -1 {
for i, p := range s.Params {
param := p.Param(-1)
if param >= 0 {
b.WriteString(strconv.Itoa(param))
}
if i < len(s.Params)-1 {
if hasMore {
if p.HasMore() {
b.WriteByte(':')
} else {
b.WriteByte(';')
}
}
return true
})
}
if i := s.Intermediate(); i != 0 {
b.WriteByte(byte(i))
}
b.WriteByte(byte(s.Command()))
if cmd := s.Command(); cmd != 0 {
b.WriteByte(byte(cmd))
}
b.Write(s.Data)
b.WriteByte(ESC)
b.WriteByte('\\')

9
vendor/github.com/charmbracelet/x/ansi/focus.go generated vendored Normal file
View File

@ -0,0 +1,9 @@
package ansi
// Focus is an escape sequence to notify the terminal that it has focus.
// This is used with [FocusEventMode].
const Focus = "\x1b[I"
// Blur is an escape sequence to notify the terminal that it has lost focus.
// This is used with [FocusEventMode].
const Blur = "\x1b[O"

28
vendor/github.com/charmbracelet/x/ansi/keypad.go generated vendored Normal file
View File

@ -0,0 +1,28 @@
package ansi
// Keypad Application Mode (DECKPAM) is a mode that determines whether the
// keypad sends application sequences or ANSI sequences.
//
// This works like enabling [DECNKM].
// Use [NumericKeypadMode] to set the numeric keypad mode.
//
// ESC =
//
// See: https://vt100.net/docs/vt510-rm/DECKPAM.html
const (
KeypadApplicationMode = "\x1b="
DECKPAM = KeypadApplicationMode
)
// Keypad Numeric Mode (DECKPNM) is a mode that determines whether the keypad
// sends application sequences or ANSI sequences.
//
// This works the same as disabling [DECNKM].
//
// ESC >
//
// See: https://vt100.net/docs/vt510-rm/DECKPNM.html
const (
KeypadNumericMode = "\x1b>"
DECKPNM = KeypadNumericMode
)

View File

@ -8,11 +8,11 @@ const (
KittyDisambiguateEscapeCodes = 1 << iota
KittyReportEventTypes
KittyReportAlternateKeys
KittyReportAllKeys
KittyReportAllKeysAsEscapeCodes
KittyReportAssociatedKeys
KittyAllFlags = KittyDisambiguateEscapeCodes | KittyReportEventTypes |
KittyReportAlternateKeys | KittyReportAllKeys | KittyReportAssociatedKeys
KittyReportAlternateKeys | KittyReportAllKeysAsEscapeCodes | KittyReportAssociatedKeys
)
// RequestKittyKeyboard is a sequence to request the terminal Kitty keyboard
@ -21,9 +21,41 @@ const (
// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
const RequestKittyKeyboard = "\x1b[?u"
// KittyKeyboard returns a sequence to request keyboard enhancements from the terminal.
// The flags argument is a bitmask of the Kitty keyboard protocol flags. While
// mode specifies how the flags should be interpreted.
//
// Possible values for flags mask:
//
// 1: Disambiguate escape codes
// 2: Report event types
// 4: Report alternate keys
// 8: Report all keys as escape codes
// 16: Report associated text
//
// Possible values for mode:
//
// 1: Set given flags and unset all others
// 2: Set given flags and keep existing flags unchanged
// 3: Unset given flags and keep existing flags unchanged
//
// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
func KittyKeyboard(flags, mode int) string {
return "\x1b[=" + strconv.Itoa(flags) + ";" + strconv.Itoa(mode) + "u"
}
// PushKittyKeyboard returns a sequence to push the given flags to the terminal
// Kitty Keyboard stack.
//
// Possible values for flags mask:
//
// 0: Disable all features
// 1: Disambiguate escape codes
// 2: Report event types
// 4: Report alternate keys
// 8: Report all keys as escape codes
// 16: Report associated text
//
// CSI > flags u
//
// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement

View File

@ -1,14 +1,163 @@
package ansi
// This file define uses multiple sequences to set (SM), reset (RM), and request
// (DECRQM) different ANSI and DEC modes.
import (
"strconv"
"strings"
)
// ModeSetting represents a mode setting.
type ModeSetting byte
// ModeSetting constants.
const (
ModeNotRecognized ModeSetting = iota
ModeSet
ModeReset
ModePermanentlySet
ModePermanentlyReset
)
// IsNotRecognized returns true if the mode is not recognized.
func (m ModeSetting) IsNotRecognized() bool {
return m == ModeNotRecognized
}
// IsSet returns true if the mode is set or permanently set.
func (m ModeSetting) IsSet() bool {
return m == ModeSet || m == ModePermanentlySet
}
// IsReset returns true if the mode is reset or permanently reset.
func (m ModeSetting) IsReset() bool {
return m == ModeReset || m == ModePermanentlyReset
}
// IsPermanentlySet returns true if the mode is permanently set.
func (m ModeSetting) IsPermanentlySet() bool {
return m == ModePermanentlySet
}
// IsPermanentlyReset returns true if the mode is permanently reset.
func (m ModeSetting) IsPermanentlyReset() bool {
return m == ModePermanentlyReset
}
// Mode represents an interface for terminal modes.
// Modes can be set, reset, and requested.
type Mode interface {
Mode() int
}
// SetMode (SM) returns a sequence to set a mode.
// The mode arguments are a list of modes to set.
//
// If one of the modes is a [DECMode], the sequence will use the DEC format.
//
// ANSI format:
//
// CSI Pd ; ... ; Pd h
//
// DEC format:
//
// CSI ? Pd ; ... ; Pd h
//
// See: https://vt100.net/docs/vt510-rm/SM.html
// See: https://vt100.net/docs/vt510-rm/RM.html
// See: https://vt100.net/docs/vt510-rm/DECRQM.html
func SetMode(modes ...Mode) string {
return setMode(false, modes...)
}
// SM is an alias for [SetMode].
func SM(modes ...Mode) string {
return SetMode(modes...)
}
// ResetMode (RM) returns a sequence to reset a mode.
// The mode arguments are a list of modes to reset.
//
// The terminal then responds to the request with a Report Mode function
// (DECRPM) in the format:
// If one of the modes is a [DECMode], the sequence will use the DEC format.
//
// ANSI format:
//
// CSI Pd ; ... ; Pd l
//
// DEC format:
//
// CSI ? Pd ; ... ; Pd l
//
// See: https://vt100.net/docs/vt510-rm/RM.html
func ResetMode(modes ...Mode) string {
return setMode(true, modes...)
}
// RM is an alias for [ResetMode].
func RM(modes ...Mode) string {
return ResetMode(modes...)
}
func setMode(reset bool, modes ...Mode) string {
if len(modes) == 0 {
return ""
}
cmd := "h"
if reset {
cmd = "l"
}
seq := "\x1b["
if len(modes) == 1 {
switch modes[0].(type) {
case DECMode:
seq += "?"
}
return seq + strconv.Itoa(modes[0].Mode()) + cmd
}
var dec bool
list := make([]string, len(modes))
for i, m := range modes {
list[i] = strconv.Itoa(m.Mode())
switch m.(type) {
case DECMode:
dec = true
}
}
if dec {
seq += "?"
}
return seq + strings.Join(list, ";") + cmd
}
// RequestMode (DECRQM) returns a sequence to request a mode from the terminal.
// The terminal responds with a report mode function [DECRPM].
//
// ANSI format:
//
// CSI Pa $ p
//
// DEC format:
//
// CSI ? Pa $ p
//
// See: https://vt100.net/docs/vt510-rm/DECRQM.html
func RequestMode(m Mode) string {
seq := "\x1b["
switch m.(type) {
case DECMode:
seq += "?"
}
return seq + strconv.Itoa(m.Mode()) + "$p"
}
// DECRQM is an alias for [RequestMode].
func DECRQM(m Mode) string {
return RequestMode(m)
}
// ReportMode (DECRPM) returns a sequence that the terminal sends to the host
// in response to a mode request [DECRQM].
//
// ANSI format:
//
@ -19,82 +168,505 @@ package ansi
// CSI ? Pa ; Ps $ y
//
// Where Pa is the mode number, and Ps is the mode value.
//
// 0: Not recognized
// 1: Set
// 2: Reset
// 3: Permanent set
// 4: Permanent reset
//
// See: https://vt100.net/docs/vt510-rm/DECRPM.html
func ReportMode(mode Mode, value ModeSetting) string {
if value > 4 {
value = 0
}
switch mode.(type) {
case DECMode:
return "\x1b[?" + strconv.Itoa(mode.Mode()) + ";" + strconv.Itoa(int(value)) + "$y"
}
return "\x1b[" + strconv.Itoa(mode.Mode()) + ";" + strconv.Itoa(int(value)) + "$y"
}
// Application Cursor Keys (DECCKM) is a mode that determines whether the
// cursor keys send ANSI cursor sequences or application sequences.
// DECRPM is an alias for [ReportMode].
func DECRPM(mode Mode, value ModeSetting) string {
return ReportMode(mode, value)
}
// ANSIMode represents an ANSI terminal mode.
type ANSIMode int //nolint:revive
// Mode returns the ANSI mode as an integer.
func (m ANSIMode) Mode() int {
return int(m)
}
// DECMode represents a private DEC terminal mode.
type DECMode int
// Mode returns the DEC mode as an integer.
func (m DECMode) Mode() int {
return int(m)
}
// Keyboard Action Mode (KAM) is a mode that controls locking of the keyboard.
// When the keyboard is locked, it cannot send data to the terminal.
//
// See: https://vt100.net/docs/vt510-rm/KAM.html
const (
KeyboardActionMode = ANSIMode(2)
KAM = KeyboardActionMode
SetKeyboardActionMode = "\x1b[2h"
ResetKeyboardActionMode = "\x1b[2l"
RequestKeyboardActionMode = "\x1b[2$p"
)
// Insert/Replace Mode (IRM) is a mode that determines whether characters are
// inserted or replaced when typed.
//
// When enabled, characters are inserted at the cursor position pushing the
// characters to the right. When disabled, characters replace the character at
// the cursor position.
//
// See: https://vt100.net/docs/vt510-rm/IRM.html
const (
InsertReplaceMode = ANSIMode(4)
IRM = InsertReplaceMode
SetInsertReplaceMode = "\x1b[4h"
ResetInsertReplaceMode = "\x1b[4l"
RequestInsertReplaceMode = "\x1b[4$p"
)
// Send Receive Mode (SRM) or Local Echo Mode is a mode that determines whether
// the terminal echoes characters back to the host. When enabled, the terminal
// sends characters to the host as they are typed.
//
// See: https://vt100.net/docs/vt510-rm/SRM.html
const (
SendReceiveMode = ANSIMode(12)
LocalEchoMode = SendReceiveMode
SRM = SendReceiveMode
SetSendReceiveMode = "\x1b[12h"
ResetSendReceiveMode = "\x1b[12l"
RequestSendReceiveMode = "\x1b[12$p"
SetLocalEchoMode = "\x1b[12h"
ResetLocalEchoMode = "\x1b[12l"
RequestLocalEchoMode = "\x1b[12$p"
)
// Line Feed/New Line Mode (LNM) is a mode that determines whether the terminal
// interprets the line feed character as a new line.
//
// When enabled, the terminal interprets the line feed character as a new line.
// When disabled, the terminal interprets the line feed character as a line feed.
//
// A new line moves the cursor to the first position of the next line.
// A line feed moves the cursor down one line without changing the column
// scrolling the screen if necessary.
//
// See: https://vt100.net/docs/vt510-rm/LNM.html
const (
LineFeedNewLineMode = ANSIMode(20)
LNM = LineFeedNewLineMode
SetLineFeedNewLineMode = "\x1b[20h"
ResetLineFeedNewLineMode = "\x1b[20l"
RequestLineFeedNewLineMode = "\x1b[20$p"
)
// Cursor Keys Mode (DECCKM) is a mode that determines whether the cursor keys
// send ANSI cursor sequences or application sequences.
//
// See: https://vt100.net/docs/vt510-rm/DECCKM.html
const (
CursorKeysMode = DECMode(1)
DECCKM = CursorKeysMode
SetCursorKeysMode = "\x1b[?1h"
ResetCursorKeysMode = "\x1b[?1l"
RequestCursorKeysMode = "\x1b[?1$p"
)
// Deprecated: use [SetCursorKeysMode] and [ResetCursorKeysMode] instead.
const (
EnableCursorKeys = "\x1b[?1h"
DisableCursorKeys = "\x1b[?1l"
RequestCursorKeys = "\x1b[?1$p"
)
// Origin Mode (DECOM) is a mode that determines whether the cursor moves to the
// home position or the margin position.
//
// See: https://vt100.net/docs/vt510-rm/DECOM.html
const (
OriginMode = DECMode(6)
DECOM = OriginMode
SetOriginMode = "\x1b[?6h"
ResetOriginMode = "\x1b[?6l"
RequestOriginMode = "\x1b[?6$p"
)
// Auto Wrap Mode (DECAWM) is a mode that determines whether the cursor wraps
// to the next line when it reaches the right margin.
//
// See: https://vt100.net/docs/vt510-rm/DECAWM.html
const (
AutoWrapMode = DECMode(7)
DECAWM = AutoWrapMode
SetAutoWrapMode = "\x1b[?7h"
ResetAutoWrapMode = "\x1b[?7l"
RequestAutoWrapMode = "\x1b[?7$p"
)
// X10 Mouse Mode is a mode that determines whether the mouse reports on button
// presses.
//
// The terminal responds with the following encoding:
//
// CSI M CbCxCy
//
// Where Cb is the button-1, where it can be 1, 2, or 3.
// Cx and Cy are the x and y coordinates of the mouse event.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
X10MouseMode = DECMode(9)
SetX10MouseMode = "\x1b[?9h"
ResetX10MouseMode = "\x1b[?9l"
RequestX10MouseMode = "\x1b[?9$p"
)
// Text Cursor Enable Mode (DECTCEM) is a mode that shows/hides the cursor.
//
// See: https://vt100.net/docs/vt510-rm/DECTCEM.html
const (
ShowCursor = "\x1b[?25h"
HideCursor = "\x1b[?25l"
TextCursorEnableMode = DECMode(25)
DECTCEM = TextCursorEnableMode
SetTextCursorEnableMode = "\x1b[?25h"
ResetTextCursorEnableMode = "\x1b[?25l"
RequestTextCursorEnableMode = "\x1b[?25$p"
)
// These are aliases for [SetTextCursorEnableMode] and [ResetTextCursorEnableMode].
const (
ShowCursor = SetTextCursorEnableMode
HideCursor = ResetTextCursorEnableMode
)
// Text Cursor Enable Mode (DECTCEM) is a mode that shows/hides the cursor.
//
// See: https://vt100.net/docs/vt510-rm/DECTCEM.html
//
// Deprecated: use [SetTextCursorEnableMode] and [ResetTextCursorEnableMode] instead.
const (
CursorEnableMode = DECMode(25)
RequestCursorVisibility = "\x1b[?25$p"
)
// Numeric Keypad Mode (DECNKM) is a mode that determines whether the keypad
// sends application sequences or numeric sequences.
//
// This works like [DECKPAM] and [DECKPNM], but uses different sequences.
//
// See: https://vt100.net/docs/vt510-rm/DECNKM.html
const (
NumericKeypadMode = DECMode(66)
DECNKM = NumericKeypadMode
SetNumericKeypadMode = "\x1b[?66h"
ResetNumericKeypadMode = "\x1b[?66l"
RequestNumericKeypadMode = "\x1b[?66$p"
)
// Backarrow Key Mode (DECBKM) is a mode that determines whether the backspace
// key sends a backspace or delete character. Disabled by default.
//
// See: https://vt100.net/docs/vt510-rm/DECBKM.html
const (
BackarrowKeyMode = DECMode(67)
DECBKM = BackarrowKeyMode
SetBackarrowKeyMode = "\x1b[?67h"
ResetBackarrowKeyMode = "\x1b[?67l"
RequestBackarrowKeyMode = "\x1b[?67$p"
)
// Left Right Margin Mode (DECLRMM) is a mode that determines whether the left
// and right margins can be set with [DECSLRM].
//
// See: https://vt100.net/docs/vt510-rm/DECLRMM.html
const (
LeftRightMarginMode = DECMode(69)
DECLRMM = LeftRightMarginMode
SetLeftRightMarginMode = "\x1b[?69h"
ResetLeftRightMarginMode = "\x1b[?69l"
RequestLeftRightMarginMode = "\x1b[?69$p"
)
// Normal Mouse Mode is a mode that determines whether the mouse reports on
// button presses and releases. It will also report modifier keys, wheel
// events, and extra buttons.
//
// It uses the same encoding as [X10MouseMode] with a few differences:
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
NormalMouseMode = DECMode(1000)
SetNormalMouseMode = "\x1b[?1000h"
ResetNormalMouseMode = "\x1b[?1000l"
RequestNormalMouseMode = "\x1b[?1000$p"
)
// VT Mouse Tracking is a mode that determines whether the mouse reports on
// button press and release.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
//
// Deprecated: use [NormalMouseMode] instead.
const (
MouseMode = DECMode(1000)
EnableMouse = "\x1b[?1000h"
DisableMouse = "\x1b[?1000l"
RequestMouse = "\x1b[?1000$p"
)
// Highlight Mouse Tracking is a mode that determines whether the mouse reports
// on button presses, releases, and highlighted cells.
//
// It uses the same encoding as [NormalMouseMode] with a few differences:
//
// On highlight events, the terminal responds with the following encoding:
//
// CSI t CxCy
// CSI T CxCyCxCyCxCy
//
// Where the parameters are startx, starty, endx, endy, mousex, and mousey.
const (
HighlightMouseMode = DECMode(1001)
SetHighlightMouseMode = "\x1b[?1001h"
ResetHighlightMouseMode = "\x1b[?1001l"
RequestHighlightMouseMode = "\x1b[?1001$p"
)
// VT Hilite Mouse Tracking is a mode that determines whether the mouse reports on
// button presses, releases, and highlighted cells.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
//
// Deprecated: use [HighlightMouseMode] instead.
const (
MouseHiliteMode = DECMode(1001)
EnableMouseHilite = "\x1b[?1001h"
DisableMouseHilite = "\x1b[?1001l"
RequestMouseHilite = "\x1b[?1001$p"
)
// Button Event Mouse Tracking is essentially the same as [NormalMouseMode],
// but it also reports button-motion events when a button is pressed.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
ButtonEventMouseMode = DECMode(1002)
SetButtonEventMouseMode = "\x1b[?1002h"
ResetButtonEventMouseMode = "\x1b[?1002l"
RequestButtonEventMouseMode = "\x1b[?1002$p"
)
// Cell Motion Mouse Tracking is a mode that determines whether the mouse
// reports on button press, release, and motion events.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
//
// Deprecated: use [ButtonEventMouseMode] instead.
const (
MouseCellMotionMode = DECMode(1002)
EnableMouseCellMotion = "\x1b[?1002h"
DisableMouseCellMotion = "\x1b[?1002l"
RequestMouseCellMotion = "\x1b[?1002$p"
)
// Any Event Mouse Tracking is the same as [ButtonEventMouseMode], except that
// all motion events are reported even if no mouse buttons are pressed.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
AnyEventMouseMode = DECMode(1003)
SetAnyEventMouseMode = "\x1b[?1003h"
ResetAnyEventMouseMode = "\x1b[?1003l"
RequestAnyEventMouseMode = "\x1b[?1003$p"
)
// All Mouse Tracking is a mode that determines whether the mouse reports on
// button press, release, motion, and highlight events.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
//
// Deprecated: use [AnyEventMouseMode] instead.
const (
MouseAllMotionMode = DECMode(1003)
EnableMouseAllMotion = "\x1b[?1003h"
DisableMouseAllMotion = "\x1b[?1003l"
RequestMouseAllMotion = "\x1b[?1003$p"
)
// SGR Mouse Extension is a mode that determines whether the mouse reports events
// formatted with SGR parameters.
// Focus Event Mode is a mode that determines whether the terminal reports focus
// and blur events.
//
// The terminal sends the following encoding:
//
// CSI I // Focus In
// CSI O // Focus Out
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Focus-Tracking
const (
FocusEventMode = DECMode(1004)
SetFocusEventMode = "\x1b[?1004h"
ResetFocusEventMode = "\x1b[?1004l"
RequestFocusEventMode = "\x1b[?1004$p"
)
// Deprecated: use [SetFocusEventMode], [ResetFocusEventMode], and
// [RequestFocusEventMode] instead.
const (
ReportFocusMode = DECMode(1004)
EnableReportFocus = "\x1b[?1004h"
DisableReportFocus = "\x1b[?1004l"
RequestReportFocus = "\x1b[?1004$p"
)
// SGR Extended Mouse Mode is a mode that changes the mouse tracking encoding
// to use SGR parameters.
//
// The terminal responds with the following encoding:
//
// CSI < Cb ; Cx ; Cy M
//
// Where Cb is the same as [NormalMouseMode], and Cx and Cy are the x and y.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
SgrExtMouseMode = DECMode(1006)
SetSgrExtMouseMode = "\x1b[?1006h"
ResetSgrExtMouseMode = "\x1b[?1006l"
RequestSgrExtMouseMode = "\x1b[?1006$p"
)
// Deprecated: use [SgrExtMouseMode] [SetSgrExtMouseMode],
// [ResetSgrExtMouseMode], and [RequestSgrExtMouseMode] instead.
const (
MouseSgrExtMode = DECMode(1006)
EnableMouseSgrExt = "\x1b[?1006h"
DisableMouseSgrExt = "\x1b[?1006l"
RequestMouseSgrExt = "\x1b[?1006$p"
)
// UTF-8 Extended Mouse Mode is a mode that changes the mouse tracking encoding
// to use UTF-8 parameters.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
Utf8ExtMouseMode = DECMode(1005)
SetUtf8ExtMouseMode = "\x1b[?1005h"
ResetUtf8ExtMouseMode = "\x1b[?1005l"
RequestUtf8ExtMouseMode = "\x1b[?1005$p"
)
// URXVT Extended Mouse Mode is a mode that changes the mouse tracking encoding
// to use an alternate encoding.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
UrxvtExtMouseMode = DECMode(1015)
SetUrxvtExtMouseMode = "\x1b[?1015h"
ResetUrxvtExtMouseMode = "\x1b[?1015l"
RequestUrxvtExtMouseMode = "\x1b[?1015$p"
)
// SGR Pixel Extended Mouse Mode is a mode that changes the mouse tracking
// encoding to use SGR parameters with pixel coordinates.
//
// This is similar to [SgrExtMouseMode], but also reports pixel coordinates.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
const (
SgrPixelExtMouseMode = DECMode(1016)
SetSgrPixelExtMouseMode = "\x1b[?1016h"
ResetSgrPixelExtMouseMode = "\x1b[?1016l"
RequestSgrPixelExtMouseMode = "\x1b[?1016$p"
)
// Alternate Screen Mode is a mode that determines whether the alternate screen
// buffer is active. When this mode is enabled, the alternate screen buffer is
// cleared.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
const (
AltScreenMode = DECMode(1047)
SetAltScreenMode = "\x1b[?1047h"
ResetAltScreenMode = "\x1b[?1047l"
RequestAltScreenMode = "\x1b[?1047$p"
)
// Save Cursor Mode is a mode that saves the cursor position.
// This is equivalent to [SaveCursor] and [RestoreCursor].
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
const (
SaveCursorMode = DECMode(1048)
SetSaveCursorMode = "\x1b[?1048h"
ResetSaveCursorMode = "\x1b[?1048l"
RequestSaveCursorMode = "\x1b[?1048$p"
)
// Alternate Screen Save Cursor Mode is a mode that saves the cursor position as in
// [SaveCursorMode], switches to the alternate screen buffer as in [AltScreenMode],
// and clears the screen on switch.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
const (
AltScreenSaveCursorMode = DECMode(1049)
SetAltScreenSaveCursorMode = "\x1b[?1049h"
ResetAltScreenSaveCursorMode = "\x1b[?1049l"
RequestAltScreenSaveCursorMode = "\x1b[?1049$p"
)
// Alternate Screen Buffer is a mode that determines whether the alternate screen
// buffer is active.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
//
// Deprecated: use [AltScreenSaveCursorMode] instead.
const (
AltScreenBufferMode = DECMode(1049)
SetAltScreenBufferMode = "\x1b[?1049h"
ResetAltScreenBufferMode = "\x1b[?1049l"
RequestAltScreenBufferMode = "\x1b[?1049$p"
EnableAltScreenBuffer = "\x1b[?1049h"
DisableAltScreenBuffer = "\x1b[?1049l"
RequestAltScreenBuffer = "\x1b[?1049$p"
@ -105,6 +677,16 @@ const (
//
// See: https://cirw.in/blog/bracketed-paste
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode
const (
BracketedPasteMode = DECMode(2004)
SetBracketedPasteMode = "\x1b[?2004h"
ResetBracketedPasteMode = "\x1b[?2004l"
RequestBracketedPasteMode = "\x1b[?2004$p"
)
// Deprecated: use [SetBracketedPasteMode], [ResetBracketedPasteMode], and
// [RequestBracketedPasteMode] instead.
const (
EnableBracketedPaste = "\x1b[?2004h"
DisableBracketedPaste = "\x1b[?2004l"
@ -116,15 +698,59 @@ const (
//
// See: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
const (
SynchronizedOutputMode = DECMode(2026)
SetSynchronizedOutputMode = "\x1b[?2026h"
ResetSynchronizedOutputMode = "\x1b[?2026l"
RequestSynchronizedOutputMode = "\x1b[?2026$p"
)
// Deprecated: use [SynchronizedOutputMode], [SetSynchronizedOutputMode], and
// [ResetSynchronizedOutputMode], and [RequestSynchronizedOutputMode] instead.
const (
SyncdOutputMode = DECMode(2026)
EnableSyncdOutput = "\x1b[?2026h"
DisableSyncdOutput = "\x1b[?2026l"
RequestSyncdOutput = "\x1b[?2026$p"
)
// Grapheme Clustering Mode is a mode that determines whether the terminal
// should look for grapheme clusters instead of single runes in the rendered
// text. This makes the terminal properly render combining characters such as
// emojis.
//
// See: https://github.com/contour-terminal/terminal-unicode-core
const (
GraphemeClusteringMode = DECMode(2027)
SetGraphemeClusteringMode = "\x1b[?2027h"
ResetGraphemeClusteringMode = "\x1b[?2027l"
RequestGraphemeClusteringMode = "\x1b[?2027$p"
)
// Deprecated: use [SetGraphemeClusteringMode], [ResetGraphemeClusteringMode], and
// [RequestGraphemeClusteringMode] instead.
const (
EnableGraphemeClustering = "\x1b[?2027h"
DisableGraphemeClustering = "\x1b[?2027l"
RequestGraphemeClustering = "\x1b[?2027$p"
)
// Win32Input is a mode that determines whether input is processed by the
// Win32 console and Conpty.
//
// See: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md
const (
Win32InputMode = DECMode(9001)
SetWin32InputMode = "\x1b[?9001h"
ResetWin32InputMode = "\x1b[?9001l"
RequestWin32InputMode = "\x1b[?9001$p"
)
// Deprecated: use [SetWin32InputMode], [ResetWin32InputMode], and
// [RequestWin32InputMode] instead.
const (
EnableWin32Input = "\x1b[?9001h"
DisableWin32Input = "\x1b[?9001l"

36
vendor/github.com/charmbracelet/x/ansi/mouse.go generated vendored Normal file
View File

@ -0,0 +1,36 @@
package ansi
import (
"fmt"
)
// MouseX10 returns an escape sequence representing a mouse event in X10 mode.
// Note that this requires the terminal support X10 mouse modes.
//
// CSI M Cb Cx Cy
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
func MouseX10(b byte, x, y int) string {
const x10Offset = 32
return "\x1b[M" + string(b+x10Offset) + string(byte(x)+x10Offset+1) + string(byte(y)+x10Offset+1)
}
// MouseSgr returns an escape sequence representing a mouse event in SGR mode.
//
// CSI < Cb ; Cx ; Cy M
// CSI < Cb ; Cx ; Cy m (release)
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
func MouseSgr(b byte, x, y int, release bool) string {
s := "M"
if release {
s = "m"
}
if x < 0 {
x = -x
}
if y < 0 {
y = -y
}
return fmt.Sprintf("\x1b[<%d;%d;%d%s", b, x+1, y+1, s)
}

13
vendor/github.com/charmbracelet/x/ansi/notification.go generated vendored Normal file
View File

@ -0,0 +1,13 @@
package ansi
// Notify sends a desktop notification using iTerm's OSC 9.
//
// OSC 9 ; Mc ST
// OSC 9 ; Mc BEL
//
// Where Mc is the notification body.
//
// See: https://iterm2.com/documentation-escape-codes.html
func Notify(s string) string {
return "\x1b]9;" + s + "\x07"
}

View File

@ -27,25 +27,26 @@ type OscSequence struct {
var _ Sequence = OscSequence{}
// Command returns the command of the OSC sequence.
func (s OscSequence) Command() int {
return s.Cmd
}
// Params returns the parameters of the OSC sequence split by ';'.
// The first element is the identifier command.
func (s OscSequence) Params() []string {
return strings.Split(string(s.Data), ";")
}
// Clone returns a copy of the OSC sequence.
func (s OscSequence) Clone() Sequence {
// Clone returns a deep copy of the OSC sequence.
func (o OscSequence) Clone() Sequence {
return OscSequence{
Data: append([]byte(nil), s.Data...),
Cmd: s.Cmd,
Data: append([]byte(nil), o.Data...),
Cmd: o.Cmd,
}
}
// Split returns a slice of data split by the semicolon with the first element
// being the identifier command.
func (o OscSequence) Split() []string {
return strings.Split(string(o.Data), ";")
}
// Command returns the OSC command. This is always gonna be a positive integer
// that identifies the OSC sequence.
func (o OscSequence) Command() int {
return o.Cmd
}
// String returns the string representation of the OSC sequence.
// To be more compatible with different terminal, this will always return a
// 7-bit formatted sequence, terminated by BEL.

View File

@ -2,6 +2,7 @@ package ansi
import (
"unicode/utf8"
"unsafe"
"github.com/charmbracelet/x/ansi/parser"
)
@ -19,128 +20,200 @@ type ParserDispatcher func(Sequence)
//
//go:generate go run ./gen.go
type Parser struct {
// Params contains the raw parameters of the sequence.
// the dispatch function to call when a sequence is complete
dispatcher ParserDispatcher
// params contains the raw parameters of the sequence.
// These parameters used when constructing CSI and DCS sequences.
Params []int
params []int
// Data contains the raw data of the sequence.
// data contains the raw data of the sequence.
// These data used when constructing OSC, DCS, SOS, PM, and APC sequences.
Data []byte
data []byte
// DataLen keeps track of the length of the data buffer.
// If DataLen is -1, the data buffer is unlimited and will grow as needed.
// Otherwise, DataLen is limited by the size of the Data buffer.
DataLen int
// dataLen keeps track of the length of the data buffer.
// If dataLen is -1, the data buffer is unlimited and will grow as needed.
// Otherwise, dataLen is limited by the size of the data buffer.
dataLen int
// ParamsLen keeps track of the number of parameters.
// This is limited by the size of the Params buffer.
ParamsLen int
// paramsLen keeps track of the number of parameters.
// This is limited by the size of the params buffer.
//
// This is also used when collecting UTF-8 runes to keep track of the
// number of rune bytes collected.
paramsLen int
// Cmd contains the raw command along with the private marker and
// cmd contains the raw command along with the private marker and
// intermediate bytes of the sequence.
// The first lower byte contains the command byte, the next byte contains
// the private marker, and the next byte contains the intermediate byte.
Cmd int
//
// This is also used when collecting UTF-8 runes treating it as a slice of
// 4 bytes.
cmd int
// RuneLen keeps track of the number of bytes collected for a UTF-8 rune.
RuneLen int
// RuneBuf contains the bytes collected for a UTF-8 rune.
RuneBuf [utf8.MaxRune]byte
// State is the current state of the parser.
State byte
// state is the current state of the parser.
state byte
}
// NewParser returns a new parser with the given sizes allocated.
// If dataSize is zero, the underlying data buffer will be unlimited and will
// NewParser returns a new parser with an optional [ParserDispatcher].
// The [Parser] uses a default size of 32 for the parameters and 64KB for the
// data buffer. Use [Parser.SetParamsSize] and [Parser.SetDataSize] to set the
// size of the parameters and data buffer respectively.
func NewParser(d ParserDispatcher) *Parser {
p := new(Parser)
p.SetDispatcher(d)
p.SetParamsSize(parser.MaxParamsSize)
p.SetDataSize(1024 * 64) // 64KB data buffer
return p
}
// SetDispatcher sets the dispatcher function to call when a sequence is
// complete.
func (p *Parser) SetDispatcher(d ParserDispatcher) {
p.dispatcher = d
}
// SetParamsSize sets the size of the parameters buffer.
// This is used when constructing CSI and DCS sequences.
func (p *Parser) SetParamsSize(size int) {
p.params = make([]int, size)
}
// SetDataSize sets the size of the data buffer.
// This is used when constructing OSC, DCS, SOS, PM, and APC sequences.
// If size is less than or equal to 0, the data buffer is unlimited and will
// grow as needed.
func NewParser(paramsSize, dataSize int) *Parser {
s := &Parser{
Params: make([]int, paramsSize),
Data: make([]byte, dataSize),
func (p *Parser) SetDataSize(size int) {
if size <= 0 {
size = 0
p.dataLen = -1
}
if dataSize <= 0 {
s.DataLen = -1
p.data = make([]byte, size)
}
return s
// Params returns the list of parsed packed parameters.
func (p *Parser) Params() []Parameter {
return unsafe.Slice((*Parameter)(unsafe.Pointer(&p.params[0])), p.paramsLen)
}
// Param returns the parameter at the given index and falls back to the default
// value if the parameter is missing. If the index is out of bounds, it returns
// the default value and false.
func (p *Parser) Param(i, def int) (int, bool) {
if i < 0 || i >= p.paramsLen {
return def, false
}
return Parameter(p.params[i]).Param(def), true
}
// Cmd returns the packed command of the last dispatched sequence.
func (p *Parser) Cmd() Command {
return Command(p.cmd)
}
// Rune returns the last dispatched sequence as a rune.
func (p *Parser) Rune() rune {
rw := utf8ByteLen(byte(p.cmd & 0xff))
if rw == -1 {
return utf8.RuneError
}
r, _ := utf8.DecodeRune((*[utf8.UTFMax]byte)(unsafe.Pointer(&p.cmd))[:rw])
return r
}
// Data returns the raw data of the last dispatched sequence.
func (p *Parser) Data() []byte {
return p.data[:p.dataLen]
}
// Reset resets the parser to its initial state.
func (p *Parser) Reset() {
p.clear()
p.State = parser.GroundState
p.state = parser.GroundState
}
// clear clears the parser parameters and command.
func (p *Parser) clear() {
if len(p.Params) > 0 {
p.Params[0] = parser.MissingParam
if len(p.params) > 0 {
p.params[0] = parser.MissingParam
}
p.ParamsLen = 0
p.Cmd = 0
p.RuneLen = 0
p.paramsLen = 0
p.cmd = 0
}
// State returns the current state of the parser.
func (p *Parser) State() parser.State {
return p.state
}
// StateName returns the name of the current state.
func (p *Parser) StateName() string {
return parser.StateNames[p.State]
return parser.StateNames[p.state]
}
// Parse parses the given dispatcher and byte buffer.
func (p *Parser) Parse(dispatcher ParserDispatcher, b []byte) {
// Deprecated: Loop over the buffer and call [Parser.Advance] instead.
func (p *Parser) Parse(b []byte) {
for i := 0; i < len(b); i++ {
p.Advance(dispatcher, b[i], i < len(b)-1)
p.Advance(b[i])
}
}
// Advance advances the parser with the given dispatcher and byte.
func (p *Parser) Advance(dispatcher ParserDispatcher, b byte, more bool) parser.Action {
switch p.State {
// Advance advances the parser using the given byte. It returns the action
// performed by the parser.
func (p *Parser) Advance(b byte) parser.Action {
switch p.state {
case parser.Utf8State:
// We handle UTF-8 here.
return p.advanceUtf8(dispatcher, b)
return p.advanceUtf8(b)
default:
return p.advance(dispatcher, b, more)
return p.advance(b)
}
}
func (p *Parser) collectRune(b byte) {
if p.RuneLen < utf8.UTFMax {
p.RuneBuf[p.RuneLen] = b
p.RuneLen++
if p.paramsLen >= utf8.UTFMax {
return
}
shift := p.paramsLen * 8
p.cmd &^= 0xff << shift
p.cmd |= int(b) << shift
p.paramsLen++
}
func (p *Parser) dispatch(s Sequence) {
if p.dispatcher != nil {
p.dispatcher(s)
}
}
func (p *Parser) advanceUtf8(dispatcher ParserDispatcher, b byte) parser.Action {
func (p *Parser) advanceUtf8(b byte) parser.Action {
// Collect UTF-8 rune bytes.
p.collectRune(b)
rw := utf8ByteLen(p.RuneBuf[0])
rw := utf8ByteLen(byte(p.cmd & 0xff))
if rw == -1 {
// We panic here because the first byte comes from the state machine,
// if this panics, it means there is a bug in the state machine!
panic("invalid rune") // unreachable
}
if p.RuneLen < rw {
return parser.NoneAction
if p.paramsLen < rw {
return parser.CollectAction
}
// We have enough bytes to decode the rune
bts := p.RuneBuf[:rw]
r, _ := utf8.DecodeRune(bts)
if dispatcher != nil {
dispatcher(Rune(r))
// We have enough bytes to decode the rune using unsafe
p.dispatch(Rune(p.Rune()))
p.state = parser.GroundState
p.paramsLen = 0
return parser.PrintAction
}
p.State = parser.GroundState
p.RuneLen = 0
return parser.NoneAction
}
func (p *Parser) advance(d ParserDispatcher, b byte, more bool) parser.Action {
state, action := parser.Table.Transition(p.State, b)
func (p *Parser) advance(b byte) parser.Action {
state, action := parser.Table.Transition(p.state, b)
// We need to clear the parser state if the state changes from EscapeState.
// This is because when we enter the EscapeState, we don't get a chance to
@ -148,60 +221,53 @@ func (p *Parser) advance(d ParserDispatcher, b byte, more bool) parser.Action {
// ST (\x1b\\ or \x9c), we dispatch the current sequence and transition to
// EscapeState. However, the parser state is not cleared in this case and
// we need to clear it here before dispatching the esc sequence.
if p.State != state {
switch p.State {
case parser.EscapeState:
p.performAction(d, parser.ClearAction, b)
if p.state != state {
if p.state == parser.EscapeState {
p.performAction(parser.ClearAction, state, b)
}
if action == parser.PutAction &&
p.State == parser.DcsEntryState && state == parser.DcsStringState {
p.state == parser.DcsEntryState && state == parser.DcsStringState {
// XXX: This is a special case where we need to start collecting
// non-string parameterized data i.e. doesn't follow the ECMA-48 §
// 5.4.1 string parameters format.
p.performAction(d, parser.StartAction, 0)
p.performAction(parser.StartAction, state, 0)
}
}
// Handle special cases
switch {
case b == ESC && p.State == parser.EscapeState:
case b == ESC && p.state == parser.EscapeState:
// Two ESCs in a row
p.performAction(d, parser.ExecuteAction, b)
if !more {
// Two ESCs at the end of the buffer
p.performAction(d, parser.ExecuteAction, b)
}
case b == ESC && !more:
// Last byte is an ESC
p.performAction(d, parser.ExecuteAction, b)
case p.State == parser.EscapeState && b == 'P' && !more:
// ESC P (DCS) at the end of the buffer
p.performAction(d, parser.DispatchAction, b)
case p.State == parser.EscapeState && b == 'X' && !more:
// ESC X (SOS) at the end of the buffer
p.performAction(d, parser.DispatchAction, b)
case p.State == parser.EscapeState && b == '[' && !more:
// ESC [ (CSI) at the end of the buffer
p.performAction(d, parser.DispatchAction, b)
case p.State == parser.EscapeState && b == ']' && !more:
// ESC ] (OSC) at the end of the buffer
p.performAction(d, parser.DispatchAction, b)
case p.State == parser.EscapeState && b == '^' && !more:
// ESC ^ (PM) at the end of the buffer
p.performAction(d, parser.DispatchAction, b)
case p.State == parser.EscapeState && b == '_' && !more:
// ESC _ (APC) at the end of the buffer
p.performAction(d, parser.DispatchAction, b)
p.performAction(parser.ExecuteAction, state, b)
default:
p.performAction(d, action, b)
p.performAction(action, state, b)
}
p.State = state
p.state = state
return action
}
func (p *Parser) performAction(dispatcher ParserDispatcher, action parser.Action, b byte) {
func (p *Parser) parseStringCmd() {
// Try to parse the command
datalen := len(p.data)
if p.dataLen >= 0 {
datalen = p.dataLen
}
for i := 0; i < datalen; i++ {
d := p.data[i]
if d < '0' || d > '9' {
break
}
if p.cmd == parser.MissingCommand {
p.cmd = 0
}
p.cmd *= 10
p.cmd += int(d - '0')
}
}
func (p *Parser) performAction(action parser.Action, state parser.State, b byte) {
switch action {
case parser.IgnoreAction:
break
@ -210,127 +276,117 @@ func (p *Parser) performAction(dispatcher ParserDispatcher, action parser.Action
p.clear()
case parser.PrintAction:
if utf8ByteLen(b) > 1 {
p.collectRune(b)
} else if dispatcher != nil {
dispatcher(Rune(b))
}
p.dispatch(Rune(b))
case parser.ExecuteAction:
if dispatcher != nil {
dispatcher(ControlCode(b))
}
p.dispatch(ControlCode(b))
case parser.MarkerAction:
// Collect private marker
// we only store the last marker
p.Cmd &^= 0xff << parser.MarkerShift
p.Cmd |= int(b) << parser.MarkerShift
p.cmd &^= 0xff << parser.MarkerShift
p.cmd |= int(b) << parser.MarkerShift
case parser.CollectAction:
if state == parser.Utf8State {
// Reset the UTF-8 counter
p.paramsLen = 0
p.collectRune(b)
} else {
// Collect intermediate bytes
// we only store the last intermediate byte
p.Cmd &^= 0xff << parser.IntermedShift
p.Cmd |= int(b) << parser.IntermedShift
p.cmd &^= 0xff << parser.IntermedShift
p.cmd |= int(b) << parser.IntermedShift
}
case parser.ParamAction:
// Collect parameters
if p.ParamsLen >= len(p.Params) {
if p.paramsLen >= len(p.params) {
break
}
if b >= '0' && b <= '9' {
if p.Params[p.ParamsLen] == parser.MissingParam {
p.Params[p.ParamsLen] = 0
if p.params[p.paramsLen] == parser.MissingParam {
p.params[p.paramsLen] = 0
}
p.Params[p.ParamsLen] *= 10
p.Params[p.ParamsLen] += int(b - '0')
p.params[p.paramsLen] *= 10
p.params[p.paramsLen] += int(b - '0')
}
if b == ':' {
p.Params[p.ParamsLen] |= parser.HasMoreFlag
p.params[p.paramsLen] |= parser.HasMoreFlag
}
if b == ';' || b == ':' {
p.ParamsLen++
if p.ParamsLen < len(p.Params) {
p.Params[p.ParamsLen] = parser.MissingParam
p.paramsLen++
if p.paramsLen < len(p.params) {
p.params[p.paramsLen] = parser.MissingParam
}
}
case parser.StartAction:
if p.DataLen < 0 {
p.Data = make([]byte, 0)
if p.dataLen < 0 && p.data != nil {
p.data = p.data[:0]
} else {
p.DataLen = 0
p.dataLen = 0
}
if p.State >= parser.DcsEntryState && p.State <= parser.DcsStringState {
if p.state >= parser.DcsEntryState && p.state <= parser.DcsStringState {
// Collect the command byte for DCS
p.Cmd |= int(b)
p.cmd |= int(b)
} else {
p.Cmd = parser.MissingCommand
p.cmd = parser.MissingCommand
}
case parser.PutAction:
switch p.State {
switch p.state {
case parser.OscStringState:
if b == ';' && p.Cmd == parser.MissingCommand {
// Try to parse the command
datalen := len(p.Data)
if p.DataLen >= 0 {
datalen = p.DataLen
}
for i := 0; i < datalen; i++ {
d := p.Data[i]
if d < '0' || d > '9' {
break
}
if p.Cmd == parser.MissingCommand {
p.Cmd = 0
}
p.Cmd *= 10
p.Cmd += int(d - '0')
}
if b == ';' && p.cmd == parser.MissingCommand {
p.parseStringCmd()
}
}
if p.DataLen < 0 {
p.Data = append(p.Data, b)
if p.dataLen < 0 {
p.data = append(p.data, b)
} else {
if p.DataLen < len(p.Data) {
p.Data[p.DataLen] = b
p.DataLen++
if p.dataLen < len(p.data) {
p.data[p.dataLen] = b
p.dataLen++
}
}
case parser.DispatchAction:
// Increment the last parameter
if p.ParamsLen > 0 && p.ParamsLen < len(p.Params)-1 ||
p.ParamsLen == 0 && len(p.Params) > 0 && p.Params[0] != parser.MissingParam {
p.ParamsLen++
if p.paramsLen > 0 && p.paramsLen < len(p.params)-1 ||
p.paramsLen == 0 && len(p.params) > 0 && p.params[0] != parser.MissingParam {
p.paramsLen++
}
if dispatcher == nil {
if p.state == parser.OscStringState && p.cmd == parser.MissingCommand {
// Ensure we have a command for OSC
p.parseStringCmd()
}
if p.dispatcher == nil {
break
}
var seq Sequence
data := p.Data
if p.DataLen >= 0 {
data = data[:p.DataLen]
data := p.data
if p.dataLen >= 0 {
data = data[:p.dataLen]
}
switch p.State {
switch p.state {
case parser.CsiEntryState, parser.CsiParamState, parser.CsiIntermediateState:
p.Cmd |= int(b)
seq = CsiSequence{Cmd: p.Cmd, Params: p.Params[:p.ParamsLen]}
p.cmd |= int(b)
seq = CsiSequence{Cmd: Command(p.cmd), Params: p.Params()}
case parser.EscapeState, parser.EscapeIntermediateState:
p.Cmd |= int(b)
seq = EscSequence(p.Cmd)
p.cmd |= int(b)
seq = EscSequence(p.cmd)
case parser.DcsEntryState, parser.DcsParamState, parser.DcsIntermediateState, parser.DcsStringState:
seq = DcsSequence{Cmd: p.Cmd, Params: p.Params[:p.ParamsLen], Data: data}
seq = DcsSequence{Cmd: Command(p.cmd), Params: p.Params(), Data: data}
case parser.OscStringState:
seq = OscSequence{Cmd: p.Cmd, Data: data}
seq = OscSequence{Cmd: p.cmd, Data: data}
case parser.SosStringState:
seq = SosSequence{Data: data}
case parser.PmStringState:
@ -339,7 +395,7 @@ func (p *Parser) performAction(dispatcher ParserDispatcher, action parser.Action
seq = ApcSequence{Data: data}
}
dispatcher(seq)
p.dispatch(seq)
}
}

View File

@ -81,6 +81,9 @@ func r(start, end byte) []byte {
// - We don't ignore 0x3A (':') when building Csi and Dcs parameters and
// instead use it to denote sub-parameters.
// - Support dispatching SosPmApc sequences.
// - The DEL (0x7F) character is executed in the Ground state.
// - The DEL (0x7F) character is collected in the DcsPassthrough string state.
// - The ST C1 control character (0x9C) is executed and not ignored.
func GenerateTransitionTable() TransitionTable {
table := NewTransitionTable(DefaultTableSize)
table.SetDefault(NoneAction, GroundState)
@ -91,7 +94,7 @@ func GenerateTransitionTable() TransitionTable {
table.AddMany([]byte{0x18, 0x1a, 0x99, 0x9a}, state, ExecuteAction, GroundState)
table.AddRange(0x80, 0x8F, state, ExecuteAction, GroundState)
table.AddRange(0x90, 0x97, state, ExecuteAction, GroundState)
table.AddOne(0x9C, state, IgnoreAction, GroundState)
table.AddOne(0x9C, state, ExecuteAction, GroundState)
// Anywhere -> Escape
table.AddOne(0x1B, state, ClearAction, EscapeState)
// Anywhere -> SosStringState
@ -107,16 +110,17 @@ func GenerateTransitionTable() TransitionTable {
// Anywhere -> OscString
table.AddOne(0x9D, state, StartAction, OscStringState)
// Anywhere -> Utf8
table.AddRange(0xC2, 0xDF, state, PrintAction, Utf8State) // UTF8 2 byte sequence
table.AddRange(0xE0, 0xEF, state, PrintAction, Utf8State) // UTF8 3 byte sequence
table.AddRange(0xF0, 0xF4, state, PrintAction, Utf8State) // UTF8 4 byte sequence
table.AddRange(0xC2, 0xDF, state, CollectAction, Utf8State) // UTF8 2 byte sequence
table.AddRange(0xE0, 0xEF, state, CollectAction, Utf8State) // UTF8 3 byte sequence
table.AddRange(0xF0, 0xF4, state, CollectAction, Utf8State) // UTF8 4 byte sequence
}
// Ground
table.AddRange(0x00, 0x17, GroundState, ExecuteAction, GroundState)
table.AddOne(0x19, GroundState, ExecuteAction, GroundState)
table.AddRange(0x1C, 0x1F, GroundState, ExecuteAction, GroundState)
table.AddRange(0x20, 0x7F, GroundState, PrintAction, GroundState)
table.AddRange(0x20, 0x7E, GroundState, PrintAction, GroundState)
table.AddOne(0x7F, GroundState, ExecuteAction, GroundState)
// EscapeIntermediate
table.AddRange(0x00, 0x17, EscapeIntermediateState, ExecuteAction, EscapeIntermediateState)
@ -209,7 +213,7 @@ func GenerateTransitionTable() TransitionTable {
table.AddOne(0x19, DcsStringState, PutAction, DcsStringState)
table.AddRange(0x1C, 0x1F, DcsStringState, PutAction, DcsStringState)
table.AddRange(0x20, 0x7E, DcsStringState, PutAction, DcsStringState)
table.AddOne(0x7F, DcsStringState, IgnoreAction, DcsStringState)
table.AddOne(0x7F, DcsStringState, PutAction, DcsStringState)
table.AddRange(0x80, 0xFF, DcsStringState, PutAction, DcsStringState) // Allow Utf8 characters by extending the printable range from 0x7F to 0xFF
// ST, CAN, SUB, and ESC terminate the sequence
table.AddOne(0x1B, DcsStringState, DispatchAction, EscapeState)

461
vendor/github.com/charmbracelet/x/ansi/parser_decode.go generated vendored Normal file
View File

@ -0,0 +1,461 @@
package ansi
import (
"unicode/utf8"
"github.com/charmbracelet/x/ansi/parser"
"github.com/rivo/uniseg"
)
// State represents the state of the ANSI escape sequence parser used by
// [DecodeSequence].
type State = byte
// ANSI escape sequence states used by [DecodeSequence].
const (
NormalState State = iota
MarkerState
ParamsState
IntermedState
EscapeState
StringState
)
// DecodeSequence decodes the first ANSI escape sequence or a printable
// grapheme from the given data. It returns the sequence slice, the number of
// bytes read, the cell width for each sequence, and the new state.
//
// The cell width will always be 0 for control and escape sequences, 1 for
// ASCII printable characters, and the number of cells other Unicode characters
// occupy. It uses the uniseg package to calculate the width of Unicode
// graphemes and characters. This means it will always do grapheme clustering
// (mode 2027).
//
// Passing a non-nil [*Parser] as the last argument will allow the decoder to
// collect sequence parameters, data, and commands. The parser cmd will have
// the packed command value that contains intermediate and marker characters.
// In the case of a OSC sequence, the cmd will be the OSC command number. Use
// [Command] and [Parameter] types to unpack command intermediates and markers as well
// as parameters.
//
// Zero [Command] means the CSI, DCS, or ESC sequence is invalid. Moreover, checking the
// validity of other data sequences, OSC, DCS, etc, will require checking for
// the returned sequence terminator bytes such as ST (ESC \\) and BEL).
//
// We store the command byte in [Command] in the most significant byte, the
// marker byte in the next byte, and the intermediate byte in the least
// significant byte. This is done to avoid using a struct to store the command
// and its intermediates and markers. The command byte is always the least
// significant byte i.e. [Cmd & 0xff]. Use the [Command] type to unpack the
// command, intermediate, and marker bytes. Note that we only collect the last
// marker character and intermediate byte.
//
// The [p.Params] slice will contain the parameters of the sequence. Any
// sub-parameter will have the [parser.HasMoreFlag] set. Use the [Parameter] type
// to unpack the parameters.
//
// Example:
//
// var state byte // the initial state is always zero [NormalState]
// p := NewParser(32, 1024) // create a new parser with a 32 params buffer and 1024 data buffer (optional)
// input := []byte("\x1b[31mHello, World!\x1b[0m")
// for len(input) > 0 {
// seq, width, n, newState := DecodeSequence(input, state, p)
// log.Printf("seq: %q, width: %d", seq, width)
// state = newState
// input = input[n:]
// }
func DecodeSequence[T string | []byte](b T, state byte, p *Parser) (seq T, width int, n int, newState byte) {
for i := 0; i < len(b); i++ {
c := b[i]
switch state {
case NormalState:
switch c {
case ESC:
if p != nil {
if len(p.params) > 0 {
p.params[0] = parser.MissingParam
}
p.cmd = 0
p.paramsLen = 0
p.dataLen = 0
}
state = EscapeState
continue
case CSI, DCS:
if p != nil {
if len(p.params) > 0 {
p.params[0] = parser.MissingParam
}
p.cmd = 0
p.paramsLen = 0
p.dataLen = 0
}
state = MarkerState
continue
case OSC, APC, SOS, PM:
if p != nil {
p.cmd = parser.MissingCommand
p.dataLen = 0
}
state = StringState
continue
}
if p != nil {
p.dataLen = 0
p.paramsLen = 0
p.cmd = 0
}
if c > US && c < DEL {
// ASCII printable characters
return b[i : i+1], 1, 1, NormalState
}
if c <= US || c == DEL || c < 0xC0 {
// C0 & C1 control characters & DEL
return b[i : i+1], 0, 1, NormalState
}
if utf8.RuneStart(c) {
seq, _, width, _ = FirstGraphemeCluster(b, -1)
i += len(seq)
return b[:i], width, i, NormalState
}
// Invalid UTF-8 sequence
return b[:i], 0, i, NormalState
case MarkerState:
if c >= '<' && c <= '?' {
if p != nil {
// We only collect the last marker character.
p.cmd &^= 0xff << parser.MarkerShift
p.cmd |= int(c) << parser.MarkerShift
}
break
}
state = ParamsState
fallthrough
case ParamsState:
if c >= '0' && c <= '9' {
if p != nil {
if p.params[p.paramsLen] == parser.MissingParam {
p.params[p.paramsLen] = 0
}
p.params[p.paramsLen] *= 10
p.params[p.paramsLen] += int(c - '0')
}
break
}
if c == ':' {
if p != nil {
p.params[p.paramsLen] |= parser.HasMoreFlag
}
}
if c == ';' || c == ':' {
if p != nil {
p.paramsLen++
if p.paramsLen < len(p.params) {
p.params[p.paramsLen] = parser.MissingParam
}
}
break
}
state = IntermedState
fallthrough
case IntermedState:
if c >= ' ' && c <= '/' {
if p != nil {
p.cmd &^= 0xff << parser.IntermedShift
p.cmd |= int(c) << parser.IntermedShift
}
break
}
if p != nil {
// Increment the last parameter
if p.paramsLen > 0 && p.paramsLen < len(p.params)-1 ||
p.paramsLen == 0 && len(p.params) > 0 && p.params[0] != parser.MissingParam {
p.paramsLen++
}
}
if c >= '@' && c <= '~' {
if p != nil {
p.cmd &^= 0xff
p.cmd |= int(c)
}
if HasDcsPrefix(b) {
// Continue to collect DCS data
if p != nil {
p.dataLen = 0
}
state = StringState
continue
}
return b[:i+1], 0, i + 1, NormalState
}
// Invalid CSI/DCS sequence
return b[:i], 0, i, NormalState
case EscapeState:
switch c {
case '[', 'P':
if p != nil {
if len(p.params) > 0 {
p.params[0] = parser.MissingParam
}
p.paramsLen = 0
p.cmd = 0
}
state = MarkerState
continue
case ']', 'X', '^', '_':
if p != nil {
p.cmd = parser.MissingCommand
p.dataLen = 0
}
state = StringState
continue
}
if c >= ' ' && c <= '/' {
if p != nil {
p.cmd &^= 0xff << parser.IntermedShift
p.cmd |= int(c) << parser.IntermedShift
}
continue
} else if c >= '0' && c <= '~' {
if p != nil {
p.cmd &^= 0xff
p.cmd |= int(c)
}
return b[:i+1], 0, i + 1, NormalState
}
// Invalid escape sequence
return b[:i], 0, i, NormalState
case StringState:
switch c {
case BEL:
if HasOscPrefix(b) {
parseOscCmd(p)
return b[:i+1], 0, i + 1, NormalState
}
case CAN, SUB:
if HasOscPrefix(b) {
// Ensure we parse the OSC command number
parseOscCmd(p)
}
// Cancel the sequence
return b[:i], 0, i, NormalState
case ST:
if HasOscPrefix(b) {
// Ensure we parse the OSC command number
parseOscCmd(p)
}
return b[:i+1], 0, i + 1, NormalState
case ESC:
if HasStPrefix(b[i:]) {
if HasOscPrefix(b) {
// Ensure we parse the OSC command number
parseOscCmd(p)
}
// End of string 7-bit (ST)
return b[:i+2], 0, i + 2, NormalState
}
// Otherwise, cancel the sequence
return b[:i], 0, i, NormalState
}
if p != nil && p.dataLen < len(p.data) {
p.data[p.dataLen] = c
p.dataLen++
// Parse the OSC command number
if c == ';' && HasOscPrefix(b) {
parseOscCmd(p)
}
}
}
}
return b, 0, len(b), state
}
func parseOscCmd(p *Parser) {
if p == nil || p.cmd != parser.MissingCommand {
return
}
for j := 0; j < p.dataLen; j++ {
d := p.data[j]
if d < '0' || d > '9' {
break
}
if p.cmd == parser.MissingCommand {
p.cmd = 0
}
p.cmd *= 10
p.cmd += int(d - '0')
}
}
// Equal returns true if the given byte slices are equal.
func Equal[T string | []byte](a, b T) bool {
return string(a) == string(b)
}
// HasPrefix returns true if the given byte slice has prefix.
func HasPrefix[T string | []byte](b, prefix T) bool {
return len(b) >= len(prefix) && Equal(b[0:len(prefix)], prefix)
}
// HasSuffix returns true if the given byte slice has suffix.
func HasSuffix[T string | []byte](b, suffix T) bool {
return len(b) >= len(suffix) && Equal(b[len(b)-len(suffix):], suffix)
}
// HasCsiPrefix returns true if the given byte slice has a CSI prefix.
func HasCsiPrefix[T string | []byte](b T) bool {
return (len(b) > 0 && b[0] == CSI) ||
(len(b) > 1 && b[0] == ESC && b[1] == '[')
}
// HasOscPrefix returns true if the given byte slice has an OSC prefix.
func HasOscPrefix[T string | []byte](b T) bool {
return (len(b) > 0 && b[0] == OSC) ||
(len(b) > 1 && b[0] == ESC && b[1] == ']')
}
// HasApcPrefix returns true if the given byte slice has an APC prefix.
func HasApcPrefix[T string | []byte](b T) bool {
return (len(b) > 0 && b[0] == APC) ||
(len(b) > 1 && b[0] == ESC && b[1] == '_')
}
// HasDcsPrefix returns true if the given byte slice has a DCS prefix.
func HasDcsPrefix[T string | []byte](b T) bool {
return (len(b) > 0 && b[0] == DCS) ||
(len(b) > 1 && b[0] == ESC && b[1] == 'P')
}
// HasSosPrefix returns true if the given byte slice has a SOS prefix.
func HasSosPrefix[T string | []byte](b T) bool {
return (len(b) > 0 && b[0] == SOS) ||
(len(b) > 1 && b[0] == ESC && b[1] == 'X')
}
// HasPmPrefix returns true if the given byte slice has a PM prefix.
func HasPmPrefix[T string | []byte](b T) bool {
return (len(b) > 0 && b[0] == PM) ||
(len(b) > 1 && b[0] == ESC && b[1] == '^')
}
// HasStPrefix returns true if the given byte slice has a ST prefix.
func HasStPrefix[T string | []byte](b T) bool {
return (len(b) > 0 && b[0] == ST) ||
(len(b) > 1 && b[0] == ESC && b[1] == '\\')
}
// HasEscPrefix returns true if the given byte slice has an ESC prefix.
func HasEscPrefix[T string | []byte](b T) bool {
return len(b) > 0 && b[0] == ESC
}
// FirstGraphemeCluster returns the first grapheme cluster in the given string or byte slice.
// This is a syntactic sugar function that wraps
// uniseg.FirstGraphemeClusterInString and uniseg.FirstGraphemeCluster.
func FirstGraphemeCluster[T string | []byte](b T, state int) (T, T, int, int) {
switch b := any(b).(type) {
case string:
cluster, rest, width, newState := uniseg.FirstGraphemeClusterInString(b, state)
return T(cluster), T(rest), width, newState
case []byte:
cluster, rest, width, newState := uniseg.FirstGraphemeCluster(b, state)
return T(cluster), T(rest), width, newState
}
panic("unreachable")
}
// Command represents a sequence command. This is used to pack/unpack a sequence
// command with its intermediate and marker characters. Those are commonly
// found in CSI and DCS sequences.
type Command int
// Marker returns the unpacked marker byte of the CSI sequence.
// This is always gonna be one of the following '<' '=' '>' '?' and in the
// range of 0x3C-0x3F.
// Zero is returned if the sequence does not have a marker.
func (c Command) Marker() int {
return parser.Marker(int(c))
}
// Intermediate returns the unpacked intermediate byte of the CSI sequence.
// An intermediate byte is in the range of 0x20-0x2F. This includes these
// characters from ' ', '!', '"', '#', '$', '%', '&', ”', '(', ')', '*', '+',
// ',', '-', '.', '/'.
// Zero is returned if the sequence does not have an intermediate byte.
func (c Command) Intermediate() int {
return parser.Intermediate(int(c))
}
// Command returns the unpacked command byte of the CSI sequence.
func (c Command) Command() int {
return parser.Command(int(c))
}
// Cmd returns a packed [Command] with the given command, marker, and
// intermediate.
// The first byte is the command, the next shift is the marker, and the next
// shift is the intermediate.
//
// Even though this function takes integers, it only uses the lower 8 bits of
// each integer.
func Cmd(marker, inter, cmd int) (c Command) {
c = Command(cmd & parser.CommandMask)
c |= Command(marker&parser.CommandMask) << parser.MarkerShift
c |= Command(inter&parser.CommandMask) << parser.IntermedShift
return
}
// Parameter represents a sequence parameter. Sequence parameters with
// sub-parameters are packed with the HasMoreFlag set. This is used to unpack
// the parameters from a CSI and DCS sequences.
type Parameter int
// Param returns the unpacked parameter at the given index.
// It returns the default value if the parameter is missing.
func (s Parameter) Param(def int) int {
p := int(s) & parser.ParamMask
if p == parser.MissingParam {
return def
}
return p
}
// HasMore unpacks the HasMoreFlag from the parameter.
func (s Parameter) HasMore() bool {
return s&parser.HasMoreFlag != 0
}
// Param returns a packed [Parameter] with the given parameter and whether this
// parameter has following sub-parameters.
func Param(p int, hasMore bool) (s Parameter) {
s = Parameter(p & parser.ParamMask)
if hasMore {
s |= Parameter(parser.HasMoreFlag)
}
return
}

29
vendor/github.com/charmbracelet/x/ansi/parser_sync.go generated vendored Normal file
View File

@ -0,0 +1,29 @@
package ansi
import (
"sync"
"github.com/charmbracelet/x/ansi/parser"
)
var parserPool = sync.Pool{
New: func() any {
p := NewParser(nil)
p.SetParamsSize(parser.MaxParamsSize)
p.SetDataSize(1024 * 1024 * 4) // 4MB of data buffer
return p
},
}
// GetParser returns a parser from a sync pool.
func GetParser() *Parser {
return parserPool.Get().(*Parser)
}
// PutParser returns a parser to a sync pool. The parser is reset
// automatically.
func PutParser(p *Parser) {
p.Reset()
p.dataLen = 0
parserPool.Put(p)
}

7
vendor/github.com/charmbracelet/x/ansi/paste.go generated vendored Normal file
View File

@ -0,0 +1,7 @@
package ansi
// BracketedPasteStart is the control sequence to enable bracketed paste mode.
const BracketedPasteStart = "\x1b[200~"
// BracketedPasteEnd is the control sequence to disable bracketed paste mode.
const BracketedPasteEnd = "\x1b[201~"

11
vendor/github.com/charmbracelet/x/ansi/reset.go generated vendored Normal file
View File

@ -0,0 +1,11 @@
package ansi
// ResetInitialState (RIS) resets the terminal to its initial state.
//
// ESC c
//
// See: https://vt100.net/docs/vt510-rm/RIS.html
const (
ResetInitialState = "\x1bc"
RIS = ResetInitialState
)

View File

@ -1,30 +1,44 @@
package ansi
import "strconv"
import (
"strconv"
"strings"
)
// EraseDisplay (ED) clears the screen or parts of the screen. Possible values:
// EraseDisplay (ED) clears the display or parts of the display. A screen is
// the shown part of the terminal display excluding the scrollback buffer.
// Possible values:
//
// Default is 0.
//
// 0: Clear from cursor to end of screen.
// 1: Clear from cursor to beginning of the screen.
// 2: Clear entire screen (and moves cursor to upper left on DOS).
// 3: Clear entire screen and delete all lines saved in the scrollback buffer.
// 3: Clear entire display which delete all lines saved in the scrollback buffer (xterm).
//
// CSI <n> J
//
// See: https://vt100.net/docs/vt510-rm/ED.html
func EraseDisplay(n int) string {
if n < 0 {
n = 0
var s string
if n > 0 {
s = strconv.Itoa(n)
}
return "\x1b[" + strconv.Itoa(n) + "J"
return "\x1b[" + s + "J"
}
// ED is an alias for [EraseDisplay].
func ED(n int) string {
return EraseDisplay(n)
}
// EraseDisplay constants.
// These are the possible values for the EraseDisplay function.
const (
EraseDisplayRight = "\x1b[0J"
EraseDisplayLeft = "\x1b[1J"
EraseEntireDisplay = "\x1b[2J"
EraseScreenBelow = "\x1b[J"
EraseScreenAbove = "\x1b[1J"
EraseEntireScreen = "\x1b[2J"
EraseEntireDisplay = "\x1b[3J"
)
// EraseLine (EL) clears the current line or parts of the line. Possible values:
@ -39,16 +53,22 @@ const (
//
// See: https://vt100.net/docs/vt510-rm/EL.html
func EraseLine(n int) string {
if n < 0 {
n = 0
var s string
if n > 0 {
s = strconv.Itoa(n)
}
return "\x1b[" + strconv.Itoa(n) + "K"
return "\x1b[" + s + "K"
}
// EL is an alias for [EraseLine].
func EL(n int) string {
return EraseLine(n)
}
// EraseLine constants.
// These are the possible values for the EraseLine function.
const (
EraseLineRight = "\x1b[0K"
EraseLineRight = "\x1b[K"
EraseLineLeft = "\x1b[1K"
EraseEntireLine = "\x1b[2K"
)
@ -56,7 +76,7 @@ const (
// ScrollUp (SU) scrolls the screen up n lines. New lines are added at the
// bottom of the screen.
//
// CSI <n> S
// CSI Pn S
//
// See: https://vt100.net/docs/vt510-rm/SU.html
func ScrollUp(n int) string {
@ -67,10 +87,20 @@ func ScrollUp(n int) string {
return "\x1b[" + s + "S"
}
// PanDown is an alias for [ScrollUp].
func PanDown(n int) string {
return ScrollUp(n)
}
// SU is an alias for [ScrollUp].
func SU(n int) string {
return ScrollUp(n)
}
// ScrollDown (SD) scrolls the screen down n lines. New lines are added at the
// top of the screen.
//
// CSI <n> T
// CSI Pn T
//
// See: https://vt100.net/docs/vt510-rm/SD.html
func ScrollDown(n int) string {
@ -81,10 +111,20 @@ func ScrollDown(n int) string {
return "\x1b[" + s + "T"
}
// PanUp is an alias for [ScrollDown].
func PanUp(n int) string {
return ScrollDown(n)
}
// SD is an alias for [ScrollDown].
func SD(n int) string {
return ScrollDown(n)
}
// InsertLine (IL) inserts n blank lines at the current cursor position.
// Existing lines are moved down.
//
// CSI <n> L
// CSI Pn L
//
// See: https://vt100.net/docs/vt510-rm/IL.html
func InsertLine(n int) string {
@ -95,10 +135,15 @@ func InsertLine(n int) string {
return "\x1b[" + s + "L"
}
// IL is an alias for [InsertLine].
func IL(n int) string {
return InsertLine(n)
}
// DeleteLine (DL) deletes n lines at the current cursor position. Existing
// lines are moved up.
//
// CSI <n> M
// CSI Pn M
//
// See: https://vt100.net/docs/vt510-rm/DL.html
func DeleteLine(n int) string {
@ -109,12 +154,66 @@ func DeleteLine(n int) string {
return "\x1b[" + s + "M"
}
// DL is an alias for [DeleteLine].
func DL(n int) string {
return DeleteLine(n)
}
// SetTopBottomMargins (DECSTBM) sets the top and bottom margins for the scrolling
// region. The default is the entire screen.
//
// Default is 1 and the bottom of the screen.
//
// CSI Pt ; Pb r
//
// See: https://vt100.net/docs/vt510-rm/DECSTBM.html
func SetTopBottomMargins(top, bot int) string {
var t, b string
if top > 0 {
t = strconv.Itoa(top)
}
if bot > 0 {
b = strconv.Itoa(bot)
}
return "\x1b[" + t + ";" + b + "r"
}
// DECSTBM is an alias for [SetTopBottomMargins].
func DECSTBM(top, bot int) string {
return SetTopBottomMargins(top, bot)
}
// SetLeftRightMargins (DECSLRM) sets the left and right margins for the scrolling
// region.
//
// Default is 1 and the right of the screen.
//
// CSI Pl ; Pr s
//
// See: https://vt100.net/docs/vt510-rm/DECSLRM.html
func SetLeftRightMargins(left, right int) string {
var l, r string
if left > 0 {
l = strconv.Itoa(left)
}
if right > 0 {
r = strconv.Itoa(right)
}
return "\x1b[" + l + ";" + r + "s"
}
// DECSLRM is an alias for [SetLeftRightMargins].
func DECSLRM(left, right int) string {
return SetLeftRightMargins(left, right)
}
// SetScrollingRegion (DECSTBM) sets the top and bottom margins for the scrolling
// region. The default is the entire screen.
//
// CSI <top> ; <bottom> r
//
// See: https://vt100.net/docs/vt510-rm/DECSTBM.html
// Deprecated: use [SetTopBottomMargins] instead.
func SetScrollingRegion(t, b int) string {
if t < 0 {
t = 0
@ -124,3 +223,187 @@ func SetScrollingRegion(t, b int) string {
}
return "\x1b[" + strconv.Itoa(t) + ";" + strconv.Itoa(b) + "r"
}
// InsertCharacter (ICH) inserts n blank characters at the current cursor
// position. Existing characters move to the right. Characters moved past the
// right margin are lost. ICH has no effect outside the scrolling margins.
//
// Default is 1.
//
// CSI Pn @
//
// See: https://vt100.net/docs/vt510-rm/ICH.html
func InsertCharacter(n int) string {
var s string
if n > 1 {
s = strconv.Itoa(n)
}
return "\x1b[" + s + "@"
}
// ICH is an alias for [InsertCharacter].
func ICH(n int) string {
return InsertCharacter(n)
}
// DeleteCharacter (DCH) deletes n characters at the current cursor position.
// As the characters are deleted, the remaining characters move to the left and
// the cursor remains at the same position.
//
// Default is 1.
//
// CSI Pn P
//
// See: https://vt100.net/docs/vt510-rm/DCH.html
func DeleteCharacter(n int) string {
var s string
if n > 1 {
s = strconv.Itoa(n)
}
return "\x1b[" + s + "P"
}
// DCH is an alias for [DeleteCharacter].
func DCH(n int) string {
return DeleteCharacter(n)
}
// SetTabEvery8Columns (DECST8C) sets the tab stops at every 8 columns.
//
// CSI ? 5 W
//
// See: https://vt100.net/docs/vt510-rm/DECST8C.html
const (
SetTabEvery8Columns = "\x1b[?5W"
DECST8C = SetTabEvery8Columns
)
// HorizontalTabSet (HTS) sets a horizontal tab stop at the current cursor
// column.
//
// This is equivalent to [HTS].
//
// ESC H
//
// See: https://vt100.net/docs/vt510-rm/HTS.html
const HorizontalTabSet = "\x1bH"
// TabClear (TBC) clears tab stops.
//
// Default is 0.
//
// Possible values:
// 0: Clear tab stop at the current column. (default)
// 3: Clear all tab stops.
//
// CSI Pn g
//
// See: https://vt100.net/docs/vt510-rm/TBC.html
func TabClear(n int) string {
var s string
if n > 0 {
s = strconv.Itoa(n)
}
return "\x1b[" + s + "g"
}
// TBC is an alias for [TabClear].
func TBC(n int) string {
return TabClear(n)
}
// RequestPresentationStateReport (DECRQPSR) requests the terminal to send a
// report of the presentation state. This includes the cursor information [DECCIR],
// and tab stop [DECTABSR] reports.
//
// Default is 0.
//
// Possible values:
// 0: Error, request ignored.
// 1: Cursor information report [DECCIR].
// 2: Tab stop report [DECTABSR].
//
// CSI Ps $ w
//
// See: https://vt100.net/docs/vt510-rm/DECRQPSR.html
func RequestPresentationStateReport(n int) string {
var s string
if n > 0 {
s = strconv.Itoa(n)
}
return "\x1b[" + s + "$w"
}
// DECRQPSR is an alias for [RequestPresentationStateReport].
func DECRQPSR(n int) string {
return RequestPresentationStateReport(n)
}
// TabStopReport (DECTABSR) is the response to a tab stop report request.
// It reports the tab stops set in the terminal.
//
// The response is a list of tab stops separated by a slash (/) character.
//
// DCS 2 $ u D ... D ST
//
// Where D is a decimal number representing a tab stop.
//
// See: https://vt100.net/docs/vt510-rm/DECTABSR.html
func TabStopReport(stops ...int) string {
var s []string
for _, v := range stops {
s = append(s, strconv.Itoa(v))
}
return "\x1bP2$u" + strings.Join(s, "/") + "\x1b\\"
}
// DECTABSR is an alias for [TabStopReport].
func DECTABSR(stops ...int) string {
return TabStopReport(stops...)
}
// CursorInformationReport (DECCIR) is the response to a cursor information
// report request. It reports the cursor position, visual attributes, and
// character protection attributes. It also reports the status of origin mode
// [DECOM] and the current active character set.
//
// The response is a list of values separated by a semicolon (;) character.
//
// DCS 1 $ u D ... D ST
//
// Where D is a decimal number representing a value.
//
// See: https://vt100.net/docs/vt510-rm/DECCIR.html
func CursorInformationReport(values ...int) string {
var s []string
for _, v := range values {
s = append(s, strconv.Itoa(v))
}
return "\x1bP1$u" + strings.Join(s, ";") + "\x1b\\"
}
// DECCIR is an alias for [CursorInformationReport].
func DECCIR(values ...int) string {
return CursorInformationReport(values...)
}
// RepeatPreviousCharacter (REP) repeats the previous character n times.
// This is identical to typing the same character n times.
//
// Default is 1.
//
// CSI Pn b
//
// See: ECMA-48 § 8.3.103
func RepeatPreviousCharacter(n int) string {
var s string
if n > 1 {
s = strconv.Itoa(n)
}
return "\x1b[" + s + "b"
}
// REP is an alias for [RepeatPreviousCharacter].
func REP(n int) string {
return RepeatPreviousCharacter(n)
}

View File

@ -8,12 +8,19 @@ import (
// Sequence represents an ANSI sequence. This can be a control sequence, escape
// sequence, a printable character, etc.
// A Sequence can be one of the following types:
// - [Rune]
// - [ControlCode]
// - [Grapheme]
// - [EscSequence]
// - [CsiSequence]
// - [OscSequence]
// - [DcsSequence]
// - [SosSequence]
// - [PmSequence]
// - [ApcSequence]
type Sequence interface {
// String returns the string representation of the sequence.
String() string
// Bytes returns the byte representation of the sequence.
Bytes() []byte
// Clone returns a copy of the sequence.
// Clone returns a deep copy of the sequence.
Clone() Sequence
}
@ -22,21 +29,24 @@ type Rune rune
var _ Sequence = Rune(0)
// Bytes implements Sequence.
func (r Rune) Bytes() []byte {
return []byte(string(r))
}
// String implements Sequence.
func (r Rune) String() string {
return string(r)
}
// Clone implements Sequence.
// Clone returns a deep copy of the rune.
func (r Rune) Clone() Sequence {
return r
}
// Grapheme represents a grapheme cluster.
type Grapheme struct {
Cluster string
Width int
}
var _ Sequence = Grapheme{}
// Clone returns a deep copy of the grapheme.
func (g Grapheme) Clone() Sequence {
return g
}
// ControlCode represents a control code character. This is a character that
// is not printable and is used to control the terminal. This would be a
// character in the C0 or C1 set in the range of 0x00-0x1F and 0x80-0x9F.
@ -54,13 +64,13 @@ func (c ControlCode) String() string {
return string(c)
}
// Clone implements Sequence.
// Clone returns a deep copy of the control code.
func (c ControlCode) Clone() Sequence {
return c
}
// EscSequence represents an escape sequence.
type EscSequence int
type EscSequence Command
var _ Sequence = EscSequence(0)
@ -71,7 +81,9 @@ func (e EscSequence) buffer() *bytes.Buffer {
if i := parser.Intermediate(int(e)); i != 0 {
b.WriteByte(byte(i))
}
b.WriteByte(byte(e.Command()))
if cmd := e.Command(); cmd != 0 {
b.WriteByte(byte(cmd))
}
return &b
}
@ -85,19 +97,19 @@ func (e EscSequence) String() string {
return e.buffer().String()
}
// Clone implements Sequence.
// Clone returns a deep copy of the escape sequence.
func (e EscSequence) Clone() Sequence {
return e
}
// Command returns the command byte of the escape sequence.
func (e EscSequence) Command() int {
return parser.Command(int(e))
return Command(e).Command()
}
// Intermediate returns the intermediate byte of the escape sequence.
func (e EscSequence) Intermediate() int {
return parser.Intermediate(int(e))
return Command(e).Intermediate()
}
// SosSequence represents a SOS sequence.
@ -106,12 +118,7 @@ type SosSequence struct {
Data []byte
}
var _ Sequence = &SosSequence{}
// Clone implements Sequence.
func (s SosSequence) Clone() Sequence {
return SosSequence{Data: append([]byte(nil), s.Data...)}
}
var _ Sequence = SosSequence{}
// Bytes implements Sequence.
func (s SosSequence) Bytes() []byte {
@ -132,18 +139,20 @@ func (s SosSequence) buffer() *bytes.Buffer {
return &b
}
// Clone returns a deep copy of the SOS sequence.
func (s SosSequence) Clone() Sequence {
return SosSequence{
Data: append([]byte(nil), s.Data...),
}
}
// PmSequence represents a PM sequence.
type PmSequence struct {
// Data contains the raw data of the sequence.
Data []byte
}
var _ Sequence = &PmSequence{}
// Clone implements Sequence.
func (s PmSequence) Clone() Sequence {
return PmSequence{Data: append([]byte(nil), s.Data...)}
}
var _ Sequence = PmSequence{}
// Bytes implements Sequence.
func (s PmSequence) Bytes() []byte {
@ -165,17 +174,26 @@ func (s PmSequence) buffer() *bytes.Buffer {
return &b
}
// Clone returns a deep copy of the PM sequence.
func (p PmSequence) Clone() Sequence {
return PmSequence{
Data: append([]byte(nil), p.Data...),
}
}
// ApcSequence represents an APC sequence.
type ApcSequence struct {
// Data contains the raw data of the sequence.
Data []byte
}
var _ Sequence = &ApcSequence{}
var _ Sequence = ApcSequence{}
// Clone implements Sequence.
func (s ApcSequence) Clone() Sequence {
return ApcSequence{Data: append([]byte(nil), s.Data...)}
// Clone returns a deep copy of the APC sequence.
func (a ApcSequence) Clone() Sequence {
return ApcSequence{
Data: append([]byte(nil), a.Data...),
}
}
// Bytes implements Sequence.

95
vendor/github.com/charmbracelet/x/ansi/sgr.go generated vendored Normal file
View File

@ -0,0 +1,95 @@
package ansi
import "strconv"
// Select Graphic Rendition (SGR) is a command that sets display attributes.
//
// Default is 0.
//
// CSI Ps ; Ps ... m
//
// See: https://vt100.net/docs/vt510-rm/SGR.html
func SelectGraphicRendition(ps ...Attr) string {
if len(ps) == 0 {
return ResetStyle
}
var s Style
for _, p := range ps {
attr, ok := attrStrings[p]
if ok {
s = append(s, attr)
} else {
if p < 0 {
p = 0
}
s = append(s, strconv.Itoa(p))
}
}
return s.String()
}
// SGR is an alias for [SelectGraphicRendition].
func SGR(ps ...Attr) string {
return SelectGraphicRendition(ps...)
}
var attrStrings = map[int]string{
ResetAttr: "0",
BoldAttr: "1",
FaintAttr: "2",
ItalicAttr: "3",
UnderlineAttr: "4",
SlowBlinkAttr: "5",
RapidBlinkAttr: "6",
ReverseAttr: "7",
ConcealAttr: "8",
StrikethroughAttr: "9",
NoBoldAttr: "21",
NormalIntensityAttr: "22",
NoItalicAttr: "23",
NoUnderlineAttr: "24",
NoBlinkAttr: "25",
NoReverseAttr: "27",
NoConcealAttr: "28",
NoStrikethroughAttr: "29",
BlackForegroundColorAttr: "30",
RedForegroundColorAttr: "31",
GreenForegroundColorAttr: "32",
YellowForegroundColorAttr: "33",
BlueForegroundColorAttr: "34",
MagentaForegroundColorAttr: "35",
CyanForegroundColorAttr: "36",
WhiteForegroundColorAttr: "37",
ExtendedForegroundColorAttr: "38",
DefaultForegroundColorAttr: "39",
BlackBackgroundColorAttr: "40",
RedBackgroundColorAttr: "41",
GreenBackgroundColorAttr: "42",
YellowBackgroundColorAttr: "43",
BlueBackgroundColorAttr: "44",
MagentaBackgroundColorAttr: "45",
CyanBackgroundColorAttr: "46",
WhiteBackgroundColorAttr: "47",
ExtendedBackgroundColorAttr: "48",
DefaultBackgroundColorAttr: "49",
ExtendedUnderlineColorAttr: "58",
DefaultUnderlineColorAttr: "59",
BrightBlackForegroundColorAttr: "90",
BrightRedForegroundColorAttr: "91",
BrightGreenForegroundColorAttr: "92",
BrightYellowForegroundColorAttr: "93",
BrightBlueForegroundColorAttr: "94",
BrightMagentaForegroundColorAttr: "95",
BrightCyanForegroundColorAttr: "96",
BrightWhiteForegroundColorAttr: "97",
BrightBlackBackgroundColorAttr: "100",
BrightRedBackgroundColorAttr: "101",
BrightGreenBackgroundColorAttr: "102",
BrightYellowBackgroundColorAttr: "103",
BrightBlueBackgroundColorAttr: "104",
BrightMagentaBackgroundColorAttr: "105",
BrightCyanBackgroundColorAttr: "106",
BrightWhiteBackgroundColorAttr: "107",
}

115
vendor/github.com/charmbracelet/x/ansi/status.go generated vendored Normal file
View File

@ -0,0 +1,115 @@
package ansi
import (
"strconv"
"strings"
)
// Status represents a terminal status report.
type Status interface {
// Status returns the status report identifier.
Status() int
}
// ANSIStatus represents an ANSI terminal status report.
type ANSIStatus int //nolint:revive
// Status returns the status report identifier.
func (s ANSIStatus) Status() int {
return int(s)
}
// DECStatus represents a DEC terminal status report.
type DECStatus int
// Status returns the status report identifier.
func (s DECStatus) Status() int {
return int(s)
}
// DeviceStatusReport (DSR) is a control sequence that reports the terminal's
// status.
// The terminal responds with a DSR sequence.
//
// CSI Ps n
// CSI ? Ps n
//
// If one of the statuses is a [DECStatus], the sequence will use the DEC
// format.
//
// See also https://vt100.net/docs/vt510-rm/DSR.html
func DeviceStatusReport(statues ...Status) string {
var dec bool
list := make([]string, len(statues))
seq := "\x1b["
for i, status := range statues {
list[i] = strconv.Itoa(status.Status())
switch status.(type) {
case DECStatus:
dec = true
}
}
if dec {
seq += "?"
}
return seq + strings.Join(list, ";") + "n"
}
// DSR is an alias for [DeviceStatusReport].
func DSR(status Status) string {
return DeviceStatusReport(status)
}
// CursorPositionReport (CPR) is a control sequence that reports the cursor's
// position.
//
// CSI Pl ; Pc R
//
// Where Pl is the line number and Pc is the column number.
//
// See also https://vt100.net/docs/vt510-rm/CPR.html
func CursorPositionReport(line, column int) string {
if line < 1 {
line = 1
}
if column < 1 {
column = 1
}
return "\x1b[" + strconv.Itoa(line) + ";" + strconv.Itoa(column) + "R"
}
// CPR is an alias for [CursorPositionReport].
func CPR(line, column int) string {
return CursorPositionReport(line, column)
}
// ExtendedCursorPositionReport (DECXCPR) is a control sequence that reports the
// cursor's position along with the page number (optional).
//
// CSI ? Pl ; Pc R
// CSI ? Pl ; Pc ; Pv R
//
// Where Pl is the line number, Pc is the column number, and Pv is the page
// number.
//
// If the page number is zero or negative, the returned sequence won't include
// the page number.
//
// See also https://vt100.net/docs/vt510-rm/DECXCPR.html
func ExtendedCursorPositionReport(line, column, page int) string {
if line < 1 {
line = 1
}
if column < 1 {
column = 1
}
if page < 1 {
return "\x1b[?" + strconv.Itoa(line) + ";" + strconv.Itoa(column) + "R"
}
return "\x1b[?" + strconv.Itoa(line) + ";" + strconv.Itoa(column) + ";" + strconv.Itoa(page) + "R"
}
// DECXCPR is an alias for [ExtendedCursorPositionReport].
func DECXCPR(line, column, page int) string {
return ExtendedCursorPositionReport(line, column, page)
}

View File

@ -12,10 +12,10 @@ import (
const ResetStyle = "\x1b[m"
// Attr is a SGR (Select Graphic Rendition) style attribute.
type Attr = string
type Attr = int
// Style represents an ANSI SGR (Select Graphic Rendition) style.
type Style []Attr
type Style []string
// String returns the ANSI SGR (Select Graphic Rendition) style sequence for
// the given style.
@ -36,186 +36,357 @@ func (s Style) Styled(str string) string {
// Reset appends the reset style attribute to the style.
func (s Style) Reset() Style {
return append(s, ResetAttr)
return append(s, resetAttr)
}
// Bold appends the bold style attribute to the style.
func (s Style) Bold() Style {
return append(s, BoldAttr)
return append(s, boldAttr)
}
// Faint appends the faint style attribute to the style.
func (s Style) Faint() Style {
return append(s, FaintAttr)
return append(s, faintAttr)
}
// Italic appends the italic style attribute to the style.
func (s Style) Italic() Style {
return append(s, ItalicAttr)
return append(s, italicAttr)
}
// Underline appends the underline style attribute to the style.
func (s Style) Underline() Style {
return append(s, UnderlineAttr)
return append(s, underlineAttr)
}
// UnderlineStyle appends the underline style attribute to the style.
func (s Style) UnderlineStyle(u UnderlineStyle) Style {
switch u {
case NoUnderlineStyle:
return s.NoUnderline()
case SingleUnderlineStyle:
return s.Underline()
case DoubleUnderlineStyle:
return append(s, doubleUnderlineStyle)
case CurlyUnderlineStyle:
return append(s, curlyUnderlineStyle)
case DottedUnderlineStyle:
return append(s, dottedUnderlineStyle)
case DashedUnderlineStyle:
return append(s, dashedUnderlineStyle)
}
return s
}
// DoubleUnderline appends the double underline style attribute to the style.
// This is a convenience method for UnderlineStyle(DoubleUnderlineStyle).
func (s Style) DoubleUnderline() Style {
return append(s, DoubleUnderlineAttr)
return s.UnderlineStyle(DoubleUnderlineStyle)
}
// CurlyUnderline appends the curly underline style attribute to the style.
// This is a convenience method for UnderlineStyle(CurlyUnderlineStyle).
func (s Style) CurlyUnderline() Style {
return append(s, CurlyUnderlineAttr)
return s.UnderlineStyle(CurlyUnderlineStyle)
}
// DottedUnderline appends the dotted underline style attribute to the style.
// This is a convenience method for UnderlineStyle(DottedUnderlineStyle).
func (s Style) DottedUnderline() Style {
return append(s, DottedUnderlineAttr)
return s.UnderlineStyle(DottedUnderlineStyle)
}
// DashedUnderline appends the dashed underline style attribute to the style.
// This is a convenience method for UnderlineStyle(DashedUnderlineStyle).
func (s Style) DashedUnderline() Style {
return append(s, DashedUnderlineAttr)
return s.UnderlineStyle(DashedUnderlineStyle)
}
// SlowBlink appends the slow blink style attribute to the style.
func (s Style) SlowBlink() Style {
return append(s, SlowBlinkAttr)
return append(s, slowBlinkAttr)
}
// RapidBlink appends the rapid blink style attribute to the style.
func (s Style) RapidBlink() Style {
return append(s, RapidBlinkAttr)
return append(s, rapidBlinkAttr)
}
// Reverse appends the reverse style attribute to the style.
func (s Style) Reverse() Style {
return append(s, ReverseAttr)
return append(s, reverseAttr)
}
// Conceal appends the conceal style attribute to the style.
func (s Style) Conceal() Style {
return append(s, ConcealAttr)
return append(s, concealAttr)
}
// Strikethrough appends the strikethrough style attribute to the style.
func (s Style) Strikethrough() Style {
return append(s, StrikethroughAttr)
return append(s, strikethroughAttr)
}
// NoBold appends the no bold style attribute to the style.
func (s Style) NoBold() Style {
return append(s, NoBoldAttr)
return append(s, noBoldAttr)
}
// NormalIntensity appends the normal intensity style attribute to the style.
func (s Style) NormalIntensity() Style {
return append(s, NormalIntensityAttr)
return append(s, normalIntensityAttr)
}
// NoItalic appends the no italic style attribute to the style.
func (s Style) NoItalic() Style {
return append(s, NoItalicAttr)
return append(s, noItalicAttr)
}
// NoUnderline appends the no underline style attribute to the style.
func (s Style) NoUnderline() Style {
return append(s, NoUnderlineAttr)
return append(s, noUnderlineAttr)
}
// NoBlink appends the no blink style attribute to the style.
func (s Style) NoBlink() Style {
return append(s, NoBlinkAttr)
return append(s, noBlinkAttr)
}
// NoReverse appends the no reverse style attribute to the style.
func (s Style) NoReverse() Style {
return append(s, NoReverseAttr)
return append(s, noReverseAttr)
}
// NoConceal appends the no conceal style attribute to the style.
func (s Style) NoConceal() Style {
return append(s, NoConcealAttr)
return append(s, noConcealAttr)
}
// NoStrikethrough appends the no strikethrough style attribute to the style.
func (s Style) NoStrikethrough() Style {
return append(s, NoStrikethroughAttr)
return append(s, noStrikethroughAttr)
}
// DefaultForegroundColor appends the default foreground color style attribute to the style.
func (s Style) DefaultForegroundColor() Style {
return append(s, DefaultForegroundColorAttr)
return append(s, defaultForegroundColorAttr)
}
// DefaultBackgroundColor appends the default background color style attribute to the style.
func (s Style) DefaultBackgroundColor() Style {
return append(s, DefaultBackgroundColorAttr)
return append(s, defaultBackgroundColorAttr)
}
// DefaultUnderlineColor appends the default underline color style attribute to the style.
func (s Style) DefaultUnderlineColor() Style {
return append(s, DefaultUnderlineColorAttr)
return append(s, defaultUnderlineColorAttr)
}
// ForegroundColor appends the foreground color style attribute to the style.
func (s Style) ForegroundColor(c Color) Style {
return append(s, ForegroundColorAttr(c))
return append(s, foregroundColorString(c))
}
// BackgroundColor appends the background color style attribute to the style.
func (s Style) BackgroundColor(c Color) Style {
return append(s, BackgroundColorAttr(c))
return append(s, backgroundColorString(c))
}
// UnderlineColor appends the underline color style attribute to the style.
func (s Style) UnderlineColor(c Color) Style {
return append(s, UnderlineColorAttr(c))
return append(s, underlineColorString(c))
}
// UnderlineStyle represents an ANSI SGR (Select Graphic Rendition) underline
// style.
type UnderlineStyle = int
const (
doubleUnderlineStyle = "4:2"
curlyUnderlineStyle = "4:3"
dottedUnderlineStyle = "4:4"
dashedUnderlineStyle = "4:5"
)
const (
// NoUnderlineStyle is the default underline style.
NoUnderlineStyle UnderlineStyle = iota
// SingleUnderlineStyle is a single underline style.
SingleUnderlineStyle
// DoubleUnderlineStyle is a double underline style.
DoubleUnderlineStyle
// CurlyUnderlineStyle is a curly underline style.
CurlyUnderlineStyle
// DottedUnderlineStyle is a dotted underline style.
DottedUnderlineStyle
// DashedUnderlineStyle is a dashed underline style.
DashedUnderlineStyle
)
// SGR (Select Graphic Rendition) style attributes.
// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
const (
ResetAttr Attr = "0"
BoldAttr Attr = "1"
FaintAttr Attr = "2"
ItalicAttr Attr = "3"
UnderlineAttr Attr = "4"
DoubleUnderlineAttr Attr = "4:2"
CurlyUnderlineAttr Attr = "4:3"
DottedUnderlineAttr Attr = "4:4"
DashedUnderlineAttr Attr = "4:5"
SlowBlinkAttr Attr = "5"
RapidBlinkAttr Attr = "6"
ReverseAttr Attr = "7"
ConcealAttr Attr = "8"
StrikethroughAttr Attr = "9"
NoBoldAttr Attr = "21" // Some terminals treat this as double underline.
NormalIntensityAttr Attr = "22"
NoItalicAttr Attr = "23"
NoUnderlineAttr Attr = "24"
NoBlinkAttr Attr = "25"
NoReverseAttr Attr = "27"
NoConcealAttr Attr = "28"
NoStrikethroughAttr Attr = "29"
DefaultForegroundColorAttr Attr = "39"
DefaultBackgroundColorAttr Attr = "49"
DefaultUnderlineColorAttr Attr = "59"
ResetAttr Attr = 0
BoldAttr Attr = 1
FaintAttr Attr = 2
ItalicAttr Attr = 3
UnderlineAttr Attr = 4
SlowBlinkAttr Attr = 5
RapidBlinkAttr Attr = 6
ReverseAttr Attr = 7
ConcealAttr Attr = 8
StrikethroughAttr Attr = 9
NoBoldAttr Attr = 21 // Some terminals treat this as double underline.
NormalIntensityAttr Attr = 22
NoItalicAttr Attr = 23
NoUnderlineAttr Attr = 24
NoBlinkAttr Attr = 25
NoReverseAttr Attr = 27
NoConcealAttr Attr = 28
NoStrikethroughAttr Attr = 29
BlackForegroundColorAttr Attr = 30
RedForegroundColorAttr Attr = 31
GreenForegroundColorAttr Attr = 32
YellowForegroundColorAttr Attr = 33
BlueForegroundColorAttr Attr = 34
MagentaForegroundColorAttr Attr = 35
CyanForegroundColorAttr Attr = 36
WhiteForegroundColorAttr Attr = 37
ExtendedForegroundColorAttr Attr = 38
DefaultForegroundColorAttr Attr = 39
BlackBackgroundColorAttr Attr = 40
RedBackgroundColorAttr Attr = 41
GreenBackgroundColorAttr Attr = 42
YellowBackgroundColorAttr Attr = 43
BlueBackgroundColorAttr Attr = 44
MagentaBackgroundColorAttr Attr = 45
CyanBackgroundColorAttr Attr = 46
WhiteBackgroundColorAttr Attr = 47
ExtendedBackgroundColorAttr Attr = 48
DefaultBackgroundColorAttr Attr = 49
ExtendedUnderlineColorAttr Attr = 58
DefaultUnderlineColorAttr Attr = 59
BrightBlackForegroundColorAttr Attr = 90
BrightRedForegroundColorAttr Attr = 91
BrightGreenForegroundColorAttr Attr = 92
BrightYellowForegroundColorAttr Attr = 93
BrightBlueForegroundColorAttr Attr = 94
BrightMagentaForegroundColorAttr Attr = 95
BrightCyanForegroundColorAttr Attr = 96
BrightWhiteForegroundColorAttr Attr = 97
BrightBlackBackgroundColorAttr Attr = 100
BrightRedBackgroundColorAttr Attr = 101
BrightGreenBackgroundColorAttr Attr = 102
BrightYellowBackgroundColorAttr Attr = 103
BrightBlueBackgroundColorAttr Attr = 104
BrightMagentaBackgroundColorAttr Attr = 105
BrightCyanBackgroundColorAttr Attr = 106
BrightWhiteBackgroundColorAttr Attr = 107
RGBColorIntroducerAttr Attr = 2
ExtendedColorIntroducerAttr Attr = 5
)
// ForegroundColorAttr returns the style SGR attribute for the given foreground
// color.
const (
resetAttr = "0"
boldAttr = "1"
faintAttr = "2"
italicAttr = "3"
underlineAttr = "4"
slowBlinkAttr = "5"
rapidBlinkAttr = "6"
reverseAttr = "7"
concealAttr = "8"
strikethroughAttr = "9"
noBoldAttr = "21"
normalIntensityAttr = "22"
noItalicAttr = "23"
noUnderlineAttr = "24"
noBlinkAttr = "25"
noReverseAttr = "27"
noConcealAttr = "28"
noStrikethroughAttr = "29"
blackForegroundColorAttr = "30"
redForegroundColorAttr = "31"
greenForegroundColorAttr = "32"
yellowForegroundColorAttr = "33"
blueForegroundColorAttr = "34"
magentaForegroundColorAttr = "35"
cyanForegroundColorAttr = "36"
whiteForegroundColorAttr = "37"
extendedForegroundColorAttr = "38"
defaultForegroundColorAttr = "39"
blackBackgroundColorAttr = "40"
redBackgroundColorAttr = "41"
greenBackgroundColorAttr = "42"
yellowBackgroundColorAttr = "43"
blueBackgroundColorAttr = "44"
magentaBackgroundColorAttr = "45"
cyanBackgroundColorAttr = "46"
whiteBackgroundColorAttr = "47"
extendedBackgroundColorAttr = "48"
defaultBackgroundColorAttr = "49"
extendedUnderlineColorAttr = "58"
defaultUnderlineColorAttr = "59"
brightBlackForegroundColorAttr = "90"
brightRedForegroundColorAttr = "91"
brightGreenForegroundColorAttr = "92"
brightYellowForegroundColorAttr = "93"
brightBlueForegroundColorAttr = "94"
brightMagentaForegroundColorAttr = "95"
brightCyanForegroundColorAttr = "96"
brightWhiteForegroundColorAttr = "97"
brightBlackBackgroundColorAttr = "100"
brightRedBackgroundColorAttr = "101"
brightGreenBackgroundColorAttr = "102"
brightYellowBackgroundColorAttr = "103"
brightBlueBackgroundColorAttr = "104"
brightMagentaBackgroundColorAttr = "105"
brightCyanBackgroundColorAttr = "106"
brightWhiteBackgroundColorAttr = "107"
)
// foregroundColorString returns the style SGR attribute for the given
// foreground color.
// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
func ForegroundColorAttr(c Color) Attr {
func foregroundColorString(c Color) string {
switch c := c.(type) {
case BasicColor:
// 3-bit or 4-bit ANSI foreground
// "3<n>" or "9<n>" where n is the color number from 0 to 7
if c < 8 {
return "3" + string('0'+c)
} else if c < 16 {
return "9" + string('0'+c-8)
switch c {
case Black:
return blackForegroundColorAttr
case Red:
return redForegroundColorAttr
case Green:
return greenForegroundColorAttr
case Yellow:
return yellowForegroundColorAttr
case Blue:
return blueForegroundColorAttr
case Magenta:
return magentaForegroundColorAttr
case Cyan:
return cyanForegroundColorAttr
case White:
return whiteForegroundColorAttr
case BrightBlack:
return brightBlackForegroundColorAttr
case BrightRed:
return brightRedForegroundColorAttr
case BrightGreen:
return brightGreenForegroundColorAttr
case BrightYellow:
return brightYellowForegroundColorAttr
case BrightBlue:
return brightBlueForegroundColorAttr
case BrightMagenta:
return brightMagentaForegroundColorAttr
case BrightCyan:
return brightCyanForegroundColorAttr
case BrightWhite:
return brightWhiteForegroundColorAttr
}
case ExtendedColor:
// 256-color ANSI foreground
@ -230,21 +401,50 @@ func ForegroundColorAttr(c Color) Attr {
strconv.FormatUint(uint64(shift(g)), 10) + ";" +
strconv.FormatUint(uint64(shift(b)), 10)
}
return DefaultForegroundColorAttr
return defaultForegroundColorAttr
}
// BackgroundColorAttr returns the style SGR attribute for the given background
// color.
// backgroundColorString returns the style SGR attribute for the given
// background color.
// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
func BackgroundColorAttr(c Color) Attr {
func backgroundColorString(c Color) string {
switch c := c.(type) {
case BasicColor:
// 3-bit or 4-bit ANSI foreground
// "4<n>" or "10<n>" where n is the color number from 0 to 7
if c < 8 {
return "4" + string('0'+c)
} else {
return "10" + string('0'+c-8)
switch c {
case Black:
return blackBackgroundColorAttr
case Red:
return redBackgroundColorAttr
case Green:
return greenBackgroundColorAttr
case Yellow:
return yellowBackgroundColorAttr
case Blue:
return blueBackgroundColorAttr
case Magenta:
return magentaBackgroundColorAttr
case Cyan:
return cyanBackgroundColorAttr
case White:
return whiteBackgroundColorAttr
case BrightBlack:
return brightBlackBackgroundColorAttr
case BrightRed:
return brightRedBackgroundColorAttr
case BrightGreen:
return brightGreenBackgroundColorAttr
case BrightYellow:
return brightYellowBackgroundColorAttr
case BrightBlue:
return brightBlueBackgroundColorAttr
case BrightMagenta:
return brightMagentaBackgroundColorAttr
case BrightCyan:
return brightCyanBackgroundColorAttr
case BrightWhite:
return brightWhiteBackgroundColorAttr
}
case ExtendedColor:
// 256-color ANSI foreground
@ -259,13 +459,13 @@ func BackgroundColorAttr(c Color) Attr {
strconv.FormatUint(uint64(shift(g)), 10) + ";" +
strconv.FormatUint(uint64(shift(b)), 10)
}
return DefaultBackgroundColorAttr
return defaultBackgroundColorAttr
}
// UnderlineColorAttr returns the style SGR attribute for the given underline
// underlineColorString returns the style SGR attribute for the given underline
// color.
// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
func UnderlineColorAttr(c Color) Attr {
func underlineColorString(c Color) string {
switch c := c.(type) {
// NOTE: we can't use 3-bit and 4-bit ANSI color codes with underline
// color, use 256-color instead.
@ -285,12 +485,5 @@ func UnderlineColorAttr(c Color) Attr {
strconv.FormatUint(uint64(shift(g)), 10) + ";" +
strconv.FormatUint(uint64(shift(b)), 10)
}
return DefaultUnderlineColorAttr
}
func shift(v uint32) uint32 {
if v > 0xff {
return v >> 8
}
return v
return defaultUnderlineColorAttr
}

View File

@ -14,7 +14,7 @@ import (
//
// See: https://man7.org/linux/man-pages/man5/terminfo.5.html
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
func RequestTermcap(caps ...string) string {
func XTGETTCAP(caps ...string) string {
if len(caps) == 0 {
return ""
}
@ -29,3 +29,13 @@ func RequestTermcap(caps ...string) string {
return s + "\x1b\\"
}
// RequestTermcap is an alias for [XTGETTCAP].
func RequestTermcap(caps ...string) string {
return XTGETTCAP(caps...)
}
// RequestTerminfo is an alias for [XTGETTCAP].
func RequestTerminfo(caps ...string) string {
return XTGETTCAP(caps...)
}

View File

@ -26,7 +26,6 @@ func Truncate(s string, length int, tail string) string {
var buf bytes.Buffer
curWidth := 0
ignoring := false
gstate := -1
pstate := parser.GroundState // initial state
b := []byte(s)
i := 0
@ -38,13 +37,10 @@ func Truncate(s string, length int, tail string) string {
// collect ANSI escape codes until we reach the end of string.
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
switch action {
case parser.PrintAction:
if utf8ByteLen(b[i]) > 1 {
if state == parser.Utf8State {
// This action happens when we transition to the Utf8State.
var width int
cluster, _, width, gstate = uniseg.FirstGraphemeCluster(b[i:], gstate)
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
// increment the index by the length of the cluster
i += len(cluster)
@ -66,16 +62,15 @@ func Truncate(s string, length int, tail string) string {
}
curWidth += width
for _, r := range cluster {
buf.WriteByte(r)
}
buf.Write(cluster)
gstate = -1 // reset grapheme state otherwise, width calculation might be off
// Done collecting, now we're back in the ground state.
pstate = parser.GroundState
continue
}
switch action {
case parser.PrintAction:
// Is this gonna be too wide?
// If so write the tail and stop collecting.
if curWidth >= length && !ignoring {

View File

@ -3,6 +3,10 @@ package ansi
import (
"fmt"
"image/color"
"strconv"
"strings"
"github.com/lucasb-eyer/go-colorful"
)
// colorToHexString returns a hex string representation of a color.
@ -27,3 +31,62 @@ func colorToHexString(c color.Color) string {
func rgbToHex(r, g, b uint32) uint32 {
return r<<16 + g<<8 + b
}
type shiftable interface {
~uint | ~uint16 | ~uint32 | ~uint64
}
func shift[T shiftable](x T) T {
if x > 0xff {
x >>= 8
}
return x
}
// XParseColor is a helper function that parses a string into a color.Color. It
// provides a similar interface to the XParseColor function in Xlib. It
// supports the following formats:
//
// - #RGB
// - #RRGGBB
// - rgb:RRRR/GGGG/BBBB
// - rgba:RRRR/GGGG/BBBB/AAAA
//
// If the string is not a valid color, nil is returned.
//
// See: https://linux.die.net/man/3/xparsecolor
func XParseColor(s string) color.Color {
switch {
case strings.HasPrefix(s, "#"):
c, err := colorful.Hex(s)
if err != nil {
return nil
}
return c
case strings.HasPrefix(s, "rgb:"):
parts := strings.Split(s[4:], "/")
if len(parts) != 3 {
return nil
}
r, _ := strconv.ParseUint(parts[0], 16, 32)
g, _ := strconv.ParseUint(parts[1], 16, 32)
b, _ := strconv.ParseUint(parts[2], 16, 32)
return color.RGBA{uint8(shift(r)), uint8(shift(g)), uint8(shift(b)), 255} //nolint:gosec
case strings.HasPrefix(s, "rgba:"):
parts := strings.Split(s[5:], "/")
if len(parts) != 4 {
return nil
}
r, _ := strconv.ParseUint(parts[0], 16, 32)
g, _ := strconv.ParseUint(parts[1], 16, 32)
b, _ := strconv.ParseUint(parts[2], 16, 32)
a, _ := strconv.ParseUint(parts[3], 16, 32)
return color.RGBA{uint8(shift(r)), uint8(shift(g)), uint8(shift(b)), uint8(shift(a))} //nolint:gosec
}
return nil
}

View File

@ -19,13 +19,7 @@ func Strip(s string) string {
// This implements a subset of the Parser to only collect runes and
// printable characters.
for i := 0; i < len(s); i++ {
var state, action byte
if pstate != parser.Utf8State {
state, action = parser.Table.Transition(pstate, s[i])
}
switch {
case pstate == parser.Utf8State:
if pstate == parser.Utf8State {
// During this state, collect rw bytes to form a valid rune in the
// buffer. After getting all the rune bytes into the buffer,
// transition to GroundState and reset the counters.
@ -37,16 +31,19 @@ func Strip(s string) string {
pstate = parser.GroundState
ri = 0
rw = 0
case action == parser.PrintAction:
continue
}
state, action := parser.Table.Transition(pstate, s[i])
switch action {
case parser.CollectAction:
if state == parser.Utf8State {
// This action happens when we transition to the Utf8State.
if w := utf8ByteLen(s[i]); w > 1 {
rw = w
rw = utf8ByteLen(s[i])
buf.WriteByte(s[i])
ri++
break
}
fallthrough
case action == parser.ExecuteAction:
case parser.PrintAction, parser.ExecuteAction:
// collects printable ASCII and non-printable characters
buf.WriteByte(s[i])
}
@ -71,7 +68,6 @@ func StringWidth(s string) int {
}
var (
gstate = -1
pstate = parser.GroundState // initial state
cluster string
width int
@ -79,16 +75,16 @@ func StringWidth(s string) int {
for i := 0; i < len(s); i++ {
state, action := parser.Table.Transition(pstate, s[i])
switch action {
case parser.PrintAction:
if utf8ByteLen(s[i]) > 1 {
if state == parser.Utf8State {
var w int
cluster, _, w, gstate = uniseg.FirstGraphemeClusterInString(s[i:], gstate)
cluster, _, w, _ = uniseg.FirstGraphemeClusterInString(s[i:], -1)
width += w
i += len(cluster) - 1
pstate = parser.GroundState
continue
}
if action == parser.PrintAction {
width++
}

View File

@ -27,7 +27,6 @@ func Hardwrap(s string, limit int, preserveSpace bool) string {
buf bytes.Buffer
curWidth int
forceNewline bool
gstate = -1
pstate = parser.GroundState // initial state
b = []byte(s)
)
@ -40,12 +39,9 @@ func Hardwrap(s string, limit int, preserveSpace bool) string {
i := 0
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
switch action {
case parser.PrintAction:
if utf8ByteLen(b[i]) > 1 {
if state == parser.Utf8State {
var width int
cluster, _, width, gstate = uniseg.FirstGraphemeCluster(b[i:], gstate)
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
i += len(cluster)
if curWidth+width > limit {
@ -61,12 +57,12 @@ func Hardwrap(s string, limit int, preserveSpace bool) string {
buf.Write(cluster)
curWidth += width
gstate = -1 // reset grapheme state otherwise, width calculation might be off
pstate = parser.GroundState
continue
}
fallthrough
case parser.ExecuteAction:
switch action {
case parser.PrintAction, parser.ExecuteAction:
if b[i] == '\n' {
addNewline()
forceNewline = false
@ -87,7 +83,9 @@ func Hardwrap(s string, limit int, preserveSpace bool) string {
}
buf.WriteByte(b[i])
if action == parser.PrintAction {
curWidth++
}
default:
buf.WriteByte(b[i])
}
@ -122,7 +120,6 @@ func Wordwrap(s string, limit int, breakpoints string) string {
space bytes.Buffer
curWidth int
wordLen int
gstate = -1
pstate = parser.GroundState // initial state
b = []byte(s)
)
@ -154,12 +151,9 @@ func Wordwrap(s string, limit int, breakpoints string) string {
i := 0
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
switch action {
case parser.PrintAction:
if utf8ByteLen(b[i]) > 1 {
if state == parser.Utf8State {
var width int
cluster, _, width, gstate = uniseg.FirstGraphemeCluster(b[i:], gstate)
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
i += len(cluster)
r, _ := utf8.DecodeRune(cluster)
@ -183,8 +177,9 @@ func Wordwrap(s string, limit int, breakpoints string) string {
pstate = parser.GroundState
continue
}
fallthrough
case parser.ExecuteAction:
switch action {
case parser.PrintAction, parser.ExecuteAction:
r := rune(b[i])
switch {
case r == '\n':
@ -253,7 +248,6 @@ func Wrap(s string, limit int, breakpoints string) string {
space bytes.Buffer
curWidth int // written width of the line
wordLen int // word buffer len without ANSI escape codes
gstate = -1
pstate = parser.GroundState // initial state
b = []byte(s)
)
@ -285,12 +279,9 @@ func Wrap(s string, limit int, breakpoints string) string {
i := 0
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
switch action {
case parser.PrintAction:
if utf8ByteLen(b[i]) > 1 {
if state == parser.Utf8State {
var width int
cluster, _, width, gstate = uniseg.FirstGraphemeCluster(b[i:], gstate)
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
i += len(cluster)
r, _ := utf8.DecodeRune(cluster)
@ -326,8 +317,8 @@ func Wrap(s string, limit int, breakpoints string) string {
continue
}
fallthrough
case parser.ExecuteAction:
switch action {
case parser.PrintAction, parser.ExecuteAction:
switch r := rune(b[i]); {
case r == '\n':
if wordLen == 0 {
@ -360,6 +351,9 @@ func Wrap(s string, limit int, breakpoints string) string {
curWidth++
}
default:
if curWidth == limit {
addNewline()
}
word.WriteRune(r)
wordLen++

View File

@ -1,11 +1,108 @@
package ansi
import "strconv"
// KeyModifierOptions (XTMODKEYS) sets/resets xterm key modifier options.
//
// Default is 0.
//
// CSI > Pp m
// CSI > Pp ; Pv m
//
// If Pv is omitted, the resource is reset to its initial value.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
func KeyModifierOptions(p int, vs ...int) string {
var pp, pv string
if p > 0 {
pp = strconv.Itoa(p)
}
if len(vs) == 0 {
return "\x1b[>" + strconv.Itoa(p) + "m"
}
v := vs[0]
if v > 0 {
pv = strconv.Itoa(v)
return "\x1b[>" + pp + ";" + pv + "m"
}
return "\x1b[>" + pp + "m"
}
// XTMODKEYS is an alias for [KeyModifierOptions].
func XTMODKEYS(p int, vs ...int) string {
return KeyModifierOptions(p, vs...)
}
// SetKeyModifierOptions sets xterm key modifier options.
// This is an alias for [KeyModifierOptions].
func SetKeyModifierOptions(pp int, pv int) string {
return KeyModifierOptions(pp, pv)
}
// ResetKeyModifierOptions resets xterm key modifier options.
// This is an alias for [KeyModifierOptions].
func ResetKeyModifierOptions(pp int) string {
return KeyModifierOptions(pp)
}
// QueryKeyModifierOptions (XTQMODKEYS) requests xterm key modifier options.
//
// Default is 0.
//
// CSI ? Pp m
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
func QueryKeyModifierOptions(pp int) string {
var p string
if pp > 0 {
p = strconv.Itoa(pp)
}
return "\x1b[?" + p + "m"
}
// XTQMODKEYS is an alias for [QueryKeyModifierOptions].
func XTQMODKEYS(pp int) string {
return QueryKeyModifierOptions(pp)
}
// Modify Other Keys (modifyOtherKeys) is an xterm feature that allows the
// terminal to modify the behavior of certain keys to send different escape
// sequences when pressed.
//
// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys
const (
SetModifyOtherKeys1 = "\x1b[>4;1m"
SetModifyOtherKeys2 = "\x1b[>4;2m"
ResetModifyOtherKeys = "\x1b[>4m"
QueryModifyOtherKeys = "\x1b[?4m"
)
// ModifyOtherKeys returns a sequence that sets XTerm modifyOtherKeys mode.
// The mode argument specifies the mode to set.
//
// 0: Disable modifyOtherKeys mode.
// 1: Enable modifyOtherKeys mode 1.
// 2: Enable modifyOtherKeys mode 2.
//
// CSI > 4 ; mode m
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys
// Deprecated: use [SetModifyOtherKeys1] or [SetModifyOtherKeys2] instead.
func ModifyOtherKeys(mode int) string {
return "\x1b[>4;" + strconv.Itoa(mode) + "m"
}
// DisableModifyOtherKeys disables the modifyOtherKeys mode.
//
// CSI > 4 ; 0 m
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys
// Deprecated: use [ResetModifyOtherKeys] instead.
const DisableModifyOtherKeys = "\x1b[>4;0m"
// EnableModifyOtherKeys1 enables the modifyOtherKeys mode 1.
@ -14,6 +111,7 @@ const DisableModifyOtherKeys = "\x1b[>4;0m"
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys
// Deprecated: use [SetModifyOtherKeys1] instead.
const EnableModifyOtherKeys1 = "\x1b[>4;1m"
// EnableModifyOtherKeys2 enables the modifyOtherKeys mode 2.
@ -22,6 +120,7 @@ const EnableModifyOtherKeys1 = "\x1b[>4;1m"
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys
// Deprecated: use [SetModifyOtherKeys2] instead.
const EnableModifyOtherKeys2 = "\x1b[>4;2m"
// RequestModifyOtherKeys requests the modifyOtherKeys mode.
@ -30,4 +129,5 @@ const EnableModifyOtherKeys2 = "\x1b[>4;2m"
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys
// Deprecated: use [QueryModifyOtherKeys] instead.
const RequestModifyOtherKeys = "\x1b[?4m"

View File

@ -206,7 +206,7 @@ func newKeyFromSeed(privateKey, seed []byte) {
func signAll(signature []byte, privateKey PrivateKey, message, ctx []byte, preHash bool) {
if len(ctx) > ContextMaxSize {
panic(fmt.Errorf("ed448: bad context length: " + strconv.Itoa(len(ctx))))
panic(fmt.Errorf("ed448: bad context length: %v", len(ctx)))
}
H := sha3.NewShake256()

View File

@ -107,4 +107,7 @@ var (
// ErrContextNotSupported is the error used if a context is not
// supported.
ErrContextNotSupported = errors.New("context not supported")
// ErrContextTooLong is the error used if the context string is too long.
ErrContextTooLong = errors.New("context string too long")
)

View File

@ -1,16 +0,0 @@
Docker
Copyright 2012-2015 Docker, Inc.
This product includes software developed at Docker, Inc. (https://www.docker.com).
The following is courtesy of our legal counsel:
Use and transfer of Docker may be subject to certain restrictions by the
United States and other governments.
It is your responsibility to ensure that your use and/or transfer does not
violate applicable laws.
For more information, please see https://www.bis.doc.gov
See also https://www.apache.org/dev/crypto.html and/or seek legal counsel.

View File

@ -1,62 +0,0 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package userns
import (
"bufio"
"fmt"
"os"
"sync"
)
var (
inUserNS bool
nsOnce sync.Once
)
// RunningInUserNS detects whether we are currently running in a user namespace.
// Originally copied from github.com/lxc/lxd/shared/util.go
func RunningInUserNS() bool {
nsOnce.Do(func() {
file, err := os.Open("/proc/self/uid_map")
if err != nil {
// This kernel-provided file only exists if user namespaces are supported
return
}
defer file.Close()
buf := bufio.NewReader(file)
l, _, err := buf.ReadLine()
if err != nil {
return
}
line := string(l)
var a, b, c int64
fmt.Sscanf(line, "%d %d %d", &a, &b, &c)
/*
* We assume we are in the initial user namespace if we have a full
* range - 4294967295 uids starting at uid 0.
*/
if a == 0 && b == 0 && c == 4294967295 {
return
}
inUserNS = true
})
return inUserNS
}

View File

@ -1,25 +0,0 @@
//go:build !linux
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package userns
// RunningInUserNS is a stub for non-Linux systems
// Always returns false
func RunningInUserNS() bool {
return false
}

View File

@ -0,0 +1,178 @@
# Changelog #
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased] ##
## [0.3.4] - 2024-10-09 ##
### Fixed ###
- Previously, some testing mocks we had resulted in us doing `import "testing"`
in non-`_test.go` code, which made some downstreams like Kubernetes unhappy.
This has been fixed. (#32)
## [0.3.3] - 2024-09-30 ##
### Fixed ###
- The mode and owner verification logic in `MkdirAll` has been removed. This
was originally intended to protect against some theoretical attacks but upon
further consideration these protections don't actually buy us anything and
they were causing spurious errors with more complicated filesystem setups.
- The "is the created directory empty" logic in `MkdirAll` has also been
removed. This was not causing us issues yet, but some pseudofilesystems (such
as `cgroup`) create non-empty directories and so this logic would've been
wrong for such cases.
## [0.3.2] - 2024-09-13 ##
### Changed ###
- Passing the `S_ISUID` or `S_ISGID` modes to `MkdirAllInRoot` will now return
an explicit error saying that those bits are ignored by `mkdirat(2)`. In the
past a different error was returned, but since the silent ignoring behaviour
is codified in the man pages a more explicit error seems apt. While silently
ignoring these bits would be the most compatible option, it could lead to
users thinking their code sets these bits when it doesn't. Programs that need
to deal with compatibility can mask the bits themselves. (#23, #25)
### Fixed ###
- If a directory has `S_ISGID` set, then all child directories will have
`S_ISGID` set when created and a different gid will be used for any inode
created under the directory. Previously, the "expected owner and mode"
validation in `securejoin.MkdirAll` did not correctly handle this. We now
correctly handle this case. (#24, #25)
## [0.3.1] - 2024-07-23 ##
### Changed ###
- By allowing `Open(at)InRoot` to opt-out of the extra work done by `MkdirAll`
to do the necessary "partial lookups", `Open(at)InRoot` now does less work
for both implementations (resulting in a many-fold decrease in the number of
operations for `openat2`, and a modest improvement for non-`openat2`) and is
far more guaranteed to match the correct `openat2(RESOLVE_IN_ROOT)`
behaviour.
- We now use `readlinkat(fd, "")` where possible. For `Open(at)InRoot` this
effectively just means that we no longer risk getting spurious errors during
rename races. However, for our hardened procfs handler, this in theory should
prevent mount attacks from tricking us when doing magic-link readlinks (even
when using the unsafe host `/proc` handle). Unfortunately `Reopen` is still
potentially vulnerable to those kinds of somewhat-esoteric attacks.
Technically this [will only work on post-2.6.39 kernels][linux-readlinkat-emptypath]
but it seems incredibly unlikely anyone is using `filepath-securejoin` on a
pre-2011 kernel.
### Fixed ###
- Several improvements were made to the errors returned by `Open(at)InRoot` and
`MkdirAll` when dealing with invalid paths under the emulated (ie.
non-`openat2`) implementation. Previously, some paths would return the wrong
error (`ENOENT` when the last component was a non-directory), and other paths
would be returned as though they were acceptable (trailing-slash components
after a non-directory would be ignored by `Open(at)InRoot`).
These changes were done to match `openat2`'s behaviour and purely is a
consistency fix (most users are going to be using `openat2` anyway).
[linux-readlinkat-emptypath]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=65cfc6722361570bfe255698d9cd4dccaf47570d
## [0.3.0] - 2024-07-11 ##
### Added ###
- A new set of `*os.File`-based APIs have been added. These are adapted from
[libpathrs][] and we strongly suggest using them if possible (as they provide
far more protection against attacks than `SecureJoin`):
- `Open(at)InRoot` resolves a path inside a rootfs and returns an `*os.File`
handle to the path. Note that the handle returned is an `O_PATH` handle,
which cannot be used for reading or writing (as well as some other
operations -- [see open(2) for more details][open.2])
- `Reopen` takes an `O_PATH` file handle and safely re-opens it to upgrade
it to a regular handle. This can also be used with non-`O_PATH` handles,
but `O_PATH` is the most obvious application.
- `MkdirAll` is an implementation of `os.MkdirAll` that is safe to use to
create a directory tree within a rootfs.
As these are new APIs, they may change in the future. However, they should be
safe to start migrating to as we have extensive tests ensuring they behave
correctly and are safe against various races and other attacks.
[libpathrs]: https://github.com/openSUSE/libpathrs
[open.2]: https://www.man7.org/linux/man-pages/man2/open.2.html
## [0.2.5] - 2024-05-03 ##
### Changed ###
- Some minor changes were made to how lexical components (like `..` and `.`)
are handled during path generation in `SecureJoin`. There is no behaviour
change as a result of this fix (the resulting paths are the same).
### Fixed ###
- The error returned when we hit a symlink loop now references the correct
path. (#10)
## [0.2.4] - 2023-09-06 ##
### Security ###
- This release fixes a potential security issue in filepath-securejoin when
used on Windows ([GHSA-6xv5-86q9-7xr8][], which could be used to generate
paths outside of the provided rootfs in certain cases), as well as improving
the overall behaviour of filepath-securejoin when dealing with Windows paths
that contain volume names. Thanks to Paulo Gomes for discovering and fixing
these issues.
### Fixed ###
- Switch to GitHub Actions for CI so we can test on Windows as well as Linux
and MacOS.
[GHSA-6xv5-86q9-7xr8]: https://github.com/advisories/GHSA-6xv5-86q9-7xr8
## [0.2.3] - 2021-06-04 ##
### Changed ###
- Switch to Go 1.13-style `%w` error wrapping, letting us drop the dependency
on `github.com/pkg/errors`.
## [0.2.2] - 2018-09-05 ##
### Changed ###
- Use `syscall.ELOOP` as the base error for symlink loops, rather than our own
(internal) error. This allows callers to more easily use `errors.Is` to check
for this case.
## [0.2.1] - 2018-09-05 ##
### Fixed ###
- Use our own `IsNotExist` implementation, which lets us handle `ENOTDIR`
properly within `SecureJoin`.
## [0.2.0] - 2017-07-19 ##
We now have 100% test coverage!
### Added ###
- Add a `SecureJoinVFS` API that can be used for mocking (as we do in our new
tests) or for implementing custom handling of lookup operations (such as for
rootless containers, where work is necessary to access directories with weird
modes because we don't have `CAP_DAC_READ_SEARCH` or `CAP_DAC_OVERRIDE`).
## 0.1.0 - 2017-07-19
This is our first release of `github.com/cyphar/filepath-securejoin`,
containing a full implementation with a coverage of 93.5% (the only missing
cases are the error cases, which are hard to mocktest at the moment).
[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.4...HEAD
[0.3.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.3...v0.3.4
[0.3.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.2...v0.3.3
[0.3.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.5...v0.3.0
[0.2.5]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.4...v0.2.5
[0.2.4]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.3...v0.2.4
[0.2.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.2...v0.2.3
[0.2.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.1...v0.2.2
[0.2.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.1.0...v0.2.0

View File

@ -1,5 +1,5 @@
Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
Copyright (C) 2017 SUSE LLC. All rights reserved.
Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are

View File

@ -1,32 +1,26 @@
## `filepath-securejoin` ##
[![Go Documentation](https://pkg.go.dev/badge/github.com/cyphar/filepath-securejoin.svg)](https://pkg.go.dev/github.com/cyphar/filepath-securejoin)
[![Build Status](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml/badge.svg)](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml)
An implementation of `SecureJoin`, a [candidate for inclusion in the Go
standard library][go#20126]. The purpose of this function is to be a "secure"
alternative to `filepath.Join`, and in particular it provides certain
guarantees that are not provided by `filepath.Join`.
### Old API ###
> **NOTE**: This code is *only* safe if you are not at risk of other processes
> modifying path components after you've used `SecureJoin`. If it is possible
> for a malicious process to modify path components of the resolved path, then
> you will be vulnerable to some fairly trivial TOCTOU race conditions. [There
> are some Linux kernel patches I'm working on which might allow for a better
> solution.][lwn-obeneath]
>
> In addition, with a slightly modified API it might be possible to use
> `O_PATH` and verify that the opened path is actually the resolved one -- but
> I have not done that yet. I might add it in the future as a helper function
> to help users verify the path (we can't just return `/proc/self/fd/<foo>`
> because that doesn't always work transparently for all users).
This library was originally just an implementation of `SecureJoin` which was
[intended to be included in the Go standard library][go#20126] as a safer
`filepath.Join` that would restrict the path lookup to be inside a root
directory.
This is the function prototype:
The implementation was based on code that existed in several container
runtimes. Unfortunately, this API is **fundamentally unsafe** against attackers
that can modify path components after `SecureJoin` returns and before the
caller uses the path, allowing for some fairly trivial TOCTOU attacks.
```go
func SecureJoin(root, unsafePath string) (string, error)
```
`SecureJoin` (and `SecureJoinVFS`) are still provided by this library to
support legacy users, but new users are strongly suggested to avoid using
`SecureJoin` and instead use the [new api](#new-api) or switch to
[libpathrs][libpathrs].
This library **guarantees** the following:
With the above limitations in mind, this library guarantees the following:
* If no error is set, the resulting string **must** be a child path of
`root` and will not contain any symlink path components (they will all be
@ -47,7 +41,7 @@ This library **guarantees** the following:
A (trivial) implementation of this function on GNU/Linux systems could be done
with the following (note that this requires root privileges and is far more
opaque than the implementation in this library, and also requires that
`readlink` is inside the `root` path):
`readlink` is inside the `root` path and is trustworthy):
```go
package securejoin
@ -70,9 +64,105 @@ func SecureJoin(root, unsafePath string) (string, error) {
}
```
[lwn-obeneath]: https://lwn.net/Articles/767547/
[libpathrs]: https://github.com/openSUSE/libpathrs
[go#20126]: https://github.com/golang/go/issues/20126
### New API ###
While we recommend users switch to [libpathrs][libpathrs] as soon as it has a
stable release, some methods implemented by libpathrs have been ported to this
library to ease the transition. These APIs are only supported on Linux.
These APIs are implemented such that `filepath-securejoin` will
opportunistically use certain newer kernel APIs that make these operations far
more secure. In particular:
* All of the lookup operations will use [`openat2`][openat2.2] on new enough
kernels (Linux 5.6 or later) to restrict lookups through magic-links and
bind-mounts (for certain operations) and to make use of `RESOLVE_IN_ROOT` to
efficiently resolve symlinks within a rootfs.
* The APIs provide hardening against a malicious `/proc` mount to either detect
or avoid being tricked by a `/proc` that is not legitimate. This is done
using [`openat2`][openat2.2] for all users, and privileged users will also be
further protected by using [`fsopen`][fsopen.2] and [`open_tree`][open_tree.2]
(Linux 5.2 or later).
[openat2.2]: https://www.man7.org/linux/man-pages/man2/openat2.2.html
[fsopen.2]: https://github.com/brauner/man-pages-md/blob/main/fsopen.md
[open_tree.2]: https://github.com/brauner/man-pages-md/blob/main/open_tree.md
#### `OpenInRoot` ####
```go
func OpenInRoot(root, unsafePath string) (*os.File, error)
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error)
func Reopen(handle *os.File, flags int) (*os.File, error)
```
`OpenInRoot` is a much safer version of
```go
path, err := securejoin.SecureJoin(root, unsafePath)
file, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)
```
that protects against various race attacks that could lead to serious security
issues, depending on the application. Note that the returned `*os.File` is an
`O_PATH` file descriptor, which is quite restricted. Callers will probably need
to use `Reopen` to get a more usable handle (this split is done to provide
useful features like PTY spawning and to avoid users accidentally opening bad
inodes that could cause a DoS).
Callers need to be careful in how they use the returned `*os.File`. Usually it
is only safe to operate on the handle directly, and it is very easy to create a
security issue. [libpathrs][libpathrs] provides far more helpers to make using
these handles safer -- there is currently no plan to port them to
`filepath-securejoin`.
`OpenatInRoot` is like `OpenInRoot` except that the root is provided using an
`*os.File`. This allows you to ensure that multiple `OpenatInRoot` (or
`MkdirAllHandle`) calls are operating on the same rootfs.
> **NOTE**: Unlike `SecureJoin`, `OpenInRoot` will error out as soon as it hits
> a dangling symlink or non-existent path. This is in contrast to `SecureJoin`
> which treated non-existent components as though they were real directories,
> and would allow for partial resolution of dangling symlinks. These behaviours
> are at odds with how Linux treats non-existent paths and dangling symlinks,
> and so these are no longer allowed.
#### `MkdirAll` ####
```go
func MkdirAll(root, unsafePath string, mode int) error
func MkdirAllHandle(root *os.File, unsafePath string, mode int) (*os.File, error)
```
`MkdirAll` is a much safer version of
```go
path, err := securejoin.SecureJoin(root, unsafePath)
err = os.MkdirAll(path, mode)
```
that protects against the same kinds of races that `OpenInRoot` protects
against.
`MkdirAllHandle` is like `MkdirAll` except that the root is provided using an
`*os.File` (the reason for this is the same as with `OpenatInRoot`) and an
`*os.File` of the final created directory is returned (this directory is
guaranteed to be effectively identical to the directory created by
`MkdirAllHandle`, which is not possible to ensure by just using `OpenatInRoot`
after `MkdirAll`).
> **NOTE**: Unlike `SecureJoin`, `MkdirAll` will error out as soon as it hits
> a dangling symlink or non-existent path. This is in contrast to `SecureJoin`
> which treated non-existent components as though they were real directories,
> and would allow for partial resolution of dangling symlinks. These behaviours
> are at odds with how Linux treats non-existent paths and dangling symlinks,
> and so these are no longer allowed. This means that `MkdirAll` will not
> create non-existent directories referenced by a dangling symlink.
### License ###
The license of this project is the same as Go, which is a BSD 3-clause license

View File

@ -1 +1 @@
0.2.5
0.3.4

39
vendor/github.com/cyphar/filepath-securejoin/doc.go generated vendored Normal file
View File

@ -0,0 +1,39 @@
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
// Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package securejoin implements a set of helpers to make it easier to write Go
// code that is safe against symlink-related escape attacks. The primary idea
// is to let you resolve a path within a rootfs directory as if the rootfs was
// a chroot.
//
// securejoin has two APIs, a "legacy" API and a "modern" API.
//
// The legacy API is [SecureJoin] and [SecureJoinVFS]. These methods are
// **not** safe against race conditions where an attacker changes the
// filesystem after (or during) the [SecureJoin] operation.
//
// The new API is made up of [OpenInRoot] and [MkdirAll] (and derived
// functions). These are safe against racing attackers and have several other
// protections that are not provided by the legacy API. There are many more
// operations that most programs expect to be able to do safely, but we do not
// provide explicit support for them because we want to encourage users to
// switch to [libpathrs](https://github.com/openSUSE/libpathrs) which is a
// cross-language next-generation library that is entirely designed around
// operating on paths safely.
//
// securejoin has been used by several container runtimes (Docker, runc,
// Kubernetes, etc) for quite a few years as a de-facto standard for operating
// on container filesystem paths "safely". However, most users still use the
// legacy API which is unsafe against various attacks (there is a fairly long
// history of CVEs in dependent as a result). Users should switch to the modern
// API as soon as possible (or even better, switch to libpathrs).
//
// This project was initially intended to be included in the Go standard
// library, but [it was rejected](https://go.dev/issue/20126). There is now a
// [new Go proposal](https://go.dev/issue/67002) for a safe path resolution API
// that shares some of the goals of filepath-securejoin. However, that design
// is intended to work like `openat2(RESOLVE_BENEATH)` which does not fit the
// usecase of container runtimes and most system tools.
package securejoin

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