begin adding support for in-document security handlers to support aes 128/256 encryption #34

This commit is contained in:
Eliot Jones
2019-06-08 14:14:51 +01:00
parent 39d05e6a47
commit a19122478d
8 changed files with 382 additions and 129 deletions

View File

@@ -0,0 +1,67 @@
namespace UglyToad.PdfPig.Encryption
{
internal class CryptDictionary
{
public static CryptDictionary Identity { get; } = new CryptDictionary();
public Method Name { get; }
public TriggerEvent Event { get; }
public int Length { get; }
public bool IsIdentity { get; }
public CryptDictionary(Method name, TriggerEvent @event, int length)
{
Name = name;
Event = @event;
Length = length;
IsIdentity = false;
}
private CryptDictionary()
{
Name = Method.None;
IsIdentity = true;
}
/// <summary>
/// The method used by the consumer application to decrypt data.
/// </summary>
public enum Method
{
/// <summary>
/// The application does not decrypt data but directs the input stream
/// to the security handler for decryption.
/// </summary>
None,
/// <summary>
/// The application asks the security handler for the encryption key
/// and implicitly decrypts data using the RC4 algorithm.
/// </summary>
V2,
/// <summary>
/// (PDF 1.6) The application asks the security handler for the encryption key and implicitly decrypts data using the AES algorithm in Cipher Block Chaining (CBC) mode
/// with a 16-byte block size and an initialization vector that is randomly generated and placed as the first 16 bytes in the stream or string.
/// </summary>
AesV2
}
/// <summary>
/// The event to be used to trigger the authorization that is required
/// to access encryption keys used by this filter.
/// </summary>
public enum TriggerEvent
{
/// <summary>
/// Authorization is required when a document is opened.
/// </summary>
DocumentOpen,
/// <summary>
/// Authorization is required when accessing embedded files.
/// </summary>
EmbeddedFileOpen
}
}
}

View File

@@ -0,0 +1,101 @@
namespace UglyToad.PdfPig.Encryption
{
using System;
using Exceptions;
using Tokens;
internal class CryptHandler
{
private readonly DictionaryToken cryptDictionary;
public CryptDictionary StreamDictionary { get; }
public CryptDictionary StringDictionary { get; }
public CryptHandler(DictionaryToken cryptDictionary,
NameToken streamName, NameToken stringName)
{
if (streamName == null)
{
throw new ArgumentNullException(nameof(streamName));
}
if (stringName == null)
{
throw new ArgumentNullException(nameof(stringName));
}
this.cryptDictionary = cryptDictionary ?? throw new ArgumentNullException(nameof(cryptDictionary));
StreamDictionary = ParseCryptDictionary(cryptDictionary, streamName);
StringDictionary = ParseCryptDictionary(cryptDictionary, stringName);
}
public CryptDictionary GetNamedCryptDictionary(NameToken name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
return ParseCryptDictionary(cryptDictionary, name);
}
private static CryptDictionary ParseCryptDictionary(DictionaryToken cryptDictionary, NameToken name)
{
if (name == NameToken.Identity)
{
return CryptDictionary.Identity;
}
if (!cryptDictionary.TryGet(name, out DictionaryToken cryptDictionaryToken))
{
throw new PdfDocumentEncryptedException($"Could not find named crypt filter {name} for decryption in crypt dictionary: {cryptDictionaryToken}.");
}
if (cryptDictionaryToken.TryGet(NameToken.Type, out NameToken typeName) && typeName != NameToken.CryptFilter)
{
throw new PdfDocumentEncryptedException($"Invalid crypt dictionary type {typeName} for crypt filter {name}: {cryptDictionaryToken}.");
}
var cfmName = cryptDictionaryToken.TryGet(NameToken.Cfm, out NameToken cfm) ? cfm : NameToken.None;
CryptDictionary.Method method;
if (cfmName == NameToken.None)
{
method = CryptDictionary.Method.None;
}
else if (cfmName == NameToken.V2)
{
method = CryptDictionary.Method.V2;
}
else if (cfmName == NameToken.Aesv2)
{
method = CryptDictionary.Method.AesV2;
}
else
{
throw new PdfDocumentEncryptedException($"Unrecognized CFM option for crypt filter {cfm}: {cryptDictionaryToken}.");
}
var eventName = cryptDictionaryToken.TryGet(NameToken.AuthEvent, out NameToken auth) ? auth : NameToken.DocOpen;
CryptDictionary.TriggerEvent @event;
if (eventName == NameToken.DocOpen)
{
@event = CryptDictionary.TriggerEvent.DocumentOpen;
}
else if (eventName == NameToken.EfOpen)
{
@event = CryptDictionary.TriggerEvent.EmbeddedFileOpen;
}
else
{
throw new PdfDocumentEncryptedException($"Unrecognized AuthEvent option for crypt filter {eventName}: {cryptDictionaryToken}.");
}
var length = cryptDictionaryToken.TryGet(NameToken.Length, out NumericToken lengthNumeric) ? lengthNumeric.Int : 0;
return new CryptDictionary(method, @event, length);
}
}
}

