Skip to content

Add secure notification test. #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions characteristic_secure_notify/device_code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const data = new Uint32Array([1]);
const devicePassKey = '141414'; // 6 digit passkey to use.
let isConnected = false;
let debug = '?';

function canShowPasskey() {
// Without a display this app cannot actually display a passkey, but return
// true because this is required in order to pair using passkeys.
// http://forum.espruino.com/comments/14922430/
return true;
}

function deviceHasDisplay() {
return typeof g !== 'undefined';
}

function updateScreen() {
if (!deviceHasDisplay())
return;
g.clear();
g.setFontBitmap();
g.drawString('Current Notify Value:');
g.drawString('Connected: ' + isConnected, 0, g.getHeight() - 10);
let msg = 'Dbg: ' + debug;
g.drawString(msg, g.getWidth() - g.stringWidth(msg), g.getHeight() - 10);
g.setFontVector(40);
let val = data[0];
g.drawString(val, (g.getWidth() - g.stringWidth(val)) / 2, 12);

g.flip();
}

function updateValue() {
data[0] = data[0] + 1;
if (data[0] == 10000) {
// Limit to four digits for display on small screens.
data[0] = 0;
}
if (isConnected) {
NRF.updateServices({
'02fc549a-244c-11eb-adc1-0242ac120002': {
'02fc549a-244c-11eb-adc1-0242ac120002':
{value: data.buffer, notify: true}
}
});
}
}

function onInit() {
// Put into a known state.
digitalWrite(LED, isConnected);

NRF.setServices({
'02fc549a-244c-11eb-adc1-0242ac120002': {
'02fc549a-244c-11eb-adc1-0242ac120002': {
value: data.buffer,
broadcast: false,
readable: true,
writable: false,
notify: true,
description: 'Notify characteristic',
security: {
read: {
encrypted: true, // Encrypt data (default: false).
mitm: true // Man In The Middle (default: false).
}
}
}
}
});

NRF.on('disconnect', (reason) => {
// Provide feedback that device no longer connected.
digitalWrite(LED, 0);
isConnected = false;
debug = reason;
updateScreen();
});

NRF.on('connect', (addr) => {
digitalWrite(LED, 1);
isConnected = true;
debug = addr;
updateScreen();
});

NRF.setSecurity({
display: canShowPasskey(), // Can this device display a passkey?
keyboard: false, // Can this device enter a passkey?
mitm: true, // Man In The Middle protection.
passkey: devicePassKey,
});

updateScreen();

setInterval(updateScreen, 1000);
setInterval(updateValue, 40);
}

90 changes: 90 additions & 0 deletions characteristic_secure_notify/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!DOCTYPE html>
<!--
Copyright 2021 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<html>

<head>
<meta charset="UTF-8">
<title>Bluetooth Secure Characteristic Read w/Notifications</title>
<script src="../shared/shared.js"></script>
<script src="web_app.js"></script>
<link rel="stylesheet" href="../css/main.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

<body onload="init()">
<h1>Bluetooth Secure Characteristic Read w/Notifications</h1>
<p>This is a simple test to subscribe to secure characteristic value change notifications.
</p>
<div id="bluetooth_available" style="visibility: visible;">
<h2>Step 1</h2>
<div class="container">
<div class="flex">
<div class="inRow">
Press the button to load the code to the Espruino IDE.
From there flash any Bluetooth capable Espruino device.
<br>
<br>
View <a href="device_code.js" title="Device source code">source</a>.
</div>
<div class="inRow">
<button id="btn_load_code" type="button" onclick="loadEspruinoDeviceCode()">Load Device Code</button>
</div>
</div>
</div>

<h2>Step 2</h2>
<div>
<div class="flex">
<div class="inRow">
Once running, start the test. When prompted for the passcode enter
"141414".
</div>
<div class="inRow">
<button id="btn_start_test" type="button" onclick="startTest()">Start Test</button>
</div>
</div>
<div class="flex">
<div class="inNote"><b>NOTE:</b></div>
<div class="inNote">
When re-running this test, this device will likely
<b>not</b> prompt for the passcode a second time, as the device is
now paired with this host. To force re-prompting for the passcode,
access the Bluetooth preferences on this host and forget/unpair
the test device.
</div>
</div>
</div>

<p>Status: <span id='test_result'></span></p>
<pre id='status'></pre>
</div>
<!-- Only one of the three below will be visible. -->
<div id="bluetooth_none" class="no-bluetooth" style="visibility: hidden;">
Web Bluetooth is not supported by this browser.
</div>
<div id="bluetooth_insecure" class="no-bluetooth" style="visibility: hidden;">
Bluetooth requires a secure context (HTTPS). For development purposes it
does support HTTP, but only the on the loopback adapter (i.e. "localhost").
</div>
<div id="bluetooth_unavailable" class="no-bluetooth" style="visibility: hidden;">
Web Bluetooth is supported by this browser but this device does not have
Bluetooth capabilities.
</div>
</body>

