Skip to content

Commit 98ee2e9

Browse files
authored
Merge pull request #274 from packdat/EnhancedEncryption
Enhance encryption capabilities
2 parents 4d8dea0 + 6033e93 commit 98ee2e9

23 files changed

+1170
-85
lines changed
61 KB
Binary file not shown.
17.4 KB
Binary file not shown.
19 KB
Binary file not shown.
22.8 KB
Binary file not shown.
23.5 KB
Binary file not shown.

PdfSharpCore.Test/IO/PdfReader.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
using System;
2-
using System.IO;
3-
using FluentAssertions;
1+
using FluentAssertions;
42
using PdfSharpCore.Pdf;
53
using PdfSharpCore.Pdf.IO;
64
using PdfSharpCore.Test.Helpers;
5+
using System;
6+
using System.IO;
77
using Xunit;
88

99
namespace PdfSharpCore.Test.IO
@@ -26,11 +26,12 @@ public void WillThrowExceptionWhenReadingInvalidPdf()
2626
act.Should().Throw<InvalidOperationException>().WithMessage("The file is not a valid PDF document.");
2727
}
2828

29-
private void AssertIsAValidPdfDocumentWithProperties(PdfDocument inputDocument, int expectedFileSize)
29+
internal static void AssertIsAValidPdfDocumentWithProperties(PdfDocument inputDocument, int expectedFileSize)
3030
{
3131
inputDocument.Should().NotBeNull();
3232
inputDocument.FileSize.Should().Be(expectedFileSize);
3333
inputDocument.Info.Should().NotBeNull();
34+
inputDocument.PageCount.Should().BeGreaterThan(0);
3435
}
3536
}
3637
}

PdfSharpCore.Test/PdfSharpCore.Test.csproj

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
</ItemGroup>
3131

3232
<ItemGroup>
33+
<None Update="Assets\AesEncrypted.pdf">
34+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
35+
</None>
3336
<None Update="Assets\FamilyTree.pdf">
3437
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3538
</None>
@@ -39,6 +42,18 @@
3942
<None Update="Assets\NotAValid.pdf">
4043
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
4144
</None>
45+
<None Update="Assets\protected-adobe.pdf">
46+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
47+
</None>
48+
<None Update="Assets\protected-ilovepdf.pdf">
49+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
50+
</None>
51+
<None Update="Assets\protected-pdfencrypt.pdf">
52+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
53+
</None>
54+
<None Update="Assets\protected-sodapdf.pdf">
55+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
56+
</None>
4257
<None Update="Assets\test.pdf">
4358
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
4459
</None>

PdfSharpCore.Test/Security/PdfSecurity.cs

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1-
using System.IO;
2-
using FluentAssertions;
1+
using FluentAssertions;
32
using PdfSharpCore.Drawing;
43
using PdfSharpCore.Pdf;
54
using PdfSharpCore.Pdf.IO;
65
using PdfSharpCore.Pdf.Security;
6+
using PdfSharpCore.Test.Helpers;
7+
using System.IO;
78
using Xunit;
9+
using Xunit.Abstractions;
810

