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