View File

@@ -0,0 +1,29 @@
namespace UglyToad.PdfPig.Encryption
{
/// <summary>
/// A code specifying the algorithm to be used in encrypting and decrypting the document.
/// </summary>
internal enum EncryptionAlgorithmCode
{
/// <summary>
/// An algorithm that is undocumented and no longer supported.
/// </summary>
Unrecognized = 0,
/// <summary>
/// RC4 or AES encryption using a key of 40 bits.
/// </summary>
Rc4OrAes40BitKey = 1,
/// <summary>
/// RC4 or AES encryption using a key of more than 40 bits.
/// </summary>
Rc4OrAesGreaterThan40BitKey = 2,
/// <summary>
/// An unpublished algorithm that permits encryption key lengths ranging from 40 to 128 bits.
/// </summary>
UnpublishedAlgorithm40To128BitKey = 3,
/// <summary>
/// The security handler defines the use of encryption and decryption in the document.
/// </summary>
SecurityHandlerInDocument
}
}

View File

@@ -1,7 +1,7 @@
namespace UglyToad.PdfPig.Encryption
{
using System;
using Tokenization.Scanner;
using Exceptions;
using Tokens;
using Util;
@@ -53,129 +53,40 @@
OwnerBytes = OtherEncodings.StringAsLatin1Bytes(ownerPasswordCheck);
UserBytes = OtherEncodings.StringAsLatin1Bytes(userPasswordCheck);
}
}
internal static class EncryptionDictionaryFactory
{
public static EncryptionDictionary Read(DictionaryToken encryptionDictionary, IPdfTokenScanner tokenScanner)
public bool TryGetCryptHandler(out CryptHandler cryptHandler)
{
if (encryptionDictionary == null)
cryptHandler = null;
if (EncryptionAlgorithmCode != EncryptionAlgorithmCode.SecurityHandlerInDocument)
{
throw new ArgumentNullException(nameof(encryptionDictionary));
return false;
}
var filter = encryptionDictionary.Get<NameToken>(NameToken.Filter, tokenScanner);
var code = EncryptionAlgorithmCode.Unrecognized;
if (encryptionDictionary.TryGetOptionalTokenDirect(NameToken.V, tokenScanner, out NumericToken vNum))
if (!Dictionary.TryGet(NameToken.Cf, out DictionaryToken cryptFilterDictionary))
{
code = (EncryptionAlgorithmCode) vNum.Int;
return false;
}
var length = default(int?);
var namedFilters = cryptFilterDictionary;
if (encryptionDictionary.TryGetOptionalTokenDirect(NameToken.Length, tokenScanner, out NumericToken lengthToken))
var streamFilterName = Dictionary.TryGet(NameToken.StmF, out NameToken streamFilterToken) ? streamFilterToken : NameToken.Identity;
var stringFilterName = Dictionary.TryGet(NameToken.StrF, out NameToken stringFilterToken) ? stringFilterToken : NameToken.Identity;
if (streamFilterName != NameToken.Identity && !namedFilters.TryGet(streamFilterName, out _))
{
length = lengthToken.Int;
throw new PdfDocumentEncryptedException($"Stream filter {streamFilterName} not found in crypt dictionary: {cryptFilterDictionary}.");
}
var revision = default(int);
if (encryptionDictionary.TryGetOptionalTokenDirect(NameToken.R, tokenScanner, out NumericToken revisionToken))
if (stringFilterName != NameToken.Identity && !namedFilters.TryGet(stringFilterName, out _))
{
revision = revisionToken.Int;
throw new PdfDocumentEncryptedException($"String filter {stringFilterName} not found in crypt dictionary: {cryptFilterDictionary}.");
}
encryptionDictionary.TryGetOptionalStringDirect(NameToken.O, tokenScanner, out var ownerString);
encryptionDictionary.TryGetOptionalStringDirect(NameToken.U, tokenScanner, out var userString);
cryptHandler = new CryptHandler(namedFilters, streamFilterName, stringFilterName);
var access = default(UserAccessPermissions);
if (encryptionDictionary.TryGetOptionalTokenDirect(NameToken.P, tokenScanner, out NumericToken accessToken))
{
access = (UserAccessPermissions) accessToken.Int;
}
encryptionDictionary.TryGetOptionalTokenDirect(NameToken.EncryptMetaData, tokenScanner, out BooleanToken encryptMetadata);
return new EncryptionDictionary(filter.Data, code, length, revision, ownerString, userString, access, encryptionDictionary,
encryptMetadata?.Data ?? false);
return true;
}
}
/// <summary>
/// A code specifying the algorithm to be used in encrypting and decrypting the document.
/// </summary>
internal enum EncryptionAlgorithmCode
{
/// <summary>
/// An algorithm that is undocumented and no longer supported.
/// </summary>
Unrecognized = 0,
/// <summary>
/// RC4 or AES encryption using a key of 40 bits.
/// </summary>
Rc4OrAes40BitKey = 1,
/// <summary>
/// RC4 or AES encryption using a key of more than 40 bits.
/// </summary>
Rc4OrAesGreaterThan40BitKey = 2,
/// <summary>
/// An unpublished algorithm that permits encryption key lengths ranging from 40 to 128 bits.
/// </summary>
UnpublishedAlgorithm40To128BitKey = 3,
/// <summary>
/// The security handler defines the use of encryption and decryption in the document.
/// </summary>
SecurityHandlerInDocument
}
[Flags]
internal enum UserAccessPermissions
{
/// <summary>
/// (Revision 2) Print the document.
/// (Revision 3 or greater) Print the document (possibly not at the highest quality level, see <see cref="PrintHighQuality"/>).
/// </summary>
Print = 1 << 2,
/// <summary>
/// Modify the contents of the document by operations other than those
/// controlled by <see cref="AddOrModifyTextAnnotationsAndFillFormFields"/>, <see cref="FillExistingFormFields"/> and <see cref="AssembleDocument"/>.
/// </summary>
Modify = 1 << 3,
/// <summary>
/// (Revision 2) Copy or otherwise extract text and graphics from the document, including extracting text and graphics
/// (in support of accessibility to users with disabilities or for other purposes).
/// (Revision 3 or greater) Copy or otherwise extract text and graphics from the document by operations other
/// than that controlled by <see cref="ExtractTextAndGraphics"/>.
/// </summary>
CopyTextAndGraphics = 1 << 4,
/// <summary>
/// Add or modify text annotations, fill in interactive form fields, and, if <see cref="Modify"/> is also set,
/// create or modify interactive form fields (including signature fields).
/// </summary>
AddOrModifyTextAnnotationsAndFillFormFields = 1 << 5,
/// <summary>
/// (Revision 3 or greater) Fill in existing interactive form fields (including signature fields),
/// even if <see cref="AddOrModifyTextAnnotationsAndFillFormFields"/> is clear.
/// </summary>
FillExistingFormFields = 1 << 8,
/// <summary>
/// (Revision 3 or greater) Extract text and graphics (in support of accessibility to users with disabilities or for other purposes).
/// </summary>
ExtractTextAndGraphics = 1 << 9,
/// <summary>
/// (Revision 3 or greater) Assemble the document (insert, rotate, or delete pages and create bookmarks or thumbnail images),
/// even if <see cref="Modify"/> is clear.
/// </summary>
AssembleDocument = 1 << 10,
/// <summary>
/// (Revision 3 or greater) Print the document to a representation from which a faithful digital copy of the PDF content could be generated.
/// When this is clear (and <see cref="Print"/> is set), printing is limited to a low-level representation of the appearance,
/// possibly of degraded quality.
/// </summary>
PrintHighQuality = 1 << 12
}
}

