@smartdcc/gbcs-parser
Version:
GBCS parser based on henrygiraldo.github.io
197 lines • 7.68 kB
JavaScript
"use strict";
/*
*
* Original copyright holders for the GBCS message parser tool:
*
* Copyright (c) 2019 Andre B. Oliveira
* 2019 Enrique Giraldo
* 2019 Cristóbal Borrero
*
* Copyright for subsequent changes, including porting to NodeJS,
* updating for TypeScript and refactor to support unit testing:
*
* Copyright (c) 2022 Smart DCC Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.gcm = gcm;
exports.ungcm = ungcm;
exports.decryptPayloadWithKey = decryptPayloadWithKey;
exports.deriveKeyFromPair = deriveKeyFromPair;
exports.decryptGbcsData = decryptGbcsData;
exports.signGroupingHeader = signGroupingHeader;
const console_1 = require("console");
const crypto_1 = require("crypto");
const parser_1 = require("./parser");
const util_1 = require("./util");
/**
* standard gcm for use with GBCS - sets the cipher size and fixes the iv
*
* @param cipherInfo - originator/target/counter from grouping header
* @param plainText - text to encrypt - set as empty buffer if none
* @param aad - additional auth data - set as empty buffer if none
* @param aesKey - output from createSecretKey
* @param authTagLength - tag length in bytes - default is 12
* @returns
*/
function gcm(cipherInfo, plainText, aad, aesKey, authTagLength) {
const iv = new Uint8Array(12);
iv.set(cipherInfo.origSysTitle, 0);
iv.set([0, 0, 0, 0], 8);
const cipher = (0, crypto_1.createCipheriv)('aes-128-gcm', aesKey, iv, {
authTagLength: authTagLength ?? 12,
});
cipher.setAAD(aad);
const cipherText = cipher.update(plainText);
cipher.final();
const tag = cipher.getAuthTag();
return { cipherText, tag };
}
/**
* standard gcm decrypt for use with GBCS - sets the cipher size and fixes the iv
*
* @param cipherInfo - originator/target/counter from grouping header
* @param cipherText - text to decrypt - set as empty buffer if none
* @param aad - additional auth data - set as empty buffer if none
* @param aesKey - output from createSecretKey
* @param tag - auth tag - default is 12
* @returns plainText or throws error in case of auth fail
*/
function ungcm(cipherInfo, cipherText, aad, aesKey, tag) {
const iv = new Uint8Array(12);
iv.set(cipherInfo.origSysTitle, 0);
iv.set([0, 0, 0, 0], 8);
const decipher = (0, crypto_1.createDecipheriv)('aes-128-gcm', aesKey, iv);
decipher.setAAD(aad);
decipher.setAuthTag(tag);
const plainText = decipher.update(cipherText);
decipher.final();
return plainText;
}
function decryptPayloadWithKey(cipherInfo, ciphertextTag,
/* Uint8Array(16) */ aesKey, doneCb) {
const iv = new Uint8Array(12);
iv.set(cipherInfo.origSysTitle, 0);
iv.set([0, 0, 0, 0], 8);
const decipher = (0, crypto_1.createDecipheriv)('aes-128-gcm', aesKey, iv);
decipher.setAAD(new Uint8Array([0x31]));
decipher.setAuthTag(ciphertextTag.subarray(-12));
const plaintext = decipher.update(ciphertextTag.subarray(0, -12));
decipher.final();
const yy = {
input: new util_1.Uint8ArrayWrapper(new Uint8Array(plaintext)),
index: 0,
end: plaintext.byteLength,
};
doneCb(yy);
}
/**
* performs kdf as described in section 4 of GBCS
*
* @param privkey
* @param pubkey
* @param cipherInfo
* @param mode tweaks the otherInfo field, if omitted "encryption"
* @returns
*/
function deriveKeyFromPair(privkey, pubkey, cipherInfo, mode) {
if (typeof privkey === 'string') {
privkey = (0, crypto_1.createPrivateKey)({ key: privkey, format: 'pem' });
}
if (typeof pubkey === 'string') {
pubkey = (0, crypto_1.createPublicKey)({ key: pubkey, format: 'pem' });
}
(0, console_1.assert)(privkey.asymmetricKeyType === 'ec', 'expected ec private key');
(0, console_1.assert)(pubkey.asymmetricKeyType === 'ec', 'expected ec public key');
const privEcKey = privkey
.export({ type: 'sec1', format: 'der' })
.subarray(7, 7 + 32);
const pubEcKey = pubkey.export({ type: 'spki', format: 'der' }).subarray(-65);
const ecdh = (0, crypto_1.createECDH)('prime256v1');
ecdh.setPrivateKey(privEcKey);
const secret = ecdh.computeSecret(pubEcKey);
const otherInfo = new Uint8Array(33);
otherInfo.set([0x60, 0x85, 0x74, 0x06, 0x08, 0x03, 0x00], 0); // algorithm-id
otherInfo.set(cipherInfo.origSysTitle, 7);
otherInfo.set([0x09], 7 + 8);
switch (mode) {
case 'command':
otherInfo.set([0x01], 7 + 8 + 1);
break;
case 'response':
otherInfo.set([0x02], 7 + 8 + 1);
break;
case 'alert':
otherInfo.set([0x03], 7 + 8 + 1);
break;
case 'encryption':
otherInfo.set([0x04], 7 + 8 + 1);
break;
}
otherInfo.set(mode === 'encryption'
? (cipherInfo.supplimentryOriginatorCounter ?? cipherInfo.origCounter)
: cipherInfo.origCounter, 7 + 8 + 2);
otherInfo.set(mode === 'encryption'
? (cipherInfo.supplimentryRemotePartyId ?? cipherInfo.recipSysTitle)
: cipherInfo.recipSysTitle, 7 + 8 + 2 + 8);
const sha256 = (0, crypto_1.createHash)('sha256');
sha256.update(new Uint8Array([0, 0, 0, 1]));
sha256.update(secret);
sha256.update(otherInfo);
const aesKey = sha256.digest().subarray(0, 16);
return (0, crypto_1.createSecretKey)(aesKey);
}
function decryptGbcsData(ctx, ciphertextAndTag, okCallback) {
ctx.decryptionList.push(function (cipherInfo, aesKey) {
decryptPayloadWithKey(cipherInfo, ciphertextAndTag, aesKey, okCallback);
});
}
/**
* Sign the output of transform, result is a base64 encoded string.
*
* @param originatorId originator id used to lookup key
* @param payload base64 encoded gbcs message
* @param keyStore
* @
*/
async function signGroupingHeader(originatorId, payload, keyStore) {
const signersKey = await keyStore(originatorId, 'DS', { privateKey: true });
let tbs = Buffer.from(payload, 'base64');
if (tbs.length === 0 || tbs[0] !== 0xdf) {
throw new Error('not general signing apdu');
}
/*
* From green book. should also remove last byte as it will be replaced with
* signature but Boxed < 1.4.1 does not currently follow specification. To
* support this, first parse the message to determine if the signature is
* present.
*/
const message = await (0, parser_1.parseGbcsMessage)(payload, keyStore);
if ('Signature' in message) {
if (message['Signature'].children['Signature Length']?.hex !== '00') {
throw new Error('already signed');
}
tbs = tbs.subarray(0, -1);
}
const signature = (0, crypto_1.sign)('SHA256', tbs.subarray(1), {
key: signersKey,
dsaEncoding: 'ieee-p1363',
});
if (signature.length !== 64) {
throw new Error('unexpected signature length');
}
return Buffer.concat([tbs, Buffer.from([64]), signature]).toString('base64');
}
//# sourceMappingURL=crypto.js.map