Bug 56076 - [PATCH] Add document protection with password support to XWPF
Summary: [PATCH] Add document protection with password support to XWPF
Status: RESOLVED FIXED
Alias: None
Product: POI
Classification: Unclassified
Component: XWPF (show other bugs)
Version: unspecified
Hardware: PC All
: P2 normal (vote)
Target Milestone: ---
Assignee: POI Developers List
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2014-01-28 13:41 UTC by Stefan Kopf
Modified: 2014-02-21 23:25 UTC (History)
0 users



Attachments
Patch file created by patch.xml to add document protection with password to XWPF (1.20 KB, patch)
2014-01-28 13:43 UTC, Stefan Kopf
Details | Diff
Password protected OOXML Word Document (Password "Example") (10.77 KB, application/vnd.openxmlformats-officedocument.wordprocessingml.document)
2014-02-17 10:40 UTC, Stefan Kopf
Details
Working example (7.69 KB, text/plain)
2014-02-19 00:22 UTC, Andreas Beeker
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Stefan Kopf 2014-01-28 13:41:42 UTC
The current implementation allows only to activate document protection without password.
This patch allows to set a password when activating document protection. This password needs to be provided as tuple (hashAlgortihmId, salt, hash). There is no code yet that creates the hash value depending on a provided plain text password and salt.

I tried to create a hash value as described in "Office Open XML, Part 4: Markup Language Reference, December 2006", but failed. The calculated hash value did not match the value calculated by MS Office 2010 :-(

I hope taht I can provide this later.
Comment 1 Stefan Kopf 2014-01-28 13:43:35 UTC
Created attachment 31257 [details]
Patch file created by patch.xml to add document protection with password to XWPF
Comment 2 Andreas Beeker 2014-02-09 01:36:32 UTC
Please attach an example word document (with an example password ;) ).

If we are lucky, it's the same algo as in http://thread.gmane.org/gmane.comp.jakarta.poi.devel/25831
Comment 3 Stefan Kopf 2014-02-17 10:40:07 UTC
Created attachment 31316 [details]
Password protected OOXML Word Document (Password "Example")

Here is a Word document protected with the password "Example".

The protection data written into this document by Word is:
<w:documentProtection
  w:edit="trackedChanges"
  w:enforcement="1"
  w:cryptProviderType="rsaFull"
  w:cryptAlgorithmClass="hash"
  w:cryptAlgorithmType="typeAny"
  w:cryptAlgorithmSid="4"
  w:cryptSpinCount="100000"
  w:hash="MUHbcmpC9AnlLsd9v3lW0j30y6E="
  w:salt="2Z+i7o/0EZyUNakVeWzU/w=="/>

The algorithm sid 4 references the SHA-1 algorithm.
Comment 4 Stefan Kopf 2014-02-17 10:53:49 UTC
Andreas,