View File

@@ -0,0 +1,55 @@
namespace UglyToad.PdfPig.Encryption
{
using System;
using Tokenization.Scanner;
using Tokens;
using Util;
internal static class EncryptionDictionaryFactory
{
public static EncryptionDictionary Read(DictionaryToken encryptionDictionary, IPdfTokenScanner tokenScanner)
{
if (encryptionDictionary == null)
{
throw new ArgumentNullException(nameof(encryptionDictionary));
}
var filter = encryptionDictionary.Get<NameToken>(NameToken.Filter, tokenScanner);
var code = EncryptionAlgorithmCode.Unrecognized;
if (encryptionDictionary.TryGetOptionalTokenDirect(NameToken.V, tokenScanner, out NumericToken vNum))
{
code = (EncryptionAlgorithmCode) vNum.Int;
}
var length = default(int?);
if (encryptionDictionary.TryGetOptionalTokenDirect(NameToken.Length, tokenScanner, out NumericToken lengthToken))
{
length = lengthToken.Int;
}
var revision = default(int);
if (encryptionDictionary.TryGetOptionalTokenDirect(NameToken.R, tokenScanner, out NumericToken revisionToken))
{
revision = revisionToken.Int;
}
encryptionDictionary.TryGetOptionalStringDirect(NameToken.O, tokenScanner, out var ownerString);
encryptionDictionary.TryGetOptionalStringDirect(NameToken.U, tokenScanner, out var userString);
var access = default(UserAccessPermissions);
if (encryptionDictionary.TryGetOptionalTokenDirect(NameToken.P, tokenScanner, out NumericToken accessToken))
{
access = (UserAccessPermissions) accessToken.Int;
}
encryptionDictionary.TryGetOptionalTokenDirect(NameToken.EncryptMetaData, tokenScanner, out BooleanToken encryptMetadata);
return new EncryptionDictionary(filter.Data, code, length, revision, ownerString, userString, access, encryptionDictionary,
encryptMetadata?.Data ?? true);
}
}
}

