[server] Update srp validation

This commit is contained in:
Neeraj Gupta
2025-01-16 15:20:50 +05:30
parent 9e8e2af51d
commit 5302f67679
10 changed files with 790 additions and 34 deletions

View File

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

View File

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

View File

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

View File

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

136
server/pkg/srp/client.go Normal file
View File

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

95
server/pkg/srp/params.go Normal file
View File

@@ -0,0 +1,95 @@
package srp
import (
"crypto"
"fmt"
"math/big"
)
// Map of bits to <g, N> 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
`)
}

111
server/pkg/srp/server.go Normal file
View File

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

107
server/pkg/srp/srp.go Normal file
View File

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

253
server/pkg/srp/srp_test.go Normal file
View File

@@ -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")
}

48
server/pkg/srp/util.go Normal file
View File

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