diff --git a/characteristic_secure_notify/device_code.js b/characteristic_secure_notify/device_code.js new file mode 100644 index 0000000..cd3627d --- /dev/null +++ b/characteristic_secure_notify/device_code.js @@ -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); +} + diff --git a/characteristic_secure_notify/index.html b/characteristic_secure_notify/index.html new file mode 100644 index 0000000..dc9c4cf --- /dev/null +++ b/characteristic_secure_notify/index.html @@ -0,0 +1,90 @@ + + + + + + + + Bluetooth Secure Characteristic Read w/Notifications + + + + + + + +

Bluetooth Secure Characteristic Read w/Notifications

+

This is a simple test to subscribe to secure characteristic value change notifications. +

+
+

Step 1

+
+
+
+ Press the button to load the code to the Espruino IDE. + From there flash any Bluetooth capable Espruino device. +
+
+ View source. +
+
+ +
+
+
+ +

Step 2

+
+
+
+ Once running, start the test. When prompted for the passcode enter + "141414". +
+
+ +
+
+
+
NOTE:
+
+ When re-running this test, this device will likely + not 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. +
+
+
+ +

Status:

+

+  
+ + + + + + + diff --git a/characteristic_secure_notify/web_app.js b/characteristic_secure_notify/web_app.js new file mode 100644 index 0000000..7c71700 --- /dev/null +++ b/characteristic_secure_notify/web_app.js @@ -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'; + } +} diff --git a/index.html b/index.html index 050f9fd..14f1504 100644 --- a/index.html +++ b/index.html @@ -38,6 +38,7 @@

Tests

  • Read and write to a characteristic
  • Subscribe to characteristic value changes
  • Read and write to a secure characteristic
  • +
  • Subscribe to secure characteristic value changes
  • Test getPrimaryServices()
  • Descriptors