View File

@@ -30,6 +30,9 @@
[CanBeNull]
private readonly EncryptionDictionary encryptionDictionary;
[CanBeNull]
private readonly CryptHandler cryptHandler;
private readonly byte[] encryptionKey;
private readonly bool useAes;
@@ -49,9 +52,18 @@
return;
}
useAes = false;
if (encryptionDictionary.EncryptionAlgorithmCode == EncryptionAlgorithmCode.SecurityHandlerInDocument)
{
throw new PdfDocumentEncryptedException("Document encrypted with unsupported algorithm.", encryptionDictionary);
if (!encryptionDictionary.TryGetCryptHandler(out var cryptHandlerLocal))
{
throw new PdfDocumentEncryptedException("Document encrypted with security handler in document but no crypt dictionary found.", encryptionDictionary);
}
cryptHandler = cryptHandlerLocal;
useAes = cryptHandlerLocal?.StreamDictionary?.Name == CryptDictionary.Method.AesV2;
}
var charset = OtherEncodings.Iso88591;
@@ -93,19 +105,15 @@
throw new PdfDocumentEncryptedException("The document was encrypted and the provided password was neither the user or owner password.", encryptionDictionary);
}
encryptionKey = CalculateKeyRevisions2To4(decryptionPasswordBytes, encryptionDictionary.OwnerBytes, (int)encryptionDictionary.UserAccessPermissions,
encryptionDictionary.StandardSecurityHandlerRevision,
encryptionKey = CalculateKeyRevisions2To4(decryptionPasswordBytes, encryptionDictionary,
length,
documentIdBytes);
useAes = false;
}
private static bool IsUserPassword(byte[] passwordBytes, EncryptionDictionary encryptionDictionary, int length, byte[] documentIdBytes)
{
// 1. Create an encryption key based on the user password string.
var calculatedEncryptionKey = CalculateKeyRevisions2To4(passwordBytes, encryptionDictionary.OwnerBytes, (int)encryptionDictionary.UserAccessPermissions,
encryptionDictionary.StandardSecurityHandlerRevision, length, documentIdBytes);
var calculatedEncryptionKey = CalculateKeyRevisions2To4(passwordBytes, encryptionDictionary, length, documentIdBytes);
byte[] output;
@@ -245,6 +253,13 @@
{
case StreamToken stream:
{
if (cryptHandler?.StreamDictionary?.IsIdentity == true
|| cryptHandler?.StreamDictionary?.Name == CryptDictionary.Method.None)
{
// TODO: No idea if this is right.
return token;
}
if (stream.StreamDictionary.TryGet(NameToken.Type, out NameToken typeName))
{
if (NameToken.Xref.Equals(typeName))
@@ -270,6 +285,13 @@
}
case StringToken stringToken:
{
if (cryptHandler?.StringDictionary?.IsIdentity == true
|| cryptHandler?.StringDictionary?.Name == CryptDictionary.Method.None)
{
// TODO: No idea if this is right.
return token;
}
var data = OtherEncodings.StringAsLatin1Bytes(stringToken.Data);
var decrypted = DecryptData(data, reference);
@@ -386,22 +408,23 @@
}
}
private static byte[] CalculateKeyRevisions2To4(byte[] password, byte[] ownerKey,
int permissions, int revision, int length, byte[] documentId)
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;
using (var md5 = MD5.Create())
{
// 2. Initialize the MD5 hash function and pass the result of step 1 as input to this function.
UpdateMd5(md5, passwordFull);
// 3. Pass the value of the encryption dictionary's owner key entry to the MD5 hash function.
UpdateMd5(md5, ownerKey);
UpdateMd5(md5, encryptionDictionary.OwnerBytes);
// 4. Treat the value of the P entry as an unsigned 4-byte integer.
var unsigned = (uint)permissions;
var unsigned = (uint)encryptionDictionary.UserAccessPermissions;
// 4. Pass these bytes to the MD5 hash function, low-order byte first.
UpdateMd5(md5, new[] { (byte)(unsigned) });
@@ -414,7 +437,7 @@
// 6. (Revision 4 or greater) If document metadata is not being encrypted, pass 4 bytes
// with the value 0xFFFFFFFF to the MD5 hash function.
if (revision >= 4)
if (revision >= 4 && !encryptionDictionary.EncryptMetadata)
{
UpdateMd5(md5, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF });
}
@@ -427,22 +450,33 @@
{
var n = length;
md5.TransformFinalBlock(EmptyArray<byte>.Instance, 0, 0);
var input = md5.Hash;
for (var i = 0; i < 50; i++)
using (var newMd5 = MD5.Create())
{
UpdateMd5(md5, input.Take(n).ToArray());
input = md5.Hash;
for (var i = 0; i < 50; i++)
{
input = newMd5.ComputeHash(input.Take(n).ToArray());
}
}
var result = new byte[length];
Array.Copy(input, result, length);
return result;
}
else
{
md5.TransformFinalBlock(EmptyArray<byte>.Instance, 0, 0);
md5.TransformFinalBlock(EmptyArray<byte>.Instance, 0, 0);
var result = new byte[length];
var result = new byte[length];
Array.Copy(md5.Hash, result, length);
Array.Copy(md5.Hash, result, length);
return result;
return result;
}
}
}

