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);
+    });
+  });
+});