@@ -20,19 +20,27 @@ function continueSession(session, password, serverData) {
2020 if ( session . message !== 'SASLInitialResponse' ) {
2121 throw new Error ( 'SASL: Last message was not SASLInitialResponse' )
2222 }
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+ }
2329
24- const sv = extractVariablesFromFirstServerMessage ( serverData )
30+ const sv = parseServerFirstMessage ( serverData )
2531
2632 if ( ! sv . nonce . startsWith ( session . clientNonce ) ) {
2733 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' )
2836 }
2937
3038 var saltBytes = Buffer . from ( sv . salt , 'base64' )
3139
3240 var saltedPassword = Hi ( password , saltBytes , sv . iteration )
3341
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 )
3644
3745 var clientFirstMessageBare = 'n=*,r=' + session . clientNonce
3846 var serverFirstMessage = 'r=' + sv . nonce + ',s=' + sv . salt + ',i=' + sv . iteration
@@ -41,12 +49,12 @@ function continueSession(session, password, serverData) {
4149
4250 var authMessage = clientFirstMessageBare + ',' + serverFirstMessage + ',' + clientFinalMessageWithoutProof
4351
44- var clientSignature = createHMAC ( storedKey , authMessage )
52+ var clientSignature = hmacSha256 ( storedKey , authMessage )
4553 var clientProofBytes = xorBuffers ( clientKey , clientSignature )
4654 var clientProof = clientProofBytes . toString ( 'base64' )
4755
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 )
5058
5159 session . message = 'SASLResponse'
5260 session . serverSignature = serverSignatureBytes . toString ( 'base64' )
@@ -57,54 +65,87 @@ function finalizeSession(session, serverData) {
5765 if ( session . message !== 'SASLResponse' ) {
5866 throw new Error ( 'SASL: Last message was not SASLResponse' )
5967 }
68+ if ( typeof serverData !== 'string' ) {
69+ throw new Error ( 'SASL: SCRAM-SERVER-FINAL-MESSAGE: serverData must be a string' )
70+ }
6071
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 )
7273
7374 if ( serverSignature !== session . serverSignature ) {
7475 throw new Error ( 'SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature does not match' )
7576 }
7677}
7778
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' )
94119 }
120+ const name = attrValue [ 0 ]
121+ const value = attrValue . substring ( 2 )
122+ return [ name , value ]
95123 } )
124+ )
125+ }
96126
127+ function parseServerFirstMessage ( data ) {
128+ const attrPairs = parseAttributePairs ( data )
129+
130+ const nonce = attrPairs . get ( 'r' )
97131 if ( ! nonce ) {
98132 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' )
99135 }
100-
136+ const salt = attrPairs . get ( 's' )
101137 if ( ! salt ) {
102138 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' )
103141 }
104-
105- if ( ! iteration ) {
142+ const iterationText = attrPairs . get ( 'i' )
143+ if ( ! iterationText ) {
106144 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' )
107147 }
148+ const iteration = parseInt ( iterationText , 10 )
108149
109150 return {
110151 nonce,
@@ -113,31 +154,48 @@ function extractVariablesFromFirstServerMessage(data) {
113154 }
114155}
115156
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+
116170function 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 ( )
130188}
131189
132- function createHMAC ( key , msg ) {
190+ function hmacSha256 ( key , msg ) {
133191 return crypto . createHmac ( 'sha256' , key ) . update ( msg ) . digest ( )
134192}
135193
136194function 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 ] ) ] ) )
138196 var ui = ui1
139197 for ( var i = 0 ; i < iterations - 1 ; i ++ ) {
140- ui1 = createHMAC ( password , ui1 )
198+ ui1 = hmacSha256 ( password , ui1 )
141199 ui = xorBuffers ( ui , ui1 )
142200 }
143201
0 commit comments