Skip to content

Commit

Permalink
Demo rework
Browse files Browse the repository at this point in the history
- New better UI
- Tester for mitigation of side-channel attack
  • Loading branch information
kenchris committed Oct 9, 2023
1 parent 34af5b1 commit 3965ad2
Show file tree
Hide file tree
Showing 10 changed files with 1,122 additions and 378 deletions.
152 changes: 152 additions & 0 deletions demo/binutils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
export function toBinaryString(value) {
let str = (value >>> 0).toString(2);
return '0b' + str.padStart(Math.ceil(str.length / 8) * 8, '0');
}

export function reverseByte(b) {
b = (b & 0b11110000) >> 4 | (b & 0b00001111) << 4;
b = (b & 0b11001100) >> 2 | (b & 0b00110011) << 2;
b = (b & 0b10101010) >> 1 | (b & 0b01010101) << 1;
return b;
}

export class BitDataView extends DataView {
#u8;

constructor(buffer) {
super(buffer);
this.#u8 = new Uint8Array(buffer);
this.length = 0;
}

getBit(index) {
const v = this.#u8[index >> 3];
const offset = index & 0x7;
return (v >> (7-offset)) & 1;
};

setBit(index, value) {
this.length = Math.max(this.length, index + 1);
const offset = index & 0x7;
if (value) {
this.#u8[index >> 3] |= (0x80 >> offset);
} else {
this.#u8[index >> 3] &= ~(0x80 >> offset);
}
};

shift16Left(amount, index = 0) {
let view = new DataView(this.buffer);
let value = view.getUint16(index, false);
view.setInt16(index, value << amount, false);
}

[Symbol.iterator]() {
return {
current: 0,
last: this.buffer.byteLength * 8,
view: this,

next() {
if (this.current <= this.last) {
return { done: false, value: this.view.getBit(this.current++) };
} else {
return { done: true };
}
}
};
}
}

export class MessageEncoder {
encode(message) {
const enc = new TextEncoder("utf-8");

// We send 32 bits per ASCII character:
// |position| with the first bit set to 1 indicating a position
// |checksum| and alignment byte for the position
// |value| Non extended ASCII character
// |checksum| and alignment byte for the value
//
// A checksum is the reversed complement, e.g for 0110 1000
// the complement will be 10010111 and reversed it will be 11101001
const letters = [...enc.encode(message)].flatMap((value, position) => {
const pos = position | 0b10000000;
const vPos = reverseByte(~pos);
const vValue = reverseByte(~value);
return [ pos, vPos, value, vValue ]
});

return new Uint8Array([...letters]);
}
}

export class MessageDecoder {
decode(uInt8Array) {
const dec = new TextDecoder("utf-8");
const view = new BitDataView(uInt8Array.buffer);
const output = [];

const POSITION_BIT = 0b10000000;
let position = 0;

for (let i = 0; i < uInt8Array.length; i += 2) {
const byte = uInt8Array[i];
const checksum = uInt8Array[i + 1];

const isPosition = (byte & POSITION_BIT) === POSITION_BIT;
const lastPosition = position;
if (isPosition) {
position = byte & 0b01111111;
}

if (byte !== reverseByte(~checksum)) {
console.warn(`Checksum ${toBinaryString(checksum)} test failed for byte ${toBinaryString(byte)}`);
if (isPosition && lastPosition + 1 === position) {
console.warm(`Position ${position} appears to be correct, so we trust it.`)
} else {
continue;
}
}

if (position > uInt8Array.length / 4) {
console.warn(`Decoded position '${position}', but value seems corrupt.`)
console.log(uInt8Array.length);
position = -1;
}

if (!isPosition) {
let char = dec.decode(new Uint8Array([byte]));
if (position < 0) {
console.warn(`Decoded '${char}', but character position is unknown.`);
continue;
}
output[position] = char;
position = -1;
}
}

return [...output.values()].map(v => v ? v : '\ufffd').join('');
}
}

//let enc = new MessageEncoder();
//let dec = new MessageDecoder();

//let encoded = enc.encode("hello world");
//console.log(encoded);

// h 0 1 2 3
// e 4 5 6 7
// l 8 9 10 11
// l 12 13 14 15
// o 16 17 18 19
// _ 20 21 22 23
// w 24 25 26 27

// encoded[5] = 0b01001111; // break position checksum
// encoded[8] = 0b01001111; // break position 2 (l)
// encoded[27] = 0b01001111; // break char 'w'

// console.log(encoded);
// console.log(dec.decode(encoded));
62 changes: 62 additions & 0 deletions demo/bit-channel-broadcaster.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { MessageEncoder, BitDataView, toBinaryString } from "./binutils.js";