View File

@@ -0,0 +1,51 @@
namespace UglyToad.PdfPig.Encryption
{
using System;
[Flags]
internal enum UserAccessPermissions
{
/// <summary>
/// (Revision 2) Print the document.
/// (Revision 3 or greater) Print the document (possibly not at the highest quality level, see <see cref="PrintHighQuality"/>).
/// </summary>
Print = 1 << 2,
/// <summary>
/// Modify the contents of the document by operations other than those
/// controlled by <see cref="AddOrModifyTextAnnotationsAndFillFormFields"/>, <see cref="FillExistingFormFields"/> and <see cref="AssembleDocument"/>.
/// </summary>
Modify = 1 << 3,
/// <summary>
/// (Revision 2) Copy or otherwise extract text and graphics from the document, including extracting text and graphics
/// (in support of accessibility to users with disabilities or for other purposes).
/// (Revision 3 or greater) Copy or otherwise extract text and graphics from the document by operations other
/// than that controlled by <see cref="ExtractTextAndGraphics"/>.
/// </summary>
CopyTextAndGraphics = 1 << 4,
/// <summary>
/// Add or modify text annotations, fill in interactive form fields, and, if <see cref="Modify"/> is also set,
/// create or modify interactive form fields (including signature fields).
/// </summary>
AddOrModifyTextAnnotationsAndFillFormFields = 1 << 5,
/// <summary>
/// (Revision 3 or greater) Fill in existing interactive form fields (including signature fields),
/// even if <see cref="AddOrModifyTextAnnotationsAndFillFormFields"/> is clear.
/// </summary>
FillExistingFormFields = 1 << 8,
/// <summary>
/// (Revision 3 or greater) Extract text and graphics (in support of accessibility to users with disabilities or for other purposes).
/// </summary>
ExtractTextAndGraphics = 1 << 9,
/// <summary>
/// (Revision 3 or greater) Assemble the document (insert, rotate, or delete pages and create bookmarks or thumbnail images),
/// even if <see cref="Modify"/> is clear.
/// </summary>
AssembleDocument = 1 << 10,
/// <summary>
/// (Revision 3 or greater) Print the document to a representation from which a faithful digital copy of the PDF content could be generated.
/// When this is clear (and <see cref="Print"/> is set), printing is limited to a low-level representation of the appearance,
/// possibly of degraded quality.
/// </summary>
PrintHighQuality = 1 << 12
}
}

