btlejuice
Version:
Bluetooth Low-Energy spoofing and MitM framework
693 lines (577 loc) • 18.1 kB
JavaScript
/* Main controller */
var BjProxy = angular.module('BjProxy', ['ui.bootstrap.contextMenu']);
/**
* UUID filter
**/
BjProxy.filter('makeUUID', function () {
return function (item) {
return formatUUID(item);
};
});
/**
* Transaction list controller
**/
BjProxy.controller('TransactionListCtrl', function($scope, $rootScope, $window){
$scope.transactions = [];
/*
$scope.options = [
['Replay', function ($itemScope) {
console.log('replay');
console.log($itemScope);
}],
['Enable hooking', function($itemScope) {
console.log('set hooking');
}],
];*/
$rootScope.$on('transactions.reset', function(){
console.log('got transactions.reset');
$scope.transactions = [];
});
$rootScope.$on('transactions.export.file', function(event, filename, format){
/* Append extension. */
if (format === 'json')
var ext = 'json';
else
var ext = 'txt';
filename += '.' + ext;
/* Format data based on selected format. */
switch(format) {
case 'json':
/* Export as JSON data, easy to process. */
var exportData = {
'target': interceptor.getProfile(),
'activity': [],
}
for (var i in $scope.transactions) {
var t = $scope.transactions[i];
exportData.activity.push({
type: t.op,
service: formatUUID(t.service),
characteristic: formatUUID(t.characteristic),
data: t.dataHex.replace(/ /g,''),
})
}
/* Convert to JSON. */
exportData = angular.toJson(exportData);
break;
/* Text export is intended to be readable. */
case 'text':
var profile = interceptor.getProfile();
var exportData = 'BtleJuice export data\n\n';
exportData += '==< Capture Information >\n';
exportData += ' BD Address : ' + profile.address + '\n';
exportData += ' Device Name: ' + profile.name + '\n';
if (profile.ad_records != null) {
exportData += ' Adv. Data : ' + profile.ad_records + '\n';
}
if (profile.scan_data != null) {
exportData += ' Scan Data : ' + profile.scan_data + '\n';
}
exportData += ' Saved on : ' + (new Date()).toUTCString() + '\n';
exportData += '==========================\n\n';
for (var i in $scope.transactions) {
var row = '';
var t = $scope.transactions[i];
if (t.op == 'event') {
if (t.service == 'connect')
row = '>>> Connection from remote device to dummy';
else if (t.service == 'disconnect')
row = '>>> Disconnection from dummy';
} else {
/* TODO: add ASCII dump. */
switch (t.op) {
case 'read':
row = 'READ from '+formatUUID(t.service)+':'+formatUUID(t.characteristic)+' -- ' + t.dataHex;
break;
case 'write':
row = 'WRITE to ' + formatUUID(t.service)+':'+formatUUID(t.characteristic)+' -- ' + t.dataHex;
break;
case 'notification':
row = 'NOTIFICATION from ' + formatUUID(t.service)+':'+formatUUID(t.characteristic)+' -- ' + t.dataHex;
break;
}
}
exportData += row + '\n';
}
break;
}
var blob = new Blob([exportData]);
var link = angular.element('<a></a>');
link.attr('href', window.URL.createObjectURL(blob));
link.attr('download',filename);
link[0].click();
});
$scope.options = function(item) {
if (item.op == 'event') {
return [];
} else {
/* Check if item is already intercepted. */
//TODO: check !
return [
['Replay', function($itemScope){
$scope.onReplayItem($itemScope.t);
}],
[function($itemScope){
if (interceptor.isHooked($itemScope.t.service, $itemScope.t.characteristic)) {
return 'Disable hook';
} else {
return 'Set hook';
}
},function($itemScope){
if (interceptor.isHooked($itemScope.t.service, $itemScope.t.characteristic)) {
$scope.onRemoveHook($itemScope.t);
} else {
$scope.onSetHook($itemScope.t);
}
}]
];
}
};
$scope.dimensions = {
'height': (window.innerHeight - 82)+'px'
};
$scope.addTransaction = function(transaction, disableRefresh) {
$scope.transactions.push(transaction);
if (!disableRefresh)
$scope.$apply();
};
$scope.onSwitchDisplay = function(transaction) {
if (transaction.data === transaction.dataHexii) {
transaction.data = transaction.dataHex;
} else {
transaction.data = transaction.dataHexii;
}
}
$scope.onReplayItem = function(transaction) {
console.log('replay');
console.log(transaction);
$rootScope.$emit('replay', transaction);
};
$scope.onRemoveHook = function(transaction) {
interceptor.removeHook(transaction.service, transaction.characteristic);
};
$scope.onSetHook = function(transaction) {
interceptor.setHook(transaction.service, transaction.characteristic);
};
})
.directive('resize', function(){
return {
restrict: 'A',
link: function(scope, elem, attr) {
angular.element(window).on('resize', function(){
console.log('resize');
scope.$apply(function(){
scope.dimensions = {
'height': (window.innerHeight - 82)+'px'
};
});
});
}
}
});
/**
* Navbar controller.
**/
BjProxy.controller('NavCtrl', function($scope, $rootScope, $element){
$scope.state = 'disconnected';
$scope.intercepting = interceptor.isInteractive();
interceptor.on('status.change', function(status){
$scope.config = interceptor.getConfig();
$scope.state = status;
$scope.$apply();
});
$scope.config = interceptor.getConfig();
console.log($scope.config);
$scope.target = null;
$scope.onSelectTarget = function() {
console.log('select target !');
/* Reset transactions. */
$scope.$emit('transactions.reset');
$scope.$emit('target.select');
};
$scope.onDisconnect = function() {
console.log('disconnect target');
$scope.$emit('target.disconnect');
};
$scope.onSettings = function(){
$scope.$emit('settings.show');
};
$scope.onServices = function(){
$scope.$emit('profile.show');
};
$scope.onEnableIntercept = function(){
interceptor.setMode(interceptor.MODE_INTERACTIVE);
$scope.intercepting = true;
};
$scope.onDisableIntercept = function(){
interceptor.setMode(interceptor.MODE_FORWARD);
$scope.intercepting = false;
};
$scope.onExport = function(){
$scope.$emit('transactions.export');
}
$rootScope.$on('target.connected', function(event, target){
console.log('target is connected !');
$scope.target = target.address;
$scope.state = 'connected';
$scope.$apply();
});
})
.directive('toggle', function(){
return {
restrict: 'A',
link: function(scope, element, attrs){
if (attrs.toggle=="tooltip"){
$(element).tooltip();
}
if (attrs.toggle=="popover"){
$(element).popover();
}
}
};
});
/**
* Target selection controller.
**/
BjProxy.controller('TargetCtrl', function($scope, $rootScope){
$scope.targets = [];
$scope.seen = {};
$scope.error = false;
$scope.target = null;
$scope.selectTarget = function(target) {
$scope.target = target;
};
$scope.isSelected = function(target) {
if ($scope.target == null)
return false;
else
return ($scope.target.address == target.address);
};
console.log('register target selection');
$rootScope.$on('target.select', function(){
console.log('event target.select fired');
interceptor.listDevices(function(peripheral, name, rssi){
if (!(peripheral in $scope.seen)) {
$scope.targets.push({
address: peripheral,
name: (name!=undefined)?name:'<unknown>',
rssi: rssi
});
$scope.seen[peripheral] = null;
$scope.$apply();
}
});
/* Popup modal. */
$scope.targets = [];
$scope.seen = {};
$('#m_target').modal();
});
$rootScope.$on('target.disconnect', function(){
console.log('event target.disconnect fired');
interceptor.disconnect();
$scope.targets = [];
$scope.seen = {};
});
$scope.onSelectClick = function(target){
/* If a target has been selected, tell the interceptor to use it. */
if ($scope.target != null) {
interceptor.selectTarget($scope.target.address, function(){
$rootScope.$emit('target.connected', $scope.target);
});
$('#m_target').modal('hide');
} else {
/* Display an error. */
$scope.error = true;
}
};
$scope.onSelectDblClick = function(target) {
/* Select target. */
$scope.target = target;
/* Simulate a click on the 'Select' button. */
$scope.onSelectClick();
};
});
/**
* Settings controller.
**/
BjProxy.controller('SettingsCtrl', function($scope, $rootScope){
$scope.config = {
reconnect: interceptor.shouldReconnect,
keepHandles: interceptor.keepHandles,
};
$rootScope.$on('settings.show', function(){
$('#m_settings').modal();
});
$scope.onSave = function(){
/* Save interceptor config parameters. */
interceptor.shouldReconnect = $scope.config.reconnect;
interceptor.keepHandles = $scope.config.keepHandles;
$('#m_settings').modal('hide');
};
$scope.onCancel = function(){
$('#m_settings').modal('hide');
};
});
/**
* Services controller.
**/
BjProxy.controller('HookCtrl', function($scope, $rootScope, $element){
$scope.title = 'Intercept:';
$scope.action = {
op: null,
service: null,
characteristic: null,
data: null,
offset: null,
withoutResponse: null,
};
interceptor.on('hooks.write', function(service, characteristic, data, offset, withoutResponse, noRefresh){
console.log('>>> got hooks.write');
console.log(arguments);
$scope.action = {
op: 'write',
service: service,
characteristic: characteristic,
data: data,
dataHexii: buffer2hexII(data),
dataHex: buffer2hex(data),
offset: offset,
withoutResponse: withoutResponse
};
if (noRefresh == null)
$scope.$apply();
$('#m_hook').modal();
});
interceptor.on('hooks.read', function(service, characteristic, data, noRefresh){
console.log('>>> got hooks.read');
$scope.action = {
op: 'read',
service: service,
characteristic: characteristic,
data: data,
dataHexii: buffer2hexII(data),
dataHex: buffer2hex(data)
};
if (noRefresh == null)
$scope.$apply();
$('#m_hook').modal();
});
interceptor.on('hooks.notify', function(service, characteristic, data, noRefresh){
console.log('>>> got hooks.notify');
$scope.action = {
op: 'notify',
service: service,
characteristic: characteristic,
data: data,
dataHexii: buffer2hexII(data),
dataHex: buffer2hex(data)
};
if (noRefresh == null)
$scope.$apply();
$('#m_hook').modal();
});
$scope.onForward = function(){
/* Check if HexII is correct, and forward. */
console.log($scope.action.dataHexii);
var data = hexII2buffer($scope.action.dataHexii);
if (data != null) {
/* Forward. */
if ($scope.action.op == 'write') {
console.log(data);
interceptor.deviceWrite(
$scope.action.service,
$scope.action.characteristic,
data,
$scope.action.offset,
$scope.action.withoutResponse,
/* Disable refresh (causes an error with angular). */
true
);
/* Remove the edit popup. */
$('#m_hook').modal('hide');
/* Editing no more. */
interceptor.processNextRequest();
} else if ($scope.action.op == 'read') {
interceptor.proxyReadResponse(
$scope.action.service,
$scope.action.characteristic,
data,
/* Disable refresh. */
true
);
/* Remove the edit popup. */
$('#m_hook').modal('hide');
/* Editing no more. */
interceptor.processNextRequest();
} else if ($scope.action.op == 'notify') {
interceptor.proxyNotifyData(
$scope.action.service,
$scope.action.characteristic,
data,
/* Disable refresh. */
true
);
/* Remove the edit popup. */
$('#m_hook').modal('hide');
/* Editing no more. */
interceptor.processNextRequest();
}
} else {
/* TODO: notify user the hexii is incorrect. */
}
};
$scope.onDismiss = function() {
/* Dismiss. */
if ($scope.action.op == 'write') {
interceptor.proxyWriteResponse(
$scope.action.service,
$scope.action.characteristic,
null
);
/* Remove the edit popup. */
$('#m_hook').modal('hide');
/* Editing no more. */
interceptor.processNextRequest();
}
};
});
/**
* Services controller.
**/
BjProxy.controller('ServicesCtrl', function($scope, $rootScope, $element){
$scope.services = {};
$scope.selectedService = null;
interceptor.on('target.profile', function(){
var profile = interceptor.getProfile();
if (profile) {
for (var i in profile.services) {
$scope.services[profile.services[i].uuid] = [];
for (var j in profile.services[i].characteristics) {
$scope.services[profile.services[i].uuid].push(profile.services[i].characteristics[j].uuid);
}
}
$scope.$apply();
}
});
$rootScope.$on('profile.show', function(){
$('#m_profile').modal();
});
$scope.onSave = function(){
alert('proxy: '+$scope.proxy + ', device:'+$scope.hciDevice);
};
$scope.onCancel = function(){
$('#m_settings').modal('hide');
};
$scope.onSelect = function(uuid) {
$scope.selected = uuid;
$scope.$apply();
};
$scope.onServiceSelect = function() {
console.log($scope.selectedService);
};
$scope.onToggle = function(item, delay) {
console.log(arguments);
$('#'+item).parent().children('ul.tree').toggle(delay);
};
});
/**
* Replay controller
**/
BjProxy.controller('ReplayCtrl', function($scope, $rootScope, $window){
$scope.op = null;
$scope.service = null;
$scope.characteristic = null;
$scope.dataHexii = null;
$scope.title = 'Replay'
$rootScope.$on('replay', function(event, transaction){
console.log(transaction);
switch(transaction.op) {
case 'read':
$scope.title = 'Replay read';
break;
case 'write':
$scope.title = 'Replay write';
break;
case 'notification':
$scope.title = 'Replay notification';
break;
}
$scope.op = transaction.op;
$scope.service = transaction.service;
$scope.characteristic = transaction.characteristic;
$scope.dataHexii = transaction.data;
$('#m_replay').modal();
});
$scope.onClose = function(){
$('#m_replay').modal('hide');
};
$scope.onRead = function(){
/* Read the characteristic from the device. */
interceptor.deviceRead($scope.service, $scope.characteristic, function(service, characteristic, data){
console.log('got data, update');
$scope.dataHexii = buffer2hexII(data);
$scope.$apply();
});
};
$scope.onWrite = function() {
/* Try to convert data to hexii. */
var data = hexII2buffer($scope.dataHexii);
if (data == null) {
console.log('error, bad data format');
} else {
/* Write the data to characteristic. */
interceptor.deviceWrite($scope.service, $scope.characteristic, data, $scope.offset, false);
}
};
$scope.onNotify = function() {
/* Try to convert data to hexii. */
var data = hexII2buffer($scope.dataHexii);
if (data == null) {
console.log('error, bad data format');
} else {
/* Write the data to characteristic. */
interceptor.proxyNotifyData($scope.service, $scope.characteristic, data, true);
}
};
});
/**
* Export controller
**/
BjProxy.controller('ExportCtrl', function($scope, $rootScope, $window){
$scope.filename = filename;
$scope.format = 'json';
$rootScope.$on('transactions.export', function(){
$scope.showExportDlg();
});
$scope.showExportDlg = function(){
/* Lil' hack to get the actual number of transactions displayed. */
var numTransactions = angular.element(document.getElementById('transactions')).scope().transactions.length;
/* Get actual date. */
var exportDate = (new Date())
.toISOString()
.replace(/T/g,'_')
.replace(/-/g,'')
.replace(/:/g,'')
.slice(0,15);
var profile = interceptor.getProfile();
if (numTransactions === 0) {
return;
} else {
var deviceAddress = profile.address.replace(/:/g,'');
var deviceName = profile.name.replace(/ /g,'_')
.replace(/[^\x20-\x7E]+/g, '');
var filename = deviceAddress+'-'+deviceName+'-'+exportDate
}
$scope.filename = filename;
$('#m_export').modal();
};
$scope.onCancel = function(){
$('#m_export').modal('hide');
};
$scope.onExport = function(){
console.log($scope.filename);
console.log($scope.format);
$scope.$emit('transactions.export.file', $scope.filename, $scope.format);
$('#m_export').modal('hide');
};
});