diff --git a/.cspell.json b/.cspell.json index 66fca29b2c..08300464cf 100644 --- a/.cspell.json +++ b/.cspell.json @@ -1,7 +1,7 @@ { "version": "0.2", "language": "en,en-gb", - "words": [], + "words": ["AUTOSAR", "Miyaguchi", "Preneel"], "dictionaries": [ "npm", "softwareTerms", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 6ad3adb776..4f8eb25fd9 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -174,6 +174,7 @@ "Citrix CTX1 Decode", "AES Key Wrap", "AES Key Unwrap", + "SHE Key Update", "Pseudo-Random Number Generator", "Pseudo-Random Prime Generator", "Enigma", diff --git a/src/core/operations/SHEKeyUpdate.mjs b/src/core/operations/SHEKeyUpdate.mjs new file mode 100644 index 0000000000..37e9f8fea0 --- /dev/null +++ b/src/core/operations/SHEKeyUpdate.mjs @@ -0,0 +1,407 @@ +/** + * @author MannXo [prmma23@gmail.com] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; +import forge from "node-forge"; +import { toHexFast } from "../lib/Hex.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * SHE key slot identifiers as defined by the AUTOSAR SHE specification. + */ +const KEY_SLOTS = { + "SECRET_KEY": 0x0, + "MASTER_ECU_KEY": 0x1, + "BOOT_MAC_KEY": 0x2, + "BOOT_MAC": 0x3, + "KEY_1": 0x4, + "KEY_2": 0x5, + "KEY_3": 0x6, + "KEY_4": 0x7, + "KEY_5": 0x8, + "KEY_6": 0x9, + "KEY_7": 0xa, + "KEY_8": 0xb, + "KEY_9": 0xc, + "KEY_10": 0xd, + "RAM_KEY": 0xe, +}; + +const SLOT_NAMES = Object.keys(KEY_SLOTS); + +const FLAGS = { + "Write protection": 0b100000, + "Boot protection": 0b010000, + "Debugger protection": 0b001000, + "Key usage": 0b000100, + "Wildcard": 0b000010, + "Verify only (SHE+)": 0b000001, +}; + +const CONSTANTS = { + "SHE": { + "enc": "010153484500800000000000000000b0", + "mac": "010253484500800000000000000000b0", + }, + "SHE+": { + "enc": "018153484500800000000000000000b0", + "mac": "018253484500800000000000000000b0", + }, +}; + +const MAX_COUNTER = 0xfffffff; + +/** + * SHE Key Update operation + */ +class SHEKeyUpdate extends Operation { + + /** + * SHEKeyUpdate constructor + */ + constructor() { + super(); + + this.name = "SHE Key Update"; + this.module = "Crypto"; + this.description = "Generates the M1-M5 memory update protocol messages used to load a key into an AUTOSAR SHE (Secure Hardware Extension) module.

The input is the new key value (KEY_NEW), 16 bytes. M1, M2 and M3 are sent to the SHE module to import the key; M4 and M5 are returned by the module for verification. Keys are derived from the authorising key with the SHE KDF (a Miyaguchi-Preneel one-way compression over AES-128).

Choose 'SHE+' for the extended memory update constants."; + this.infoURL = "https://www.autosar.org/fileadmin/standards/R22-11/FO/AUTOSAR_TR_SecureHardwareExtensions.pdf"; + this.inputType = "ArrayBuffer"; + this.outputType = "string"; + this.args = [ + { + "name": "Authorising key (KeyAUTH)", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "UID", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "New key slot (ID)", + "type": "option", + "value": SLOT_NAMES, + "defaultIndex": SLOT_NAMES.indexOf("KEY_1") + }, + { + "name": "Authorising key slot (AuthID)", + "type": "option", + "value": SLOT_NAMES, + "defaultIndex": SLOT_NAMES.indexOf("MASTER_ECU_KEY") + }, + { + "name": "Counter (CID)", + "type": "number", + "value": 1, + "min": 0, + "max": MAX_COUNTER + }, + { + "name": "Variant", + "type": "option", + "value": ["SHE", "SHE+"] + }, + { + "name": "Write protection", + "type": "boolean", + "value": false + }, + { + "name": "Boot protection", + "type": "boolean", + "value": false + }, + { + "name": "Debugger protection", + "type": "boolean", + "value": false + }, + { + "name": "Key usage", + "type": "boolean", + "value": false + }, + { + "name": "Wildcard", + "type": "boolean", + "value": false + }, + { + "name": "Verify only (SHE+)", + "type": "boolean", + "value": false + } + ]; + } + + /** + * @param {ArrayBuffer} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const keyNew = new Uint8Array(input); + const keyAuth = strToBytes(Utils.convertToByteString(args[0].string, args[0].option)); + const uid = strToBytes(Utils.convertToByteString(args[1].string, args[1].option)); + const id = KEY_SLOTS[args[2]]; + const authId = KEY_SLOTS[args[3]]; + const counter = args[4]; + const variant = args[5]; + const flagValues = args.slice(6); + + if (keyNew.length !== 16) { + throw new OperationError(`The new key (input) must be 16 bytes (currently ${keyNew.length} bytes)`); + } + if (keyAuth.length !== 16) { + throw new OperationError(`The authorising key must be 16 bytes (currently ${keyAuth.length} bytes)`); + } + if (uid.length !== 15) { + throw new OperationError(`The UID must be 15 bytes (currently ${uid.length} bytes)`); + } + if (counter < 0 || counter > MAX_COUNTER) { + throw new OperationError("The counter (CID) must be a 28-bit value between 0 and 268435455"); + } + + const flagOrder = Object.keys(FLAGS); + let fid = 0; + flagValues.forEach((enabled, i) => { + if (enabled) fid |= FLAGS[flagOrder[i]]; + }); + + const c = CONSTANTS[variant]; + const encC = fromHex(c.enc); + const macC = fromHex(c.mac); + + const k1 = kdf(keyAuth, encC); + const k2 = kdf(keyAuth, macC); + const k3 = kdf(keyNew, encC); + const k4 = kdf(keyNew, macC); + + const m1 = concat(uid, new Uint8Array([(id << 4) | (authId & 0x0f)])); + + const m2Plain = new Uint8Array(32); + m2Plain.set(uint32ToBytes(counter * 16 + (0x0f & (fid >> 2))), 0); + m2Plain[4] = (fid << 6) & 0xc0; + m2Plain.set(keyNew, 16); + const m2 = aesCbcEncrypt(k1, m2Plain); + + const m3 = aesCmac(k2, concat(m1, m2)); + + const m4Star = aesEcbEncryptBlock(k3, concat(uint32ToBytes(counter * 16 + 0x08), new Uint8Array(12))); + const m4 = concat(m1, m4Star); + + const m5 = aesCmac(k4, m4); + + return [ + `M1: ${toHexFast(m1)}`, + `M2: ${toHexFast(m2)}`, + `M3: ${toHexFast(m3)}`, + `M4: ${toHexFast(m4)}`, + `M5: ${toHexFast(m5)}`, + ].join("\n"); + } + +} + +/** + * Converts a byte string into a Uint8Array. + * + * @param {string} str + * @returns {Uint8Array} + */ +function strToBytes(str) { + const out = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + out[i] = str.charCodeAt(i); + } + return out; +} + +/** + * Parses a hex string into a Uint8Array. + * + * @param {string} hex + * @returns {Uint8Array} + */ +function fromHex(hex) { + const out = new Uint8Array(hex.length / 2); + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(hex.substr(i * 2, 2), 16); + } + return out; +} + +/** + * Encodes an unsigned integer as a 4-byte big-endian Uint8Array. + * + * @param {number} value + * @returns {Uint8Array} + */ +function uint32ToBytes(value) { + return new Uint8Array([ + Math.floor(value / 0x1000000) & 0xff, + Math.floor(value / 0x10000) & 0xff, + Math.floor(value / 0x100) & 0xff, + value & 0xff, + ]); +} + +/** + * Concatenates two Uint8Arrays. + * + * @param {Uint8Array} a + * @param {Uint8Array} b + * @returns {Uint8Array} + */ +function concat(a, b) { + const out = new Uint8Array(a.length + b.length); + out.set(a, 0); + out.set(b, a.length); + return out; +} + +/** + * XORs two equal-length Uint8Arrays. + * + * @param {Uint8Array} a + * @param {Uint8Array} b + * @returns {Uint8Array} + */ +function xor(a, b) { + const out = new Uint8Array(a.length); + for (let i = 0; i < a.length; i++) { + out[i] = a[i] ^ b[i]; + } + return out; +} + +/** + * Converts a Uint8Array into a forge byte string. + * + * @param {Uint8Array} bytes + * @returns {string} + */ +function toForge(bytes) { + let str = ""; + for (let i = 0; i < bytes.length; i++) { + str += String.fromCharCode(bytes[i]); + } + return str; +} + +/** + * AES-128 ECB encryption of a single 16-byte block. + * + * @param {Uint8Array} key + * @param {Uint8Array} block + * @returns {Uint8Array} + */ +function aesEcbEncryptBlock(key, block) { + const cipher = forge.cipher.createCipher("AES-ECB", toForge(key)); + cipher.start(); + cipher.update(forge.util.createBuffer(toForge(block))); + cipher.finish(); + const out = cipher.output.getBytes(); + return strToBytes(out.substring(0, 16)); +} + +/** + * AES-128 CBC encryption with a zero IV, no padding. + * + * @param {Uint8Array} key + * @param {Uint8Array} data + * @returns {Uint8Array} + */ +function aesCbcEncrypt(key, data) { + const cipher = forge.cipher.createCipher("AES-CBC", toForge(key)); + cipher.start({iv: toForge(new Uint8Array(16))}); + cipher.update(forge.util.createBuffer(toForge(data))); + cipher.finish(); + const out = cipher.output.getBytes(); + return strToBytes(out.substring(0, data.length)); +} + +/** + * SHE key derivation function: the Miyaguchi-Preneel one-way compression + * of the key concatenated with the given constant. + * + * @param {Uint8Array} key + * @param {Uint8Array} constant + * @returns {Uint8Array} + */ +function kdf(key, constant) { + const data = concat(key, constant); + let g = new Uint8Array(16); + for (let i = 0; i < data.length; i += 16) { + const block = data.subarray(i, i + 16); + g = xor(xor(aesEcbEncryptBlock(g, block), block), g); + } + return g; +} + +/** + * AES-128-CMAC (RFC 4493). + * + * @param {Uint8Array} key + * @param {Uint8Array} message + * @returns {Uint8Array} + */ +function aesCmac(key, message) { + const Rb = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x87]); + const cipher = forge.cipher.createCipher("AES-ECB", toForge(key)); + const encrypt = function(block) { + cipher.start(); + cipher.update(forge.util.createBuffer(toForge(block))); + cipher.finish(); + return strToBytes(cipher.output.getBytes().substring(0, 16)); + }; + + const leftShift1 = function(a) { + const out = new Uint8Array(a.length); + let carry = 0; + for (let i = a.length - 1; i >= 0; i--) { + out[i] = ((a[i] << 1) | carry) & 0xff; + carry = a[i] >> 7; + } + return out; + }; + + const L = encrypt(new Uint8Array(16)); + let k1 = leftShift1(L); + if (L[0] & 0x80) k1 = xor(k1, Rb); + let k2 = leftShift1(k1); + if (k1[0] & 0x80) k2 = xor(k2, Rb); + + const n = Math.ceil(message.length / 16); + let lastBlock; + if (n === 0) { + lastBlock = new Uint8Array(k2); + lastBlock[0] ^= 0x80; + } else { + const last = message.subarray(16 * (n - 1)); + if (last.length === 16) { + lastBlock = xor(last, k1); + } else { + const padded = new Uint8Array(16); + padded.set(last, 0); + padded[last.length] = 0x80; + lastBlock = xor(padded, k2); + } + } + + let x = new Uint8Array(16); + for (let i = 0; i < n - 1; i++) { + x = encrypt(xor(x, message.subarray(16 * i, 16 * i + 16))); + } + return encrypt(xor(lastBlock, x)); +} + +export default SHEKeyUpdate; diff --git a/tests/operations/tests/SHEKeyUpdate.mjs b/tests/operations/tests/SHEKeyUpdate.mjs new file mode 100644 index 0000000000..744ddb4ca5 --- /dev/null +++ b/tests/operations/tests/SHEKeyUpdate.mjs @@ -0,0 +1,146 @@ +/** + * SHE Key Update tests. + * + * Test vectors are taken from the AUTOSAR SHE memory update protocol examples. + * + * @author MannXo [prmma23@gmail.com] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "SHE Key Update: AUTOSAR example (basic SHE)", + input: "0f0e0d0c0b0a09080706050403020100", + expectedOutput: [ + "M1: 00000000000000000000000000000141", + "M2: 2b111e2d93f486566bcbba1d7f7a9797c94643b050fc5d4d7de14cff682203c3", + "M3: b9d745e5ace7d41860bc63c2b9f5bb46", + "M4: 00000000000000000000000000000141b472e8d8727d70d57295e74849a27917", + "M5: 820d8d95dc11b4668878160cb2a4e23e", + ].join("\n"), + recipeConfig: [ + { + op: "From Hex", + args: ["None"] + }, + { + op: "SHE Key Update", + args: [ + {option: "Hex", string: "000102030405060708090a0b0c0d0e0f"}, + {option: "Hex", string: "000000000000000000000000000001"}, + "KEY_1", + "MASTER_ECU_KEY", + 1, + "SHE", + false, false, false, false, false, false + ] + } + ] + }, + { + name: "SHE Key Update: AUTOSAR example with KEY_USAGE flag (basic SHE)", + input: "0f0e0d0c0b0a09080706050403020100", + expectedOutput: [ + "M1: 00000000000000000000000000000011", + "M2: 74c3a812bf192a6b52d89d79d9b04ac87f19526c70790d7fcdb707a77dfdf5a8", + "M3: 70c1ebfa56bc2fffff1c9f33048fc294", + "M4: 00000000000000000000000000000011b472e8d8727d70d57295e74849a27917", + "M5: ec4a8474b925eaae19feef74620fad7f", + ].join("\n"), + recipeConfig: [ + { + op: "From Hex", + args: ["None"] + }, + { + op: "SHE Key Update", + args: [ + {option: "Hex", string: "000102030405060708090a0b0c0d0e0f"}, + {option: "Hex", string: "000000000000000000000000000000"}, + "MASTER_ECU_KEY", + "MASTER_ECU_KEY", + 1, + "SHE", + false, false, false, true, false, false + ] + } + ] + }, + { + name: "SHE Key Update: AUTOSAR example (SHE+ constants)", + input: "0f0e0d0c0b0a09080706050403020100", + expectedOutput: [ + "M1: 00000000000000000000000000000141", + "M2: a6c4d8f632faed103d8e3eef2b7694a92b214b1efad16a4c32964afa37ddadef", + "M3: 22eb8f2385cb16a0082aabc106b7dbc6", + "M4: 00000000000000000000000000000141b059c21adbcb938000c9805434852637", + "M5: e3073b876fa53173da072802bd2c8871", + ].join("\n"), + recipeConfig: [ + { + op: "From Hex", + args: ["None"] + }, + { + op: "SHE Key Update", + args: [ + {option: "Hex", string: "000102030405060708090a0b0c0d0e0f"}, + {option: "Hex", string: "000000000000000000000000000001"}, + "KEY_1", + "MASTER_ECU_KEY", + 1, + "SHE+", + false, false, false, false, false, false + ] + } + ] + }, + { + name: "SHE Key Update: rejects a new key that is not 16 bytes", + input: "0f0e0d0c0b0a0908070605040302", + expectedOutput: "The new key (input) must be 16 bytes (currently 14 bytes)", + recipeConfig: [ + { + op: "From Hex", + args: ["None"] + }, + { + op: "SHE Key Update", + args: [ + {option: "Hex", string: "000102030405060708090a0b0c0d0e0f"}, + {option: "Hex", string: "000000000000000000000000000001"}, + "KEY_1", + "MASTER_ECU_KEY", + 1, + "SHE", + false, false, false, false, false, false + ] + } + ] + }, + { + name: "SHE Key Update: rejects a UID that is not 15 bytes", + input: "0f0e0d0c0b0a09080706050403020100", + expectedOutput: "The UID must be 15 bytes (currently 14 bytes)", + recipeConfig: [ + { + op: "From Hex", + args: ["None"] + }, + { + op: "SHE Key Update", + args: [ + {option: "Hex", string: "000102030405060708090a0b0c0d0e0f"}, + {option: "Hex", string: "0000000000000000000000000000"}, + "KEY_1", + "MASTER_ECU_KEY", + 1, + "SHE", + false, false, false, false, false, false + ] + } + ] + } +]);