/ src / rlpx / ecies.js
ecies.js
  1  const crypto = require('crypto')
  2  const secp256k1 = require('secp256k1')
  3  const Buffer = require('safe-buffer').Buffer
  4  const rlp = require('rlp-encoding')
  5  const util = require('../util')
  6  const MAC = require('./mac')
  7  
  8  function ecdhX (publicKey, privateKey) {
  9    // return (publicKey * privateKey).x
 10    return secp256k1.ecdhUnsafe(publicKey, privateKey, true).slice(1)
 11  }
 12  
 13  // a straigth rip from python interop w/go ecies implementation
 14  // for sha3, blocksize is 136 bytes
 15  // for sha256, blocksize is 64 bytes
 16  // NIST SP 800-56a Concatenation Key Derivation Function (see section 5.8.1).
 17  // https://github.com/ethereum/pydevp2p/blob/master/devp2p/crypto.py#L295
 18  // https://github.com/ethereum/go-ethereum/blob/fe532a98f9f32bb81ef0d8d013cf44327830d11e/crypto/ecies/ecies.go#L165
 19  // https://github.com/ethereum/cpp-ethereum/blob/develop/libdevcrypto/CryptoPP.cpp#L36
 20  function concatKDF (keyMaterial, keyLength) {
 21    const SHA256BlockSize = 64
 22    const reps = ((keyLength + 7) * 8) / (SHA256BlockSize * 8)
 23  
 24    const buffers = []
 25    for (let counter = 0, tmp = Buffer.allocUnsafe(4); counter <= reps;) {
 26      counter += 1
 27      tmp.writeUInt32BE(counter)
 28      buffers.push(crypto.createHash('sha256').update(tmp).update(keyMaterial).digest())
 29    }
 30  
 31    return Buffer.concat(buffers).slice(0, keyLength)
 32  }
 33  
 34  class ECIES {
 35    constructor (privateKey, id, remoteId) {
 36      this._privateKey = privateKey
 37      this._publicKey = util.id2pk(id)
 38      this._remotePublicKey = remoteId ? util.id2pk(remoteId) : null
 39  
 40      this._nonce = crypto.randomBytes(32)
 41      this._remoteNonce = null
 42  
 43      this._initMsg = null
 44      this._remoteInitMsg = null
 45  
 46      this._gotEIP8Auth = false
 47      this._gotEIP8Ack = false
 48  
 49      this._ingressAes = null
 50      this._egressAes = null
 51  
 52      this._ingressMac = null
 53      this._egressMac = null
 54  
 55      this._ephemeralPrivateKey = util.genPrivateKey()
 56      this._ephemeralPublicKey = secp256k1.publicKeyCreate(this._ephemeralPrivateKey, false)
 57      this._remoteEphemeralPublicKey = null // we don't need store this key, but why don't?
 58      this._ephemeralSharedSecret = null
 59  
 60      this._bodySize = null
 61    }
 62  
 63    _encryptMessage (data, sharedMacData = null) {
 64      const privateKey = util.genPrivateKey()
 65      const x = ecdhX(this._remotePublicKey, privateKey)
 66      const key = concatKDF(x, 32)
 67      const ekey = key.slice(0, 16) // encryption key
 68      const mkey = crypto.createHash('sha256').update(key.slice(16, 32)).digest() // MAC key
 69  
 70      // encrypt
 71      const IV = crypto.randomBytes(16)
 72      const cipher = crypto.createCipheriv('aes-128-ctr', ekey, IV)
 73      const encryptedData = cipher.update(data)
 74      const dataIV = Buffer.concat([ IV, encryptedData ])
 75  
 76      // create tag
 77      if (!sharedMacData) {
 78        sharedMacData = Buffer.from([])
 79      }
 80      const tag = crypto.createHmac('sha256', mkey).update(Buffer.concat([dataIV, sharedMacData])).digest()
 81  
 82      const publicKey = secp256k1.publicKeyCreate(privateKey, false)
 83      return Buffer.concat([ publicKey, dataIV, tag ])
 84    }
 85  
 86    _decryptMessage (data, sharedMacData = null) {
 87      util.assertEq(data.slice(0, 1), Buffer.from('04', 'hex'), 'wrong ecies header (possible cause: EIP8 upgrade)')
 88  
 89      const publicKey = data.slice(0, 65)
 90      const dataIV = data.slice(65, -32)
 91      const tag = data.slice(-32)
 92  
 93      // derive keys
 94      const x = ecdhX(publicKey, this._privateKey)
 95      const key = concatKDF(x, 32)
 96      const ekey = key.slice(0, 16) // encryption key
 97      const mkey = crypto.createHash('sha256').update(key.slice(16, 32)).digest() // MAC key
 98  
 99      // check the tag
100      if (!sharedMacData) {
101        sharedMacData = Buffer.from([])
102      }
103      const _tag = crypto.createHmac('sha256', mkey).update(Buffer.concat([dataIV, sharedMacData])).digest()
104      util.assertEq(_tag, tag, 'should have valid tag')
105  
106      // decrypt data
107      const IV = dataIV.slice(0, 16)
108      const encryptedData = dataIV.slice(16)
109      const decipher = crypto.createDecipheriv('aes-128-ctr', ekey, IV)
110      return decipher.update(encryptedData)
111    }
112  
113    _setupFrame (remoteData, incoming) {
114      const nonceMaterial = incoming
115        ? Buffer.concat([ this._nonce, this._remoteNonce ])
116        : Buffer.concat([ this._remoteNonce, this._nonce ])
117      const hNonce = util.keccak256(nonceMaterial)
118  
119      const IV = Buffer.allocUnsafe(16).fill(0x00)
120      const sharedSecret = util.keccak256(this._ephemeralSharedSecret, hNonce)
121  
122      const aesSecret = util.keccak256(this._ephemeralSharedSecret, sharedSecret)
123      this._ingressAes = crypto.createDecipheriv('aes-256-ctr', aesSecret, IV)
124      this._egressAes = crypto.createDecipheriv('aes-256-ctr', aesSecret, IV)
125  
126      const macSecret = util.keccak256(this._ephemeralSharedSecret, aesSecret)
127      this._ingressMac = new MAC(macSecret)
128      this._ingressMac.update(Buffer.concat([ util.xor(macSecret, this._nonce), remoteData ]))
129      this._egressMac = new MAC(macSecret)
130      this._egressMac.update(Buffer.concat([ util.xor(macSecret, this._remoteNonce), this._initMsg ]))
131    }
132  
133    createAuthEIP8 () {
134      const x = ecdhX(this._remotePublicKey, this._privateKey)
135      const sig = secp256k1.sign(util.xor(x, this._nonce), this._ephemeralPrivateKey)
136      const data = [
137        Buffer.concat([sig.signature, Buffer.from([ sig.recovery ])]),
138        // util.keccak256(util.pk2id(this._ephemeralPublicKey)),
139        util.pk2id(this._publicKey),
140        this._nonce,
141        Buffer.from([ 0x04 ])
142      ]
143  
144      const dataRLP = rlp.encode(data)
145      const pad = crypto.randomBytes(100 + Math.floor(Math.random() * 151)) // Random padding between 100, 250
146      const authMsg = Buffer.concat([dataRLP, pad])
147      const overheadLength = 113
148      const sharedMacData = util.int2buffer(authMsg.length + overheadLength)
149      this._initMsg = Buffer.concat([sharedMacData, this._encryptMessage(authMsg, sharedMacData)])
150      return this._initMsg
151    }
152  
153    createAuthNonEIP8 () {
154      const x = ecdhX(this._remotePublicKey, this._privateKey)
155      const sig = secp256k1.sign(util.xor(x, this._nonce), this._ephemeralPrivateKey)
156      const data = Buffer.concat([
157        sig.signature,
158        Buffer.from([ sig.recovery ]),
159        util.keccak256(util.pk2id(this._ephemeralPublicKey)),
160        util.pk2id(this._publicKey),
161        this._nonce,
162        Buffer.from([ 0x00 ])
163      ])
164  
165      this._initMsg = this._encryptMessage(data)
166      return this._initMsg
167    }
168  
169    parseAuthPlain (data, sharedMacData = null) {
170      const prefix = sharedMacData !== null ? sharedMacData : Buffer.from([])
171      this._remoteInitMsg = Buffer.concat([prefix, data])
172      const decrypted = this._decryptMessage(data, sharedMacData)
173  
174      var signature = null
175      var recoveryId = null
176      var heid = null
177      var remotePublicKey = null
178      var nonce = null
179  
180      if (!this._gotEIP8Auth) {
181        util.assertEq(decrypted.length, 194, 'invalid packet length')
182  
183        signature = decrypted.slice(0, 64)
184        recoveryId = decrypted[64]
185        heid = decrypted.slice(65, 97) // 32 bytes
186        remotePublicKey = util.id2pk(decrypted.slice(97, 161))
187        nonce = decrypted.slice(161, 193)
188      } else {
189        const decoded = rlp.decode(decrypted)
190  
191        signature = decoded[0].slice(0, 64)
192        recoveryId = decoded[0][64]
193        remotePublicKey = util.id2pk(decoded[1])
194        nonce = decoded[2]
195      }
196  
197      // parse packet
198      this._remotePublicKey = remotePublicKey // 64 bytes
199      this._remoteNonce = nonce // 32 bytes
200      // util.assertEq(decrypted[193], 0, 'invalid postfix')
201  
202      const x = ecdhX(this._remotePublicKey, this._privateKey)
203      this._remoteEphemeralPublicKey = secp256k1.recover(util.xor(x, this._remoteNonce), signature, recoveryId, false)
204      this._ephemeralSharedSecret = ecdhX(this._remoteEphemeralPublicKey, this._ephemeralPrivateKey)
205  
206      if (heid !== null) {
207        var _heid = util.keccak256(util.pk2id(this._remoteEphemeralPublicKey))
208        util.assertEq(_heid, heid, 'the hash of the ephemeral key should match')
209      }
210    }
211  
212    parseAuthEIP8 (data) {
213      const size = util.buffer2int(data.slice(0, 2)) + 2
214      util.assertEq(data.length, size, 'message length different from specified size (EIP8)')
215      this.parseAuthPlain(data.slice(2), data.slice(0, 2))
216    }
217  
218    createAckEIP8 () {
219      const data = [
220        util.pk2id(this._ephemeralPublicKey),
221        this._nonce,
222        Buffer.from([ 0x04 ])
223      ]
224  
225      const dataRLP = rlp.encode(data)
226      const pad = crypto.randomBytes(100 + Math.floor(Math.random() * 151)) // Random padding between 100, 250
227      const ackMsg = Buffer.concat([dataRLP, pad])
228      const overheadLength = 113
229      const sharedMacData = util.int2buffer(ackMsg.length + overheadLength)
230      this._initMsg = Buffer.concat([sharedMacData, this._encryptMessage(ackMsg, sharedMacData)])
231      this._setupFrame(this._remoteInitMsg, true)
232      return this._initMsg
233    }
234  
235    createAckOld () {
236      const data = Buffer.concat([
237        util.pk2id(this._ephemeralPublicKey),
238        this._nonce,
239        Buffer.from([ 0x00 ])
240      ])
241  
242      this._initMsg = this._encryptMessage(data)
243      this._setupFrame(this._remoteInitMsg, true)
244      return this._initMsg
245    }
246  
247    parseAckPlain (data, sharedMacData = null) {
248      const decrypted = this._decryptMessage(data, sharedMacData)
249  
250      var remoteEphemeralPublicKey = null
251      var remoteNonce = null
252  
253      if (!this._gotEIP8Ack) {
254        util.assertEq(decrypted.length, 97, 'invalid packet length')
255        util.assertEq(decrypted[96], 0, 'invalid postfix')
256  
257        remoteEphemeralPublicKey = util.id2pk(decrypted.slice(0, 64))
258        remoteNonce = decrypted.slice(64, 96)
259      } else {
260        const decoded = rlp.decode(decrypted)
261  
262        remoteEphemeralPublicKey = util.id2pk(decoded[0])
263        remoteNonce = decoded[1]
264      }
265  
266      // parse packet
267      this._remoteEphemeralPublicKey = remoteEphemeralPublicKey
268      this._remoteNonce = remoteNonce
269  
270      this._ephemeralSharedSecret = ecdhX(this._remoteEphemeralPublicKey, this._ephemeralPrivateKey)
271      if (!sharedMacData) {
272        sharedMacData = Buffer.from([])
273      }
274      this._setupFrame(Buffer.concat([sharedMacData, data]), false)
275    }
276  
277    parseAckEIP8 (data) { // eslint-disable-line
278      const size = util.buffer2int(data.slice(0, 2)) + 2
279      util.assertEq(data.length, size, 'message length different from specified size (EIP8)')
280      this.parseAckPlain(data.slice(2), data.slice(0, 2))
281    }
282  
283    createHeader (size) {
284      size = util.zfill(util.int2buffer(size), 3)
285      let header = Buffer.concat([ size, rlp.encode([ 0, 0 ]) ]) // TODO: the rlp will contain something else someday
286      header = util.zfill(header, 16, false)
287      header = this._egressAes.update(header)
288  
289      this._egressMac.updateHeader(header)
290      const tag = this._egressMac.digest()
291  
292      return Buffer.concat([ header, tag ])
293    }
294  
295    parseHeader (data) {
296      // parse header
297      let header = data.slice(0, 16)
298      const mac = data.slice(16, 32)
299  
300      this._ingressMac.updateHeader(header)
301      const _mac = this._ingressMac.digest()
302      util.assertEq(_mac, mac, 'Invalid MAC')
303  
304      header = this._ingressAes.update(header)
305      this._bodySize = util.buffer2int(header.slice(0, 3))
306      return this._bodySize
307    }
308  
309    createBody (data) {
310      data = util.zfill(data, Math.ceil(data.length / 16) * 16, false)
311      const encryptedData = this._egressAes.update(data)
312      this._egressMac.updateBody(encryptedData)
313      const tag = this._egressMac.digest()
314      return Buffer.concat([ encryptedData, tag ])
315    }
316  
317    parseBody (data) {
318      if (this._bodySize === null) throw new Error('need to parse header first')
319  
320      const body = data.slice(0, -16)
321      const mac = data.slice(-16)
322      this._ingressMac.updateBody(body)
323      const _mac = this._ingressMac.digest()
324      util.assertEq(_mac, mac, 'Invalid MAC')
325  
326      const size = this._bodySize
327      this._bodySize = null
328      return this._ingressAes.update(body).slice(0, size)
329    }
330  }
331  
332  module.exports = ECIES