From d0a3cd398f4398929ff4d3a90023a35dbdc6394f Mon Sep 17 00:00:00 2001 From: Eliot Jones Date: Sun, 9 Jun 2019 13:27:03 +0100 Subject: [PATCH] start adding support for revision 5 aes-256 encrypted documents #34 --- .../Encryption/EncryptionDictionary.cs | 23 ++- .../Encryption/EncryptionDictionaryFactory.cs | 16 +- .../Encryption/EncryptionHandler.cs | 164 ++++++++++++++++-- 3 files changed, 187 insertions(+), 16 deletions(-) diff --git a/src/UglyToad.PdfPig/Encryption/EncryptionDictionary.cs b/src/UglyToad.PdfPig/Encryption/EncryptionDictionary.cs index 37ea691d..19d56f46 100644 --- a/src/UglyToad.PdfPig/Encryption/EncryptionDictionary.cs +++ b/src/UglyToad.PdfPig/Encryption/EncryptionDictionary.cs @@ -4,6 +4,7 @@ using Exceptions; using Tokens; using Util; + using Util.JetBrains.Annotations; internal class EncryptionDictionary { @@ -13,7 +14,7 @@ public int? KeyLength { get; } - public int StandardSecurityHandlerRevision { get; } + public int Revision { get; } public string OwnerPasswordCheck { get; } @@ -23,6 +24,18 @@ public byte[] UserBytes { get; } + /// + /// Required if is 5 or above. A 32-byte string, based on the owner and user passwords that is used in computing the encryption key. + /// + [CanBeNull] + public byte[] OwnerEncryptionBytes { get; } + + /// + /// Required if is 5 or above. A 32-byte string, based on the user password that is used in computing the encryption key. + /// + [CanBeNull] + public byte[] UserEncryptionBytes { get; } + public UserAccessPermissions UserAccessPermissions { get; } public bool IsStandardFilter => string.Equals(Filter, "Standard", StringComparison.OrdinalIgnoreCase); @@ -33,9 +46,11 @@ public EncryptionDictionary(string filter, EncryptionAlgorithmCode encryptionAlgorithmCode, int? keyLength, - int standardSecurityHandlerRevision, + int revision, string ownerPasswordCheck, string userPasswordCheck, + byte[] ownerEncryptionBytes, + byte[] userEncryptionBytes, UserAccessPermissions userAccessPermissions, DictionaryToken dictionary, bool encryptMetadata) @@ -43,9 +58,11 @@ Filter = filter; EncryptionAlgorithmCode = encryptionAlgorithmCode; KeyLength = keyLength; - StandardSecurityHandlerRevision = standardSecurityHandlerRevision; + Revision = revision; OwnerPasswordCheck = ownerPasswordCheck; UserPasswordCheck = userPasswordCheck; + OwnerEncryptionBytes = ownerEncryptionBytes; + UserEncryptionBytes = userEncryptionBytes; UserAccessPermissions = userAccessPermissions; Dictionary = dictionary; EncryptMetadata = encryptMetadata; diff --git a/src/UglyToad.PdfPig/Encryption/EncryptionDictionaryFactory.cs b/src/UglyToad.PdfPig/Encryption/EncryptionDictionaryFactory.cs index 0065eb9b..3f64f50c 100644 --- a/src/UglyToad.PdfPig/Encryption/EncryptionDictionaryFactory.cs +++ b/src/UglyToad.PdfPig/Encryption/EncryptionDictionaryFactory.cs @@ -46,9 +46,23 @@ access = (UserAccessPermissions) accessToken.Int; } + byte[] userEncryptionBytes = null, ownerEncryptionBytes = null; + if (revision >= 5) + { + var oe = encryptionDictionary.Get(NameToken.Oe, tokenScanner); + var ue = encryptionDictionary.Get(NameToken.Ue, tokenScanner); + + ownerEncryptionBytes = OtherEncodings.StringAsLatin1Bytes(oe.Data); + userEncryptionBytes = OtherEncodings.StringAsLatin1Bytes(ue.Data); + } + encryptionDictionary.TryGetOptionalTokenDirect(NameToken.EncryptMetaData, tokenScanner, out BooleanToken encryptMetadata); - return new EncryptionDictionary(filter.Data, code, length, revision, ownerString, userString, access, encryptionDictionary, + return new EncryptionDictionary(filter.Data, code, length, revision, ownerString, userString, + ownerEncryptionBytes, + userEncryptionBytes, + access, + encryptionDictionary, encryptMetadata?.Data ?? true); } } diff --git a/src/UglyToad.PdfPig/Encryption/EncryptionHandler.cs b/src/UglyToad.PdfPig/Encryption/EncryptionHandler.cs index 93019bad..7cef4a02 100644 --- a/src/UglyToad.PdfPig/Encryption/EncryptionHandler.cs +++ b/src/UglyToad.PdfPig/Encryption/EncryptionHandler.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; @@ -68,13 +69,10 @@ var charset = OtherEncodings.Iso88591; - if (encryptionDictionary.StandardSecurityHandlerRevision == 5 || encryptionDictionary.StandardSecurityHandlerRevision == 6) + if (encryptionDictionary.Revision == 5 || encryptionDictionary.Revision == 6) { // ReSharper disable once RedundantAssignment charset = Encoding.UTF8; - - throw new PdfDocumentEncryptedException($"Revision of {encryptionDictionary.StandardSecurityHandlerRevision} not supported, please raise an issue.", - encryptionDictionary); } var passwordBytes = charset.GetBytes(password); @@ -85,13 +83,15 @@ ? 5 : encryptionDictionary.KeyLength.GetValueOrDefault() / 8; + var isUserPassword = false; if (IsUserPassword(passwordBytes, encryptionDictionary, length, documentIdBytes)) { decryptionPasswordBytes = passwordBytes; + isUserPassword = true; } else if (IsOwnerPassword(passwordBytes, encryptionDictionary, length, documentIdBytes, out var userPassBytes)) { - if (encryptionDictionary.StandardSecurityHandlerRevision == 5 || encryptionDictionary.StandardSecurityHandlerRevision == 6) + if (encryptionDictionary.Revision == 5 || encryptionDictionary.Revision == 6) { decryptionPasswordBytes = passwordBytes; } @@ -105,19 +105,25 @@ throw new PdfDocumentEncryptedException("The document was encrypted and the provided password was neither the user or owner password.", encryptionDictionary); } - encryptionKey = CalculateKeyRevisions2To4(decryptionPasswordBytes, encryptionDictionary, + encryptionKey = CalculateEncryptionKey(decryptionPasswordBytes, encryptionDictionary, length, - documentIdBytes); + documentIdBytes, + isUserPassword); } private static bool IsUserPassword(byte[] passwordBytes, EncryptionDictionary encryptionDictionary, int length, byte[] documentIdBytes) { + if (encryptionDictionary.Revision == 5 || encryptionDictionary.Revision == 6) + { + return IsUserPasswordRevision5And6(passwordBytes, encryptionDictionary); + } + // 1. Create an encryption key based on the user password string. var calculatedEncryptionKey = CalculateKeyRevisions2To4(passwordBytes, encryptionDictionary, length, documentIdBytes); byte[] output; - if (encryptionDictionary.StandardSecurityHandlerRevision >= 3) + if (encryptionDictionary.Revision >= 3) { using (var md5 = MD5.Create()) { @@ -154,7 +160,7 @@ output = RC4.Encrypt(calculatedEncryptionKey, PaddingBytes); } - if (encryptionDictionary.StandardSecurityHandlerRevision >= 3) + if (encryptionDictionary.Revision >= 3) { return encryptionDictionary.UserBytes.Take(16).SequenceEqual(output.Take(16)); } @@ -162,11 +168,38 @@ return encryptionDictionary.UserBytes.SequenceEqual(output); } + private static bool IsUserPasswordRevision5And6(byte[] passwordBytes, EncryptionDictionary encryptionDictionary) + { + // Test the password against the user key by computing the SHA-256 hash of the UTF-8 password concatenated with the 8 bytes of User Validation Salt. + // If the 32-byte result matches the first 32 bytes of the U string, this is the user password + var truncatedPassword = TruncatePasswordTo127Bytes(passwordBytes); + + // The 48-byte string consisting of the 32-byte hash followed by the User Validation Salt followed by the User Key Salt is stored as the U key + var userPasswordHash = new byte[32]; + var userValidationSalt = new byte[8]; + Array.Copy(encryptionDictionary.UserBytes, userPasswordHash, 32); + Array.Copy(encryptionDictionary.UserBytes, 32, userValidationSalt, 0, 8); + + if (encryptionDictionary.Revision == 6) + { + throw new PdfDocumentEncryptedException($"Support for revision 6 encryption not implemented: {encryptionDictionary}."); + } + + var result = ComputeSha256Hash(truncatedPassword, userValidationSalt); + + return result.SequenceEqual(userPasswordHash); + } + private static bool IsOwnerPassword(byte[] passwordBytes, EncryptionDictionary encryptionDictionary, int length, byte[] documentIdBytes, out byte[] userPassword) { userPassword = null; + if (encryptionDictionary.Revision == 5 || encryptionDictionary.Revision == 6) + { + return IsOwnerPasswordRevision5And6(passwordBytes, encryptionDictionary); + } + // 1. Pad or truncate the owner password string, if there is no owner password use the user password instead. var paddedPassword = GetPaddedPassword(passwordBytes); @@ -176,7 +209,7 @@ var hash = md5.ComputeHash(paddedPassword); // 3. (Revision 3 or greater) Do the following 50 times: - if (encryptionDictionary.StandardSecurityHandlerRevision >= 3) + if (encryptionDictionary.Revision >= 3) { // Take the output from the previous MD5 hash and pass it as input into a new MD5 hash. for (var i = 0; i < 50; i++) @@ -189,7 +222,7 @@ // where n is always 5 for revision 2 but for revision 3 or greater depends on the value of the encryption dictionary's Length entry. var key = hash.Take(length).ToArray(); - if (encryptionDictionary.StandardSecurityHandlerRevision == 2) + if (encryptionDictionary.Revision == 2) { // 5. (Revision 2 only) Decrypt the value of the encryption dictionary's owner entry, // using an RC4 encryption function with the encryption key computed in step 1 - 4. @@ -226,6 +259,29 @@ } } + private static bool IsOwnerPasswordRevision5And6(byte[] passwordBytes, EncryptionDictionary encryptionDictionary) + { + // Test the password against the user key by computing the SHA-256 hash of the UTF-8 password concatenated with the 8 bytes of Owner Validation Salt and the 48 byte U string. + // If the 32 byte result matches the first 32 bytes of the O string, this is the user password. + + var truncatedPassword = TruncatePasswordTo127Bytes(passwordBytes); + + // The 48-byte string consisting of the 32-byte hash followed by the Owner Validation Salt followed by the Owner Key Salt is stored as the O key. + var ownerHash = new byte[32]; + var validationSalt = new byte[8]; + Array.Copy(encryptionDictionary.OwnerBytes, ownerHash, ownerHash.Length); + Array.Copy(encryptionDictionary.OwnerBytes, ownerHash.Length, validationSalt, 0, validationSalt.Length); + + if (encryptionDictionary.Revision == 6) + { + throw new PdfDocumentEncryptedException($"Support for revision 6 encryption not implemented: {encryptionDictionary}."); + } + + var result = ComputeSha256Hash(truncatedPassword, validationSalt); + + return result.SequenceEqual(ownerHash); + } + public IToken Decrypt(IndirectReference reference, IToken token) { if (token == null) @@ -408,12 +464,27 @@ } } + private static byte[] CalculateEncryptionKey(byte[] password, EncryptionDictionary encryptionDictionary, int length, byte[] documentId, bool isUserPassword) + { + if (encryptionDictionary.Revision >= 2 && encryptionDictionary.Revision <= 4) + { + return CalculateKeyRevisions2To4(password, encryptionDictionary, length, documentId); + } + + if (encryptionDictionary.Revision <= 6) + { + return CalculateKeyRevisions5And6(password, encryptionDictionary, isUserPassword); + } + + throw new PdfDocumentEncryptedException($"PDF encrypted with unrecognized revision: {encryptionDictionary}."); + } + private static byte[] CalculateKeyRevisions2To4(byte[] password, EncryptionDictionary encryptionDictionary, int length, byte[] documentId) { // 1. Pad or truncate the password string to exactly 32 bytes. var passwordFull = GetPaddedPassword(password); - var revision = encryptionDictionary.StandardSecurityHandlerRevision; + var revision = encryptionDictionary.Revision; using (var md5 = MD5.Create()) { @@ -480,6 +551,63 @@ } } + private static byte[] CalculateKeyRevisions5And6(byte[] password, EncryptionDictionary encryptionDictionary, bool isUserPassword) + { + // Truncate the UTF-8 representation of the password to 127 bytes if it is longer than 127 bytes + password = TruncatePasswordTo127Bytes(password); + + // If the password is the owner password: + // Compute an intermediate owner key by computing the SHA-256 hash of the UTF-8 password concatenated with the 8 bytes of owner Key Salt, + // concatenated with the 48-byte U string. The 32-byte result is the key used to decrypt the 32-byte OE string using AES-256 in CBC mode + // with no padding and an initialization vector of zero. The 32-byte result is the file encryption key. + if (!isUserPassword) + { + throw new PdfDocumentEncryptedException($"Unsupported owner key encryption with revision: {encryptionDictionary.Revision}."); + } + + // If the password is the user password: + // Compute an intermediate user key by computing the SHA-256 hash of the UTF-8 password concatenated with the 8 bytes of user Key Salt. + // The 32-byte result is the key used to decrypt the 32-byte UE string using AES-256 in CBC mode with no padding and an initialization vector of zero. + // The 32-byte result is the file encryption key. + var userKeySalt = new byte[8]; + Array.Copy(encryptionDictionary.UserBytes, 40, userKeySalt, 0, 8); + + var intermediateKey = ComputeSha256Hash(password, userKeySalt); + + var iv = new byte[16]; + + using (var rijndaelManaged = new RijndaelManaged { Key = intermediateKey, IV = iv, Mode = CipherMode.CBC, Padding = PaddingMode.None }) + using (var memoryStream = new MemoryStream(encryptionDictionary.UserEncryptionBytes)) + using (var output = new MemoryStream()) + using (var cryptoStream = new CryptoStream(memoryStream, rijndaelManaged.CreateDecryptor(intermediateKey, iv), CryptoStreamMode.Read)) + { + cryptoStream.CopyTo(output); + var result = output.ToArray(); + + return result; + } + } + + private static byte[] ComputeSha256Hash(byte[] input1, byte[] input2, byte[] input3 = null) + { + using (var sha = SHA256.Create()) + { + sha.TransformBlock(input1, 0, input1.Length, null, 0); + sha.TransformBlock(input2, 0, input2.Length, null, 0); + + if (input3 != null) + { + sha.TransformFinalBlock(input3, 0, input3.Length); + } + else + { + sha.TransformFinalBlock(EmptyArray.Instance, 0, 0); + } + + return sha.Hash; + } + } + private static void UpdateMd5(MD5 md5, byte[] data) { md5.TransformBlock(data, 0, data.Length, null, 0); @@ -507,5 +635,17 @@ return result; } + + private static byte[] TruncatePasswordTo127Bytes(byte[] password) + { + if (password.Length <= 127) + { + return password; + } + + var result = new byte[127]; + Array.Copy(password, result, 127); + return result; + } } } \ No newline at end of file