From 104b2783cbf0f94b78f543c94956b98e3611aa4f Mon Sep 17 00:00:00 2001 From: Manuel Iglesias <6154160+manueliglesias@users.noreply.github.com> Date: Wed, 17 Feb 2021 17:33:25 -0800 Subject: [PATCH] fix: AuthenticationHelper - Handle negative BigIntegers (#7618) * fix: Handle negative BigIntegers Using two's complement for negative numbers * Address PR comments * Address comments * Remove unused variable * Update packages/amazon-cognito-identity-js/src/AuthenticationHelper.js Co-authored-by: crockeea * Update packages/amazon-cognito-identity-js/src/AuthenticationHelper.js Co-authored-by: crockeea * Address PR comments * Move special case handling to negative block * Pad SaltToHashDevices * Make SaltToHashDevices to be unambiguously represented as a postive integer Co-authored-by: Sam Martinez Co-authored-by: crockeea --- .../__tests__/AuthenticationHelper-test.js | 554 ++++++++++++++++++ .../{src => __tests__}/BigInteger.test.js | 2 +- .../amazon-cognito-identity-js/package.json | 7 +- .../src/AuthenticationHelper.js | 100 +++- 4 files changed, 644 insertions(+), 19 deletions(-) create mode 100644 packages/amazon-cognito-identity-js/__tests__/AuthenticationHelper-test.js rename packages/amazon-cognito-identity-js/{src => __tests__}/BigInteger.test.js (89%) diff --git a/packages/amazon-cognito-identity-js/__tests__/AuthenticationHelper-test.js b/packages/amazon-cognito-identity-js/__tests__/AuthenticationHelper-test.js new file mode 100644 index 00000000000..39e6a25c6de --- /dev/null +++ b/packages/amazon-cognito-identity-js/__tests__/AuthenticationHelper-test.js @@ -0,0 +1,554 @@ +import AuthenticationHelper from '../src/AuthenticationHelper'; + +import BigInteger from '../src/BigInteger'; + +describe('AuthenticatorHelper', () => { + const instance = new AuthenticationHelper('TestPoolName'); + + /* + Test cases generated in Java with: + + import java.math.BigInteger; + public class Main + { + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + public static void main(String[] args) { + for(int i = -256; i <=256; i++) { + byte arr[] = BigInteger.valueOf(i).toByteArray(); + System.out.println("[" + i +", '" + bytesToHex(arr) + "'],"); + } + } + } + */ + test.each([ + [-256, 'FF00'], + [-255, 'FF01'], + [-254, 'FF02'], + [-253, 'FF03'], + [-252, 'FF04'], + [-251, 'FF05'], + [-250, 'FF06'], + [-249, 'FF07'], + [-248, 'FF08'], + [-247, 'FF09'], + [-246, 'FF0A'], + [-245, 'FF0B'], + [-244, 'FF0C'], + [-243, 'FF0D'], + [-242, 'FF0E'], + [-241, 'FF0F'], + [-240, 'FF10'], + [-239, 'FF11'], + [-238, 'FF12'], + [-237, 'FF13'], + [-236, 'FF14'], + [-235, 'FF15'], + [-234, 'FF16'], + [-233, 'FF17'], + [-232, 'FF18'], + [-231, 'FF19'], + [-230, 'FF1A'], + [-229, 'FF1B'], + [-228, 'FF1C'], + [-227, 'FF1D'], + [-226, 'FF1E'], + [-225, 'FF1F'], + [-224, 'FF20'], + [-223, 'FF21'], + [-222, 'FF22'], + [-221, 'FF23'], + [-220, 'FF24'], + [-219, 'FF25'], + [-218, 'FF26'], + [-217, 'FF27'], + [-216, 'FF28'], + [-215, 'FF29'], + [-214, 'FF2A'], + [-213, 'FF2B'], + [-212, 'FF2C'], + [-211, 'FF2D'], + [-210, 'FF2E'], + [-209, 'FF2F'], + [-208, 'FF30'], + [-207, 'FF31'], + [-206, 'FF32'], + [-205, 'FF33'], + [-204, 'FF34'], + [-203, 'FF35'], + [-202, 'FF36'], + [-201, 'FF37'], + [-200, 'FF38'], + [-199, 'FF39'], + [-198, 'FF3A'], + [-197, 'FF3B'], + [-196, 'FF3C'], + [-195, 'FF3D'], + [-194, 'FF3E'], + [-193, 'FF3F'], + [-192, 'FF40'], + [-191, 'FF41'], + [-190, 'FF42'], + [-189, 'FF43'], + [-188, 'FF44'], + [-187, 'FF45'], + [-186, 'FF46'], + [-185, 'FF47'], + [-184, 'FF48'], + [-183, 'FF49'], + [-182, 'FF4A'], + [-181, 'FF4B'], + [-180, 'FF4C'], + [-179, 'FF4D'], + [-178, 'FF4E'], + [-177, 'FF4F'], + [-176, 'FF50'], + [-175, 'FF51'], + [-174, 'FF52'], + [-173, 'FF53'], + [-172, 'FF54'], + [-171, 'FF55'], + [-170, 'FF56'], + [-169, 'FF57'], + [-168, 'FF58'], + [-167, 'FF59'], + [-166, 'FF5A'], + [-165, 'FF5B'], + [-164, 'FF5C'], + [-163, 'FF5D'], + [-162, 'FF5E'], + [-161, 'FF5F'], + [-160, 'FF60'], + [-159, 'FF61'], + [-158, 'FF62'], + [-157, 'FF63'], + [-156, 'FF64'], + [-155, 'FF65'], + [-154, 'FF66'], + [-153, 'FF67'], + [-152, 'FF68'], + [-151, 'FF69'], + [-150, 'FF6A'], + [-149, 'FF6B'], + [-148, 'FF6C'], + [-147, 'FF6D'], + [-146, 'FF6E'], + [-145, 'FF6F'], + [-144, 'FF70'], + [-143, 'FF71'], + [-142, 'FF72'], + [-141, 'FF73'], + [-140, 'FF74'], + [-139, 'FF75'], + [-138, 'FF76'], + [-137, 'FF77'], + [-136, 'FF78'], + [-135, 'FF79'], + [-134, 'FF7A'], + [-133, 'FF7B'], + [-132, 'FF7C'], + [-131, 'FF7D'], + [-130, 'FF7E'], + [-129, 'FF7F'], + [-128, '80'], + [-127, '81'], + [-126, '82'], + [-125, '83'], + [-124, '84'], + [-123, '85'], + [-122, '86'], + [-121, '87'], + [-120, '88'], + [-119, '89'], + [-118, '8A'], + [-117, '8B'], + [-116, '8C'], + [-115, '8D'], + [-114, '8E'], + [-113, '8F'], + [-112, '90'], + [-111, '91'], + [-110, '92'], + [-109, '93'], + [-108, '94'], + [-107, '95'], + [-106, '96'], + [-105, '97'], + [-104, '98'], + [-103, '99'], + [-102, '9A'], + [-101, '9B'], + [-100, '9C'], + [-99, '9D'], + [-98, '9E'], + [-97, '9F'], + [-96, 'A0'], + [-95, 'A1'], + [-94, 'A2'], + [-93, 'A3'], + [-92, 'A4'], + [-91, 'A5'], + [-90, 'A6'], + [-89, 'A7'], + [-88, 'A8'], + [-87, 'A9'], + [-86, 'AA'], + [-85, 'AB'], + [-84, 'AC'], + [-83, 'AD'], + [-82, 'AE'], + [-81, 'AF'], + [-80, 'B0'], + [-79, 'B1'], + [-78, 'B2'], + [-77, 'B3'], + [-76, 'B4'], + [-75, 'B5'], + [-74, 'B6'], + [-73, 'B7'], + [-72, 'B8'], + [-71, 'B9'], + [-70, 'BA'], + [-69, 'BB'], + [-68, 'BC'], + [-67, 'BD'], + [-66, 'BE'], + [-65, 'BF'], + [-64, 'C0'], + [-63, 'C1'], + [-62, 'C2'], + [-61, 'C3'], + [-60, 'C4'], + [-59, 'C5'], + [-58, 'C6'], + [-57, 'C7'], + [-56, 'C8'], + [-55, 'C9'], + [-54, 'CA'], + [-53, 'CB'], + [-52, 'CC'], + [-51, 'CD'], + [-50, 'CE'], + [-49, 'CF'], + [-48, 'D0'], + [-47, 'D1'], + [-46, 'D2'], + [-45, 'D3'], + [-44, 'D4'], + [-43, 'D5'], + [-42, 'D6'], + [-41, 'D7'], + [-40, 'D8'], + [-39, 'D9'], + [-38, 'DA'], + [-37, 'DB'], + [-36, 'DC'], + [-35, 'DD'], + [-34, 'DE'], + [-33, 'DF'], + [-32, 'E0'], + [-31, 'E1'], + [-30, 'E2'], + [-29, 'E3'], + [-28, 'E4'], + [-27, 'E5'], + [-26, 'E6'], + [-25, 'E7'], + [-24, 'E8'], + [-23, 'E9'], + [-22, 'EA'], + [-21, 'EB'], + [-20, 'EC'], + [-19, 'ED'], + [-18, 'EE'], + [-17, 'EF'], + [-16, 'F0'], + [-15, 'F1'], + [-14, 'F2'], + [-13, 'F3'], + [-12, 'F4'], + [-11, 'F5'], + [-10, 'F6'], + [-9, 'F7'], + [-8, 'F8'], + [-7, 'F9'], + [-6, 'FA'], + [-5, 'FB'], + [-4, 'FC'], + [-3, 'FD'], + [-2, 'FE'], + [-1, 'FF'], + [0, '00'], + [1, '01'], + [2, '02'], + [3, '03'], + [4, '04'], + [5, '05'], + [6, '06'], + [7, '07'], + [8, '08'], + [9, '09'], + [10, '0A'], + [11, '0B'], + [12, '0C'], + [13, '0D'], + [14, '0E'], + [15, '0F'], + [16, '10'], + [17, '11'], + [18, '12'], + [19, '13'], + [20, '14'], + [21, '15'], + [22, '16'], + [23, '17'], + [24, '18'], + [25, '19'], + [26, '1A'], + [27, '1B'], + [28, '1C'], + [29, '1D'], + [30, '1E'], + [31, '1F'], + [32, '20'], + [33, '21'], + [34, '22'], + [35, '23'], + [36, '24'], + [37, '25'], + [38, '26'], + [39, '27'], + [40, '28'], + [41, '29'], + [42, '2A'], + [43, '2B'], + [44, '2C'], + [45, '2D'], + [46, '2E'], + [47, '2F'], + [48, '30'], + [49, '31'], + [50, '32'], + [51, '33'], + [52, '34'], + [53, '35'], + [54, '36'], + [55, '37'], + [56, '38'], + [57, '39'], + [58, '3A'], + [59, '3B'], + [60, '3C'], + [61, '3D'], + [62, '3E'], + [63, '3F'], + [64, '40'], + [65, '41'], + [66, '42'], + [67, '43'], + [68, '44'], + [69, '45'], + [70, '46'], + [71, '47'], + [72, '48'], + [73, '49'], + [74, '4A'], + [75, '4B'], + [76, '4C'], + [77, '4D'], + [78, '4E'], + [79, '4F'], + [80, '50'], + [81, '51'], + [82, '52'], + [83, '53'], + [84, '54'], + [85, '55'], + [86, '56'], + [87, '57'], + [88, '58'], + [89, '59'], + [90, '5A'], + [91, '5B'], + [92, '5C'], + [93, '5D'], + [94, '5E'], + [95, '5F'], + [96, '60'], + [97, '61'], + [98, '62'], + [99, '63'], + [100, '64'], + [101, '65'], + [102, '66'], + [103, '67'], + [104, '68'], + [105, '69'], + [106, '6A'], + [107, '6B'], + [108, '6C'], + [109, '6D'], + [110, '6E'], + [111, '6F'], + [112, '70'], + [113, '71'], + [114, '72'], + [115, '73'], + [116, '74'], + [117, '75'], + [118, '76'], + [119, '77'], + [120, '78'], + [121, '79'], + [122, '7A'], + [123, '7B'], + [124, '7C'], + [125, '7D'], + [126, '7E'], + [127, '7F'], + [128, '0080'], + [129, '0081'], + [130, '0082'], + [131, '0083'], + [132, '0084'], + [133, '0085'], + [134, '0086'], + [135, '0087'], + [136, '0088'], + [137, '0089'], + [138, '008A'], + [139, '008B'], + [140, '008C'], + [141, '008D'], + [142, '008E'], + [143, '008F'], + [144, '0090'], + [145, '0091'], + [146, '0092'], + [147, '0093'], + [148, '0094'], + [149, '0095'], + [150, '0096'], + [151, '0097'], + [152, '0098'], + [153, '0099'], + [154, '009A'], + [155, '009B'], + [156, '009C'], + [157, '009D'], + [158, '009E'], + [159, '009F'], + [160, '00A0'], + [161, '00A1'], + [162, '00A2'], + [163, '00A3'], + [164, '00A4'], + [165, '00A5'], + [166, '00A6'], + [167, '00A7'], + [168, '00A8'], + [169, '00A9'], + [170, '00AA'], + [171, '00AB'], + [172, '00AC'], + [173, '00AD'], + [174, '00AE'], + [175, '00AF'], + [176, '00B0'], + [177, '00B1'], + [178, '00B2'], + [179, '00B3'], + [180, '00B4'], + [181, '00B5'], + [182, '00B6'], + [183, '00B7'], + [184, '00B8'], + [185, '00B9'], + [186, '00BA'], + [187, '00BB'], + [188, '00BC'], + [189, '00BD'], + [190, '00BE'], + [191, '00BF'], + [192, '00C0'], + [193, '00C1'], + [194, '00C2'], + [195, '00C3'], + [196, '00C4'], + [197, '00C5'], + [198, '00C6'], + [199, '00C7'], + [200, '00C8'], + [201, '00C9'], + [202, '00CA'], + [203, '00CB'], + [204, '00CC'], + [205, '00CD'], + [206, '00CE'], + [207, '00CF'], + [208, '00D0'], + [209, '00D1'], + [210, '00D2'], + [211, '00D3'], + [212, '00D4'], + [213, '00D5'], + [214, '00D6'], + [215, '00D7'], + [216, '00D8'], + [217, '00D9'], + [218, '00DA'], + [219, '00DB'], + [220, '00DC'], + [221, '00DD'], + [222, '00DE'], + [223, '00DF'], + [224, '00E0'], + [225, '00E1'], + [226, '00E2'], + [227, '00E3'], + [228, '00E4'], + [229, '00E5'], + [230, '00E6'], + [231, '00E7'], + [232, '00E8'], + [233, '00E9'], + [234, '00EA'], + [235, '00EB'], + [236, '00EC'], + [237, '00ED'], + [238, '00EE'], + [239, '00EF'], + [240, '00F0'], + [241, '00F1'], + [242, '00F2'], + [243, '00F3'], + [244, '00F4'], + [245, '00F5'], + [246, '00F6'], + [247, '00F7'], + [248, '00F8'], + [249, '00F9'], + [250, '00FA'], + [251, '00FB'], + [252, '00FC'], + [253, '00FD'], + [254, '00FE'], + [255, '00FF'], + [256, '0100'], + ])('padHex(bigInteger.fromInt(%p))\t=== %p', (i, expected) => { + const bigInt = new BigInteger(); + bigInt.fromInt(i); + + const x = instance.padHex(bigInt); + + expect(x.toLowerCase()).toBe(expected.toLowerCase()); + }); +}); diff --git a/packages/amazon-cognito-identity-js/src/BigInteger.test.js b/packages/amazon-cognito-identity-js/__tests__/BigInteger.test.js similarity index 89% rename from packages/amazon-cognito-identity-js/src/BigInteger.test.js rename to packages/amazon-cognito-identity-js/__tests__/BigInteger.test.js index cf7db5871be..6876dd841d4 100644 --- a/packages/amazon-cognito-identity-js/src/BigInteger.test.js +++ b/packages/amazon-cognito-identity-js/__tests__/BigInteger.test.js @@ -1,4 +1,4 @@ -import BigInteger from './BigInteger'; +import BigInteger from '../src/BigInteger'; describe('BigInteger', () => { describe('.toString(radix)', () => { diff --git a/packages/amazon-cognito-identity-js/package.json b/packages/amazon-cognito-identity-js/package.json index 940eda6bc48..960aea2c35b 100644 --- a/packages/amazon-cognito-identity-js/package.json +++ b/packages/amazon-cognito-identity-js/package.json @@ -52,7 +52,7 @@ "doc": "jsdoc src -d docs", "lint": "eslint src", "lint2": "eslint enhance-rn.js", - "test": "jest -w 1 --passWithNoTests", + "test": "jest -w 1", "format": "echo \"Not implemented\"" }, "main": "lib/index.js", @@ -102,7 +102,8 @@ "esnext.asynciterable", "es2017.object" ], - "allowJs": true + "allowJs": true, + "esModuleInterop": true } } }, @@ -135,4 +136,4 @@ "lib" ] } -} +} \ No newline at end of file diff --git a/packages/amazon-cognito-identity-js/src/AuthenticationHelper.js b/packages/amazon-cognito-identity-js/src/AuthenticationHelper.js index 1f4dfaed5ed..a2fe9b91264 100644 --- a/packages/amazon-cognito-identity-js/src/AuthenticationHelper.js +++ b/packages/amazon-cognito-identity-js/src/AuthenticationHelper.js @@ -22,12 +22,23 @@ import SHA256 from 'crypto-js/sha256'; import HmacSHA256 from 'crypto-js/hmac-sha256'; import WordArray from './utils/WordArray'; -const randomBytes = function(nBytes) { +/** + * Returns a Buffer with a sequence of random nBytes + * + * @param {number} nBytes + * @returns {Buffer} fixed-length sequence of random bytes + */ +function randomBytes(nBytes) { return Buffer.from(new WordArray().random(nBytes).toString(), 'hex'); }; import BigInteger from './BigInteger'; +/** + * Tests if a hex string has it most significant bit set (case-insensitive regex) + */ +const HEX_MSB_REGEX = /^[89a-f]/i; + const initN = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1' + '29024E088A67CC74020BBEA63B139B22514A08798E3404DD' + @@ -58,12 +69,12 @@ export default class AuthenticationHelper { this.N = new BigInteger(initN, 16); this.g = new BigInteger('2', 16); this.k = new BigInteger( - this.hexHash(`00${this.N.toString(16)}0${this.g.toString(16)}`), + this.hexHash(`${this.padHex(this.N)}${this.padHex(this.g)}`), 16 ); this.smallAValue = this.generateRandomSmallA(); - this.getLargeAValue(() => {}); + this.getLargeAValue(() => { }); this.infoBits = Buffer.from('Caldera Derived Key', 'utf8'); @@ -102,12 +113,14 @@ export default class AuthenticationHelper { * @private */ generateRandomSmallA() { + // This will be interpreted as a postive 128-bit integer const hexRandom = randomBytes(128).toString('hex'); const randomBigInt = new BigInteger(hexRandom, 16); - const smallABigInt = randomBigInt.mod(this.N); - return smallABigInt; + // There is no need to do randomBigInt.mod(this.N - 1) as N (3072-bit) is > 128 bytes (1024-bit) + + return randomBigInt; } /** @@ -153,6 +166,8 @@ export default class AuthenticationHelper { const hashedString = this.hash(combinedString); const hexRandom = randomBytes(16).toString('hex'); + + // The random hex will be unambiguously represented as a postive integer this.SaltToHashDevices = this.padHex(new BigInteger(hexRandom, 16)); this.g.modPow( @@ -293,7 +308,7 @@ export default class AuthenticationHelper { const hkdf = this.computehkdf( Buffer.from(this.padHex(sValue), 'hex'), - Buffer.from(this.padHex(this.UValue.toString(16)), 'hex') + Buffer.from(this.padHex(this.UValue), 'hex') ); callback(null, hkdf); @@ -337,17 +352,72 @@ export default class AuthenticationHelper { } /** - * Converts a BigInteger (or hex string) to hex format padded with zeroes for hashing - * @param {BigInteger|String} bigInt Number or string to pad. - * @returns {String} Padded hex string. + * Returns an unambiguous, even-length hex string of the two's complement encoding of an integer. + * + * It is compatible with the hex encoding of Java's BigInteger's toByteArray(), wich returns a + * byte array containing the two's-complement representation of a BigInteger. The array contains + * the minimum number of bytes required to represent the BigInteger, including at least one sign bit. + * + * Examples showing how ambiguity is avoided by left padding with: + * "00" (for positive values where the most-significant-bit is set) + * "FF" (for negative values where the most-significant-bit is set) + * + * padHex(bigInteger.fromInt(-236)) === "FF14" + * padHex(bigInteger.fromInt(20)) === "14" + * + * padHex(bigInteger.fromInt(-200)) === "FF38" + * padHex(bigInteger.fromInt(56)) === "38" + * + * padHex(bigInteger.fromInt(-20)) === "EC" + * padHex(bigInteger.fromInt(236)) === "00EC" + * + * padHex(bigInteger.fromInt(-56)) === "C8" + * padHex(bigInteger.fromInt(200)) === "00C8" + * + * @param {BigInteger} bigInt Number to encode. + * @returns {String} even-length hex string of the two's complement encoding. */ padHex(bigInt) { - let hashStr = bigInt.toString(16); - if (hashStr.length % 2 === 1) { - hashStr = `0${hashStr}`; - } else if ('89ABCDEFabcdef'.indexOf(hashStr[0]) !== -1) { - hashStr = `00${hashStr}`; + if (!(bigInt instanceof BigInteger)) { + throw new Error('Not a BigInteger'); + } + + const isNegative = bigInt.compareTo(BigInteger.ZERO) < 0; + + /* Get a hex string for abs(bigInt) */ + let hexStr = bigInt.abs().toString(16); + + /* Pad hex to even length if needed */ + hexStr = hexStr.length % 2 !== 0 ? `0${hexStr}` : hexStr; + + /* Prepend "00" if the most significant bit is set */ + hexStr = HEX_MSB_REGEX.test(hexStr) ? `00${hexStr}` : hexStr; + + if (isNegative) { + /* Flip the bits of the representation */ + const invertedNibbles = hexStr.split('').map(x => { + const invertedNibble = ~parseInt(x, 16) & 0xf; + return '0123456789ABCDEF'.charAt(invertedNibble); + }).join(''); + + /* After flipping the bits, add one to get the 2's complement representation */ + const flippedBitsBI = new BigInteger(invertedNibbles, 16).add(BigInteger.ONE); + + hexStr = flippedBitsBI.toString(16); + + /* + For hex strings starting with 'FF8', 'FF' can be dropped, e.g. 0xFFFF80=0xFF80=0x80=-128 + + Any sequence of '1' bits on the left can always be substituted with a single '1' bit + without changing the represented value. + + This only happens in the case when the input is 80...00 + */ + if (hexStr.toUpperCase().startsWith('FF8')) { + hexStr = hexStr.substring(2); + } } - return hashStr; + + return hexStr; } }