The algorithm is different from the one you referenced here :-(.

It is defined in "Office Open XML Part 4 - Markup Language Reference" chapter 2.15.1.28 on page 1158.

At first, the password is hashed with the legacy algortithm used in .doc files. I have provided an implementation of this algorithm in bug 56077. I can tell that this algorithm is correct by comparing my results to the example given in the spec.

Next, the result of this is hashed by the hash algorithm (SHA-1 in the example above) with the salt prependen. The output of this is used as input  for the next round of hashing (without a round key prepended). This is repeated for spin-count rounds.

But the result of this does not match the hash calculated by MS Office.

Here is the implementation I used to test it:

package com.alfresco.sparta.research.office.changetracking;

import java.security.MessageDigest;

import org.apache.commons.codec.binary.Base64;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianConsts;

public class TestSha1
{

    public static void main(String[] args) throws Exception
    {

        String password = "Example";
        byte[] salt = Base64.decodeBase64("2Z+i7o/0EZyUNakVeWzU/w==");
        byte[] expectedHash = Base64.decodeBase64("MUHbcmpC9AnlLsd9v3lW0j30y6E=");
        int rounds = 100000;

        System.out.println("Salt: "  +  hexBytes(salt));
        System.out.println("ExpectedHash: "  +  hexBytes(expectedHash));

        
        int wordHash = wordPasswordHash(password);
        System.out.print("WordHash: 0x");
        System.out.format("%H",wordHash);
        System.out.println();
        
        byte[] reversedWordHash = new byte[4];
        reversedWordHash[0] = (byte) (wordHash & 0x000000FF);
        reversedWordHash[1] = (byte) ( (wordHash & 0x0000FF00) >> 8);
        reversedWordHash[2] = (byte) ( (wordHash & 0x00FF0000) >> 16);
        reversedWordHash[3] = (byte) ( (wordHash & 0xFF000000) >> 24);
        System.out.println("ReversedWordHash: "  +  hexBytes(reversedWordHash));
        
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        
        md.update(salt);
        byte[] hash = md.digest(reversedWordHash);
        
        byte[] iterator = new byte[LittleEndianConsts.INT_SIZE];
        for(int i = 0; i < rounds; i++)
        {
            LittleEndian.putInt(iterator, 0, i);
            md.reset();
            //md.update(iterator);
            md.update(hash);
            md.digest(hash, 0, hash.length);
        }
        System.out.println("GeneratedHash: "  +  hexBytes(hash));
    }
    
    static String hexBytes(byte[] b)
    {
        StringBuilder result = new StringBuilder("0x");
        for(int i = 0; i < b.length;  i++)
        {            
            String s = String.format("%02X", b[i]);
            result.append(s);
        }
        return result.toString();
    }

    
    private static char[] initialValue = { 0xE1F0, 0x1D0F, 0xCC9C, 0x84C0, 0x110C, 0x0E10, 0xF1CE, 0x313E,
            0x1872, 0xE139, 0xD40F, 0x84F9, 0x280C, 0xA96A, 0x4EC3 };
    
    private static char[][] encryptionMatrix =
      { { 0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09 },
        { 0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF },
        { 0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0 },
        { 0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40 },
        { 0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5 },
        { 0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A },
        { 0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9 },
        { 0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0 },
        { 0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC },
        { 0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10 },
        { 0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168 },
        { 0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C },
        { 0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD },
        { 0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC },
        { 0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4 } };
    
    public static int wordPasswordHash(String password)
    {
        if( (password == null) || (password.length() == 0) )
        {
            return 0x00000000;
        }
        // prepare password bytes from unicode string
        if(password.length() > 15)
        {
            password = password.substring(0, 15);
        }
        byte[] passwordBytes = new byte[password.length()];
        for(int i = 0; i < password.length(); i++)
        {
            char c = password.charAt(i);
            byte lowByte = (byte)(c & 0x00FF);
            if(lowByte != 0)
            {
                passwordBytes[i] = lowByte;
            }
            else
            {
                byte highByte = (byte)((c & 0xFF00) >> 8);
                passwordBytes[i] = highByte;
            }
        }
        // Compute the high-order word
        char highOrderWord = initialValue[passwordBytes.length-1];
        for(int pos = 0; pos < passwordBytes.length; pos++)
        {
            int matrixRow = 14 - (passwordBytes.length - 1 - pos);
            char[] encryptionVector = encryptionMatrix[matrixRow];
            for(int bitPos = 0; bitPos  < 7; bitPos++)
            {
                if((passwordBytes[pos] & (1 << bitPos)) != 0)
                {
                    highOrderWord = (char)(highOrderWord ^ encryptionVector[bitPos]);
                }
            }
        }
        // compute low-order word
        char lowOrderWord = 0;
        for(int pos = passwordBytes.length-1; pos >= 0; pos--)
        {
            lowOrderWord = (char) ((((lowOrderWord >> 14) & 0x0001) | ((lowOrderWord << 1) & 0x7FFF)) ^ passwordBytes[pos]);
        }
        lowOrderWord = (char) ((((lowOrderWord >> 14) & 0x0001) | ((lowOrderWord << 1) & 0x7FFF)) ^ passwordBytes.length ^ 0xCE4B);
        return (highOrderWord << 16) | lowOrderWord;
    }

}
Comment 5 Andreas Beeker 2014-02-19 00:22:52 UTC
Created attachment 31327 [details]
Working example

Thank you for the example file.
After googling for your documentation/code, I found probably the original sources @ [1]

Running the C# version side-by-side the Java version, I managed to get the example working.
Before I implement it into POI, I'd like to find out what's the meaning of the other cryptAlgorithmSid [2] ... but that won't be tonight anymore ;)


[1] http://blogs.msdn.com/b/vsod/archive/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0.aspx
[2] http://msdn.microsoft.com/en-us/library/ff535662(v=office.12).aspx
Comment 6 Andreas Beeker 2014-02-21 23:25:33 UTC
Thanks for the patch.
Applied with SVN ver r1570750 with quite some changes.