mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-09-22 20:13:58 +08:00
begin adding support for in-document security handlers to support aes 128/256 encryption #34
This commit is contained in:
67
src/UglyToad.PdfPig/Encryption/CryptDictionary.cs
Normal file
67
src/UglyToad.PdfPig/Encryption/CryptDictionary.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
101
src/UglyToad.PdfPig/Encryption/CryptHandler.cs
Normal file
101
src/UglyToad.PdfPig/Encryption/CryptHandler.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
src/UglyToad.PdfPig/Encryption/EncryptionAlgorithmCode.cs
Normal file
29
src/UglyToad.PdfPig/Encryption/EncryptionAlgorithmCode.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
namespace UglyToad.PdfPig.Encryption
|
namespace UglyToad.PdfPig.Encryption
|
||||||
{
|
{
|
||||||
using System;
|
using System;
|
||||||
using Tokenization.Scanner;
|
using Exceptions;
|
||||||
using Tokens;
|
using Tokens;
|
||||||
using Util;
|
using Util;
|
||||||
|
|
||||||
@@ -53,129 +53,40 @@
|
|||||||
OwnerBytes = OtherEncodings.StringAsLatin1Bytes(ownerPasswordCheck);
|
OwnerBytes = OtherEncodings.StringAsLatin1Bytes(ownerPasswordCheck);
|
||||||
UserBytes = OtherEncodings.StringAsLatin1Bytes(userPasswordCheck);
|
UserBytes = OtherEncodings.StringAsLatin1Bytes(userPasswordCheck);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
internal static class EncryptionDictionaryFactory
|
public bool TryGetCryptHandler(out CryptHandler cryptHandler)
|
||||||
{
|
|
||||||
public static EncryptionDictionary Read(DictionaryToken encryptionDictionary, IPdfTokenScanner tokenScanner)
|
|
||||||
{
|
{
|
||||||
if (encryptionDictionary == null)
|
cryptHandler = null;
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(encryptionDictionary));
|
|
||||||
}
|
|
||||||
|
|
||||||
var filter = encryptionDictionary.Get<NameToken>(NameToken.Filter, tokenScanner);
|
|
||||||
|
|
||||||
var code = EncryptionAlgorithmCode.Unrecognized;
|
if (EncryptionAlgorithmCode != EncryptionAlgorithmCode.SecurityHandlerInDocument)
|
||||||
|
|
||||||
if (encryptionDictionary.TryGetOptionalTokenDirect(NameToken.V, tokenScanner, out NumericToken vNum))
|
|
||||||
{
|
{
|
||||||
code = (EncryptionAlgorithmCode) vNum.Int;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var length = default(int?);
|
if (!Dictionary.TryGet(NameToken.Cf, out DictionaryToken cryptFilterDictionary))
|
||||||
|
|
||||||
if (encryptionDictionary.TryGetOptionalTokenDirect(NameToken.Length, tokenScanner, out NumericToken lengthToken))
|
|
||||||
{
|
{
|
||||||
length = lengthToken.Int;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var revision = default(int);
|
var namedFilters = cryptFilterDictionary;
|
||||||
if (encryptionDictionary.TryGetOptionalTokenDirect(NameToken.R, tokenScanner, out NumericToken revisionToken))
|
|
||||||
|
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 _))
|
||||||
{
|
{
|
||||||
revision = revisionToken.Int;
|
throw new PdfDocumentEncryptedException($"Stream filter {streamFilterName} not found in crypt dictionary: {cryptFilterDictionary}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptionDictionary.TryGetOptionalStringDirect(NameToken.O, tokenScanner, out var ownerString);
|
if (stringFilterName != NameToken.Identity && !namedFilters.TryGet(stringFilterName, out _))
|
||||||
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;
|
throw new PdfDocumentEncryptedException($"String filter {stringFilterName} not found in crypt dictionary: {cryptFilterDictionary}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptionDictionary.TryGetOptionalTokenDirect(NameToken.EncryptMetaData, tokenScanner, out BooleanToken encryptMetadata);
|
cryptHandler = new CryptHandler(namedFilters, streamFilterName, stringFilterName);
|
||||||
|
|
||||||
return new EncryptionDictionary(filter.Data, code, length, revision, ownerString, userString, access, encryptionDictionary,
|
return true;
|
||||||
encryptMetadata?.Data ?? false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -29,7 +29,10 @@
|
|||||||
|
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
private readonly EncryptionDictionary encryptionDictionary;
|
private readonly EncryptionDictionary encryptionDictionary;
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
private readonly CryptHandler cryptHandler;
|
||||||
|
|
||||||
private readonly byte[] encryptionKey;
|
private readonly byte[] encryptionKey;
|
||||||
|
|
||||||
private readonly bool useAes;
|
private readonly bool useAes;
|
||||||
@@ -49,9 +52,18 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useAes = false;
|
||||||
|
|
||||||
if (encryptionDictionary.EncryptionAlgorithmCode == EncryptionAlgorithmCode.SecurityHandlerInDocument)
|
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;
|
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);
|
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,
|
encryptionKey = CalculateKeyRevisions2To4(decryptionPasswordBytes, encryptionDictionary,
|
||||||
encryptionDictionary.StandardSecurityHandlerRevision,
|
|
||||||
length,
|
length,
|
||||||
documentIdBytes);
|
documentIdBytes);
|
||||||
|
|
||||||
useAes = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsUserPassword(byte[] passwordBytes, EncryptionDictionary encryptionDictionary, int length, byte[] documentIdBytes)
|
private static bool IsUserPassword(byte[] passwordBytes, EncryptionDictionary encryptionDictionary, int length, byte[] documentIdBytes)
|
||||||
{
|
{
|
||||||
// 1. Create an encryption key based on the user password string.
|
// 1. Create an encryption key based on the user password string.
|
||||||
var calculatedEncryptionKey = CalculateKeyRevisions2To4(passwordBytes, encryptionDictionary.OwnerBytes, (int)encryptionDictionary.UserAccessPermissions,
|
var calculatedEncryptionKey = CalculateKeyRevisions2To4(passwordBytes, encryptionDictionary, length, documentIdBytes);
|
||||||
encryptionDictionary.StandardSecurityHandlerRevision, length, documentIdBytes);
|
|
||||||
|
|
||||||
byte[] output;
|
byte[] output;
|
||||||
|
|
||||||
@@ -245,6 +253,13 @@
|
|||||||
{
|
{
|
||||||
case StreamToken stream:
|
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 (stream.StreamDictionary.TryGet(NameToken.Type, out NameToken typeName))
|
||||||
{
|
{
|
||||||
if (NameToken.Xref.Equals(typeName))
|
if (NameToken.Xref.Equals(typeName))
|
||||||
@@ -270,6 +285,13 @@
|
|||||||
}
|
}
|
||||||
case StringToken stringToken:
|
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 data = OtherEncodings.StringAsLatin1Bytes(stringToken.Data);
|
||||||
|
|
||||||
var decrypted = DecryptData(data, reference);
|
var decrypted = DecryptData(data, reference);
|
||||||
@@ -386,22 +408,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] CalculateKeyRevisions2To4(byte[] password, byte[] ownerKey,
|
private static byte[] CalculateKeyRevisions2To4(byte[] password, EncryptionDictionary encryptionDictionary, int length, byte[] documentId)
|
||||||
int permissions, int revision, int length, byte[] documentId)
|
|
||||||
{
|
{
|
||||||
// 1. Pad or truncate the password string to exactly 32 bytes.
|
// 1. Pad or truncate the password string to exactly 32 bytes.
|
||||||
var passwordFull = GetPaddedPassword(password);
|
var passwordFull = GetPaddedPassword(password);
|
||||||
|
|
||||||
|
var revision = encryptionDictionary.StandardSecurityHandlerRevision;
|
||||||
|
|
||||||
using (var md5 = MD5.Create())
|
using (var md5 = MD5.Create())
|
||||||
{
|
{
|
||||||
// 2. Initialize the MD5 hash function and pass the result of step 1 as input to this function.
|
// 2. Initialize the MD5 hash function and pass the result of step 1 as input to this function.
|
||||||
UpdateMd5(md5, passwordFull);
|
UpdateMd5(md5, passwordFull);
|
||||||
|
|
||||||
// 3. Pass the value of the encryption dictionary's owner key entry to the MD5 hash function.
|
// 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.
|
// 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.
|
// 4. Pass these bytes to the MD5 hash function, low-order byte first.
|
||||||
UpdateMd5(md5, new[] { (byte)(unsigned) });
|
UpdateMd5(md5, new[] { (byte)(unsigned) });
|
||||||
@@ -414,7 +437,7 @@
|
|||||||
|
|
||||||
// 6. (Revision 4 or greater) If document metadata is not being encrypted, pass 4 bytes
|
// 6. (Revision 4 or greater) If document metadata is not being encrypted, pass 4 bytes
|
||||||
// with the value 0xFFFFFFFF to the MD5 hash function.
|
// 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 });
|
UpdateMd5(md5, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF });
|
||||||
}
|
}
|
||||||
@@ -427,22 +450,33 @@
|
|||||||
{
|
{
|
||||||
var n = length;
|
var n = length;
|
||||||
|
|
||||||
|
md5.TransformFinalBlock(EmptyArray<byte>.Instance, 0, 0);
|
||||||
|
|
||||||
var input = md5.Hash;
|
var input = md5.Hash;
|
||||||
|
using (var newMd5 = MD5.Create())
|
||||||
for (var i = 0; i < 50; i++)
|
|
||||||
{
|
{
|
||||||
UpdateMd5(md5, input.Take(n).ToArray());
|
for (var i = 0; i < 50; i++)
|
||||||
input = md5.Hash;
|
{
|
||||||
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
51
src/UglyToad.PdfPig/Encryption/UserAccessPermissions.cs
Normal file
51
src/UglyToad.PdfPig/Encryption/UserAccessPermissions.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@@ -39,6 +39,7 @@
|
|||||||
public static readonly NameToken Ascii85Decode = new NameToken("ASCII85Decode");
|
public static readonly NameToken Ascii85Decode = new NameToken("ASCII85Decode");
|
||||||
public static readonly NameToken Ascii85DecodeAbbreviation = new NameToken("A85");
|
public static readonly NameToken Ascii85DecodeAbbreviation = new NameToken("A85");
|
||||||
public static readonly NameToken Attached = new NameToken("Attached");
|
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 Author = new NameToken("Author");
|
||||||
public static readonly NameToken AvgWidth = new NameToken("AvgWidth");
|
public static readonly NameToken AvgWidth = new NameToken("AvgWidth");
|
||||||
#endregion
|
#endregion
|
||||||
@@ -126,6 +127,7 @@
|
|||||||
public static readonly NameToken Creator = new NameToken("Creator");
|
public static readonly NameToken Creator = new NameToken("Creator");
|
||||||
public static readonly NameToken CropBox = new NameToken("CropBox");
|
public static readonly NameToken CropBox = new NameToken("CropBox");
|
||||||
public static readonly NameToken Crypt = new NameToken("Crypt");
|
public static readonly NameToken Crypt = new NameToken("Crypt");
|
||||||
|
public static readonly NameToken CryptFilter = new NameToken("CryptFilter");
|
||||||
public static readonly NameToken Cs = new NameToken("CS");
|
public static readonly NameToken Cs = new NameToken("CS");
|
||||||
// D
|
// D
|
||||||
public static readonly NameToken D = new NameToken("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 Dl = new NameToken("DL");
|
||||||
public static readonly NameToken Dm = new NameToken("Dm");
|
public static readonly NameToken Dm = new NameToken("Dm");
|
||||||
public static readonly NameToken Doc = new NameToken("Doc");
|
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 DocChecksum = new NameToken("DocChecksum");
|
||||||
public static readonly NameToken DocTimeStamp = new NameToken("DocTimeStamp");
|
public static readonly NameToken DocTimeStamp = new NameToken("DocTimeStamp");
|
||||||
public static readonly NameToken Docmdp = new NameToken("DocMDP");
|
public static readonly NameToken Docmdp = new NameToken("DocMDP");
|
||||||
@@ -181,6 +184,7 @@
|
|||||||
public static readonly NameToken E = new NameToken("E");
|
public static readonly NameToken E = new NameToken("E");
|
||||||
public static readonly NameToken EarlyChange = new NameToken("EarlyChange");
|
public static readonly NameToken EarlyChange = new NameToken("EarlyChange");
|
||||||
public static readonly NameToken Ef = new NameToken("EF");
|
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 EmbeddedFdfs = new NameToken("EmbeddedFDFs");
|
||||||
public static readonly NameToken EmbeddedFiles = new NameToken("EmbeddedFiles");
|
public static readonly NameToken EmbeddedFiles = new NameToken("EmbeddedFiles");
|
||||||
public static readonly NameToken Empty = new NameToken("");
|
public static readonly NameToken Empty = new NameToken("");
|
||||||
@@ -527,6 +531,7 @@
|
|||||||
public static readonly NameToken UserUnit = new NameToken("UserUnit");
|
public static readonly NameToken UserUnit = new NameToken("UserUnit");
|
||||||
// V
|
// V
|
||||||
public static readonly NameToken V = new NameToken("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 VerisignPpkvs = new NameToken("VeriSign.PPKVS");
|
||||||
public static readonly NameToken Version = new NameToken("Version");
|
public static readonly NameToken Version = new NameToken("Version");
|
||||||
public static readonly NameToken Vertices = new NameToken("Vertices");
|
public static readonly NameToken Vertices = new NameToken("Vertices");
|
||||||
|
Reference in New Issue
Block a user