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