From 3965ad20ebb6177e9d187272b9196b3b4051b7d0 Mon Sep 17 00:00:00 2001 From: Kenneth Rohde Christiansen Date: Tue, 16 May 2023 14:28:08 +0200 Subject: [PATCH] Demo rework - New better UI - Tester for mitigation of side-channel attack --- demo/binutils.js | 152 +++++++++++ demo/bit-channel-broadcaster.js | 62 +++++ demo/bit-channel-observer.js | 110 ++++++++ demo/index.html | 326 +++++++++++++++-------- demo/mandelbrot-player.js | 152 +++++++++++ demo/mandelbrot-worker-asm.js | 8 +- demo/mandelbrot.js | 456 +++++++++++++------------------- demo/pressure-calibrator.js | 113 ++++++++ demo/pressure-emoji.js | 106 ++++++++ demo/wc-utils.js | 15 ++ 10 files changed, 1122 insertions(+), 378 deletions(-) create mode 100644 demo/binutils.js create mode 100644 demo/bit-channel-broadcaster.js create mode 100644 demo/bit-channel-observer.js create mode 100644 demo/mandelbrot-player.js create mode 100644 demo/pressure-calibrator.js create mode 100644 demo/pressure-emoji.js create mode 100644 demo/wc-utils.js diff --git a/demo/binutils.js b/demo/binutils.js new file mode 100644 index 0000000..c381e04 --- /dev/null +++ b/demo/binutils.js @@ -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)); \ No newline at end of file diff --git a/demo/bit-channel-broadcaster.js b/demo/bit-channel-broadcaster.js new file mode 100644 index 0000000..1446532 --- /dev/null +++ b/demo/bit-channel-broadcaster.js @@ -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); + } + } + } +}; \ No newline at end of file diff --git a/demo/bit-channel-observer.js b/demo/bit-channel-observer.js new file mode 100644 index 0000000..d004141 --- /dev/null +++ b/demo/bit-channel-observer.js @@ -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 } })); + } +} \ No newline at end of file diff --git a/demo/index.html b/demo/index.html index 28fb588..6dd3e31 100644 --- a/demo/index.html +++ b/demo/index.html @@ -2,7 +2,7 @@ - Compute Pressure Demo + Compute Pressure Demo @@ -10,142 +10,258 @@ body { font-family: Arial, sans-serif; margin: 3em 3em; + display: flex; + flex-wrap: wrap; + gap: 16px; } + #status { margin-bottom: 1em; } - .enabled { - color: green; + + .card { + display: flex; + flex-direction: column; + flex: 0 0 640px; + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + transition: 0.3s; + padding: 16px; } - .disabled { - color: red; + + .card:hover { + box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); } - #emoji { - font-size: 10em; + .card > * { + padding: 2px 16px; } - #wrapper { - width: 600px; - height: 400px; - position: relative; + .card > .content { + flex-grow: 1; display: flex; align-items: center; justify-content: center; } - #wrapper > div { - position: absolute; - color: white; - font-size: 8em; + summary { + user-select: none; + font-size: 2em; + margin-block-start: 0.67__qem; + margin-block-end: 0.67em; + margin-inline-start: 0; + margin-inline-end: 0; + font-weight: bold; + } + + #card-list { + display: flex; + flex-wrap: wrap; + } + + .invisible { + opacity: 0; + } + + .disabled { + color: red; + } + + .hidden { + display: none; + } + + #manual { + border: 2px solid #ccc; + padding: 6px; + display: grid; + grid-template-columns: 0.4fr 1fr; + grid-template-rows: repeat(4, 1fr); + grid-column-gap: 16px; + grid-row-gap: 0px; } + + -

Compute Pressure demo

-
- not available -
- -
- - 😴 - - Not observing - + +