From 910ba0b91fa13e543916386ff72b33413640d502 Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Sat, 12 May 2012 21:34:32 -0500 Subject: [PATCH] Fixes user and owner passwords logic --- src/crypto.js | 84 ++++++++++++++++++++++++++++++++-------- test/unit/crypto_spec.js | 63 ++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 17 deletions(-) diff --git a/src/crypto.js b/src/crypto.js index 038c0e332..dcd820554 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -419,13 +419,14 @@ var CipherTransform = (function CipherTransformClosure() { })(); var CipherTransformFactory = (function CipherTransformFactoryClosure() { + var defaultPasswordBytes = new Uint8Array([ + 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, + 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, + 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, + 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A]); + function prepareKeyData(fileId, password, ownerPassword, userPassword, flags, revision, keyLength, encryptMetadata) { - var defaultPasswordBytes = new Uint8Array([ - 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, - 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, - 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, - 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A]); var hashData = new Uint8Array(100), i = 0, j, n; if (password) { n = Math.min(32, password.length); @@ -462,9 +463,8 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { var cipher, checkData; if (revision >= 3) { - // padded password in hashData, we can use this array for user - // password check - i = 32; + for (i = 0; i < 32; ++i) + hashData[i] = defaultPasswordBytes[i]; for (j = 0, n = fileId.length; j < n; ++j) hashData[i++] = fileId[j]; cipher = new ARCFourCipher(encryptionKey); @@ -477,16 +477,53 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { cipher = new ARCFourCipher(derivedKey); checkData = cipher.encryptBlock(checkData); } + for (j = 0, n = checkData.length; j < n; ++j) { + if (userPassword[j] != checkData[j]) + return null; + } } else { cipher = new ARCFourCipher(encryptionKey); - checkData = cipher.encryptBlock(hashData.subarray(0, 32)); - } - for (j = 0, n = checkData.length; j < n; ++j) { - if (userPassword[j] != checkData[j]) - error('incorrect password'); + checkData = cipher.encryptBlock(defaultPasswordBytes); + for (j = 0, n = checkData.length; j < n; ++j) { + if (userPassword[j] != checkData[j]) + return null; + } } return encryptionKey; } + function decodeUserPassword(password, ownerPassword, revision, keyLength) { + var hashData = new Uint8Array(32), i = 0, j, n; + n = Math.min(32, password.length); + for (; i < n; ++i) + hashData[i] = password[i]; + j = 0; + while (i < 32) { + hashData[i++] = defaultPasswordBytes[j++]; + } + var hash = calculateMD5(hashData, 0, i); + var keyLengthInBytes = keyLength >> 3; + if (revision >= 3) { + for (j = 0; j < 50; ++j) { + hash = calculateMD5(hash, 0, hash.length); + } + } + + var cipher, userPassword; + if (revision >= 3) { + userPassword = ownerPassword; + var derivedKey = new Uint8Array(keyLengthInBytes), k; + for (j = 19; j >= 0; j--) { + for (k = 0; k < keyLengthInBytes; ++k) + derivedKey[k] = hash[k] ^ j; + cipher = new ARCFourCipher(derivedKey); + userPassword = cipher.encryptBlock(userPassword); + } + } else { + cipher = new ARCFourCipher(hash.subarray(0, keyLengthInBytes)); + userPassword = cipher.encryptBlock(ownerPassword); + } + return userPassword; + } var identityName = new Name('Identity'); @@ -516,10 +553,23 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { if (password) passwordBytes = stringToBytes(password); - this.encryptionKey = prepareKeyData(fileIdBytes, passwordBytes, - ownerPassword, userPassword, - flags, revision, - keyLength, encryptMetadata); + var encryptionKey = prepareKeyData(fileIdBytes, passwordBytes, + ownerPassword, userPassword, flags, + revision, keyLength, encryptMetadata); + if (!encryptionKey && password) { + // Attempting use the password as an owner password + var decodedPassword = decodeUserPassword(passwordBytes, ownerPassword, + revision, keyLength); + encryptionKey = prepareKeyData(fileIdBytes, decodedPassword, + ownerPassword, userPassword, flags, + revision, keyLength, encryptMetadata); + } + + if (!encryptionKey) + error('incorrect password or encryption data'); + + this.encryptionKey = encryptionKey; + if (algorithm == 4) { this.cf = dict.get('CF'); this.stmf = dict.get('StmF') || identityName; diff --git a/test/unit/crypto_spec.js b/test/unit/crypto_spec.js index 0b82b5ccb..28dc4c872 100644 --- a/test/unit/crypto_spec.js +++ b/test/unit/crypto_spec.js @@ -185,3 +185,66 @@ describe('crypto', function() { }); }); +describe('CipherTransformFactory', function() { + function DictMock(map) { + this.map = map; + } + DictMock.prototype = { + get: function(key) { + return this.map[key]; + } + }; + + var map1 = { + Filter: new Name('Standard'), + V: 2, + Length: 128, + O: unescape('%80%C3%04%96%91o%20sl%3A%E6%1B%13T%91%F2%0DV%12%E3%FF%5E%BB%' + + 'E9VO%D8k%9A%CA%7C%5D'), + U: unescape('j%0C%8D%3EY%19%00%BCjd%7D%91%BD%AA%00%18%00%00%00%00%00%00%0' + + '0%00%00%00%00%00%00%00%00%00'), + P: -1028, + R: 3 + }; + var fileID1 = unescape('%F6%C6%AF%17%F3rR%8DRM%9A%80%D1%EF%DF%18'); + + var map2 = { + Filter: new Name('Standard'), + V: 4, + Length: 128, + O: unescape('sF%14v.y5%27%DB%97%0A5%22%B3%E1%D4%AD%BD%9B%3C%B4%A5%89u%15%' + + 'B2Y%F1h%D9%E9%F4'), + U: unescape('%93%04%89%A9%BF%8AE%A6%88%A2%DB%C2%A0%A8gn%00%00%00%00%00%00' + + '%00%00%00%00%00%00%00%00%00%00'), + P: -1084, + R: 4 + }; + var fileID2 = unescape('%3CL_%3AD%96%AF@%9A%9D%B3%3Cx%1Cv%AC'); + + describe('#ctor', function() { + it('should accept user password', function() { + var factory = new CipherTransformFactory(new DictMock(map1), fileID1, + '123456'); + }); + + it('should accept owner password', function() { + var factory = new CipherTransformFactory(new DictMock(map1), fileID1, + '654321'); + }); + + it('should not accept wrong password', function() { + var thrown = false; + try { + var factory = new CipherTransformFactory(new DictMock(map1), fileID1, + 'wrong'); + } catch (e) { + thrown = true; + } + expect(thrown).toEqual(true); + }); + + it('should accept no password', function() { + var factory = new CipherTransformFactory(new DictMock(map2), fileID2); + }); + }); +});