diff --git a/go.mod b/go.mod index 147399d..9648325 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/jackc/pgx/v5 v5.6.0 github.com/joho/godotenv v1.5.1 github.com/pkg/errors v0.9.1 - github.com/redis/go-redis/v9 v9.6.0 + github.com/redis/go-redis/v9 v9.6.1 github.com/rs/zerolog v1.33.0 github.com/sendgrid/rest v2.6.9+incompatible github.com/sendgrid/sendgrid-go v3.14.0+incompatible @@ -35,6 +35,7 @@ require ( github.com/twmb/franz-go v1.17.0 github.com/twmb/franz-go/pkg/kadm v1.12.0 github.com/vmihailenco/msgpack/v5 v5.4.1 + github.com/xlzd/gotp v0.1.0 github.com/zeebo/xxh3 v1.0.2 golang.org/x/net v0.27.0 google.golang.org/api v0.189.0 @@ -45,9 +46,9 @@ require ( cloud.google.com/go/auth v0.7.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.5.0 // indirect - cloud.google.com/go/firestore v1.15.0 // indirect - cloud.google.com/go/iam v1.1.11 // indirect - cloud.google.com/go/longrunning v0.5.10 // indirect + cloud.google.com/go/firestore v1.16.0 // indirect + cloud.google.com/go/iam v1.1.12 // indirect + cloud.google.com/go/longrunning v0.5.11 // indirect cloud.google.com/go/storage v1.43.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/KyleBanks/depth v1.2.1 // indirect @@ -160,9 +161,9 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.23.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20240722135656-d784300faade // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect + google.golang.org/genproto v0.0.0-20240723171418-e6d459c13d2a // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240723171418-e6d459c13d2a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a // indirect google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index a28dd66..8cbb94a 100644 --- a/go.sum +++ b/go.sum @@ -7,12 +7,12 @@ cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAg cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= -cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= -cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= -cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw= -cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ= -cloud.google.com/go/longrunning v0.5.10 h1:eB/BniENNRKhjz/xgiillrdcH3G74TGSl3BXinGlI7E= -cloud.google.com/go/longrunning v0.5.10/go.mod h1:tljz5guTr5oc/qhlUjBlk7UAIFMOGuPNxkNDZXlLics= +cloud.google.com/go/firestore v1.16.0 h1:YwmDHcyrxVRErWcgxunzEaZxtNbc8QoFYA/JOEwDPgc= +cloud.google.com/go/firestore v1.16.0/go.mod h1:+22v/7p+WNBSQwdSwP57vz47aZiY+HrDkrOsJNhk7rg= +cloud.google.com/go/iam v1.1.12 h1:JixGLimRrNGcxvJEQ8+clfLxPlbeZA6MuRJ+qJNQ5Xw= +cloud.google.com/go/iam v1.1.12/go.mod h1:9LDX8J7dN5YRyzVHxwQzrQs9opFFqn0Mxs9nAeB+Hhg= +cloud.google.com/go/longrunning v0.5.11 h1:Havn1kGjz3whCfoD8dxMLP73Ph5w+ODyZB9RUsDxtGk= +cloud.google.com/go/longrunning v0.5.11/go.mod h1:rDn7//lmlfWV1Dx6IB4RatCPenTwwmqXuiP0/RgoEO4= cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= cosmossdk.io/math v1.3.0 h1:RC+jryuKeytIiictDslBP9i1fhkVm6ZDmZEoNP316zE= @@ -301,8 +301,8 @@ github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/quic-go v0.45.1 h1:tPfeYCk+uZHjmDRwHHQmvHRYL2t44ROTujLeFVBmjCA= github.com/quic-go/quic-go v0.45.1/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= -github.com/redis/go-redis/v9 v9.6.0 h1:NLck+Rab3AOTHw21CGRpvQpgTrAU4sgdCswqGtlhGRA= -github.com/redis/go-redis/v9 v9.6.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= @@ -369,6 +369,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= +github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -498,12 +500,12 @@ google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg= -google.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= -google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade h1:WxZOF2yayUHpHSbUE6NMzumUzBxYc3YGwo0YHnbzsJY= -google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto v0.0.0-20240723171418-e6d459c13d2a h1:hPbLwHFm59QoSKUT0uGaL19YN4U9W5lY4+iNXlUBNj0= +google.golang.org/genproto v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:+7gIV7FP6jBo5hiY2lsWA//NkNORQVj0J1Isc/4HzR4= +google.golang.org/genproto/googleapis/api v0.0.0-20240723171418-e6d459c13d2a h1:YIa/rzVqMEokBkPtydCkx1VLmv3An1Uw7w1P1m6EhOY= +google.golang.org/genproto/googleapis/api v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a h1:hqK4+jJZXCU4pW7jsAdGOVFIfLHQeV7LaizZKnZ84HI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/totp/.testdata/application.yaml b/totp/.testdata/application.yaml new file mode 100644 index 0000000..7632d40 --- /dev/null +++ b/totp/.testdata/application.yaml @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: ice License 1.0 + +development: true +logger: + encoder: console + level: debug +self: + wintr/totp: + issuer: ice.io \ No newline at end of file diff --git a/totp/contract.go b/totp/contract.go new file mode 100644 index 0000000..e2fa4dc --- /dev/null +++ b/totp/contract.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: ice License 1.0 + +package totp + +import ( + stdlibtime "time" + + "github.com/ice-blockchain/wintr/time" +) + +type ( + TOTP interface { + Generator + Verifier + } + Generator interface { + GenerateURI(userSecret, account string) string + } + Verifier interface { + Verify(now *time.Time, userSecret, totpCode string) bool + } +) + +type ( + totp struct { + generator codeGenerator + cfg *config + } + config struct { + WintrTOTP struct { + Issuer string `yaml:"issuer" mapstructure:"issuer"` + } `yaml:"wintr/totp" mapstructure:"wintr/totp"` //nolint:tagliatelle // . + } + codeGenerator interface { + CreateCode(userSecret string) totpCode + } + totpCode interface { + ProvisioningUri(accountName, issuerName string) string + VerifyTime(code string, t stdlibtime.Time) bool + } + gotpGenerator struct{} +) + +const ( + digitsInCode = 6 + rotationDuration = 30 +) diff --git a/totp/totp.go b/totp/totp.go new file mode 100644 index 0000000..5e1c1a9 --- /dev/null +++ b/totp/totp.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: ice License 1.0 + +package totp + +import ( + "encoding/base32" + + "github.com/xlzd/gotp" + + appcfg "github.com/ice-blockchain/wintr/config" + "github.com/ice-blockchain/wintr/time" +) + +func New(applicationYamlKey string) TOTP { + gotpGen := newGotp() + var cfg config + appcfg.MustLoadFromKey(applicationYamlKey, &cfg) + + return &totp{generator: gotpGen, cfg: &cfg} +} + +func (t *totp) GenerateURI(userSecret, account string) string { + code := t.generator.CreateCode(userSecret) + + return code.ProvisioningUri(account, t.cfg.WintrTOTP.Issuer) +} + +func (t *totp) Verify(now *time.Time, userSecret, totpCode string) bool { + code := t.generator.CreateCode(userSecret) + + return code.VerifyTime(totpCode, *now.Time) +} + +func newGotp() codeGenerator { + return &gotpGenerator{} +} + +func (*gotpGenerator) CreateCode(secret string) totpCode { + encodedSecret := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString([]byte(secret)) + code := gotp.NewTOTP(encodedSecret, digitsInCode, rotationDuration, nil) + + return code +} diff --git a/totp/totp_test.go b/totp/totp_test.go new file mode 100644 index 0000000..369b957 --- /dev/null +++ b/totp/totp_test.go @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: ice License 1.0 + +package totp + +import ( + "testing" + stdlibtime "time" + + "github.com/stretchr/testify/require" + + "github.com/ice-blockchain/wintr/time" +) + +func TestTOTP(t *testing.T) { + t.Parallel() + totp := New("self") + secret := "bogusSecret" //nolint:gosec // Bogus. + uri := totp.GenerateURI(secret, "bogusAccount") + require.Equal(t, "otpauth://totp/ice.io:bogusAccount?issuer=ice.io&secret=MJXWO5LTKNSWG4TFOQ", uri) + now := time.New(stdlibtime.Date(2024, 7, 25, 8, 15, 56, 0, stdlibtime.UTC)) + validCode := "799503" + validCodeAfter30s := "395417" + require.True(t, totp.Verify(now, secret, validCode)) + require.True(t, totp.Verify(time.New(now.Add(3*stdlibtime.Second)), secret, validCode)) + require.False(t, totp.Verify(now, secret, "697025")) + require.False(t, totp.Verify(time.New(now.Add(31*stdlibtime.Second)), secret, validCode)) + require.True(t, totp.Verify(time.New(now.Add(31*stdlibtime.Second)), secret, validCodeAfter30s)) + require.False(t, totp.Verify(now, "wrongSecret", validCode)) + require.False(t, totp.Verify(time.New(now.Add(31*stdlibtime.Second)), "wrongSecret", validCode)) + require.False(t, totp.Verify(time.Now(), secret, "")) +}