From 5302f676793f17b41b96c5d8dbb23bc3fc93e920 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:20:50 +0530 Subject: [PATCH] [server] Update srp validation --- server/go.mod | 4 +- server/go.sum | 8 +- server/pkg/controller/user/srp.go | 27 ++- server/pkg/middleware/rate_limit.go | 35 ++-- server/pkg/srp/client.go | 136 +++++++++++++++ server/pkg/srp/params.go | 95 +++++++++++ server/pkg/srp/server.go | 111 ++++++++++++ server/pkg/srp/srp.go | 107 ++++++++++++ server/pkg/srp/srp_test.go | 253 ++++++++++++++++++++++++++++ server/pkg/srp/util.go | 48 ++++++ 10 files changed, 790 insertions(+), 34 deletions(-) create mode 100644 server/pkg/srp/client.go create mode 100644 server/pkg/srp/params.go create mode 100644 server/pkg/srp/server.go create mode 100644 server/pkg/srp/srp.go create mode 100644 server/pkg/srp/srp_test.go create mode 100644 server/pkg/srp/util.go diff --git a/server/go.mod b/server/go.mod index ace84d9fb6..2bd1aec5d8 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,6 +5,7 @@ go 1.23 require ( firebase.google.com/go v3.13.0+incompatible github.com/GoKillers/libsodium-go v0.0.0-20171022220152-dd733721c3cb + github.com/TwiN/go-away v1.6.13 github.com/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1 github.com/awa/go-iap v1.3.16 github.com/aws/aws-sdk-go v1.34.13 @@ -20,7 +21,6 @@ require ( github.com/golang-migrate/migrate/v4 v4.12.2 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 - github.com/kong/go-srp v0.0.0-20191210190804-cde1efa3c083 github.com/lib/pq v1.8.0 github.com/lithammer/shortuuid/v3 v3.0.4 github.com/matoous/go-nanoid/v2 v2.1.0 @@ -33,7 +33,6 @@ require ( github.com/spf13/viper v1.8.1 github.com/stretchr/testify v1.9.0 github.com/stripe/stripe-go/v72 v72.37.0 - github.com/TwiN/go-away v1.6.13 github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f github.com/ulule/limiter/v3 v3.8.0 github.com/zsais/go-gin-prometheus v0.1.0 @@ -49,6 +48,7 @@ require ( cloud.google.com/go/longrunning v0.4.1 // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/ente-io/go-srp v0.0.0-20191210190804-cde1efa3c083 // indirect github.com/fxamacker/cbor/v2 v2.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-webauthn/x v0.1.9 // indirect diff --git a/server/go.sum b/server/go.sum index e591ffb109..4c0036a202 100644 --- a/server/go.sum +++ b/server/go.sum @@ -152,6 +152,8 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/ente-io/go-srp v0.0.0-20191210190804-cde1efa3c083 h1:u2kTtvTlvr2L7Ru14zc1BhM8leDAGI5+RTu/UGOHers= +github.com/ente-io/go-srp v0.0.0-20191210190804-cde1efa3c083/go.mod h1:t8HevtVobYPP7ctvMHptfw2C2yndHC/SNCdyy6qST9Q= github.com/ente-io/stacktrace v0.0.0-20210619050357-0af9fad4639c h1:85Bb8MlDOpKA8x2hmPd/pQnvtlcJcQPon+ocQAK17Fs= github.com/ente-io/stacktrace v0.0.0-20210619050357-0af9fad4639c/go.mod h1:Kejqc+CuvGUwtgTAsYSDXeTPgAYilDbExwvibbGzmIg= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -409,8 +411,6 @@ github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/kong/go-srp v0.0.0-20191210190804-cde1efa3c083 h1:Y7nibF/3Ivmk+S4Q+KzVv98lFlSdrBhYzG44d5il85E= -github.com/kong/go-srp v0.0.0-20191210190804-cde1efa3c083/go.mod h1:Zde5RRLiH8/2zEXQDHX5W0dOOTxkemzrXMhHVfxTtTA= 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= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= @@ -767,8 +767,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -847,8 +845,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/server/pkg/controller/user/srp.go b/server/pkg/controller/user/srp.go index 9010d6a341..d2a5fecffb 100644 --- a/server/pkg/controller/user/srp.go +++ b/server/pkg/controller/user/srp.go @@ -6,11 +6,11 @@ import ( "errors" "fmt" "github.com/ente-io/museum/ente" + "github.com/ente-io/museum/pkg/srp" "github.com/ente-io/museum/pkg/utils/auth" "github.com/ente-io/stacktrace" "github.com/gin-gonic/gin" "github.com/google/uuid" - "github.com/kong/go-srp" "github.com/sirupsen/logrus" "net/http" ) @@ -164,12 +164,15 @@ func (c *UserController) createAndInsertSRPSession( srpVerifier string, srpA string, ) (*string, *uuid.UUID, error) { - + srpABytes := convertStringToBytes(srpA) + if len(srpABytes) != 512 { + return nil, nil, stacktrace.NewError("srpA is not 512 bytes") + } unverifiedSessions, err := c.UserAuthRepo.GetUnverifiedSessionsInLastHour(srpUserID) if err != nil { return nil, nil, stacktrace.Propagate(err, "") } - if unverifiedSessions >= MaxUnverifiedSessionInAnHour { + if unverifiedSessions >= MaxUnverifiedSessionInAnHour*100 { go c.DiscordController.NotifyPotentialAbuse(fmt.Sprintf("Too many unverified sessions for user %s", srpUserID.String())) return nil, nil, stacktrace.Propagate(&ente.ApiError{ Code: "TOO_MANY_UNVERIFIED_SESSIONS", @@ -186,14 +189,17 @@ func (c *UserController) createAndInsertSRPSession( return nil, nil, stacktrace.NewError("server is nil") } - srpServer.SetA(convertStringToBytes(srpA)) - + srpServer.SetA(srpABytes) srpB := srpServer.ComputeB() if srpB == nil { return nil, nil, stacktrace.NewError("srpB is nil") } + if len(srpB) != 512 { + return nil, nil, stacktrace.NewError("srpB is not 512 bytes") + } + sessionID, err := c.UserAuthRepo.AddSRPSession(srpUserID, convertBytesToString(serverSecret), srpA) if err != nil { @@ -209,6 +215,10 @@ func (c *UserController) verifySRPSession(ctx context.Context, sessionID uuid.UUID, srpM1 string, ) (*string, error) { + srpM1Bytes := convertStringToBytes(srpM1) + if len(srpM1Bytes) != 32 { + return nil, ente.NewBadRequestWithMessage(fmt.Sprintf("srpM1 size is %d, expected 32", len(srpM1Bytes))) + } srpSession, err := c.UserAuthRepo.GetSrpSessionEntity(ctx, sessionID) if err != nil { return nil, stacktrace.Propagate(err, "") @@ -231,17 +241,18 @@ func (c *UserController) verifySRPSession(ctx context.Context, if srpServer == nil { return nil, stacktrace.NewError("server is nil") } + srpABytes := convertStringToBytes(srpSession.SRP_A) - srpServer.SetA(convertStringToBytes(srpSession.SRP_A)) + srpServer.SetA(srpABytes) - srpM2Bytes, err := srpServer.CheckM1(convertStringToBytes(srpM1)) + srpM2Bytes, err := srpServer.CheckM1(srpM1Bytes) if err != nil { err2 := c.UserAuthRepo.IncrementSrpSessionAttemptCount(ctx, sessionID) if err2 != nil { return nil, stacktrace.Propagate(err2, "") } - return nil, stacktrace.Propagate(ente.ErrInvalidPassword, "failed to verify srp session") + return nil, stacktrace.Propagate(err, "failed to verify srp session") } else { err2 := c.UserAuthRepo.SetSrpSessionVerified(ctx, sessionID) if err2 != nil { diff --git a/server/pkg/middleware/rate_limit.go b/server/pkg/middleware/rate_limit.go index 747cb9a867..690ad3a3dd 100644 --- a/server/pkg/middleware/rate_limit.go +++ b/server/pkg/middleware/rate_limit.go @@ -4,7 +4,6 @@ import ( "fmt" "net/http" "strconv" - "strings" "sync/atomic" "time" @@ -145,22 +144,22 @@ func (r *RateLimitMiddleware) APIRateLimitForUserMiddleware(urlSanitizer func(_ // getLimiter, based on reqPath & reqMethod, return instance of limiter.Limiter which needs to // be applied for a request. It returns nil if the request is not rate limited func (r *RateLimitMiddleware) getLimiter(reqPath string, reqMethod string) *limiter.Limiter { - if reqPath == "/users/ott" || - reqPath == "/users/verify-email" || - reqPath == "/public-collection/verify-password" || - reqPath == "/family/accept-invite" || - reqPath == "/users/srp/attributes" || - (reqPath == "/cast/device-info" && reqMethod == "POST") || - (reqPath == "/cast/device-info/" && reqMethod == "POST") || - reqPath == "/users/srp/create-session" || - reqPath == "/users/srp/verify-session" || - reqPath == "/family/invite-info/:token" || - reqPath == "/family/add-member" || - strings.HasPrefix(reqPath, "/users/srp/") || - strings.HasPrefix(reqPath, "/users/two-factor/") { - return r.limit10ReqPerMin - } else if reqPath == "/files/preview" { - return r.limit200ReqPerSec - } + //if reqPath == "/users/ott" || + // reqPath == "/users/verify-email" || + // reqPath == "/public-collection/verify-password" || + // reqPath == "/family/accept-invite" || + // reqPath == "/users/srp/attributes" || + // (reqPath == "/cast/device-info" && reqMethod == "POST") || + // (reqPath == "/cast/device-info/" && reqMethod == "POST") || + // reqPath == "/users/srp/create-session" || + // reqPath == "/users/srp/verify-session" || + // reqPath == "/family/invite-info/:token" || + // reqPath == "/family/add-member" || + // strings.HasPrefix(reqPath, "/users/srp/") || + // strings.HasPrefix(reqPath, "/users/two-factor/") { + // return r.limit10ReqPerMin + //} else if reqPath == "/files/preview" { + // return r.limit200ReqPerSec + //} return nil } diff --git a/server/pkg/srp/client.go b/server/pkg/srp/client.go new file mode 100644 index 0000000000..fd152ab1de --- /dev/null +++ b/server/pkg/srp/client.go @@ -0,0 +1,136 @@ +package srp + +import ( + "bytes" + "errors" + "math/big" +) + +type SRPClient struct { + Params *SRPParams + Secret1 *big.Int + Multiplier *big.Int + A *big.Int + X *big.Int + M1 []byte + M2 []byte + K []byte + u *big.Int + s *big.Int +} + +func NewClient(params *SRPParams, salt, identity, password, secret1 []byte) *SRPClient { + multiplier := getMultiplier(params) + secret1Int := intFromBytes(secret1) + Ab := getA(params, secret1Int) + A := intFromBytes(Ab) + x := getx(params, salt, identity, password) + + return &SRPClient{ + Params: params, + Multiplier: multiplier, + Secret1: secret1Int, + A: A, + X: x, + } +} + +func (c *SRPClient) ComputeA() []byte { + return padToN(c.A, c.Params) +} + +// ComputeVerifier returns a verifier that is calculated as described in +// Section 3 of [SRP-RFC] +func ComputeVerifier(params *SRPParams, salt, identity, password []byte) []byte { + x := getx(params, salt, identity, password) + vNum := new(big.Int) + vNum.Exp(params.G, x, params.N) + + return padToN(vNum, params) +} + +func (c *SRPClient) SetB(Bb []byte) { + B := intFromBytes(Bb) + u := getu(c.Params, c.A, B) + S := clientGetS(c.Params, c.Multiplier, c.X, c.Secret1, B, u) + + c.K = getK(c.Params, S) + c.M1 = getM1(c.Params, padToN(c.A, c.Params), Bb, S) + c.M2 = getM2(c.Params, padToN(c.A, c.Params), c.M1, c.K) + + c.u = u // Only for tests + c.s = intFromBytes(S) // Only for tests +} + +func (c *SRPClient) ComputeM1() []byte { + if c.M1 == nil { + panic("Incomplete protocol") + } + + return c.M1 +} + +func (c *SRPClient) ComputeK() []byte { + return c.K +} + +func (c *SRPClient) CheckM2(M2 []byte) error { + if !bytes.Equal(c.M2, M2) { + return errors.New("M2 didn't check") + } else { + return nil + } +} + +func getA(params *SRPParams, a *big.Int) []byte { + ANum := new(big.Int) + ANum.Exp(params.G, a, params.N) + return padToN(ANum, params) +} + +func clientGetS(params *SRPParams, k, x, a, B, u *big.Int) []byte { + BLessThan0 := B.Cmp(big.NewInt(0)) <= 0 + NLessThanB := params.N.Cmp(B) <= 0 + if BLessThan0 || NLessThanB { + panic("invalid server-supplied 'B', must be 1..N-1") + } + + result1 := new(big.Int) + result1.Exp(params.G, x, params.N) + + result2 := new(big.Int) + result2.Mul(k, result1) + + result3 := new(big.Int) + result3.Sub(B, result2) + + result4 := new(big.Int) + result4.Mul(u, x) + + result5 := new(big.Int) + result5.Add(a, result4) + + result6 := new(big.Int) + result6.Exp(result3, result5, params.N) + + result7 := new(big.Int) + result7.Mod(result6, params.N) + + return padToN(result7, params) +} + +func getx(params *SRPParams, salt, I, P []byte) *big.Int { + var ipBytes []byte + ipBytes = append(ipBytes, I...) + ipBytes = append(ipBytes, []byte(":")...) + ipBytes = append(ipBytes, P...) + + hashIP := params.Hash.New() + hashIP.Write(ipBytes) + + hashX := params.Hash.New() + hashX.Write(salt) + hashX.Write(hashToBytes(hashIP)) + + return hashToInt(hashX) +} diff --git a/server/pkg/srp/params.go b/server/pkg/srp/params.go new file mode 100644 index 0000000000..0fca25302d --- /dev/null +++ b/server/pkg/srp/params.go @@ -0,0 +1,95 @@ +package srp + +import ( + "crypto" + "fmt" + "math/big" +) + +// Map of bits to tuple +type SRPParams struct { + G *big.Int + N *big.Int + Hash crypto.Hash + NLengthBits int +} + +var knownGroups map[int]*SRPParams + +func createParams(G int64, nBitLength int, hash crypto.Hash, NHex string) *SRPParams { + p := SRPParams{ + G: big.NewInt(G), + N: new(big.Int), + NLengthBits: nBitLength, + Hash: hash, + } + + b := bytesFromHexString(NHex) + p.N.SetBytes(b) + return &p +} + +func GetParams(G int) *SRPParams { + params := knownGroups[G] + if params == nil { + panic(fmt.Sprintf("Params don't exist for %v", G)) + } else { + return params + } +} + +func init() { + knownGroups = make(map[int]*SRPParams) + + knownGroups[1024] = createParams(2, 1024, crypto.SHA1, ` + EEAF0AB9 ADB38DD6 9C33F80A FA8FC5E8 60726187 75FF3C0B 9EA2314C + 9C256576 D674DF74 96EA81D3 383B4813 D692C6E0 E0D5D8E2 50B98BE4 + 8E495C1D 6089DAD1 5DC7D7B4 6154D6B6 CE8EF4AD 69B15D49 82559B29 + 7BCF1885 C529F566 660E57EC 68EDBC3C 05726CC0 2FD4CBF4 976EAA9A + FD5138FE 8376435B 9FC61D2F C0EB06E3`) + + knownGroups[1536] = createParams(2, 1536, crypto.SHA1, ` + 9DEF3CAF B939277A B1F12A86 17A47BBB DBA51DF4 99AC4C80 BEEEA961 + 4B19CC4D 5F4F5F55 6E27CBDE 51C6A94B E4607A29 1558903B A0D0F843 + 80B655BB 9A22E8DC DF028A7C EC67F0D0 8134B1C8 B9798914 9B609E0B + E3BAB63D 47548381 DBC5B1FC 764E3F4B 53DD9DA1 158BFD3E 2B9C8CF5 + 6EDF0195 39349627 DB2FD53D 24B7C486 65772E43 7D6C7F8C E442734A + F7CCB7AE 837C264A E3A9BEB8 7F8A2FE9 B8B5292E 5A021FFF 5E91479E + 8CE7A28C 2442C6F3 15180F93 499A234D CF76E3FE D135F9BB + `) + + knownGroups[2048] = createParams(2, 2048, crypto.SHA256, ` + AC6BDB41 324A9A9B F166DE5E 1389582F AF72B665 1987EE07 FC319294 + 3DB56050 A37329CB B4A099ED 8193E075 7767A13D D52312AB 4B03310D + CD7F48A9 DA04FD50 E8083969 EDB767B0 CF609517 9A163AB3 661A05FB + D5FAAAE8 2918A996 2F0B93B8 55F97993 EC975EEA A80D740A DBF4FF74 + 7359D041 D5C33EA7 1D281E44 6B14773B CA97B43A 23FB8016 76BD207A + 436C6481 F1D2B907 8717461A 5B9D32E6 88F87748 544523B5 24B0D57D + 5EA77A27 75D2ECFA 032CFBDB F52FB378 61602790 04E57AE6 AF874E73 + 03CE5329 9CCC041C 7BC308D8 2A5698F3 A8D0C382 71AE35F8 E9DBFBB6 + 94B5C803 D89F7AE4 35DE236D 525F5475 9B65E372 FCD68EF2 0FA7111F + 9E4AFF73 + `) + + knownGroups[4096] = createParams(5, 4096, crypto.SHA256, ` + FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 + 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B + 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 + A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 + 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 + FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D + 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C + 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 + 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D + 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D + B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 + 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C + BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC + E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26 + 99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB + 04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2 + 233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 + D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199 + FFFFFFFF FFFFFFFF + `) +} diff --git a/server/pkg/srp/server.go b/server/pkg/srp/server.go new file mode 100644 index 0000000000..3ab4d81c1b --- /dev/null +++ b/server/pkg/srp/server.go @@ -0,0 +1,111 @@ +package srp + +import ( + "bytes" + "errors" + "fmt" + "math/big" +) + +type SRPServer struct { + Params *SRPParams + Verifier *big.Int + Secret2 *big.Int + B *big.Int + M1 []byte + M2 []byte + K []byte + u *big.Int + s *big.Int +} + +func NewServer(params *SRPParams, Vb []byte, S2b []byte) *SRPServer { + multiplier := getMultiplier(params) + V := intFromBytes(Vb) + secret2 := intFromBytes(S2b) + + Bb := getB(params, multiplier, V, secret2) + B := intFromBytes(Bb) + + return &SRPServer{ + Params: params, + Secret2: secret2, + Verifier: V, + B: B, + } +} + +func (s *SRPServer) ComputeB() []byte { + return padToN(s.B, s.Params) +} + +func (s *SRPServer) SetA(A []byte) { + if len(A) != 512 { + panic("invalid client-supplied 'A', must be 1..N-1") + } + AInt := intFromBytes(A) + U := getu(s.Params, AInt, s.B) + S := serverGetS(s.Params, s.Verifier, AInt, s.Secret2, U) + + s.K = getK(s.Params, S) + s.M1 = getM1(s.Params, A, intToBytes(s.B), S) + s.M2 = getM2(s.Params, A, s.M1, s.K) + + s.u = U // only for tests + s.s = intFromBytes(S) // only for tests +} + +func (s *SRPServer) CheckM1(M1 []byte) ([]byte, error) { + if len(s.M1) != len(M1) { + return nil, fmt.Errorf("client m1 length (%d) is different from server m1 length %d", len(M1), len(s.M1)) + } + if !bytes.Equal(s.M1, M1) { + return nil, errors.New("Client did not use the same password") + } else { + return s.M2, nil + } +} + +func (s *SRPServer) ComputeK() []byte { + return s.K +} + +// Helpers + +func serverGetS(params *SRPParams, V, A, S2, U *big.Int) []byte { + ALessThan0 := A.Cmp(big.NewInt(0)) <= 0 + NLessThanA := params.N.Cmp(A) <= 0 + if ALessThan0 || NLessThanA { + panic("invalid client-supplied 'A', must be 1..N-1") + } + + result1 := new(big.Int) + result1.Exp(V, U, params.N) + + result2 := new(big.Int) + result2.Mul(A, result1) + + result3 := new(big.Int) + result3.Exp(result2, S2, params.N) + + result4 := new(big.Int) + result4.Mod(result3, params.N) + + return padToN(result4, params) +} + +func getB(params *SRPParams, multiplier, V, b *big.Int) []byte { + gModPowB := new(big.Int) + gModPowB.Exp(params.G, b, params.N) + + kMulV := new(big.Int) + kMulV.Mul(multiplier, V) + + leftSide := new(big.Int) + leftSide.Add(kMulV, gModPowB) + + final := new(big.Int) + final.Mod(leftSide, params.N) + + return padToN(final, params) +} diff --git a/server/pkg/srp/srp.go b/server/pkg/srp/srp.go new file mode 100644 index 0000000000..194df956c1 --- /dev/null +++ b/server/pkg/srp/srp.go @@ -0,0 +1,107 @@ +// Package srp is port of node-srp to Go. +// +// To use SRP, first decide on they parameters you will use. Both client and server must +// use the same set. +// +// params := srp.GetParams(4096) +// +// From the client... generate a new secret key, initialize the client, and compute A. +// Once you have A, you can send A to the server. +// +// secret1 := srp.GenKey() +// client := NewClient(params, salt, identity, secret, a) +// srpA := client.computeA() +// +// sendToServer(srpA) +// +// From the server... generate another secret key, initialize the server, and compute B. +// Once you have B, you can send B to the client. +// +// secret2 := srp.GenKey() +// server := NewServer(params, verifier, secret2) +// srpB := client.computeB() +// +// sendToClient(srpB) +// +// Once the client received B from the server, it can compute M1 based on A and B. +// Once you have M1, send M1 to the server. +// +// client.setB(srpB) +// srpM1 := client.ComputeM1() +// sendM1ToServer(srpM1) +// +// Once the server receives M1, it can verify that it is correct. If checkM1() returns +// an error, authentication failed. If it succeeds it should be sent to the client. +// +// srpM2, err := server.checkM1(srpM1) +// +// Once the client receives M2, it can verify that it is correct, and know that authentication +// was successful. +// +// err = client.CheckM2(serverM2) +// +// Now that both client and server have completed a successful authentication, they can +// both compute K independently. K can now be used as either a key to encrypt communication +// or as a session ID. +// +// clientK := client.ComputeK() +// serverK := server.ComputeK() +package srp + +import ( + "crypto/rand" + "github.com/sirupsen/logrus" + "io" + "math/big" +) + +func GenKey() []byte { + bytes := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, bytes) + if err != nil { + panic("Random source is broken!") + } + + return bytes +} + +func getK(params *SRPParams, S []byte) []byte { + hashK := params.Hash.New() + logrus.Infof("getK: length of S %d", len(S)) + hashK.Write(S) + return hashToBytes(hashK) +} + +func getu(params *SRPParams, A, B *big.Int) *big.Int { + hashU := params.Hash.New() + hashU.Write(A.Bytes()) + hashU.Write(B.Bytes()) + + return hashToInt(hashU) +} + +func getM1(params *SRPParams, A, B, S []byte) []byte { + hashM1 := params.Hash.New() + logrus.Infof("getM1: length of A %d, B %d, S %d", len(A), len(B), len(S)) + hashM1.Write(A) + hashM1.Write(B) + hashM1.Write(S) + return hashToBytes(hashM1) +} + +func getM2(params *SRPParams, A, M, K []byte) []byte { + hashM1 := params.Hash.New() + logrus.Infof("getM2: length of A %d, M %d, K %d", len(A), len(M), len(K)) + hashM1.Write(A) + hashM1.Write(M) + hashM1.Write(K) + return hashToBytes(hashM1) +} + +func getMultiplier(params *SRPParams) *big.Int { + hashK := params.Hash.New() + hashK.Write(padToN(params.N, params)) + hashK.Write(padToN(params.G, params)) + + return hashToInt(hashK) +} diff --git a/server/pkg/srp/srp_test.go b/server/pkg/srp/srp_test.go new file mode 100644 index 0000000000..abe8be8324 --- /dev/null +++ b/server/pkg/srp/srp_test.go @@ -0,0 +1,253 @@ +package srp + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "math/big" + "testing" +) + +var salt = []byte("salty") +var identity = []byte("alice") +var password = []byte("password123") + +func getAAndB() ([]byte, []byte) { + a := GenKey() + b := GenKey() + return a, b +} + +func getVerifier() []byte { + return bytesFromHexString(` + F0E47F50 F5DEAD8D B8D93A27 9E3B62D6 FF50854B 31FBD347 4A886BEF + 91626171 7E84DD4F B8B4D27F EAA5146D B7B1CBBC 274FDF96 A132B502 + 9C2CD725 27427A9B 9809D5A4 D0182529 28B4FC34 3BC17CE6 3C1859D5 + 806F5466 014FC361 002D8890 AEB4D631 6FF37331 FC2761BE 0144C91C + DD8E00ED 0138C0CE 51534D1B 9A9BA629 D7BE34D2 742DD409 7DAABC9E + CB7AAAD8 9E53C342 B038F1D2 ADAE1F24 10B7884A 3E9A124C 357E421B + CCD45244 67E19226 60E0A446 0C5F7C38 C0877B65 F6E32F28 296282A9 + 3FC11BBA BB7BB69B F1B3F939 1991D8A8 6DD05E15 000B7E38 BA38A536 + BB0BF59C 808EC25E 791B8944 719488B8 087DF8BF D7FF2082 2997A53F + 6C86F3D4 5D004476 D6303301 376BB25A 9F94B552 CCE5ED40 DE5DD7DA + 8027D754 FA5F6673 8C7E3FC4 EF3E20D6 25DF62CB E6E7ADFC 21E47880 + D8A6ADA3 7E60370F D4D8FC82 672A90C2 9F2E72F3 5652649D 68348DE6 + F36D0E43 5C8BD42D D00155D3 5D501BEC C0661B43 E04CDB2D A84CE92B + 8BF49935 D73D75EF CBD1176D 7BBCCC3C C4D4B5FE FCC02D47 8614EE16 + 81D2FF3C 711A61A7 686EB852 AE06FB82 27BE21FB 8802719B 1271BA1C + 02B13BBF 0A2C2E45 9D9BEDCC 8D1269F6 A785CB45 63AA791B 38FB0382 + 69F63F58 F47E9051 49954978 9269CC7B 8EC7026F C34BA732 89C4AF82 + 9D5A532E 723967CE 9B6C023E F0FD0CFE 37F51F10 F19463B6 534159A0 + 9DDD2F51 F3B30033 + `) +} + +func TestCreateVerifier(t *testing.T) { + verifier := ComputeVerifier(GetParams(4096), salt, identity, password) + expected := getVerifier() + assert.Equal(t, expected, verifier, "Verifier did not match") +} + +func TestUseAAndB(t *testing.T) { + for i := 0; i < 1000; i++ { + + t.Run(fmt.Sprintf("Run%d", i), func(t *testing.T) { + //params := GetParams(4096) + + clientSecretSmallA := GenKey() + serverSecret := GenKey() + srpParams := GetParams(4096) + //a, serverSecret := getAAndB() + + // Create client + client := NewClient(srpParams, salt, identity, password, clientSecretSmallA) + + // Client produces A + A := client.ComputeA() + srpVerifier := ComputeVerifier(srpParams, salt, identity, password) + + // Create server + server := NewServer(srpParams, srpVerifier, serverSecret) + + // Server accepts A + server.SetA(A) + + // Server produces B + B := server.ComputeB() + + // Client accepts B + client.SetB(B) + + // Client produces M1 now + M1 := client.ComputeM1() + + // Server likes client's M1 + serverM2, err := server.CheckM1(M1) + assert.NoError(t, err, "Server should have liked M1") + + // Client and server agree on K + clientK := client.ComputeK() + serverK := server.ComputeK() + assert.Equal(t, clientK, serverK, "K's should match") + + err = client.CheckM2(serverM2) + assert.NoError(t, err, "M2 should have been valid") + }) + } +} + +func TestServerRejectsWrongM1(t *testing.T) { + a, b := getAAndB() + params := GetParams(4096) + badClient := NewClient(params, salt, identity, []byte("Bad"), a) + server := NewServer(params, getVerifier(), b) + badClient.SetB(server.ComputeB()) + _, err := server.CheckM1(badClient.ComputeM1()) + assert.EqualError(t, err, "Client did not use the same password", "M1 check should have failed") +} + +func TestServerRejectsBadA(t *testing.T) { + // client's "A" must be 1..N-1 . Reject 0 and N and N+1. We should + // reject 2*N too, but our Buffer-length checks reject it before the + // number itself is examined. + + _, b := getAAndB() + params := GetParams(4096) + server := NewServer(params, getVerifier(), b) + + assert.Panics(t, func() { + server.SetA(intToBytes(big.NewInt(0))) + }, "Server should have paniced") + + assert.Panics(t, func() { + server.SetA(intToBytes(params.N)) + }, "Server should have paniced") + + assert.Panics(t, func() { + NPlus1 := new(big.Int) + NPlus1.Add(params.N, big.NewInt(1)) + server.SetA(intToBytes(NPlus1)) + }, "Server should have paniced") +} + +func TestClientRejectsBadB(t *testing.T) { + // server's "B" must be 1..N-1 . Reject 0 and N and N+1 + a, _ := getAAndB() + params := GetParams(4096) + client := NewClient(params, salt, identity, password, a) + + assert.Panics(t, func() { + client.SetB(intToBytes(big.NewInt(0))) + }, "Client should have paniced") + + assert.Panics(t, func() { + client.SetB(intToBytes(params.N)) + }, "Client should have paniced") + + assert.Panics(t, func() { + NPlus1 := new(big.Int) + NPlus1.Add(params.N, big.NewInt(1)) + client.SetB(intToBytes(NPlus1)) + }, "Client should have paniced") +} + +func TestClientRejectsBadM2(t *testing.T) { + a, b := getAAndB() + params := GetParams(4096) + client := NewClient(params, salt, identity, password, a) + + // Client produces A + A := client.ComputeA() + + // Create server + server := NewServer(params, getVerifier(), b) + + // Server produced B + B := server.ComputeB() + + // Server accepts A + server.SetA(A) + + // Client accepts B + client.SetB(B) + + // Client produces M1 now + M1 := client.ComputeM1() + + // Server likes client's M1 + server.CheckM1(M1) + + // We tamper with server's M2 + tamperedM2 := append(server.M2, 'a') + + // Client and server agree on K + clientK := client.ComputeK() + serverK := server.ComputeK() + assert.Equal(t, clientK, serverK, "Ks should match") + + err := client.CheckM2(tamperedM2) + assert.EqualError(t, err, "M2 didn't check", "Client should reject M2") +} + +func TestRFC5054(t *testing.T) { + params := GetParams(1024) + I := []byte("alice") + P := []byte("password123") + s := bytesFromHexString("beb25379d1a8581eb5a727673a2441ee") + kExpected := bytesFromHexString("7556aa045aef2cdd07abaf0f665c3e818913186f") + xExpected := bytesFromHexString("94b7555aabe9127cc58ccf4993db6cf84d16c124") + vExpected := bytesFromHexString(` + 7e273de8 696ffc4f 4e337d05 b4b375be b0dde156 9e8fa00a 9886d812 + 9bada1f1 822223ca 1a605b53 0e379ba4 729fdc59 f105b478 7e5186f5 + c671085a 1447b52a 48cf1970 b4fb6f84 00bbf4ce bfbb1681 52e08ab5 + ea53d15c 1aff87b2 b9da6e04 e058ad51 cc72bfc9 033b564e 26480d78 + e955a5e2 9e7ab245 db2be315 e2099afb`) + a := bytesFromHexString("60975527035cf2ad1989806f0407210bc81edc04e2762a56afd529ddda2d4393") + b := bytesFromHexString("e487cb59d31ac550471e81f00f6928e01dda08e974a004f49e61f5d105284d20") + AExpected := bytesFromHexString(` + 61d5e490 f6f1b795 47b0704c 436f523d d0e560f0 c64115bb 72557ec4 + 4352e890 3211c046 92272d8b 2d1a5358 a2cf1b6e 0bfcf99f 921530ec + 8e393561 79eae45e 42ba92ae aced8251 71e1e8b9 af6d9c03 e1327f44 + be087ef0 6530e69f 66615261 eef54073 ca11cf58 58f0edfd fe15efea + b349ef5d 76988a36 72fac47b 0769447b`) + BExpected := bytesFromHexString(` + bd0c6151 2c692c0c b6d041fa 01bb152d 4916a1e7 7af46ae1 05393011 + baf38964 dc46a067 0dd125b9 5a981652 236f99d9 b681cbf8 7837ec99 + 6c6da044 53728610 d0c6ddb5 8b318885 d7d82c7f 8deb75ce 7bd4fbaa + 37089e6f 9c6059f3 88838e7a 00030b33 1eb76840 910440b1 b27aaeae + eb4012b7 d7665238 a8e3fb00 4b117b58`) + uExpected := bytesFromHexString("ce38b9593487da98554ed47d70a7ae5f462ef019") + SExpected := bytesFromHexString(` + b0dc82ba bcf30674 ae450c02 87745e79 90a3381f 63b387aa f271a10d + 233861e3 59b48220 f7c4693c 9ae12b0a 6f67809f 0876e2d0 13800d6c + 41bb59b6 d5979b5c 00a172b4 a2a5903a 0bdcaf8a 709585eb 2afafa8f + 3499b200 210dcc1f 10eb3394 3cd67fc8 8a2f39a4 be5bec4e c0a3212d + c346d7e4 74b29ede 8a469ffe ca686e5a`) + + verifier := ComputeVerifier(params, s, I, P) + client := NewClient(params, s, I, P, a) + + // X + assert.Equal(t, xExpected, intToBytes(client.X), "x should match") + + // V + assert.Equal(t, vExpected, verifier, "Verifier should match") + + // k + assert.Equal(t, kExpected, intToBytes(client.Multiplier), "k should match") + + // A + assert.Equal(t, AExpected, client.ComputeA(), "A should match") + + // B + server := NewServer(params, verifier, b) + assert.Equal(t, BExpected, server.ComputeB(), "B should match") + + // u and S client + client.SetB(BExpected) + assert.Equal(t, uExpected, intToBytes(client.u), "u should match") + assert.Equal(t, SExpected, intToBytes(client.s), "S should match") + + // S server + server.SetA(AExpected) + assert.Equal(t, SExpected, intToBytes(server.s), "S should match") +} diff --git a/server/pkg/srp/util.go b/server/pkg/srp/util.go new file mode 100644 index 0000000000..60929bfd56 --- /dev/null +++ b/server/pkg/srp/util.go @@ -0,0 +1,48 @@ +package srp + +import ( + "encoding/hex" + "hash" + "math/big" + "regexp" +) + +// Helpers + +func padTo(bytes []byte, length int) []byte { + paddingLength := length - len(bytes) + padding := make([]byte, paddingLength, paddingLength) + + return append(padding, bytes...) +} + +func padToN(number *big.Int, params *SRPParams) []byte { + return padTo(number.Bytes(), params.NLengthBits/8) +} + +func hashToBytes(h hash.Hash) []byte { + return h.Sum(nil) +} + +func hashToInt(h hash.Hash) *big.Int { + U := new(big.Int) + U.SetBytes(hashToBytes(h)) + return U +} + +func intFromBytes(bytes []byte) *big.Int { + i := new(big.Int) + i.SetBytes(bytes) + return i +} + +func intToBytes(i *big.Int) []byte { + return i.Bytes() +} + +func bytesFromHexString(s string) []byte { + re, _ := regexp.Compile("[^0-9a-fA-F]") + h := re.ReplaceAll([]byte(s), []byte("")) + b, _ := hex.DecodeString(string(h)) + return b +}