</html>
153 changes: 153 additions & 0 deletions characteristic_secure_notify/web_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// The test service defined in device_code.js
const testService = '02fc549a-244c-11eb-adc1-0242ac120002';
// The test characteristic defined in device_code.js
const testCharacteristic = '02fc549a-244c-11eb-adc1-0242ac120002';
const requiredNumUpdates = 100;

let gattServer = undefined;

/**
* Load the device code to the Espruino IDE.
*/
function loadEspruinoDeviceCode() {
fetch('device_code.js').then(response => response.text()).then(data => {
let url = 'http://www.espruino.com/webide?code=' + encodeURIComponent(data);
window.open(url, '_window');
});
}

function onGattDisconnected(evt) {
const device = evt.target;
logInfo(`Disconnected from GATT on device ${device.name}.`);
assertFalse(gattServer.connected, 'Server connected');
}

async function startTest() {
clearStatus();
logInfo('Starting test');

$('btn_start_test').disabled = true;
$('btn_load_code').disabled = true;
let notifyCharacteristic = undefined;
let updateNum = 0;
let lastValue = null;

const resetTest = async () => {
if (notifyCharacteristic) {
await notifyCharacteristic.stopNotifications();
}
if (gattServer && gattServer.connected) {
logInfo('Disconnecting from GATT.');
gattServer.disconnect();
}
$('btn_start_test').disabled = false;
$('btn_load_code').disabled = false;
}

const checkCharacteristicValue = (value) => {
if (value > 9999) {
throw `Invalid characteristic value ${value}. Should be val <= 9999.`;
}
if (!lastValue) {
return;
}
if (lastValue === 9999) {
assertEquals(0, value);
}
assertEquals(lastValue + 1, value, 'Skipped value');
}

const onCharacteristicChanged = (evt) => {
updateNum += 1;
try {
const characteristic = evt.target;
const dataView = characteristic.value;
const val = dataView.getUint32(0, /*littleEndian=*/true);
checkCharacteristicValue(val);
lastValue = val;
if (updateNum == requiredNumUpdates) {
logInfo('Test success.');
}
} catch (error) {
logError(`Unexpected failure: ${error}`);
updateNum = requiredNumUpdates; // Force test to stop.
}
if (updateNum >= requiredNumUpdates) {
resetTest();
testDone();
}
}

try {
const options = {
filters: [{ services: [getEspruinoPrimaryService()] }],
optionalServices: [testService]
};
logInfo(`Requesting Bluetooth device with service ${testService}`);
const device = await navigator.bluetooth.requestDevice(options);

device.addEventListener('gattserverdisconnected', onGattDisconnected);
logInfo(`Connecting to GATT server for device \"${device.name}\"...`);
gattServer = await device.gatt.connect();
assertEquals(gattServer.device, device, 'Server device mismatch');
assertTrue(gattServer.connected, 'server.connected should be true');

logInfo(`Connected to GATT, requesting service: ${testService}...`);
const service = await gattServer.getPrimaryService(testService);
assertEquals(service.device, device, 'service device mismatch');

logInfo(`Connected to service uuid:${service.uuid}, primary:${service.isPrimary}`);
logInfo(`Requesting characteristic ${testCharacteristic}...`);
const characteristic = await service.getCharacteristic(testCharacteristic);
assertEquals(characteristic.service, service,
'characteristic service mismatch');

logInfo(`Got characteristic, reading value...`);
let dataView = await characteristic.readValue();
let val = dataView.getUint32(0, /*littleEndian=*/true);
checkCharacteristicValue(val);

notifyCharacteristic = await characteristic.startNotifications();
notifyCharacteristic.addEventListener(
'characteristicvaluechanged', onCharacteristicChanged);
} catch (error) {
logError(`Unexpected failure: ${error}`);
resetTest();
testDone();
}
}

async function init() {
if (!isBluetoothSupported()) {
console.log('Bluetooth not supported.');
$('bluetooth_available').style.display = 'none';
if (window.isSecureContext == 'https') {
$('bluetooth_none').style.visibility = 'visible';
} else {
$('bluetooth_insecure').style.visibility = 'visible';
}
return;
}

const available = await navigator.bluetooth.getAvailability();
if (!available) {
$('bluetooth_available').style.display = 'none';
$('bluetooth_unavailable').style.visibility = 'visible';
}
}
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ <h2>Tests</h2>
<li><a href="characteristic_readwrite">Read and write to a characteristic</a></li>
<li><a href="characteristic_notify">Subscribe to characteristic value changes</a></li>
<li><a href="characteristic_secure">Read and write to a secure characteristic</a></li>
<li><a href="characteristic_secure_notify">Subscribe to secure characteristic value changes</a></li>
<li><a href="get_primary_services">Test getPrimaryServices()</a></li>
<li><a href="descriptors">Descriptors</a></li>
</ol>
Expand Down