sasl.js 6.2 KB

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