View File

@@ -39,6 +39,7 @@
public static readonly NameToken Ascii85Decode = new NameToken("ASCII85Decode");
public static readonly NameToken Ascii85DecodeAbbreviation = new NameToken("A85");
public static readonly NameToken Attached = new NameToken("Attached");
public static readonly NameToken AuthEvent = new NameToken("AuthEvent");
public static readonly NameToken Author = new NameToken("Author");
public static readonly NameToken AvgWidth = new NameToken("AvgWidth");
#endregion
@@ -126,6 +127,7 @@
public static readonly NameToken Creator = new NameToken("Creator");
public static readonly NameToken CropBox = new NameToken("CropBox");
public static readonly NameToken Crypt = new NameToken("Crypt");
public static readonly NameToken CryptFilter = new NameToken("CryptFilter");
public static readonly NameToken Cs = new NameToken("CS");
// D
public static readonly NameToken D = new NameToken("D");
@@ -164,6 +166,7 @@
public static readonly NameToken Dl = new NameToken("DL");
public static readonly NameToken Dm = new NameToken("Dm");
public static readonly NameToken Doc = new NameToken("Doc");
public static readonly NameToken DocOpen = new NameToken("DocOpen");
public static readonly NameToken DocChecksum = new NameToken("DocChecksum");
public static readonly NameToken DocTimeStamp = new NameToken("DocTimeStamp");
public static readonly NameToken Docmdp = new NameToken("DocMDP");
@@ -181,6 +184,7 @@
public static readonly NameToken E = new NameToken("E");
public static readonly NameToken EarlyChange = new NameToken("EarlyChange");
public static readonly NameToken Ef = new NameToken("EF");
public static readonly NameToken EfOpen = new NameToken("EFOpen");
public static readonly NameToken EmbeddedFdfs = new NameToken("EmbeddedFDFs");
public static readonly NameToken EmbeddedFiles = new NameToken("EmbeddedFiles");
public static readonly NameToken Empty = new NameToken("");
@@ -527,6 +531,7 @@
public static readonly NameToken UserUnit = new NameToken("UserUnit");
// V
public static readonly NameToken V = new NameToken("V");
public static readonly NameToken V2 = new NameToken("V2");
public static readonly NameToken VerisignPpkvs = new NameToken("VeriSign.PPKVS");
public static readonly NameToken Version = new NameToken("Version");
public static readonly NameToken Vertices = new NameToken("Vertices");