export class BitChannelBroadcaster extends EventTarget {
#pressureGeneratorFn;

constructor(pressureGeneratorFn) {
super();
this.#pressureGeneratorFn = pressureGeneratorFn;
}

async sendMessage(msg, calibration) {
const enc = new MessageEncoder();
const bits = new BitDataView(enc.encode(msg).buffer);

const timeout = delay => new Promise(resolve => setTimeout(resolve, delay));

while (true) {
let i = 0;
let lastDwordIndex = 0;
for (let bit of bits) {
let byteIndex = Math.floor(i++ / 8);
let dwordIndex = byteIndex % 4;

this.dispatchEvent(new CustomEvent("bitsent", { detail: { value: bit } }));

let byte = bits.getUint8(byteIndex);
let chkByte = bits.getUint8(byteIndex + 1);

if (dwordIndex !== lastDwordIndex) {
if (dwordIndex === 0) {
let pos = byte - 128;
this.dispatchEvent(new CustomEvent("datachange", {
detail: {
type: "position",
value: pos,
data: new Uint8Array([byte, chkByte])
}
}));
} else if (dwordIndex === 2) {
const dec = new TextDecoder("utf-8");
const ch = dec.decode(bits.buffer.slice(byteIndex, byteIndex + 1));
this.dispatchEvent(new CustomEvent("datachange", {
detail: {
type: "character",
value: ch,
data: new Uint8Array([byte, chkByte])
}
}));
}
}
lastDwordIndex = dwordIndex;

this.#pressureGeneratorFn(bit ? calibration.one : calibration.zero);
await timeout(calibration.delay);

console.log("Sending 'reset'");
this.#pressureGeneratorFn(calibration.reset);
await timeout(2 * calibration.delay);
}
}
}
};
110 changes: 110 additions & 0 deletions demo/bit-channel-observer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { MessageEncoder, MessageDecoder, BitDataView, reverseByte } from "./binutils.js";

export class BitChannelObserver extends EventTarget {
#lastTimestamp;
#lastState = 1;
#millis = [0, 0, 0]; // zero, reset, one
#observer;

#bitIndex = 0;
#align = 0;
#decoder = new MessageDecoder;
#view = new BitDataView(new ArrayBuffer(2, { maxByteLength: 256 }));

constructor() {
super();
let lastValue = -1;

const map = state => {
switch(state) {
case "nominal":
case "fair":
return 0;
case "serious":
return 1;
case "critical":
return 2;
}
}

this.#observer = new PressureObserver(changes => {
let value = map(changes[0].state);
if (value !== lastValue) {
this.#processRawState(value);
}
lastValue = value;
});
}

async observe(runTest = false) {
if (!runTest) {
this.#lastTimestamp = performance.now();
return await this.#observer.observe("cpu");
}

const enc = new MessageEncoder();
const data = [...enc.encode("hello world")];
// Add some disalignment
const brokenData = [23, 19,
...data.slice(0, 8),
15, 101,
...data.slice(8)];

const bits = new BitDataView(new Uint8Array(brokenData).buffer);
console.log(bits)
for (let bit of bits) {
this.#processData(bit);
}
}

#processRawState(state) {
let start = this.#lastTimestamp;
this.#lastTimestamp = performance.now();
let time = (this.#lastTimestamp - start);

this.#millis[this.#lastState] += time;
this.#lastState = state;

if (this.#millis[1] > 6_000) {
const toDispatch = this.#millis[0] > this.#millis[2] ? 0 : 1;
this.#processData(toDispatch);
this.#millis = [0, 0, 0];
}
}

#processData(bit) {
this.#view.setBit(this.#bitIndex, bit);

this.dispatchEvent(new CustomEvent("bitreceived", { detail: { value: bit } }));

if (++this.#bitIndex % 16 !== 0) {
return;
}

// We have one word (two bytes) now!

const byteIndex = this.#view.buffer.byteLength - 2;

const value = this.#view.getUint8(byteIndex);
const checksum = this.#view.getUint8(byteIndex + 1);

if (value !== reverseByte(~checksum)) {
// Checksum for byte doesn't match.
// We might have gotten a wrong value, or a value too much or too little.
// In most cases this requires around two new bytes to find alignment.
console.log(`Misaligned or corrupt data, shifting to find new alignment (${++this.#align})...`);
//console.log(toBinaryString(view.getUint8(byteIndex)), toBinaryString(view.getUint8(byteIndex + 1)));
this.#bitIndex--;
this.#view.shift16Left(1, byteIndex);
return;
}

// We have a valid word (two bytes) now!

const message = this.#decoder.decode(new Uint8Array(this.#view.buffer));
this.#view.buffer.resize(this.#view.buffer.byteLength + 2);
this.#align = 0;

this.dispatchEvent(new CustomEvent("messagechange", { detail: { value: message } }));
}
}
Loading

0 comments on commit 3965ad2

Please sign in to comment.