bittorrent-protocol
Advanced tools
Comparing version 3.3.2 to 3.4.0
451
index.js
@@ -5,6 +5,10 @@ /*! bittorrent-protocol. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */ | ||
const BitField = require('bitfield').default | ||
const crypto = require('crypto') | ||
const debug = require('debug')('bittorrent-protocol') | ||
const randombytes = require('randombytes') | ||
const sha1 = require('simple-sha1') | ||
const speedometer = require('speedometer') | ||
const stream = require('readable-stream') | ||
const xor = require('buffer-xor') | ||
const RC4 = require('rc4') | ||
@@ -24,2 +28,8 @@ const BITFIELD_GROW = 400000 | ||
const DH_PRIME = 'ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a63a36210000000000090563' | ||
const DH_GENERATOR = 2 | ||
const VC = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) | ||
const CRYPTO_PROVIDE = Buffer.from([0x00, 0x00, 0x01, 0x02]) | ||
const CRYPTO_SELECT = Buffer.from([0x00, 0x00, 0x00, 0x02]) // always try to choose RC4 encryption instead of plaintext | ||
class Request { | ||
@@ -35,3 +45,3 @@ constructor (piece, offset, length, callback) { | ||
class Wire extends stream.Duplex { | ||
constructor () { | ||
constructor (type = null, retries = 0, peEnabled = false) { | ||
super() | ||
@@ -44,3 +54,3 @@ | ||
this.peerIdBuffer = null // remote peer id (buffer) | ||
this.type = null // connection type ('webrtc', 'tcpIncoming', 'tcpOutgoing', 'webSeed') | ||
this.type = type // connection type ('webrtc', 'tcpIncoming', 'tcpOutgoing', 'webSeed') | ||
@@ -94,5 +104,35 @@ this.amChoking = true // are we choking the peer? | ||
this._peEnabled = peEnabled | ||
if (peEnabled) { | ||
this._dh = crypto.createDiffieHellman(DH_PRIME, 'hex', DH_GENERATOR) // crypto object used to generate keys/secret | ||
this._myPubKey = this._dh.generateKeys('hex') // my DH public key | ||
} else { | ||
this._myPubKey = null | ||
} | ||
this._peerPubKey = null // peer's DH public key | ||
this._sharedSecret = null // shared DH secret | ||
this._peerCryptoProvide = [] // encryption methods provided by peer; we expect this to always contain 0x02 | ||
this._cryptoHandshakeDone = false | ||
this._cryptoSyncPattern = null // the pattern to search for when resynchronizing after receiving pe1/pe2 | ||
this._waitMaxBytes = null // the maximum number of bytes resynchronization must occur within | ||
this._encryptionMethod = null // 1 for plaintext, 2 for RC4 | ||
this._encryptGenerator = null // RC4 keystream generator for encryption | ||
this._decryptGenerator = null // RC4 keystream generator for decryption | ||
this._setGenerators = false // a flag for whether setEncrypt() has successfully completed | ||
this.once('finish', () => this._onFinish()) | ||
this._parseHandshake() | ||
this.on('finish', this._onFinish) | ||
this._debug('type:', this.type) | ||
if (this.type === 'tcpIncoming' && this._peEnabled) { | ||
// If we are not the initiator, we should wait to see if the client begins | ||
// with PE/MSE handshake or the standard bittorrent handshake. | ||
this._determineHandshakeType() | ||
} else if (this.type === 'tcpOutgoing' && this._peEnabled && retries === 0) { | ||
this._parsePe2() | ||
} else { | ||
this._parseHandshake(null) | ||
} | ||
} | ||
@@ -185,2 +225,56 @@ | ||
sendPe1 () { | ||
if (this._peEnabled) { | ||
var padALen = Math.floor(Math.random() * 513) | ||
var padA = randombytes(padALen) | ||
this._push(Buffer.concat([Buffer.from(this._myPubKey, 'hex'), padA])) | ||
} | ||
} | ||
sendPe2 () { | ||
var padBLen = Math.floor(Math.random() * 513) | ||
var padB = randombytes(padBLen) | ||
this._push(Buffer.concat([Buffer.from(this._myPubKey, 'hex'), padB])) | ||
} | ||
sendPe3 (infoHash) { | ||
this.setEncrypt(this._sharedSecret, infoHash) | ||
var hash1Buffer = Buffer.from(sha1.sync(Buffer.from(this._utfToHex('req1') + this._sharedSecret, 'hex')), 'hex') | ||
var hash2Buffer = Buffer.from(sha1.sync(Buffer.from(this._utfToHex('req2') + infoHash, 'hex')), 'hex') | ||
var hash3Buffer = Buffer.from(sha1.sync(Buffer.from(this._utfToHex('req3') + this._sharedSecret, 'hex')), 'hex') | ||
var hashesXorBuffer = xor(hash2Buffer, hash3Buffer) | ||
var padCLen = randombytes(2).readUInt16BE(0) % 512 | ||
var padCBuffer = randombytes(padCLen) | ||
var vcAndProvideBuffer = Buffer.alloc(8 + 4 + 2 + padCLen + 2) | ||
VC.copy(vcAndProvideBuffer) | ||
CRYPTO_PROVIDE.copy(vcAndProvideBuffer, 8) | ||
vcAndProvideBuffer.writeInt16BE(padCLen, 12) // pad C length | ||
padCBuffer.copy(vcAndProvideBuffer, 14) | ||
vcAndProvideBuffer.writeInt16BE(0, 14 + padCLen) // IA length | ||
vcAndProvideBuffer = this._encryptHandshake(vcAndProvideBuffer) | ||
this._push(Buffer.concat([hash1Buffer, hashesXorBuffer, vcAndProvideBuffer])) | ||
} | ||
sendPe4 (infoHash) { | ||
this.setEncrypt(this._sharedSecret, infoHash) | ||
var padDLen = randombytes(2).readUInt16BE(0) % 512 | ||
var padDBuffer = randombytes(padDLen) | ||
var vcAndSelectBuffer = Buffer.alloc(8 + 4 + 2 + padDLen) | ||
VC.copy(vcAndSelectBuffer) | ||
CRYPTO_SELECT.copy(vcAndSelectBuffer, 8) | ||
vcAndSelectBuffer.writeInt16BE(padDLen, 12) // lenD? | ||
padDBuffer.copy(vcAndSelectBuffer, 14) | ||
vcAndSelectBuffer = this._encryptHandshake(vcAndSelectBuffer) | ||
this._push(vcAndSelectBuffer) | ||
this._cryptoHandshakeDone = true | ||
this._debug('completed crypto handshake') | ||
} | ||
/** | ||
@@ -209,2 +303,4 @@ * Message: "handshake" <pstrlen><pstr><reserved><info_hash><peer_id> | ||
this._infoHash = infoHashBuffer | ||
if (infoHashBuffer.length !== 20 || peerIdBuffer.length !== 20) { | ||
@@ -398,2 +494,63 @@ throw new Error('infoHash and peerId MUST have length 20') | ||
/** | ||
* Sets the encryption method for this wire, as per PSE/ME specification | ||
* | ||
* @param {string} sharedSecret: A hex-encoded string, which is the shared secret agreed | ||
* upon from DH key exchange | ||
* @returns boolean, true if encryption setting succeeds, false if it fails. | ||
*/ | ||
setEncrypt (sharedSecret, infoHash) { | ||
var encryptKey | ||
var decryptKey | ||
var encryptKeyBuf | ||
var encryptKeyIntArray | ||
var decryptKeyBuf | ||
var decryptKeyIntArray | ||
switch (this.type) { | ||
case 'tcpIncoming': | ||
encryptKey = sha1.sync(Buffer.from(this._utfToHex('keyB') + sharedSecret + infoHash, 'hex')) | ||
decryptKey = sha1.sync(Buffer.from(this._utfToHex('keyA') + sharedSecret + infoHash, 'hex')) | ||
encryptKeyBuf = Buffer.from(encryptKey, 'hex') | ||
encryptKeyIntArray = [] | ||
for (const value of encryptKeyBuf.values()) { | ||
encryptKeyIntArray.push(value) | ||
} | ||
decryptKeyBuf = Buffer.from(decryptKey, 'hex') | ||
decryptKeyIntArray = [] | ||
for (const value of decryptKeyBuf.values()) { | ||
decryptKeyIntArray.push(value) | ||
} | ||
this._encryptGenerator = new RC4(encryptKeyIntArray) | ||
this._decryptGenerator = new RC4(decryptKeyIntArray) | ||
break | ||
case 'tcpOutgoing': | ||
encryptKey = sha1.sync(Buffer.from(this._utfToHex('keyA') + sharedSecret + infoHash, 'hex')) | ||
decryptKey = sha1.sync(Buffer.from(this._utfToHex('keyB') + sharedSecret + infoHash, 'hex')) | ||
encryptKeyBuf = Buffer.from(encryptKey, 'hex') | ||
encryptKeyIntArray = [] | ||
for (const value of encryptKeyBuf.values()) { | ||
encryptKeyIntArray.push(value) | ||
} | ||
decryptKeyBuf = Buffer.from(decryptKey, 'hex') | ||
decryptKeyIntArray = [] | ||
for (const value of decryptKeyBuf.values()) { | ||
decryptKeyIntArray.push(value) | ||
} | ||
this._encryptGenerator = new RC4(encryptKeyIntArray) | ||
this._decryptGenerator = new RC4(decryptKeyIntArray) | ||
break | ||
default: | ||
return false | ||
} | ||
// Discard the first 1024 bytes, as per MSE/PE implementation | ||
for (var i = 0; i < 1024; i++) { | ||
this._encryptGenerator.randomByte() | ||
this._decryptGenerator.randomByte() | ||
} | ||
this._setGenerators = true | ||
return true | ||
} | ||
/** | ||
* Duplex stream method. Called whenever the remote peer stream wants data. No-op | ||
@@ -423,2 +580,5 @@ * since we'll just push data whenever we get it. | ||
if (this._finished) return | ||
if (this._encryptionMethod === 2 && this._cryptoHandshakeDone) { | ||
data = this._encrypt(data) | ||
} | ||
return this.push(data) | ||
@@ -436,2 +596,52 @@ } | ||
_onPe1 (pubKeyBuffer) { | ||
this._peerPubKey = pubKeyBuffer.toString('hex') | ||
this._sharedSecret = this._dh.computeSecret(this._peerPubKey, 'hex', 'hex') | ||
this.emit('pe1') | ||
} | ||
_onPe2 (pubKeyBuffer) { | ||
this._peerPubKey = pubKeyBuffer.toString('hex') | ||
this._sharedSecret = this._dh.computeSecret(this._peerPubKey, 'hex', 'hex') | ||
this.emit('pe2') | ||
} | ||
_onPe3 (hashesXorBuffer) { | ||
var hash3 = sha1.sync(Buffer.from(this._utfToHex('req3') + this._sharedSecret, 'hex')) | ||
var sKeyHash = xor(hashesXorBuffer, Buffer.from(hash3, 'hex')).toString('hex') | ||
this.emit('pe3', sKeyHash) | ||
} | ||
_onPe3Encrypted (vcBuffer, peerProvideBuffer, padCBuffer, iaBuffer) { | ||
var self = this | ||
if (!vcBuffer.equals(VC)) { | ||
self._debug('Error: verification constant did not match') | ||
self.destroy() | ||
return | ||
} | ||
for (const provideByte of peerProvideBuffer.values()) { | ||
if (provideByte !== 0) { | ||
this._peerCryptoProvide.push(provideByte) | ||
} | ||
} | ||
if (this._peerCryptoProvide.includes(2)) { | ||
this._encryptionMethod = 2 | ||
} else { | ||
self._debug('Error: RC4 encryption method not provided by peer') | ||
self.destroy() | ||
} | ||
} | ||
_onPe4 (peerSelectBuffer) { | ||
this._encryptionMethod = peerSelectBuffer.readUInt8(3) | ||
if (!CRYPTO_PROVIDE.includes(this._encryptionMethod)) { | ||
this._debug('Error: peer selected invalid crypto method') | ||
this.destroy() | ||
} | ||
this._cryptoHandshakeDone = true | ||
this._debug('crypto handshake done') | ||
this.emit('pe4') | ||
} | ||
_onHandshake (infoHashBuffer, peerIdBuffer, extensions) { | ||
@@ -592,14 +802,36 @@ const infoHash = infoHashBuffer.toString('hex') | ||
_write (data, encoding, cb) { | ||
if (this._encryptionMethod === 2 && this._cryptoHandshakeDone) { | ||
data = this._decrypt(data) | ||
} | ||
this._bufferSize += data.length | ||
this._buffer.push(data) | ||
if (this._buffer.length > 1) { | ||
this._buffer = [Buffer.concat(this._buffer, this._bufferSize)] | ||
} | ||
// now this._buffer is an array containing a single Buffer | ||
if (this._cryptoSyncPattern) { | ||
const index = this._buffer[0].indexOf(this._cryptoSyncPattern) | ||
if (index !== -1) { | ||
this._buffer[0] = this._buffer[0].slice(index + this._cryptoSyncPattern.length) | ||
this._bufferSize -= (index + this._cryptoSyncPattern.length) | ||
this._cryptoSyncPattern = null | ||
} else if (this._bufferSize + data.length > this._waitMaxBytes + this._cryptoSyncPattern.length) { | ||
this._debug('Error: could not resynchronize') | ||
this.destroy() | ||
return | ||
} | ||
} | ||
while (this._bufferSize >= this._parserSize) { | ||
const buffer = (this._buffer.length === 1) | ||
? this._buffer[0] | ||
: Buffer.concat(this._buffer, this._bufferSize) | ||
this._bufferSize -= this._parserSize | ||
this._buffer = this._bufferSize | ||
? [buffer.slice(this._parserSize)] | ||
: [] | ||
this._parser(buffer.slice(0, this._parserSize)) | ||
while (this._bufferSize >= this._parserSize && !this._cryptoSyncPattern) { | ||
if (this._parserSize === 0) { | ||
this._parser(Buffer.from([])) | ||
} else { | ||
var buffer = this._buffer[0] | ||
// console.log('buffer:', this._buffer) | ||
this._bufferSize -= this._parserSize | ||
this._buffer = this._bufferSize | ||
? [buffer.slice(this._parserSize)] | ||
: [] | ||
this._parser(buffer.slice(0, this._parserSize)) | ||
} | ||
} | ||
@@ -653,2 +885,7 @@ | ||
_parseUntil (pattern, maxBytes) { | ||
this._cryptoSyncPattern = pattern | ||
this._waitMaxBytes = maxBytes | ||
} | ||
/** | ||
@@ -716,22 +953,122 @@ * Handle the first 4 bytes of a message, to determine the length of bytes that must be | ||
_determineHandshakeType () { | ||
var self = this | ||
self._parse(1, function (pstrLenBuffer) { | ||
var pstrlen = pstrLenBuffer.readUInt8(0) | ||
if (pstrlen === 19) { | ||
self._parse(pstrlen + 48, self._onHandshakeBuffer) | ||
} else { | ||
this._parsePe1(pstrLenBuffer) | ||
} | ||
}) | ||
} | ||
_parsePe1 (pubKeyPrefix) { | ||
var self = this | ||
self._parse(95, function (pubKeySuffix) { | ||
self._onPe1(Buffer.concat([pubKeyPrefix, pubKeySuffix])) | ||
self._parsePe3() | ||
}) | ||
} | ||
_parsePe2 () { | ||
var self = this | ||
self._parse(96, function (pubKey) { | ||
self._onPe2(pubKey) | ||
while (!self._setGenerators) { | ||
// Wait until generators have been set | ||
} | ||
self._parsePe4() | ||
}) | ||
} | ||
// Handles the unencrypted portion of step 4 | ||
_parsePe3 () { | ||
var self = this | ||
var hash1Buffer = Buffer.from(sha1.sync(Buffer.from(this._utfToHex('req1') + this._sharedSecret, 'hex')), 'hex') | ||
// synchronize on HASH('req1', S) | ||
self._parseUntil(hash1Buffer, 512) | ||
self._parse(20, function (buffer) { | ||
self._onPe3(buffer) | ||
while (!self._setGenerators) { | ||
// Wait until generators have been set | ||
} | ||
self._parsePe3Encrypted() | ||
}) | ||
} | ||
_parsePe3Encrypted () { | ||
var self = this | ||
self._parse(14, function (buffer) { | ||
var vcBuffer = self._decryptHandshake(buffer.slice(0, 8)) | ||
var peerProvideBuffer = self._decryptHandshake(buffer.slice(8, 12)) | ||
var padCLen = self._decryptHandshake(buffer.slice(12, 14)).readUInt16BE(0) | ||
self._parse(padCLen, function (padCBuffer) { | ||
padCBuffer = self._decryptHandshake(padCBuffer) | ||
self._parse(2, function (iaLenBuf) { | ||
var iaLen = self._decryptHandshake(iaLenBuf).readUInt16BE(0) | ||
self._parse(iaLen, function (iaBuffer) { | ||
iaBuffer = self._decryptHandshake(iaBuffer) | ||
self._onPe3Encrypted(vcBuffer, peerProvideBuffer, padCBuffer, iaBuffer) | ||
var pstrlen = iaLen ? iaBuffer.readUInt8(0) : null | ||
var protocol = iaLen ? iaBuffer.slice(1, 20) : null | ||
if (pstrlen === 19 && protocol.toString() === 'BitTorrent protocol') { | ||
self._onHandshakeBuffer(iaBuffer.slice(1)) | ||
} else { | ||
self._parseHandshake() | ||
} | ||
}) | ||
}) | ||
}) | ||
}) | ||
} | ||
_parsePe4 () { | ||
var self = this | ||
// synchronize on ENCRYPT(VC). | ||
// since we encrypt using bitwise xor, decryption and encryption are the same operation. | ||
// calling _decryptHandshake here advances the decrypt generator keystream forward 8 bytes | ||
var vcBufferEncrypted = self._decryptHandshake(VC) | ||
self._parseUntil(vcBufferEncrypted, 512) | ||
self._parse(6, function (buffer) { | ||
var peerSelectBuffer = self._decryptHandshake(buffer.slice(0, 4)) | ||
var padDLen = self._decryptHandshake(buffer.slice(4, 6)).readUInt16BE(0) | ||
self._parse(padDLen, function (padDBuf) { | ||
self._decryptHandshake(padDBuf) | ||
self._onPe4(peerSelectBuffer) | ||
self._parseHandshake(null) | ||
}) | ||
}) | ||
} | ||
/** | ||
* Reads the handshake as specified by the bittorrent wire protocol. | ||
*/ | ||
_parseHandshake () { | ||
this._parse(1, buffer => { | ||
const pstrlen = buffer.readUInt8(0) | ||
this._parse(pstrlen + 48, handshake => { | ||
const protocol = handshake.slice(0, pstrlen) | ||
if (protocol.toString() !== 'BitTorrent protocol') { | ||
this._debug('Error: wire not speaking BitTorrent protocol (%s)', protocol.toString()) | ||
this.end() | ||
return | ||
} | ||
handshake = handshake.slice(pstrlen) | ||
this._onHandshake(handshake.slice(8, 28), handshake.slice(28, 48), { | ||
dht: !!(handshake[7] & 0x01), // see bep_0005 | ||
extended: !!(handshake[5] & 0x10) // see bep_0010 | ||
}) | ||
this._parse(4, this._onMessageLength) | ||
}) | ||
if (pstrlen !== 19) { | ||
this._debug('Error: wire not speaking BitTorrent protocol (%s)', pstrlen.toString()) | ||
this.end() | ||
return | ||
} | ||
this._parse(pstrlen + 48, this._onHandshakeBuffer) | ||
}) | ||
} | ||
_onHandshakeBuffer (handshake) { | ||
var protocol = handshake.slice(0, 19) | ||
if (protocol.toString() !== 'BitTorrent protocol') { | ||
this._debug('Error: wire not speaking BitTorrent protocol (%s)', protocol.toString()) | ||
this.end() | ||
return | ||
} | ||
handshake = handshake.slice(19) | ||
this._onHandshake(handshake.slice(8, 28), handshake.slice(28, 48), { | ||
dht: !!(handshake[7] & 0x01), // see bep_0005 | ||
extended: !!(handshake[5] & 0x10) // see bep_0010 | ||
}) | ||
this._parse(4, this._onMessageLength) | ||
} | ||
_onFinish () { | ||
@@ -771,4 +1108,66 @@ this._finished = true | ||
} | ||
_encryptHandshake (buf) { | ||
var crypt = Buffer.from(buf) | ||
if (!this._encryptGenerator) { | ||
this._debug('Warning: Encrypting without any generator') | ||
return crypt | ||
} | ||
for (var i = 0; i < buf.length; i++) { | ||
var keystream = this._encryptGenerator.randomByte() | ||
crypt[i] = crypt[i] ^ keystream | ||
} | ||
return crypt | ||
} | ||
_encrypt (buf) { | ||
var crypt = Buffer.from(buf) | ||
if (!this._encryptGenerator || this._encryptionMethod !== 2) { | ||
return crypt | ||
} | ||
for (var i = 0; i < buf.length; i++) { | ||
var keystream = this._encryptGenerator.randomByte() | ||
crypt[i] = crypt[i] ^ keystream | ||
} | ||
return crypt | ||
} | ||
_decryptHandshake (buf) { | ||
var decrypt = Buffer.from(buf) | ||
if (!this._decryptGenerator) { | ||
this._debug('Warning: Decrypting without any generator') | ||
return decrypt | ||
} | ||
for (var i = 0; i < buf.length; i++) { | ||
var keystream = this._decryptGenerator.randomByte() | ||
decrypt[i] = decrypt[i] ^ keystream | ||
} | ||
return decrypt | ||
} | ||
_decrypt (buf) { | ||
var decrypt = Buffer.from(buf) | ||
if (!this._decryptGenerator || this._encryptionMethod !== 2) { | ||
return decrypt | ||
} | ||
for (var i = 0; i < buf.length; i++) { | ||
var keystream = this._decryptGenerator.randomByte() | ||
decrypt[i] = decrypt[i] ^ keystream | ||
} | ||
return decrypt | ||
} | ||
_utfToHex (str) { | ||
return Buffer.from(str, 'utf8').toString('hex') | ||
} | ||
} | ||
module.exports = Wire |
{ | ||
"name": "bittorrent-protocol", | ||
"description": "Simple, robust, BitTorrent peer wire protocol implementation", | ||
"version": "3.3.2", | ||
"version": "3.4.0", | ||
"author": { | ||
@@ -16,5 +16,8 @@ "name": "WebTorrent LLC", | ||
"bitfield": "^4.0.0", | ||
"buffer-xor": "^2.0.2", | ||
"debug": "^4.3.1", | ||
"randombytes": "^2.1.0", | ||
"rc4": "^0.1.5", | ||
"readable-stream": "^3.6.0", | ||
"simple-sha1": "^3.0.0", | ||
"speedometer": "^1.1.0", | ||
@@ -21,0 +24,0 @@ "unordered-array-remove": "^1.0.2" |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
47830
1004
0
10
+ Addedbuffer-xor@^2.0.2
+ Addedrc4@^0.1.5
+ Addedsimple-sha1@^3.0.0
+ Addedbuffer-xor@2.0.2(transitive)
+ Addedqueue-microtask@1.2.3(transitive)
+ Addedrc4@0.1.5(transitive)
+ Addedrusha@0.8.14(transitive)
+ Addedsimple-sha1@3.1.0(transitive)