Skip to content

Commit 98ee2e9

Browse files
authored
Merge pull request ststeiger#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.
Binary file not shown.
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);

PdfSharpCore/Pdf.IO/Lexer.cs

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
using System.IO;
3535
using PdfSharpCore.Internal;
3636
using PdfSharpCore.Pdf.Internal;
37+
using System.Collections.Generic;
38+
using System.Linq;
3739

3840
namespace PdfSharpCore.Pdf.IO
3941
{
@@ -170,6 +172,19 @@ public Symbol ScanNextToken()
170172
/// Reads the raw content of a stream.
171173
/// </summary>
172174
public byte[] ReadStream(int length)
175+
{
176+
var pos = MoveToStartOfStream();
177+
_pdfSteam.Position = pos;
178+
byte[] bytes = new byte[length];
179+
int read = _pdfSteam.Read(bytes, 0, length);
180+
Debug.Assert(read == length);
181+
182+
// Synchronize idxChar etc.
183+
Position = pos + length;
184+
return bytes;
185+
}
186+
187+
internal long MoveToStartOfStream()
173188
{
174189
long pos;
175190

@@ -187,15 +202,45 @@ public byte[] ReadStream(int length)
187202
}
188203
else
189204
pos = _idxChar + 1;
205+
return pos;
206+
}
190207

191-
_pdfSteam.Position = pos;
192-
byte[] bytes = new byte[length];
193-
int read = _pdfSteam.Read(bytes, 0, length);
194-
Debug.Assert(read == length);
208+
/// <summary>
209+
/// Scans the input stream for the specified marker.<br></br>
210+
/// Returns the bytes from the current position up to the start of the marker or the end of the stream.<br></br>
211+
/// The position of the input-stream is the byte right after the marker (if found) or the end of the stream.
212+
/// </summary>
213+
/// <param name="marker">The marker to scan for</param>
214+
/// <param name="markerFound">Receives a boolean that indicates whether the marker was found</param>
215+
/// <returns></returns>
216+
internal byte[] ScanUntilMarker(byte[] marker, out bool markerFound)
217+
{
218+
markerFound = false;
219+
var result = new List<byte>();
220+
while (true)
221+
{
222+
var markerIndex = 0;
223+
while (_currChar != Chars.EOF && _currChar != marker[markerIndex])
224+
{
225+
result.Add((byte)_currChar);
226+
ScanNextChar(false);
227+
}
228+
while (_currChar != Chars.EOF && markerIndex < marker.Length && _currChar == marker[markerIndex])
229+
{
230+
markerIndex++;
231+
ScanNextChar(false);
232+
}
233+
if (_currChar == Chars.EOF || markerIndex == marker.Length)
234+
{
235+
if (markerIndex == marker.Length)
236+
markerFound = true;
237+
break;
238+
}
239+
// only part of the marker was found, add to result and continue
240+
result.AddRange(marker.Take(markerIndex));
241+
}
195242

196-
// Synchronize idxChar etc.
197-
Position = pos + length;
198-
return bytes;
243+
return result.ToArray();
199244
}
200245

201246
/// <summary>
@@ -722,7 +767,6 @@ public char MoveToNonWhiteSpace()
722767
}
723768
return _currChar;
724769
}
725-
726770
// #if DEBUG
727771
// public string SurroundingsOfCurrentPosition(bool hex)
728772
// {

PdfSharpCore/Pdf.IO/Parser.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
using PdfSharpCore.Exceptions;
3636
using PdfSharpCore.Internal;
3737
using PdfSharpCore.Pdf.Advanced;
38+
using PdfSharpCore.Pdf.Internal;
3839
using PdfSharpCore.Pdf.IO.enums;
3940

4041
namespace PdfSharpCore.Pdf.IO
@@ -272,6 +273,7 @@ public PdfObject ReadObject(PdfObject pdfObject, PdfObjectID objectID, bool incl
272273
#if true_
273274
ReadStream(dict);
274275
#else
276+
var startOfStream = _lexer.Position;
275277
int length = GetStreamLength(dict);
276278
byte[] bytes = _lexer.ReadStream(length);
277279
#if true_
@@ -301,7 +303,21 @@ public PdfObject ReadObject(PdfObject pdfObject, PdfObjectID objectID, bool incl
301303
#endif
302304
PdfDictionary.PdfStream stream = new PdfDictionary.PdfStream(bytes, dict);
303305
dict.Stream = stream;
304-
ReadSymbol(Symbol.EndStream);
306+
try
307+
{
308+
ReadSymbol(Symbol.EndStream);
309+
}
310+
catch (PdfReaderException)
311+
{
312+
// stream length may be incorrect, scan byte by byte up to the "endstream" keyword
313+
_lexer.Position = startOfStream;
314+
_lexer.Position = _lexer.MoveToStartOfStream();
315+
bytes = _lexer.ScanUntilMarker(PdfEncoders.RawEncoding.GetBytes("\nendstream"), out var markerFound);
316+
if (!markerFound)
317+
throw;
318+
stream = new PdfDictionary.PdfStream(bytes, dict);
319+
dict.Stream = stream;
320+
}
305321
symbol = ScanNextToken();
306322
#endif
307323
if (symbol == Symbol.Eof)

PdfSharpCore/Pdf.IO/PdfWriter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,8 @@ public void Write(PdfString value)
198198
WriteSeparator(CharCat.Delimiter);
199199
PdfStringEncoding encoding = (PdfStringEncoding)(value.Flags & PdfStringFlags.EncodingMask);
200200
string pdf = (value.Flags & PdfStringFlags.HexLiteral) == 0 ?
201-
PdfEncoders.ToStringLiteral(value.Value, encoding, SecurityHandler) :
202-
PdfEncoders.ToHexStringLiteral(value.Value, encoding, SecurityHandler);
201+
PdfEncoders.ToStringLiteral(value.EncryptionValue, encoding == PdfStringEncoding.Unicode, SecurityHandler) :
202+
PdfEncoders.ToHexStringLiteral(value.EncryptionValue, encoding == PdfStringEncoding.Unicode, SecurityHandler);
203203
WriteRaw(pdf);
204204

205205
_lastCat = CharCat.Delimiter;

PdfSharpCore/Pdf.Internal/RawUnicodeEncoding.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ public override int GetCharCount(byte[] bytes, int index, int count)
6464

6565
public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
6666
{
67-
for (int count = byteCount; count > 0; byteIndex += 2, charIndex++, count--)
67+
for (int count = byteCount; count > 0; byteIndex += 2, charIndex++, count -= 2)
6868
{
69-
chars[charIndex] = (char)((int)bytes[byteIndex] << 8 + (int)bytes[byteIndex + 1]);
69+
chars[charIndex] = (char)((int)(bytes[byteIndex] << 8) + (int)bytes[byteIndex + 1]);
7070
}
7171
return byteCount;
7272
}

0 commit comments

Comments
 (0)