From c973d66334551a6f47d57a8b30f7f0a3b7db2451 Mon Sep 17 00:00:00 2001 From: ArockiaRajamanickam Date: Sat, 1 Aug 2026 17:26:24 +0530 Subject: [PATCH 1/2] Fix RC2 effective-key-length mask per RFC 2268 RFC 2268 section 2 defines the mask as T8 = (T1+7)/8 TM = 255 MOD 2^(8 + T1 - 8*T8) and states that TM has its 8 - (8*T8 - T1) least significant bits set. The number of bits shifted out is therefore 8*T8 - T1, which equals (-T1) MOD 8, but the code shifted out T1 MOD 8. The two agree only when T1 is a multiple of 8, which is why the 64- and 128-bit paths and the existing tests never caught it. For T1 = 129 the mask should be 0x01 and was 0x7f. That byte seeds the backward pass of the expansion, so a single wrong mask corrupts the whole 128-byte expanded key and the resulting ciphertext. Two of the eight RFC 2268 section 5 vectors failed before this change and pass after it. Added all the vectors that exercise the boundary. Fixes #1120 --- CHANGELOG.md | 8 ++++++++ lib/rc2.js | 5 ++++- tests/unit/rc2.js | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 934bf31c1..7eedcba9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ Forge ChangeLog =============== +## 1.4.1 - 2026-xx-xx + +### Fixed +- [rc2] Compute the effective-key-length mask as RFC 2268 defines it. The mask + was shifting out `T1 MOD 8` bits instead of `8*T8 - T1`, so key expansion was + wrong whenever the effective key size was not a multiple of 8 bits. Two of + the eight RFC 2268 section 5 test vectors failed; both now pass. + ## 1.4.0 - 2026-03-24 ### Security diff --git a/lib/rc2.js b/lib/rc2.js index e33f78a7f..a890b1bc9 100644 --- a/lib/rc2.js +++ b/lib/rc2.js @@ -81,7 +81,10 @@ forge.rc2.expandKey = function(key, effKeyBits) { var T = key.length(); var T1 = effKeyBits; var T8 = Math.ceil(T1 / 8); - var TM = 0xff >> (T1 & 0x07); + /* RFC 2268: TM = 255 MOD 2^(8 + T1 - 8*T8), ie. TM has its + 8 - (8*T8 - T1) least significant bits set. The number of bits shifted + out is therefore 8*T8 - T1, which is (-T1) MOD 8, not T1 MOD 8. */ + var TM = 0xff >> (-T1 & 0x07); var i; for(i = T; i < 128; i++) { diff --git a/tests/unit/rc2.js b/tests/unit/rc2.js index c6ad393df..be64ade3a 100644 --- a/tests/unit/rc2.js +++ b/tests/unit/rc2.js @@ -84,5 +84,41 @@ var UTIL = require('../../lib/util'); cipher.finish(); ASSERT.equal(cipher.output, 'revolution'); }); + + // RFC 2268 section 5 test vectors. Only the first output block is + // compared, since forge appends a PKCS#7 padding block. + function rc2Vector(keyHex, effKeyBits, plainHex, expectedHex) { + var cipher = RC2.createEncryptionCipher( + UTIL.hexToBytes(keyHex), effKeyBits); + cipher.start(null); + cipher.update(new UTIL.createBuffer(UTIL.hexToBytes(plainHex))); + cipher.finish(); + ASSERT.equal(cipher.output.toHex().substr(0, 16), expectedHex); + } + + it('should match RFC 2268 vector w/8 byte key, 63 effective bits', function() { + rc2Vector('0000000000000000', 63, '0000000000000000', + 'ebb773f993278eff'); + }); + + it('should match RFC 2268 vector w/8 byte key, 64 effective bits', function() { + rc2Vector('ffffffffffffffff', 64, 'ffffffffffffffff', + '278b27e42e2f0d49'); + }); + + it('should match RFC 2268 vector w/1 byte key, 64 effective bits', function() { + rc2Vector('88', 64, '0000000000000000', '61a8a244adacccf0'); + }); + + it('should match RFC 2268 vector w/16 byte key, 128 effective bits', function() { + rc2Vector('88bca90e90875a7f0f79c384627bafb2', 128, + '0000000000000000', '2269552ab0f85ca6'); + }); + + it('should match RFC 2268 vector w/33 byte key, 129 effective bits', function() { + rc2Vector( + '88bca90e90875a7f0f79c384627bafb216f80a6f85920584c42fceb0be255daf1e', + 129, '0000000000000000', '5b78d3a43dfff1f1'); + }); }); })(); From 19fc6cd730b416693b2073c0e89757cef8a7bef0 Mon Sep 17 00:00:00 2001 From: ArockiaRajamanickam Date: Sat, 1 Aug 2026 17:26:34 +0530 Subject: [PATCH 2/2] Drop a stray describe.only that hides the rest of the suite tests/unit/jsbn.js used describe.only, so mocha ran only that file and npm test reported 5 passing. With it removed the suite runs 828 passing and 4 pending, with no failures, so nothing was being hidden other than the tests themselves. This is separable from the RC2 fix; it is here because the new RC2 tests would otherwise never run. --- tests/unit/jsbn.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/jsbn.js b/tests/unit/jsbn.js index 352939eae..8e274e1d0 100644 --- a/tests/unit/jsbn.js +++ b/tests/unit/jsbn.js @@ -1,7 +1,7 @@ var ASSERT = require('assert'); var JSBN = require('../../lib/jsbn'); -describe.only('jsbn', function() { +describe('jsbn', function() { describe('GHSA-5m6q-g25r-mvwx', function() { // regression tests for GHSA-5m6q-g25r-mvwx // test BigInteger.modInverse does not infinite loop with 0 inputs.