911
namespace PdfSharpCore.Test.Security
1012
{
1113
public class PdfSecurity
1214
{
15+
private readonly ITestOutputHelper output;
16+
17+
public PdfSecurity(ITestOutputHelper testOutputHelper)
18+
{
19+
output = testOutputHelper;
20+
}
21+
1322
[Theory]
1423
[InlineData(PdfDocumentSecurityLevel.Encrypted40Bit, "hunter1")]
1524
[InlineData(PdfDocumentSecurityLevel.Encrypted128Bit, "hunter1")]
@@ -19,6 +28,8 @@ public void CreateAndReadPasswordProtectedPdf(PdfDocumentSecurityLevel securityL
1928
var pageNewRenderer = document.AddPage();
2029
var renderer = XGraphics.FromPdfPage(pageNewRenderer);
2130
renderer.DrawString("Test Test Test", new XFont("Arial", 12), XBrushes.Black, new XPoint(12, 12));
31+
// validate correct handling of unicode strings (issue #264)
32+
document.Outlines.Add("The only page", pageNewRenderer);
2233
document.SecuritySettings.DocumentSecurityLevel = securityLevel;
2334
document.SecuritySettings.UserPassword = password;
2435

@@ -30,6 +41,86 @@ public void CreateAndReadPasswordProtectedPdf(PdfDocumentSecurityLevel securityL
3041
delegate(PdfPasswordProviderArgs args) { args.Password = password; });
3142

3243
loadDocument.PageCount.Should().Be(1);
44+
loadDocument.Outlines[0].Title.Should().Be("The only page");
45+
loadDocument.Info.Producer.Should().Contain("PDFsharp");
46+
}
47+
48+
[Fact]
49+
public void ShouldBeAbleToOpenAesEncryptedDocuments()
50+
{
51+
// this document has a V value of 4 (see PdfReference 1.7, Chapter 7.6.1, Table 20)
52+
// and an R value of 4 (see PdfReference 1.7, Chapter 7.6.3.2, Table 21)
53+
// see also: Adobe Supplement to the ISO 32000, BaseVersion: 1.7, ExtensionLevel: 3
54+
// Chapter 3.5.2, Table 3.19
55+
var file = PathHelper.GetInstance().GetAssetPath("AesEncrypted.pdf");
56+
var fi = new FileInfo(file);
57+
var document = Pdf.IO.PdfReader.Open(file, PdfDocumentOpenMode.Import);
58+
59+
// verify document was actually AES-encrypted
60+
var cf = document.SecurityHandler.Elements.GetDictionary("/CF");
61+
var stdCf = cf.Elements.GetDictionary("/StdCF");
62+
stdCf.Elements.GetString("/CFM").Should().Be("/AESV2");
63+
64+
IO.PdfReader.AssertIsAValidPdfDocumentWithProperties(document, (int)fi.Length);
65+
}
66+
67+
[Fact]
68+
public void DocumentWithUserPasswordCannotBeOpenedWithoutPassword()
69+
{
70+
var file = PathHelper.GetInstance().GetAssetPath("AesEncrypted.pdf");
71+
var document = Pdf.IO.PdfReader.Open(file, PdfDocumentOpenMode.Import);
72+
73+
// import pages into a new document
74+
var encryptedDoc = new PdfDocument();
75+
foreach (var page in document.Pages)
76+
encryptedDoc.AddPage(page);
77+
78+
// save enrypted
79+
encryptedDoc.SecuritySettings.UserPassword = "supersecret!11";
80+
var saveFileName = PathHelper.GetInstance().GetAssetPath("SavedEncrypted.pdf");
81+
encryptedDoc.Save(saveFileName);
82+
83+
// should throw because no password was provided
84+
var ex = Assert.Throws<PdfReaderException>(() =>
85+
{
86+
var readBackDoc = Pdf.IO.PdfReader.Open(saveFileName, PdfDocumentOpenMode.Import);
87+
});
88+
ex.Message.Should().Contain("A password is required to open the PDF document");
89+
90+
// check with password
91+
// TODO: should be checked in a separate test, but i was lazy...
92+
var fi = new FileInfo(saveFileName);
93+
var readBackDoc = Pdf.IO.PdfReader.Open(saveFileName, "supersecret!11", PdfDocumentOpenMode.Import);
94+
IO.PdfReader.AssertIsAValidPdfDocumentWithProperties(readBackDoc, (int)fi.Length);
95+
readBackDoc.PageCount.Should().Be(document.PageCount);
96+
}
97+
98+
// Same PDF protected by different tools or online-services
99+
[Theory]
100+
// https://www.ilovepdf.com/protect-pdf, 128 bit, /V 2 /R 3
101+
[InlineData(@"protected-ilovepdf.pdf", "test123")]
102+
103+
// https://www.adobe.com/de/acrobat/online/password-protect-pdf.html, 128 bit, /V 4 /R 4
104+
[InlineData(@"protected-adobe.pdf", "test123")]
105+
106+
// https://pdfencrypt.net, 256 bit, /V 5 /R 5
107+
[InlineData(@"protected-pdfencrypt.pdf", "test123")]
108+
109+
// https://www.sodapdf.com/password-protect-pdf/
110+
// this is the only tool tested, that encrypts with the latest known algorithm (256 bit, /V 5 /R 6)
111+
// Note: SodaPdf also produced a pdf that would be considered "invalid" by PdfSharp, because of incorrect stream-lengths
112+
// (in the Stream-Dictionary, the length was reported as 32, but in fact the length was 16)
113+
// this needed to be handled as well
114+
[InlineData(@"protected-sodapdf.pdf", "test123")]
115+
public void CanReadPdfEncryptedWithSupportedAlgorithms(string fileName, string password)
116+
{
117+
var path = PathHelper.GetInstance().GetAssetPath(fileName);
118+
119+
var doc = Pdf.IO.PdfReader.Open(path, password, PdfDocumentOpenMode.Import);
120+
doc.Should().NotBeNull();
121+
doc.PageCount.Should().BeGreaterThan(0);
122+
output.WriteLine("Creator : {0}", doc.Info.Creator);
123+
output.WriteLine("Producer: {0}", doc.Info.Producer);
33124
}
34125
}
35126
}

PdfSharpCore/Pdf.Advanced/PdfObjectStream.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ public PdfObjectStream(PdfDocument document)
5656
internal PdfObjectStream(PdfDictionary dict)
5757
: base(dict)
5858
{
59+
// while objects inside an object-stream are not encrypted, the object-streams themself ARE !
60+
// 7.5.7, Page 47: In an encrypted file (i.e., entire object stream is encrypted),
61+
// strings occurring anywhere in an object stream shall not be separately encrypted.
62+
if (_document._trailer.Elements[PdfTrailer.Keys.Encrypt] is PdfReference)
63+
_document.SecurityHandler.EncryptObject(dict);
64+
5965
int n = Elements.GetInteger(Keys.N);
6066
int first = Elements.GetInteger(Keys.First);
6167
Stream.TryUnfilter();

PdfSharpCore/Pdf.Advanced/PdfTrailer.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -201,15 +201,9 @@ internal void Finish()
201201
iref = _document._trailer.Elements[Keys.Encrypt] as PdfReference;
202202
if (iref != null)
203203
{
204-
iref = _document._irefTable[iref.ObjectID];
205-
Debug.Assert(iref.Value != null);
206-
_document._trailer.Elements[Keys.Encrypt] = iref;
207-
208-
// The encryption dictionary (security handler) was read in before the XRefTable construction
209-
// was completed. The next lines fix that state (it took several hours to find these bugs...).
210-
iref.Value = _document._trailer._securityHandler;
211-
_document._trailer._securityHandler.Reference = iref;
212-
iref.Value.Reference = iref;
204+
_document._irefTable.Remove(_document._irefTable[iref.ObjectID]);
205+
_document._irefTable.Add(_document._trailer._securityHandler);
206+
_document._trailer.Elements[Keys.Encrypt] = _document._trailer._securityHandler;
213207
}
214208

215209
Elements.Remove(Keys.Prev);

0 commit comments

Comments
 (0)