Mii QR Code scanner
using sjcl + qr-scanner, although it is a fork that allows binary data. beware it uses a private method of sjcl due to difficulties I experienced with the library.
by arian_
HTML
<h2>Scan QR Code</h2>
<button id="start-camera">From camera</button>
<button id="stop-button">Cancel</button>
<div>
<label for="cam-list">Select camera:</label>
<select id="cam-list">
<option value="environment" selected>Back Camera (default)</option>
<option value="user">User/Front Facing Camera</option>
<option class="device-camera" disabled>(Open the camera for more options)</option>
</select>
</div>
<input type="file" id="file-input" accept="image/*">
<video id="qr-video" style="max-width: 100%"></video>
<ul id="result-list"></ul>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/sjcl.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/core/codecBytes.js"></script>
<!--<script src="https://cdn.jsdelivr.net/npm/[email protected]/qr-scanner.umd.min.js"></script>-->
<!-- custom FORK!!!!!! of qr-scanner that supports binary data
https://github.com/nimiq/qr-scanner/pull/191
-->
<script src="https://cdn.jsdelivr.net/gh/criteo-forks/qr-scanner/qr-scanner.umd.min.js"></script>
CSS
pre {
white-space: pre-wrap;
word-break: break-all;
}
JavaScript
// FOR LOGGING
function buf2hex(buffer) { // buffer is an ArrayBuffer
return [...new Uint8Array(buffer)]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
function decryptAesCcm(encryptedData) {
// if the length is smaller than the standard mii qr code size
if(encryptedData.length < 112) {
throw new Error('Mii QR codes should be 112 or more bytes long, yours is ' + encryptedData.length);
}
// Extract nonce and encrypted content
const nonce = encryptedData.slice(0, 8);
const encryptedContent = encryptedData.slice(8);
//const key = sjcl.codec.hex.toBits('59FC817E6446EA6190347B20E9BDCE52');
// hardcoding the key, sjcl formats keys in these huge 32 bit ints
const cipher = new sjcl.cipher.aes([1509720446, 1682369121, -1875608800, -373436846]);
// Convert nonce and encrypted content to bits, adjusting the nonce to full size
const encryptedBits = sjcl.codec.bytes.toBits(Array.from(encryptedContent));
let nonceBits = sjcl.codec.bytes.toBits([...nonce, 0, 0, 0, 0]);
// Isolate the actual ciphertext from the tag and adjust IV
const tlen = 128; // Tag length in bits
let out = sjcl.bitArray.clamp(encryptedBits,
// remove tag from out, tag length = 128
sjcl.bitArray.bitLength(encryptedBits) - tlen);
// regex to find the _ctrMode function: 6 arguments and calls "bitSlice"
const ctrModeFuncRegex = /\([^)]*,[^)]*,[^)]*,[^)]*,[^)]*,[^)]*\)\s*.*?bitSlice/;
// jsdelivr (1.0.8 sjcl.min.js) minifies this function name to "C"
const ctrDecrypt = sjcl.mode.ccm._ctrMode || sjcl.mode.ccm.C ||
// attempt to find the private _ctrMode func using our regex
Object.entries(sjcl.mode.ccm).find(
// match string representation of the function
([_, fn]) => fn.toString().match(ctrModeFuncRegex)
)[1];
// may throw IndexError??
if (!ctrDecrypt) throw new Error('WE CANNOT FIND HIDDEN sjcl.mode.ccm._ctrMode DECRYPT FUNCTION!!!!!!');
const decryptedBits = ctrDecrypt(cipher, out, nonceBits, [], tlen, 3) //...