Skip to content

Commit

Permalink
feat: added setAddress function to change mac address
Browse files Browse the repository at this point in the history
  • Loading branch information
stoprocent committed Oct 25, 2023
1 parent 7e645ab commit 286a8e5
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 24 deletions.
4 changes: 4 additions & 0 deletions lib/hci-socket/bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ NobleBindings.prototype.setScanParameters = function (interval, window) {
this._gap.setScanParameters(interval, window);
};

NobleBindings.prototype.setAddress = function (address) {
this._hci.setAddress(address);
};

NobleBindings.prototype.startScanning = function (
serviceUuids,
allowDuplicates
Expand Down
36 changes: 17 additions & 19 deletions lib/hci-socket/hci.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const events = require('events');
const util = require('util');

const BluetoothHciSocket = require('@stoprocent/bluetooth-hci-socket');
const vendorSpecific = require('./vs');

const HCI_COMMAND_PKT = 0x01;
const HCI_ACLDATA_PKT = 0x02;
Expand Down Expand Up @@ -32,7 +33,6 @@ const OCF_DISCONNECT = 0x0006;
const OGF_HOST_CTL = 0x03;
const OCF_SET_EVENT_MASK = 0x0001;
const OCF_RESET = 0x0003;
const OCF_SET_RANDOM_MAC = 0x0005;
const OCF_SET_PHY = 0x0031;
const OCF_READ_LE_HOST_SUPPORTED = 0x006c;
const OCF_WRITE_LE_HOST_SUPPORTED = 0x006d;
Expand Down Expand Up @@ -96,6 +96,7 @@ const STATUS_MAPPER = require('./hci-status');

const Hci = function (options) {
options = options || {};
this._manufacturer = null;
this._socket = new BluetoothHciSocket();
this._isDevUp = null;
this._isExtended = 'extended' in options && options.extended;
Expand Down Expand Up @@ -219,26 +220,20 @@ Hci.prototype.setCodedPhySupport = function () {
this._socket.write(cmd);
};

Hci.prototype.setRandomMAC = function () {
const cmd = Buffer.alloc(10);
Hci.prototype.setAddress = function (address) {
// Command
const addr_cmd = vendorSpecific.setAddressCmd(this._manufacturer, address);

// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(OCF_SET_RANDOM_MAC | (OGF_LE_CTL << 10), 1);
if (addr_cmd !== null && Buffer.isBuffer(addr_cmd)) {
// Make Command Buffer
const cmd = Buffer.alloc(1 + addr_cmd.byteLength);
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
addr_cmd.copy(cmd, 1);

// length
cmd.writeUInt8(0x06, 3);

// data
cmd.writeUInt8(0x05, 4); // mac 6 byte
cmd.writeUInt8(0x04, 5); // mac 5 byte
cmd.writeUInt8(0x03, 6); // mac 4 byte
cmd.writeUInt8(0x02, 7); // mac 3 byte
cmd.writeUInt8(0x01, 8); // mac 2 byte
cmd.writeUInt8(0x00, 9); // mac 1 byte

debug(`set random mac address - writing: ${cmd.toString('hex')}`);
this._socket.write(cmd);
debug(`set address - writing: ${cmd.toString('hex')}`);
this._socket.write(cmd);
this.readBdAddr();
}
};

Hci.prototype.setSocketFilter = function () {
Expand Down Expand Up @@ -939,6 +934,9 @@ Hci.prototype.processCmdCompleteEvent = function (cmd, status, result) {
this.setScanParameters();
}

// Update manufacturer
this._manufacturer = manufacturer;

this.emit(
'readLocalVersion',
hciVer,
Expand Down
152 changes: 152 additions & 0 deletions lib/hci-socket/vs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// This file is based on the bluez implementation
// https://github.com/bluez/bluez/blob/master/tools/bdaddr.c

const OGF_VENDOR_CMD = 0x3f;

const OCF_ERICSSON_WRITE_BD_ADDR = 0x000d;
const OCF_TI_WRITE_BD_ADDR = 0x0006;
const OCF_LINUX_FOUNDATION_WRITE_BD_ADDR = 0x0006;
const OCF_BCM_WRITE_BD_ADDR = 0x0001;
const OCF_ZEEVO_WRITE_BD_ADDR = 0x0001;
const OCF_MRVL_WRITE_BD_ADDR = 0x0022;
const OCF_ERICSSON_STORE_IN_FLASH = 0x0022;
const ERICSSON_STORE_IN_FLASH_CP_SIZE = 0xFF;

function parseAddress(address) {
// Parse MAC Address as in 00:00:00:00:00:00 into Buffer (needs to reverse byte order)
let macAddress = Buffer.from(address.split(':').reverse().join(''), 'hex');

if (Buffer.isBuffer(macAddress) && macAddress.byteLength !== 6) {
throw new Error("Invalid MAC Address. Should be formated as 00:00:00:00:00:00 string.");
}

return macAddress;
}

function csr_write_bd_addr(address) {
// Parse MAC Address
const macAddress = parseAddress(address);

if (macAddress === null) {
return null;
}

// Base command
const base = Buffer.from([
0x02, 0x00, 0x0c, 0x00, 0x11, 0x47, 0x03, 0x70,
0x00, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]);

// Command
const cmd = Buffer.alloc(3 + base.byteLength);

cmd.writeUInt16LE(0x00 | OGF_VENDOR_CMD << 10, 0);
cmd.writeUInt8(0xC2, 2);

base.writeUint8(macAddress.readUInt8(2), 16);
base.writeUint8(0x00, 17);
base.writeUint8(macAddress.readUInt8(0), 18);
base.writeUint8(macAddress.readUInt8(1), 19);
base.writeUint8(macAddress.readUInt8(3), 20);
base.writeUint8(0x00, 21);
base.writeUint8(macAddress.readUInt8(4), 22);
base.writeUint8(macAddress.readUInt8(5), 23);
base.copy(cmd, 3);

return cmd;
}

function ericsson_store_in_flash(user_id, data) {
// Check Data
if (Buffer.isBuffer(data) === false || data.byteLength > OCF_ERICSSON_STORE_IN_FLASH - 2) {
return null;
}

// Command
const cmd = Buffer.alloc(3 + ERICSSON_STORE_IN_FLASH_CP_SIZE);

cmd.writeUInt16LE(OCF_ERICSSON_STORE_IN_FLASH | OGF_VENDOR_CMD << 10, 0);
cmd.writeUInt8(ERICSSON_STORE_IN_FLASH_CP_SIZE, 2);
cmd.writeUInt8(user_id, 3); // user_id
cmd.writeUInt8(data.byteLength, 4); // flash_length
data.copy(cmd, 5); // flash_data

return cmd;
}

function st_write_bd_addr(address) {
// Parse MAC Address
const macAddress = parseAddress(address);

if (macAddress === null) {
return null;
}

return ericsson_store_in_flash(0xFE, macAddress);
}

function mrvl_write_bd_addr(address) {
// Parse MAC Address
const macAddress = parseAddress(address);

if (macAddress === null) {
return null;
}

// Command
const cmd = Buffer.alloc(11);

cmd.writeUInt16LE(OCF_MRVL_WRITE_BD_ADDR | OGF_VENDOR_CMD << 10, 0);
cmd.writeUInt8(0x08, 2);
cmd.writeUInt8(0xFE, 3); // parameter_id
cmd.writeUInt8(0x06, 4); // bdaddr_len
macAddress.copy(cmd, 5); // bdaddr

return cmd;
}

function write_common_bd_addr(OCF_VS_WRITE_BD_ADDR) {
// Return a function
return (address) => {

// Parse MAC Address
const macAddress = parseAddress(address);

if (macAddress === null) {
return null;
}

// Command
const cmd = Buffer.alloc(9);

cmd.writeUInt16LE(OCF_VS_WRITE_BD_ADDR | OGF_VENDOR_CMD << 10, 0);
cmd.writeUInt8(0x06, 2);
macAddress.copy(cmd, 3); // bdaddr

return cmd;
}
}

const vendors = new Map();

vendors.set(0, write_common_bd_addr(OCF_ERICSSON_WRITE_BD_ADDR));
vendors.set(10, csr_write_bd_addr);
vendors.set(13, write_common_bd_addr(OCF_TI_WRITE_BD_ADDR));
vendors.set(15, write_common_bd_addr(OCF_BCM_WRITE_BD_ADDR));
vendors.set(18, write_common_bd_addr(OCF_ZEEVO_WRITE_BD_ADDR));
vendors.set(48, st_write_bd_addr);
vendors.set(57, write_common_bd_addr(OCF_ERICSSON_WRITE_BD_ADDR));
vendors.set(72, mrvl_write_bd_addr);
vendors.set(1521, write_common_bd_addr(OCF_LINUX_FOUNDATION_WRITE_BD_ADDR));

module.exports = {
// Vendor Specific Set Address
setAddressCmd: (manufacturer, address) => {
const generateCommand = vendors.get(manufacturer);
if (typeof generateCommand === 'function' ) {
return generateCommand(address) || null;
}
return null;
}
}
9 changes: 9 additions & 0 deletions lib/noble.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ Noble.prototype.onScanParametersSet = function () {
this.emit('scanParametersSet');
};

Noble.prototype.setAddress = function (address) {
if (this._bindings.setAddress) {
this._bindings.setAddress(address);
}
else {
this.emit('warning', 'current binding does not implement setAddress method.');
}
}

const startScanning = function (serviceUuids, allowDuplicates, callback) {
if (typeof serviceUuids === 'function') {
this.emit('warning', 'calling startScanning(callback) is deprecated');
Expand Down
51 changes: 46 additions & 5 deletions test/lib/hci-socket/hci.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('hci-socket hci', () => {
Socket.prototype.isDevUp = sinon.stub();
Socket.prototype.removeAllListeners = sinon.stub();
Socket.prototype.setFilter = sinon.stub();
Socket.prototype.setAddress = sinon.stub();
Socket.prototype.write = sinon.stub();

hci = new Hci({});
Expand Down Expand Up @@ -230,11 +231,6 @@ describe('hci-socket hci', () => {
assert.calledOnceWithExactly(hci._socket.write, Buffer.from([0x01, 0x31, 0x20, 0x03, 0x00, 0x05, 0x05]));
});

it('should write randomMAC command', () => {
hci.setRandomMAC();
assert.calledOnceWithExactly(hci._socket.write, Buffer.from([0x01, 0x05, 0x20, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00]));
});

it('should setSocketFilter', () => {
hci.setSocketFilter();
assert.calledOnceWithExactly(hci._socket.setFilter, Buffer.from([0x16, 0, 0, 0, 0x20, 0xc1, 0x08, 0, 0, 0, 0, 0x40, 0, 0]));
Expand Down Expand Up @@ -270,6 +266,51 @@ describe('hci-socket hci', () => {
assert.calledOnceWithExactly(hci._socket.write, Buffer.from([1, 9, 0x10, 0]));
});

describe('setAddress', () => {

it('should write vendor specific (Linux Foundation) command based on read local version response', () => {
hci.readBdAddr = sinon.spy();
hci.setScanEnabled = sinon.spy();
hci.setScanParameters = sinon.spy();

const cmd = 4097;
const status = 0;
// hciVer=12, hciRev=0, lmpVer=12, manufacturer=1521, lmpSubVer=65535
const result = Buffer.from([0x0C, 0x00, 0x00, 0x0C, 0xF1, 0x05, 0xFF, 0xFF]);

hci.processCmdCompleteEvent(cmd, status, result);

hci.setAddress("11:22:33:44:55:66");
assert.calledOnceWithExactly(hci._socket.write, Buffer.from([0x01, 0x06, 0xfc, 0x06, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11]));
});

it('should write vendor specific (Ericsson) command based on manufacturer value (', () => {
hci._manufacturer = 0;
hci.readBdAddr = sinon.spy();
hci.setAddress("11:22:33:44:55:66");
assert.calledOnceWithExactly(hci._socket.write, Buffer.from([0x01, 0x0d, 0xfc, 0x06, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11]));
});

it('should write vendor specific (Texas Instrument) command based on manufacturer value', () => {
hci._manufacturer = 13;
hci.readBdAddr = sinon.spy();
hci.setAddress("11:22:33:44:55:66");
assert.calledOnceWithExactly(hci._socket.write, Buffer.from([0x01, 0x06, 0xfc, 0x06, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11]));
});

it('should write vendor specific (BCM) command based on manufacturer value', () => {
hci._manufacturer = 15;
hci.readBdAddr = sinon.spy();
hci.setAddress("11:22:33:44:55:66");
assert.calledOnceWithExactly(hci._socket.write, Buffer.from([0x01, 0x01, 0xfc, 0x06, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11]));
});

it('should not write vendor specific command', () => {
hci.setAddress("11:22:33:44:55:66");
assert.notCalled(hci._socket.write);
});
})

describe('setLeEventMask', () => {
it('should setLeEventMask', () => {
hci.setLeEventMask();
Expand Down
26 changes: 26 additions & 0 deletions test/lib/hci-socket/vs.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const should = require('should');
const sinon = require('sinon');
const proxyquire = require('proxyquire').noCallThru();

const fakeOs = {};
const { assert } = sinon;
const vendorSpecific = proxyquire('../../../lib/hci-socket/vs', { os: fakeOs });

describe('hci-socket vs', () => {

afterEach(() => {
sinon.reset();
});

describe('parseAddress', () => {
it('should convert to Buffer', () => {
assert.match(vendorSpecific.setAddressCmd(0, "00:11:22:33:44:55").slice(3), Buffer.from([0x55, 0x44, 0x33, 0x22, 0x11, 0x00]))
});

it('should not convert to Buffer and throw an Error', () => {
should.throws(function() {
vendorSpecific.setAddressCmd(0, "00:11:22:33:44")
});
});
});
});

0 comments on commit 286a8e5

Please sign in to comment.