mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-10-15 19:54:52 +08:00
start adding support for undocumented revision 6 encryption
revision 6 was added in the pdf 2.0 specification which is document iso 32000-2:2017. because iso are rent-seeking they charge money to view this specification so it is effectively undocumented. this site details some of the algorithm https://web.archive.org/web/20180311160224/esec-lab.sogeti.com/posts/2011/09/14/the-undocumented-password-validation-algorithm-of-adobe-reader-x.html. the code in this change ports the pdfbox logic line by line. it doesn't implement the correct behaviour for owner password yet.
This commit is contained in:
@@ -28,6 +28,10 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The security handler defines the use of encryption and decryption in the document with a key length of 256 bits.
|
/// The security handler defines the use of encryption and decryption in the document with a key length of 256 bits.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
SecurityHandlerInDocument256 = 5
|
SecurityHandlerInDocument256 = 5,
|
||||||
|
/// <summary>
|
||||||
|
/// Since ISO isn't fit for purpose they charge £200 to see the PDF 2 spec so it's not possible to know what the specification for this revision is.
|
||||||
|
/// </summary>
|
||||||
|
UndocumentedDueToIso = 6
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -182,12 +182,15 @@
|
|||||||
Array.Copy(encryptionDictionary.UserBytes, userPasswordHash, 32);
|
Array.Copy(encryptionDictionary.UserBytes, userPasswordHash, 32);
|
||||||
Array.Copy(encryptionDictionary.UserBytes, 32, userValidationSalt, 0, 8);
|
Array.Copy(encryptionDictionary.UserBytes, 32, userValidationSalt, 0, 8);
|
||||||
|
|
||||||
|
byte[] result;
|
||||||
if (encryptionDictionary.Revision == 6)
|
if (encryptionDictionary.Revision == 6)
|
||||||
{
|
{
|
||||||
throw new PdfDocumentEncryptedException($"Support for revision 6 encryption not implemented: {encryptionDictionary}.");
|
result = ComputeStupidIsoHash(truncatedPassword, userValidationSalt, null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = ComputeSha256Hash(truncatedPassword, userValidationSalt);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = ComputeSha256Hash(truncatedPassword, userValidationSalt);
|
|
||||||
|
|
||||||
return result.SequenceEqual(userPasswordHash);
|
return result.SequenceEqual(userPasswordHash);
|
||||||
}
|
}
|
||||||
@@ -359,15 +362,15 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case HexToken hexToken:
|
case HexToken hexToken:
|
||||||
{
|
{
|
||||||
var data = hexToken.Bytes.ToArray();
|
var data = hexToken.Bytes.ToArray();
|
||||||
|
|
||||||
var decrypted = DecryptData(data, reference);
|
var decrypted = DecryptData(data, reference);
|
||||||
|
|
||||||
token = new HexToken(Hex.GetString(decrypted).ToCharArray());
|
token = new HexToken(Hex.GetString(decrypted).ToCharArray());
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DictionaryToken dictionary:
|
case DictionaryToken dictionary:
|
||||||
{
|
{
|
||||||
// PDFBOX-2936: avoid orphan /CF dictionaries found in US govt "I-" files
|
// PDFBOX-2936: avoid orphan /CF dictionaries found in US govt "I-" files
|
||||||
@@ -584,8 +587,16 @@
|
|||||||
var userKeySalt = new byte[8];
|
var userKeySalt = new byte[8];
|
||||||
Array.Copy(encryptionDictionary.UserBytes, 40, userKeySalt, 0, 8);
|
Array.Copy(encryptionDictionary.UserBytes, 40, userKeySalt, 0, 8);
|
||||||
|
|
||||||
var intermediateKey = ComputeSha256Hash(password, userKeySalt);
|
byte[] intermediateKey;
|
||||||
|
if (encryptionDictionary.Revision == 6)
|
||||||
|
{
|
||||||
|
intermediateKey = ComputeStupidIsoHash(password, userKeySalt, null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
intermediateKey = ComputeSha256Hash(password, userKeySalt);
|
||||||
|
}
|
||||||
|
|
||||||
var iv = new byte[16];
|
var iv = new byte[16];
|
||||||
|
|
||||||
using (var rijndaelManaged = new RijndaelManaged { Key = intermediateKey, IV = iv, Mode = CipherMode.CBC, Padding = PaddingMode.None })
|
using (var rijndaelManaged = new RijndaelManaged { Key = intermediateKey, IV = iv, Mode = CipherMode.CBC, Padding = PaddingMode.None })
|
||||||
@@ -659,5 +670,117 @@
|
|||||||
Array.Copy(password, result, 127);
|
Array.Copy(password, result, 127);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is the revision 6 algorithm the specification for which can't be viewed unless you pay the ISO a lot of money (which makes me extremely angry).
|
||||||
|
/// This is an attempt to port directly from PDFBox code.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="password">The truncated user or owner password.</param>
|
||||||
|
/// <param name="salt">The 8 byte user or owner key validation salt.</param>
|
||||||
|
/// <param name="vector">Used when hashing owner password in which case it is the 48 byte /U key.</param>
|
||||||
|
private static byte[] ComputeStupidIsoHash(byte[] password, byte[] salt, byte[] vector)
|
||||||
|
{
|
||||||
|
// There are some details here https://web.archive.org/web/20180311160224/esec-lab.sogeti.com/posts/2011/09/14/the-undocumented-password-validation-algorithm-of-adobe-reader-x.html
|
||||||
|
if (vector == null)
|
||||||
|
{
|
||||||
|
vector = EmptyArray<byte>.Instance;
|
||||||
|
}
|
||||||
|
else if (vector.Length > 0 && vector.Length < 48)
|
||||||
|
{
|
||||||
|
throw new PdfDocumentEncryptedException($"Vector for revision 6 owner password check (/U) is the wrong length, expected 48 bytes got {vector.Length} bytes.");
|
||||||
|
}
|
||||||
|
else if (vector.Length > 48)
|
||||||
|
{
|
||||||
|
var temp = new byte[48];
|
||||||
|
Array.Copy(vector, temp, temp.Length);
|
||||||
|
vector = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
password = TruncatePasswordTo127Bytes(password);
|
||||||
|
|
||||||
|
byte[] input;
|
||||||
|
using (var sha256 = SHA256.Create())
|
||||||
|
{
|
||||||
|
sha256.TransformBlock(password, 0, password.Length, null, 0);
|
||||||
|
sha256.TransformBlock(salt, 0, salt.Length, null, 0);
|
||||||
|
sha256.TransformBlock(vector, 0, vector.Length, null, 0);
|
||||||
|
sha256.TransformFinalBlock(EmptyArray<byte>.Instance, 0, 0);
|
||||||
|
input = sha256.Hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = new byte[16];
|
||||||
|
Array.Copy(input, key, 16);
|
||||||
|
|
||||||
|
var iv = new byte[16];
|
||||||
|
Array.Copy(input, 16, iv, 0, iv.Length);
|
||||||
|
|
||||||
|
byte[] x = null;
|
||||||
|
var i = 0;
|
||||||
|
while (i < 64 || i < x[x.Length - 1] + 32)
|
||||||
|
{
|
||||||
|
var roundResult = new byte[64 * (password.Length + input.Length + vector.Length)];
|
||||||
|
|
||||||
|
var position = 0;
|
||||||
|
for (var j = 0; j < 64; j++)
|
||||||
|
{
|
||||||
|
Array.Copy(password, 0, roundResult, position, password.Length);
|
||||||
|
position += password.Length;
|
||||||
|
Array.Copy(input, 0, roundResult, position, input.Length);
|
||||||
|
position += input.Length;
|
||||||
|
if (vector.Length > 0)
|
||||||
|
{
|
||||||
|
Array.Copy(vector, 0, roundResult, position, vector.Length);
|
||||||
|
position += vector.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var aes = Aes.Create())
|
||||||
|
{
|
||||||
|
aes.Key = key;
|
||||||
|
aes.IV = iv;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.None;
|
||||||
|
|
||||||
|
using (var decryptor = aes.CreateEncryptor())
|
||||||
|
{
|
||||||
|
x = decryptor.TransformFinalBlock(roundResult, 0, roundResult.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sumOfFirstSixteenBytesOfX = x.Take(16).Sum(v => (long)v);
|
||||||
|
var mod3 = sumOfFirstSixteenBytesOfX % 3;
|
||||||
|
|
||||||
|
HashAlgorithm nextHash;
|
||||||
|
switch (mod3)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
nextHash = SHA256.Create();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
nextHash = SHA384.Create();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
nextHash = SHA512.Create();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new PdfDocumentEncryptedException("Invalid remainder from summing first sixteen bytes of this round's hash.");
|
||||||
|
}
|
||||||
|
|
||||||
|
input = nextHash.ComputeHash(x);
|
||||||
|
Array.Copy(input, key, 16);
|
||||||
|
Array.Copy(input, 16, iv, 0, 16);
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.Length > 32)
|
||||||
|
{
|
||||||
|
var result = new byte[32];
|
||||||
|
Array.Copy(input, result, result.Length);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user