Zigbee Install Code Converter

Derive Link Key from an Install Code via AES-MMO

HTML

<script src="https://cdn.rawgit.com/ricmoo/aes-js/e27b99df/index.js"></script>
<!DOCTYPE html>
<html lang="en">
	<head>
	</head>
	<body>
		<h1>Zigbee Install Code Converter</h1>
		<textarea id="input" placeholder="Install Code"></textarea>
		<p>
			Link Key: <span id="output"></span>
		</p>
		<input type="button" value="Convert" onclick="convert()" />
	</body>
</html>

CSS

body {
			font-family: sans-serif;
}

JavaScript

const BLOCKSIZE = 16;

function convert() {
  let input = document.getElementById("input").value;
  let hash = calculateHash(extractInstallCode(input));
  document.getElementById("output").innerHTML = aesjs.utils.hex.fromBytes(hash);
}

// Based on https://github.com/Koenkk/zigbee-herdsman/blob/15e9af9f28284d8596a5af277167ed5d2d4e071d/src/controller/controller.ts#L214

function extractInstallCode(installCode) {
  const aqaraMatch = installCode.match(/^G\$M:.+\$A:(.+)\$I:(.+)$/);
  let key;
  if (aqaraMatch) {
    key = aqaraMatch[2];
  } 
  else if (installCode.length === 95 || installCode.length === 91) {
    const keyStart = installCode.length - (installCode.length === 95 ? 36 : 32);
    key = installCode.substring(keyStart, installCode.length);
  }
  else {
    key = installCode;
  }

  return aesjs.utils.hex.toBytes(key);
}

// AES-MMO Implementation based on https://github.com/zsmartsystems/com.zsmartsystems.zigbee/blob/master/com.zsmartsystems.zigbee/src/main/java/com/zsmartsystems/zigbee/security/MmoHash.java

function calculateHash(input) {
  let output = new Uint8Array(BLOCKSIZE);
  let cipher_in = new Uint8Array(BLOCKSIZE);

  let inputCnt = 0;
  let outputCnt = 0;

  while (inputCnt < input.length) {
    cipher_in[outputCnt++] = input[inputCnt++];

    if (outputCnt >= BLOCKSIZE) {
      output = doBlock(output, cipher_in);
      outputCnt = 0;
    }
  }

  cipher_in[outputCnt++] = 0x80;

  while (outputCnt != BLOCKSIZE - 2) {
    if (outputCnt >= BLOCKSIZE) {
      output = doBlock(output, cipher_in);
      outputCnt = 0;
    }
    cipher_in[outputCnt++] = 0x00;
  }

  cipher_in[outputCnt++] = ((input.length * 8) >> 8);
  cipher_in[outputCnt] = ((input.length * 8) >> 0);

  return doBlock(output, cipher_in);
}

function doBlock(keyBytes, inBytes) {
  let aesEcb = new aesjs.ModeOfOperation.ecb(keyBytes);
  let output = aesEcb.encrypt(inBytes);

  for (let i = 0; i < BLOCKSIZE; i++) {
    output[i] ^= inBytes[i];
  }

  return output;
}