@@ -20,19 +20,27 @@ function continueSession(session, password, serverData) {
20
20
if ( session . message !== 'SASLInitialResponse' ) {
21
21
throw new Error ( 'SASL: Last message was not SASLInitialResponse' )
22
22
}
23
+ if ( typeof password !== 'string' ) {
24
+ throw new Error ( 'SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string' )
25
+ }
26
+ if ( typeof serverData !== 'string' ) {
27
+ throw new Error ( 'SASL: SCRAM-SERVER-FIRST-MESSAGE: serverData must be a string' )
28
+ }
23
29
24
- const sv = extractVariablesFromFirstServerMessage ( serverData )
30
+ const sv = parseServerFirstMessage ( serverData )
25
31
26
32
if ( ! sv . nonce . startsWith ( session . clientNonce ) ) {
27
33
throw new Error ( 'SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce does not start with client nonce' )
34
+ } else if ( sv . nonce . length === session . clientNonce . length ) {
35
+ throw new Error ( 'SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce is too short' )
28
36
}
29
37
30
38
var saltBytes = Buffer . from ( sv . salt , 'base64' )
31
39
32
40
var saltedPassword = Hi ( password , saltBytes , sv . iteration )
33
41
34
- var clientKey = createHMAC ( saltedPassword , 'Client Key' )
35
- var storedKey = crypto . createHash ( ' sha256' ) . update ( clientKey ) . digest ( )
42
+ var clientKey = hmacSha256 ( saltedPassword , 'Client Key' )
43
+ var storedKey = sha256 ( clientKey )
36
44
37
45
var clientFirstMessageBare = 'n=*,r=' + session . clientNonce
38
46
var serverFirstMessage = 'r=' + sv . nonce + ',s=' + sv . salt + ',i=' + sv . iteration
@@ -41,12 +49,12 @@ function continueSession(session, password, serverData) {
41
49
42
50
var authMessage = clientFirstMessageBare + ',' + serverFirstMessage + ',' + clientFinalMessageWithoutProof
43
51
44
- var clientSignature = createHMAC ( storedKey , authMessage )
52
+ var clientSignature = hmacSha256 ( storedKey , authMessage )
45
53
var clientProofBytes = xorBuffers ( clientKey , clientSignature )
46
54
var clientProof = clientProofBytes . toString ( 'base64' )
47
55
48
- var serverKey = createHMAC ( saltedPassword , 'Server Key' )
49
- var serverSignatureBytes = createHMAC ( serverKey , authMessage )
56
+ var serverKey = hmacSha256 ( saltedPassword , 'Server Key' )
57
+ var serverSignatureBytes = hmacSha256 ( serverKey , authMessage )
50
58
51
59
session . message = 'SASLResponse'
52
60
session . serverSignature = serverSignatureBytes . toString ( 'base64' )
@@ -57,54 +65,87 @@ function finalizeSession(session, serverData) {
57
65
if ( session . message !== 'SASLResponse' ) {
58
66
throw new Error ( 'SASL: Last message was not SASLResponse' )
59
67
}
68
+ if ( typeof serverData !== 'string' ) {
69
+ throw new Error ( 'SASL: SCRAM-SERVER-FINAL-MESSAGE: serverData must be a string' )
70
+ }
60
71
61
- var serverSignature
62
-
63
- String ( serverData )
64
- . split ( ',' )
65
- . forEach ( function ( part ) {
66
- switch ( part [ 0 ] ) {
67
- case 'v' :
68
- serverSignature = part . substr ( 2 )
69
- break
70
- }
71
- } )
72
+ const { serverSignature } = parseServerFinalMessage ( serverData )
72
73
73
74
if ( serverSignature !== session . serverSignature ) {
74
75
throw new Error ( 'SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature does not match' )
75
76
}
76
77
}
77
78
78
- function extractVariablesFromFirstServerMessage ( data ) {
79
- var nonce , salt , iteration
80
-
81
- String ( data )
82
- . split ( ',' )
83
- . forEach ( function ( part ) {
84
- switch ( part [ 0 ] ) {
85
- case 'r' :
86
- nonce = part . substr ( 2 )
87
- break
88
- case 's' :
89
- salt = part . substr ( 2 )
90
- break
91
- case 'i' :
92
- iteration = parseInt ( part . substr ( 2 ) , 10 )
93
- break
79
+ /**
80
+ * printable = %x21-2B / %x2D-7E
81
+ * ;; Printable ASCII except ",".
82
+ * ;; Note that any "printable" is also
83
+ * ;; a valid "value".
84
+ */
85
+ function isPrintableChars ( text ) {
86
+ if ( typeof text !== 'string' ) {
87
+ throw new TypeError ( 'SASL: text must be a string' )
88
+ }
89
+ return text
90
+ . split ( '' )
91
+ . map ( ( _ , i ) => text . charCodeAt ( i ) )
92
+ . every ( ( c ) => ( c >= 0x21 && c <= 0x2b ) || ( c >= 0x2d && c <= 0x7e ) )
93
+ }
94
+
95
+ /**
96
+ * base64-char = ALPHA / DIGIT / "/" / "+"
97
+ *
98
+ * base64-4 = 4base64-char
99
+ *
100
+ * base64-3 = 3base64-char "="
101
+ *
102
+ * base64-2 = 2base64-char "=="
103
+ *
104
+ * base64 = *base64-4 [base64-3 / base64-2]
105
+ */
106
+ function isBase64 ( text ) {
107
+ return / ^ (?: [ a - z A - Z 0 - 9 + / ] { 4 } ) * (?: [ a - z A - Z 0 - 9 + / ] { 2 } = = | [ a - z A - Z 0 - 9 + / ] { 3 } = ) ? $ / . test ( text )
108
+ }
109
+
110
+ function parseAttributePairs ( text ) {
111
+ if ( typeof text !== 'string' ) {
112
+ throw new TypeError ( 'SASL: attribute pairs text must be a string' )
113
+ }
114
+
115
+ return new Map (
116
+ text . split ( ',' ) . map ( ( attrValue ) => {
117
+ if ( ! / ^ .= / . test ( attrValue ) ) {
118
+ throw new Error ( 'SASL: Invalid attribute pair entry' )
94
119
}
120
+ const name = attrValue [ 0 ]
121
+ const value = attrValue . substring ( 2 )
122
+ return [ name , value ]
95
123
} )
124
+ )
125
+ }
96
126
127
+ function parseServerFirstMessage ( data ) {
128
+ const attrPairs = parseAttributePairs ( data )
129
+
130
+ const nonce = attrPairs . get ( 'r' )
97
131
if ( ! nonce ) {
98
132
throw new Error ( 'SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce missing' )
133
+ } else if ( ! isPrintableChars ( nonce ) ) {
134
+ throw new Error ( 'SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce must only contain printable characters' )
99
135
}
100
-
136
+ const salt = attrPairs . get ( 's' )
101
137
if ( ! salt ) {
102
138
throw new Error ( 'SASL: SCRAM-SERVER-FIRST-MESSAGE: salt missing' )
139
+ } else if ( ! isBase64 ( salt ) ) {
140
+ throw new Error ( 'SASL: SCRAM-SERVER-FIRST-MESSAGE: salt must be base64' )
103
141
}
104
-
105
- if ( ! iteration ) {
142
+ const iterationText = attrPairs . get ( 'i' )
143
+ if ( ! iterationText ) {
106
144
throw new Error ( 'SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration missing' )
145
+ } else if ( ! / ^ [ 1 - 9 ] [ 0 - 9 ] * $ / . test ( iterationText ) ) {
146
+ throw new Error ( 'SASL: SCRAM-SERVER-FIRST-MESSAGE: invalid iteration count' )
107
147
}
148
+ const iteration = parseInt ( iterationText , 10 )
108
149
109
150
return {
110
151
nonce,
@@ -113,31 +154,48 @@ function extractVariablesFromFirstServerMessage(data) {
113
154
}
114
155
}
115
156
157
+ function parseServerFinalMessage ( serverData ) {
158
+ const attrPairs = parseAttributePairs ( serverData )
159
+ const serverSignature = attrPairs . get ( 'v' )
160
+ if ( ! serverSignature ) {
161
+ throw new Error ( 'SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature is missing' )
162
+ } else if ( ! isBase64 ( serverSignature ) ) {
163
+ throw new Error ( 'SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature must be base64' )
164
+ }
165
+ return {
166
+ serverSignature,
167
+ }
168
+ }
169
+
116
170
function xorBuffers ( a , b ) {
117
- if ( ! Buffer . isBuffer ( a ) ) a = Buffer . from ( a )
118
- if ( ! Buffer . isBuffer ( b ) ) b = Buffer . from ( b )
119
- var res = [ ]
120
- if ( a . length > b . length ) {
121
- for ( var i = 0 ; i < b . length ; i ++ ) {
122
- res . push ( a [ i ] ^ b [ i ] )
123
- }
124
- } else {
125
- for ( var j = 0 ; j < a . length ; j ++ ) {
126
- res . push ( a [ j ] ^ b [ j ] )
127
- }
128
- }
129
- return Buffer . from ( res )
171
+ if ( ! Buffer . isBuffer ( a ) ) {
172
+ throw new TypeError ( 'first argument must be a Buffer' )
173
+ }
174
+ if ( ! Buffer . isBuffer ( b ) ) {
175
+ throw new TypeError ( 'second argument must be a Buffer' )
176
+ }
177
+ if ( a . length !== b . length ) {
178
+ throw new Error ( 'Buffer lengths must match' )
179
+ }
180
+ if ( a . length === 0 ) {
181
+ throw new Error ( 'Buffers cannot be empty' )
182
+ }
183
+ return Buffer . from ( a . map ( ( _ , i ) => a [ i ] ^ b [ i ] ) )
184
+ }
185
+
186
+ function sha256 ( text ) {
187
+ return crypto . createHash ( 'sha256' ) . update ( text ) . digest ( )
130
188
}
131
189
132
- function createHMAC ( key , msg ) {
190
+ function hmacSha256 ( key , msg ) {
133
191
return crypto . createHmac ( 'sha256' , key ) . update ( msg ) . digest ( )
134
192
}
135
193
136
194
function Hi ( password , saltBytes , iterations ) {
137
- var ui1 = createHMAC ( password , Buffer . concat ( [ saltBytes , Buffer . from ( [ 0 , 0 , 0 , 1 ] ) ] ) )
195
+ var ui1 = hmacSha256 ( password , Buffer . concat ( [ saltBytes , Buffer . from ( [ 0 , 0 , 0 , 1 ] ) ] ) )
138
196
var ui = ui1
139
197
for ( var i = 0 ; i < iterations - 1 ; i ++ ) {
140
- ui1 = createHMAC ( password , ui1 )
198
+ ui1 = hmacSha256 ( password , ui1 )
141
199
ui = xorBuffers ( ui , ui1 )
142
200
}
143
201
0 commit comments