miitomo tomodachi miitopia qr decrypt experiment

by arian_

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <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.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/criteo-forks/qr-scanner/qr-scanner.umd.min.js"></script>
  </head>
  <body>

  <!-- rudimentary js error handling text -->
  <div id="error-container" style="display: none;">
    <h1>😭 JavaScript Error!! 😭</h1>
    <p id="error-message" style="color: red;"></p>
    <pre id="error-stacktrace" style="display: none;"></pre>
    <pre id="error-at">At <span></span></pre>
  </div>

<!-- Tabbing Section 
<div>
  <button class="tab-button active" onclick="showTab('encode-tab')">Encode</button>
  <button class="tab-button" onclick="showTab('decode-tab')">Decode</button>
</div>
-->

<!-- Encode Section -->
<div id="encode-tab" class="tab-content active">
  <h1>encrypt/encode mii qr code</h1>
  <div>
    <b>StoreData hex input:</b>
    <div>(you can also paste Base64 into this, or, load from a cfsd file below)</div>
  </div>
  <table id="hex-editor-storedata" border>
    <tr>
      <td></td>
      <td><pre class="table-padding"></pre></td>
    </tr>
    <tr>
      <td class="line-numbers" width="80"></td>
      <td>
        <textarea spellcheck="false" class="hex-textarea" cols="48"></textarea>
      </td>
      <td width="160" class="ascii-output">.</td>
    </tr>
  </table>
  <input type="file" id="load-storedata-btn" accept=".cfsd,.ffsd,.3dsmii,.cfcd,.ffcd,.bin,.dat" onchange="loadStoreDataFromFile(event)">
  
  <details>
    <summary>load extra data for tomodachi life/miitomo/miitopia 3ds:</summary>
    <table id="hex-editor-extra" border>
    <tr>
      <td></td>
      <td><pre class="table-padding"></pre></td>
    </tr>
    <tr>
      <td...

CSS

body {
  font-family: sans-serif;
}

.tab-content {
  display: none;
}
.active {
  display: block;
}
.tab-button {
  padding: 10px;
  cursor: pointer;
  display: inline-block;
  border: 1px solid #ddd;
  background-color: #f0f0f0;
  margin-right: 5px;
}
.tab-button.active {
  background-color: #ddd;
}

table, table * {
  margin: 0;
  padding: 0;
  vertical-align: top;
  font: 1em/1em monospace;
}
.hex-textarea {
  height: 1.5em;
  resize: none;
  width: 100%;
}
.table-padding {
  padding: 0 2px;
}
.file-input {
  position: absolute;
  opacity: .001;
}
.ascii-output {
  overflow: hidden;
}
#extra-data-warning {
    color: red;
}

JavaScript

// AES keys.
const AES_CCM_KEYSLOT_0x31_HEX     = '59FC817E6446EA6190347B20E9BDCE52'; // Type 2, slot 0x31
                                                                          // https://www.3dbrew.org/wiki/PSPXI:EncryptDecryptAes#Key_Types
const AES_CCM_KEYSLOT_0x31_DEV_HEX = '12DF92B6FFD438AB291C4FD4D7CE256D'; // Dev variant of above key.

const AES_CTR_KEY_HEX              = '30819F300D06092A864886F70D010101';

// Converted AES-CCM keys for sjcl
const AES_CCM_KEYSLOT_0x31_BITS     = sjcl.codec.hex.toBits(AES_CCM_KEYSLOT_0x31_HEX);
const AES_CCM_KEYSLOT_0x31_DEV_BITS = sjcl.codec.hex.toBits(AES_CCM_KEYSLOT_0x31_DEV_HEX);

let gAESCCMKeyPrimary = AES_CCM_KEYSLOT_0x31_BITS; // Reassigned to dev/prod.
const toggleAESCCMKeyMode = isDev => // true controls whether is dev or not
	{ gAESCCMKeyPrimary = isDev ? AES_CCM_KEYSLOT_0x31_DEV_BITS : AES_CCM_KEYSLOT_0x31_BITS };

const crpyto = window['crypt'+'o']; // jsfiddle blocks this word???
const sc = crpyto.subtle;           // Shortcut to SubtleCr*pto.
// ^^ Needed or else the fiddle will not save

// Utility: Hex <-> Uint8Array conversion
function hexToUint8Array(hex) {
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
  }
  return bytes;
}

function uint8ArrayToHex(bytes) {
  return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}

// Utility: Base64 <-> Uint8Array conversion
function base64ToUint8Array(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes;
}

// Helper function to check if a string is valid hex
function isHex(str) {
  const hexRegex = /^[0-9A-Fa-f]+$/;
  return hexRegex.test(str);
}

// Helper function to check if a string is valid base64
function isBase64(str) {
  try {
    atob(str);  // `atob` will throw an error if the string is not valid base64
...