UNPKG

3.96 kBJavaScriptView Raw
1'use strict'
2const crypto = require('crypto')
3
4function startSession(mechanisms) {
5 if (mechanisms.indexOf('SCRAM-SHA-256') === -1) {
6 throw new Error('SASL: Only mechanism SCRAM-SHA-256 is currently supported')
7 }
8
9 const clientNonce = crypto.randomBytes(18).toString('base64')
10
11 return {
12 mechanism: 'SCRAM-SHA-256',
13 clientNonce,
14 response: 'n,,n=*,r=' + clientNonce,
15 message: 'SASLInitialResponse',
16 }
17}
18
19function continueSession(session, password, serverData) {
20 if (session.message !== 'SASLInitialResponse') {
21 throw new Error('SASL: Last message was not SASLInitialResponse')
22 }
23
24 const sv = extractVariablesFromFirstServerMessage(serverData)
25
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 }
29
30 var saltBytes = Buffer.from(sv.salt, 'base64')
31
32 var saltedPassword = Hi(password, saltBytes, sv.iteration)
33
34 var clientKey = createHMAC(saltedPassword, 'Client Key')
35 var storedKey = crypto.createHash('sha256').update(clientKey).digest()
36
37 var clientFirstMessageBare = 'n=*,r=' + session.clientNonce
38 var serverFirstMessage = 'r=' + sv.nonce + ',s=' + sv.salt + ',i=' + sv.iteration
39
40 var clientFinalMessageWithoutProof = 'c=biws,r=' + sv.nonce
41
42 var authMessage = clientFirstMessageBare + ',' + serverFirstMessage + ',' + clientFinalMessageWithoutProof
43
44 var clientSignature = createHMAC(storedKey, authMessage)
45 var clientProofBytes = xorBuffers(clientKey, clientSignature)
46 var clientProof = clientProofBytes.toString('base64')
47
48 var serverKey = createHMAC(saltedPassword, 'Server Key')
49 var serverSignatureBytes = createHMAC(serverKey, authMessage)
50
51 session.message = 'SASLResponse'
52 session.serverSignature = serverSignatureBytes.toString('base64')
53 session.response = clientFinalMessageWithoutProof + ',p=' + clientProof
54}
55
56function finalizeSession(session, serverData) {
57 if (session.message !== 'SASLResponse') {
58 throw new Error('SASL: Last message was not SASLResponse')
59 }
60
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
73 if (serverSignature !== session.serverSignature) {
74 throw new Error('SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature does not match')
75 }
76}
77
78function 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
94 }
95 })
96
97 if (!nonce) {
98 throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce missing')
99 }
100
101 if (!salt) {
102 throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: salt missing')
103 }
104
105 if (!iteration) {
106 throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration missing')
107 }
108
109 return {
110 nonce,
111 salt,
112 iteration,
113 }
114}
115
116function 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)
130}
131
132function createHMAC(key, msg) {
133 return crypto.createHmac('sha256', key).update(msg).digest()
134}
135
136function Hi(password, saltBytes, iterations) {
137 var ui1 = createHMAC(password, Buffer.concat([saltBytes, Buffer.from([0, 0, 0, 1])]))
138 var ui = ui1
139 for (var i = 0; i < iterations - 1; i++) {
140 ui1 = createHMAC(password, ui1)
141 ui = xorBuffers(ui, ui1)
142 }
143
144 return ui
145}
146
147module.exports = {
148 startSession,
149 continueSession,
150 finalizeSession,
151}