Table of contents
In this article, I will discuss a PDF password recovery tool that I created in WPF using Visual Studio 2015. Before I go further, I want to emphasize that the PDF password recovery tool is not intended to hack anyone's PDF password. The main purpose of this tool is to recover lost PDF passwords. Furthermore this article should give you insight on how to better protect your PDF documents. The most recent code can be found in my Github repository here, the ClickOnce deployment can be launched here.
This password recovery tool is a Windows 10 desktop application which provides a way of recovering PDF passwords. It uses the itextsharp library to retrieve the hashed user and owner password, after which they are used to validate potential passwords. The application user can supply potential passwords in four different ways, manually by clicking the lock icon, using dictionaries, regular expressions and using a brute force approach. Several print screens of the application are shown below. You can click the pictures to enlarge them.
Figure 1: The dictionary editor.
Figure 2: The regular expression editor.
Figure 3: The brute force editor.
Before discussing the application architecture, I will discuss some PDF security features that are used by this recovery tool. PDF files are decrypted using an encryption key that is generated from the user's password. When the correct password is entered, a program like Adobe Reader calculates the key and uses it to decrypt and display the PDF file. If an invalid password is entered, the key generated will also be wrong and the file cannot be decrypted. Hence, in order to access a password protected PDF all you have to do is find the encryption key. The maximum number of keys that must be tested in order to find the correct encryption key can be calculated using the formula 2key length. Although the encryption key collection is finite, a direct key attack is only feasible for PDF files created in Acrobat 2-4 (RC4 40-bit key length). The password recovery tool discussed in this article does not use a direct key attack to access a password protected PDF. Instead, it compares the hashed user and owner password extracted from the PDF with potential passwords supplied by the application user. The algorithm to validate potential passwords depends on the encryption type of the PDF file. Table 1 shows an overview of all the different Adobe PDF formats and their corresponding encryption type.
Table 1: Encryption type used by different Adobe PDF versions.
As can be seen in table 1, early versions of the Adobe PDF format used the RC4 40-bit algorithm for encryption.
By today’s standards, RC4 40-bit encryption algorithm has a short 40-bit key, this weakness allows a direct attack on the encryption key. As a result of this, the RC4 40-bit encryption key of a password protected PDF can be recovered within a day.
Subsequent versions of the Adobe PDF format used the RC4 128-bit and the AES 128-bit algorithm for encryption. The major improvement of these encryption algorithms was that the length of the encryption key was increased to 128 bits. Another improvement was the key stretching algorithm, MD5 + RC4 were replaced with 50xMD5 + 20xRC4.
Adobe Acrobat 9 introduced a new format, Adobe PDF 1.7 Extension Level 3, with improved security. The encryption key was increased to 256 bits and the MD5 hash algorithm was replaced with SHA-256. However, due to the lack of a key stretching algorithm, the encryption algorithm is vulnerable to a brute force attack, as can be seen in graph 1.
The latest versions of the Adobe PDF format drastically improved the security of encrypted PDF files. Adobe has brought back the key stretching algorithm therebye making it more time consuming to validate potential passwords. In the latest PDF versions, the encryption key is generated by a single iteration of SHA-256 followed by a variable set of key transformations using the algorithms SHA-256, SHA-384 and SHA-512. In addition, AES key encryption has been added. The corresponding password validation algorithm is shown in code snippet 9.
The password recovery tool uses the MVVM and IOC concept to ensure that the code (application logic) is effectively decoupled from the View. Each View has a corresponding ViewModel which contains the code. Communication between Views and ViewModels is accomplished through data binding. In addition, ViewModels can interact with Views through the various service interfaces referenced in the ViewModelBase class. The referenced service interfaces are:
- IFileService allows the ViewModel to show the CommonOpenFileDialog to let the user select one or more file(s) for opening . This dialog is present in the Microsoft.WindowsAPICodecPack.Shell package which is available as a nuget package.
- IDialogService allows the ViewModel to show a Window with its corresponding ViewModel.
- IFolderBrowserService allows the ViewModel to show the CommonOpenFileDialog to let the user select a directory.
All ViewModels inherit from the ViewModelBase class and therefore have access to the defined services.
protected readonly IFileService fileService;
protected readonly IDialogService dialogService;
protected readonly IFolderBrowserService folderPickerService;
public ViewModelBase()
{
fileService = ServiceContainer.Instance.GetService<IFileService>();
dialogService = ServiceContainer.Instance.GetService<IDialogService>();
folderPickerService = ServiceContainer.Instance.GetService<IFolderBrowserService>();
}
Code snippet 1: The services that are made available by the ViewModelBase class.
In the Code snippet below, the IDialogService is used to show the PDF properties Window. The datacontext of this Window is set to its corresponding ViewModel.
private async void OnShowFilePropertiesCmdExecute()
{
FilePropertiesView filePropertiesView = new FilePropertiesView();
FilePropertiesViewModel filePropertiesViewModel =
new FilePropertiesViewModel(filePropertiesView, SelectedFile);
bool? result = await dialogService.InitDialog(filePropertiesView, filePropertiesViewModel);
filePropertiesViewModel.Dispose();
}
Code snippet 2: The IDialogService is used to show the PDF properties Window.
The dictionary, regular expression and brute force editor are contained within a TabControl. Each TabItem of this control is bound to one of the following ViewModel types: DictionaryManagerViewModel, SmartManagerViewModel or the BruteForceManagerViewModel. DataTemplates are used to render (tell WPF how to draw) each ViewModel with a specific UserControl. This approach keeps the business logic (ViewModels) completely separate from the UI (Views). The TabControl is defined in the MainView as follows:
<TabControl Background="LightGray"
TabStripPlacement="Left"
ItemsSource="{Binding TabItems, Mode=OneWay}"
SelectedItem="{Binding SelectedTabItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type viewmodels:DictionaryManagerViewModel}">
<views:DictionaryManagerView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:SmartManagerViewModel}">
<views:SmartManagerView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:BruteForceManagerViewModel}">
<views:BruteForceManagerView />
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}" >
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal"
FlowDirection="LeftToRight">
<Rectangle Width="16"
Height="16"
VerticalAlignment="Center"
Stretch="Uniform"
Fill="{Binding HeaderIcon}"/>
<TextBlock Width="80"
FontSize="20"
Foreground="Black"
Margin="10 0"
Text="{Binding Header}" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
Code snippet 3: The dictionary, regular expression and brute force editor are contained within a TabControl.
The TabControl ItemsSource is bound to a collection of ViewModels. This collection is created in the MainViewModel.
public MainViewModel()
{
TabItems = new ObservableCollection<ITabViewModel>();
TabItems.Add(new DictionaryManagerViewModel("List",
Application.Current.TryFindResource("Dictionary") as DrawingBrush));
TabItems.Add(new SmartManagerViewModel("Smart",
Application.Current.TryFindResource("Smart") as DrawingBrush));
TabItems.Add(new BruteForceManagerViewModel("Brute",
Application.Current.TryFindResource("BruteForce") as DrawingBrush));
SelectedTabItem = TabItems.First();
}
private ObservableCollection<ITabViewModel> tabItems = null;
public ObservableCollection<ITabViewModel> TabItems
{
get { return tabItems; }
private set { SetProperty(ref this.tabItems, value); }
}
private ITabViewModel selectedTabItem = null;
public ITabViewModel SelectedTabItem
{
get { return selectedTabItem; }
set { SetProperty(ref this.selectedTabItem, value); }
}
Code snippet 4: Creating TabItems which are bound to the TabControl.
Each TabItem needs to implement the interfaces ITabViewModel. This interface mandates that the ViewModel implements three properties. The HeaderIcon and Header property are used for the Header property of the TabItem. The PasswordIterator property is used to reference a class that implements the IPasswordIterator interface, which can be one of the following classes: the Dictionary, Password or the RegularExpression class.
public interface ITabViewModel
{
string Header { get; set; }
DrawingBrush HeaderIcon { get; set; }
IPasswordIterator PasswordIterator { get; set; }
}
Code snippet 5: ITabViewModel interface.
The application user can select one of the three main Views on top of the application, the Home, the Settings and the About View. When the application user clicks one of the View buttons, a flag is set and the corresponding grid visibility is set to visible, as shown in the code snippet below.
-->
<Grid Grid.Row="2"
Background="LightGray">
<Grid.Visibility>
<Binding Path="SelectedView"
Mode="OneWay"
UpdateSourceTrigger="PropertyChanged"
ConverterParameter="{x:Static common:AvailableViews.Home}">
<Binding.Converter>
<converters:EnumToVisibilityConverter></converters:EnumToVisibilityConverter>
</Binding.Converter>
</Binding>
</Grid.Visibility>
Code snippet 6: The SelectedView property determines which grid is visible.
PDF documents have a trailer dictionary which holds references to important objects and optionally to an encryption dictionary. If the encryption dictionary is present, it contains the information needed to validate potential passwords.
Figure 4: Example of a PDF trailer and encryption dictionary.
In order to retrieve the encryption dictionary from a PDF file, the itextsharp library was modified. As can be seen in the code snippet below, the new added method returns PDF encryption data needed to validate passwords. The modified iTextSharp library can be found here.
public PdfReader(String filename,
out char pdfversion,
out byte[] documentID,
out byte[] uValue,
out byte[] oValue,
out long pValue,
out int rValue,
out int cryptoMode,
out bool encrypted,
out int lengthValue)
{
IRandomAccessSource byteSource = null;
this.certificateKey = null;
this.certificate = null;
this.password = null;
this.partial = false;
try
{
byteSource = new RandomAccessSourceFactory()
.SetForceRead(false)
.CreateBestSource(filename);
tokens = GetOffsetTokeniser(byteSource);
ReadPdf();
}
catch (iTextSharp.text.exceptions.BadPasswordException)
{
this.encrypted = true;
}
catch (IOException e)
{
const int rValAes256Iso = 6;
if (e.Message ==
MessageLocalization.GetComposedMessage("unknown.encryption.type.r.eq.1", rValAes256Iso))
{
this.encrypted = true;
byteSource.Close();
}
else
{
byteSource.Close();
throw e;
}
}
finally
{
uValue = this.uValue;
oValue = this.oValue;
pValue = this.pValue;
rValue = this.rValue;
encrypted = this.encrypted;
cryptoMode = this.cryptoMode;
documentID = this.documentID;
lengthValue = this.lengthValue;
pdfversion = this.pdfVersion;
GetCounter().Read(fileLength);
}
}
Code snippet 7: The modified PdfReader.cs of the iTextSharp library enables us to retrieve PDF encryption data.
Encryption data retrieved from the PDF file can be viewed by double clicking the PDF entry in the PDF file datagrid.
Figure 5: Encryption data retrieved from the PDF file using the iTextSharp library.
The password validation process works as follows: a new password is processed using a dedicated algorithm, the resulting hashed password is then compared with the hashed password extracted from the PDF encryption dictionary as described in the previous section. If they are equal, the correct password is found and can be used to open the decrypted PDF file. The dedicated algorithm used to calculate the hashed password depends on the encryption type of the PDF and is obtained using the factory pattern, as shown in the code snippet below.
public class DecryptorFactory
{
#region [ Methods ]
public static IDecryptor Get(EncryptionRecord encryptionInfo)
{
switch (encryptionInfo.encryptionType)
{
case PdfEncryptionType.StandardEncryption40Bit:
return new RC4Decryptor40Bit(encryptionInfo);
case PdfEncryptionType.StandardEncryption128Bit:
case PdfEncryptionType.AesEncryption128Bit:
return new RC4Decryptor128Bit(encryptionInfo);
case PdfEncryptionType.AesEncryption256Bit:
return new AESDecryptor256Bit(encryptionInfo);
case PdfEncryptionType.AesIsoEncryption256Bit :
return new AESISODecryptor256Bit(encryptionInfo);
default:
throw new Exception("Unsupported encryption type.");
}
}
#endregion
}
Code snippet 8: The algorithm used to calculate the hashed password is obtained using the factory pattern.
One example of a dedicated algorithm used to calculate and validate a potential password is shown below. This algorithm is used in PDF 1.7 Extension Level 5 and 8 and has a strong key stretching algorithm.
private PasswordValidity ValidateUserPassword(string userPassword)
{
byte[] paddedPassword = null, password = null;
password = Encoding.UTF8.GetBytes(userPassword);
Array.Resize(ref password, Math.Min(password.Length, Constants.MaxPasswordSizeV2));
paddedPassword = new byte[password.Length + Constants.SaltLength];
Array.Copy(password, 0, paddedPassword, 0, password.Length);
Array.Copy(EncryptionInfo.uValue, Constants.SaltOffset, paddedPassword,
password.Length, Constants.SaltLength);
byte[] hash = ValidatePassword(paddedPassword, password, new byte[0]);
if (arrayMath.ArraysAreEqual(hash, EncryptionInfo.uValue, Constants.CompareSize))
return PasswordValidity.UserPasswordIsValid;
else
return PasswordValidity.Invalid;
}
private byte[] ValidatePassword(byte[] paddedPassword, byte[] password, byte [] uValue)
{
byte[] key = new byte[Constants.KeySize];
byte[] iv = new byte[Constants.VectorSize];
byte[] E16 = new byte[16];
byte[] array = null;
byte[] E = null;
byte[] K1 = null;
int idx = 0;
byte[] K = sha256.ComputeHash(paddedPassword, 0, paddedPassword.Length);
while (idx < 64 || E[E.Length - 1] + 32 > idx)
{
Array.Copy(K, key, key.Length);
Array.Copy(K, 16, iv, 0, iv.Length);
K1 = new byte[(password.Length + K.Length + uValue.Length) * 64];
array = arrayMath.ConcatByteArrays(password, K);
array = arrayMath.ConcatByteArrays(array, uValue);
for (int j = 0, pos = 0; j < 64; j++, pos += array.Length)
{
Array.Copy(array, 0, K1, pos, array.Length);
}
E = aes.CreateEncryptor(key, iv).TransformFinalBlock(K1, 0, K1.Length);
Array.Copy(E, E16, E16.Length);
BigInteger bigInteger = new BigInteger(E16.Reverse().Concat(new byte[] { 0x00 }).ToArray());
byte[] result = BigInteger.Remainder(bigInteger, 3).ToByteArray();
switch (result[0])
{
case 0x00:
K = sha256.ComputeHash(E, 0, E.Length);
break;
case 0x01:
K = sha384.ComputeHash(E, 0, E.Length);
break;
case 0x02:
K = sha512.ComputeHash(E, 0, E.Length);
break;
default:
throw new Exception("Unexpected result while computing the remainder, modulo 3.");
}
idx++;
}
return K;
}
Code snippet 9: PDF 1.7 Extension Level 5 and 8 has a strong key stretching algorithm.
If a password successfully passes validation, it will be saved in the location you specified in the Settings View.
The dictionary editor allows you to add text files containing a list of passwords. All the added files are put in an Observable Collection that is bound to the dictionary editor view. When the decryption process starts, each text file in the Observable Collection is read using a StreamReader and subsequently validated. In the settings view you can configure the casing of passwords read from dictionaries. You could for example change the casing to UPPERCASE, lowercase or Titlecase.
public string GetNextPassword()
{
string result = string.Empty;
if (reader != null)
{
while((result = reader.ReadLine()) != null &&
(string.IsNullOrEmpty(result) ||
string.IsNullOrWhiteSpace(result))) { };
if (result == null)
{
reader.Close();
reader = null;
}
else
{
Progress++;
}
}
return result;
}
Code snippet 10: Reading passwords from a text file using a StreamReader.
Figure 2 shows the regular expression editor. You can add a new regular expression by clicking the plus button, which opens the regular expression editor shown below. This editor allows you to enter a regular expression which is then used to generate all possible strings that match the expression. The regular expression editor allows you to preview the regular expression matches by pressing the start button.
Figure 6: The regular expression editor.
In order to generate all possible strings that match a regular expression, I used the Generex library which can be downloaded here. There was one small challenge though: the Generex library needed to be converted from java to c#. I accomplished this using IKVM.NET framework which can be downloaded here.
Generex generex = new Generex("[0-3]([a-c]|[e-g]{1,2})");
Iterator iterator = generex.iterator();
while (iterator.hasNext())
{
System.out.print(iterator.next() + " ");
}
Code snippet 11: Example of using the Generex iterator.
The brute force editor allows you to specify a charset and a password length. Optionally you can specify a sweep direction, such as increasing or decreasing the password length. After you have filled in the brute force editor you can click the preview button, after which a summary of the used charset and the expected iterations are shown. As you can see in figure 3, the initial password length is set to 8 characters. This value is based on a password research article found here. The graph below illustrates the password length distribution for gmail accounts.
Figure 7: The password length distribution for gmail accounts.
The brute force iteration algorithm is shown below, which is based on this article.
public string GetNextPassword()
{
string result = string.Empty;
while(IteratorCounters.Any() &&
CurrentIndex < IteratorCounters.Count &&
IteratorCounters[CurrentIndex].Status != IteratorStatus.Good)
{
CurrentIndex++;
}
if(IteratorCounters.Any() &&
CurrentIndex < IteratorCounters.Count &&
IteratorCounters[CurrentIndex].Status == IteratorStatus.Good)
{
ulong val = IteratorCounters[CurrentIndex].Count;
for (int j = 0; j < IteratorCounters[CurrentIndex].PasswordLenght; j++)
{
int ch = (int)(val % (ulong)charactersToUse.Count);
result = charactersToUse[ch].CharValue + result;
val = val / (ulong)charactersToUse.Count;
}
iteratorCounters[CurrentIndex].Count++;
if (iteratorCounters[CurrentIndex].Count >=
iteratorCounters[CurrentIndex].MaxCount)
IteratorCounters[CurrentIndex].Status = IteratorStatus.Finished;
}
return result;
}
Code snippet 12: The brute force iteration algorithm used to generate passwords.
The charset used by the brute force iteration algorithm can be configured by the application user, as can be seen in figure 3. For example the user can choose to only use digits by checking the corresponding checkbox. Alternatively, the user can specify the charset in unicode by selecting the unicode radio button. The entered unicode range is validated using one of the two regular expressions, which are shown in the code snippet below.
When the encryptiontype of a PDF file is AES256 or AES256ISO, its corresponding password can consist out of any combination of unicode characters. For other PDF encryptiontypes, only ASCII (U0000-U00FF) characters are allowed.
private static readonly string unicodeRangePattern =
@"^(([a-f0-9]{4}-[a-f0-9]{4},)*|([a-f0-9]{4},)*)*((([a-f0-9]{4},)*([a-f0-9]{4}))|([a-f0-9]{4}-[a-f0-9]{4})){1};?$";
private static readonly string asciiUnicodeRangePattern =
@"^((00[a-f0-9]{2}-00[a-f0-9]{2},)*|(00[a-f0-9]{2},)*)*(((00[a-f0-9]{2},)*(00[a-f0-9]{2}))|(00[a-f0-9]{2}-00[a-f0-9]{2})){1};?$";
Code snippet 13: Regular expressions used to validate the entered unicode charset.
After you have configured the brute force editor, you can click the preview button in the brute force editor, after which a summary view of the used charset and the expected iteration count is shown. In the example below, the used charset only consists of digits and the password length ranges between 1 and 15. As you can see from the summary view below, the password iteration sequence is symmetrical, which means that the iteration starts with a password length of 8 digits, next a password length of 9 digits will be iterated and then a password length of 7 digits.
Figure 8: Brute force iteration summary view
As mentioned earlier, the password validation speed depends to a great extent on the encryption algorithm used.
In the graph below a summary of the password validation speed for different encryption algorithms is shown. As can be seen from the graph below, the PDF password recovery tool reaches its highest validation speed when processing Adobe PDF 1.7 Extension Level 3 files. You can click on the graph to enlarge it.
Graph 1: Speedtest (Intel T4200, @2.00 GHz), number of user passwords that can be validated per minute.
Furthermore, the graph shows that the latest versions of the Adobe PDF format offers more protection than earlier versions. The increased protection is caused by the new introduced key stretching algorithm, which reduces the validation speed.
The Icons used in the PDF password recovery tool are derived from Vector Icons. If you look on the internet and type the word "SVG Icons", you will find a lot of websites offering free or paid SVG Icons. The website that I used to download most of the used Icons can be found here. After I downloaded the SVG Icons, I converted them to DrawingBrush objects using ViewerSvg. One advantage of using DrawingBrush objects is the ease of color customization.
<DrawingBrush x:Key="Start">
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="Green" Geometry="M3.004,0L3,46.001 43,22.997z"/>
</DrawingGroup>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
Code snippet 14: SVG Icon converted to a DrawingBrush object using ViewerSvg.
<Button ToolTip="Start preview process."
Margin="8,0"
VerticalAlignment="Center"
Command="{Binding StartCmd}">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal"
FlowDirection="RightToLeft">
<TextBlock Margin="1,0,0,0"
VerticalAlignment="Center"
TextWrapping="NoWrap"
Text="Start"></TextBlock>
<Rectangle Margin="5,0,0,0"
RenderTransformOrigin="0.5,0.5"
Width="16"
Height="16"
VerticalAlignment="Center"
Fill="{StaticResource Start}">
<Rectangle.RenderTransform>
<RotateTransform Angle="180"></RotateTransform>
</Rectangle.RenderTransform>
</Rectangle>
</StackPanel>
</Button>
Code snippet 15: The Start DrawingBrush can be referenced as a Static or Dynamic Resource.
I used the NUnit framework to create unit tests for the password recovery tool. One advantage of this framework is that it allows parameterized testing. Parameterized testing is simply passing values into a test method through method parameters rather than hard coding values within the method itself. As you can see in the code snippet below, you can add multiple TestCase attributes to a single unit test. By adding more TestCase attributes with the appropriate values, you can increase the amount of coverage provided by the test.
[Test]
[TestCase("laboratory-report-rc-40bit-no-userpw.pdf", PdfEncryptionType.StandardEncryption40Bit)]
[TestCase("laboratory-report-rc-128bit-no-userpw.pdf", PdfEncryptionType.StandardEncryption128Bit)]
[TestCase("laboratory-report-aes-128bit-no-userpw.pdf", PdfEncryptionType.AesEncryption128Bit)]
[TestCase("laboratory-report-aes-256bit-no-userpw.pdf", PdfEncryptionType.AesIsoEncryption256Bit)]
[TestCase("unicodeTestcase.pdf", PdfEncryptionType.AesEncryption256Bit)]
public void EncryptionType(string PDF,
PdfEncryptionType pdfEncryptionType)
{
string errorMsg = string.Empty;
string filePath = Path.Combine(TestHelper.DataFolder, PDF);
Assert.IsTrue(File.Exists(filePath));
PdfFile pdfFile = new PdfFile(filePath);
pdfFile.Open(ref errorMsg);
Assert.IsTrue(string.IsNullOrEmpty(errorMsg));
Assert.AreEqual(pdfFile.EncryptionRecordInfo.encryptionType, pdfEncryptionType);
}
Code snippet 16: Parameterized unit tests.
- NUnit Parameterized Testing article
- An alternative for the Generex library is Rex which can be downloaded here.
- Another alternative for the Generex library is Fare which can be downloaded here.
- CPDF is a free tool that can be used to generate encrypted pdfs for testing.
- Mozilla implementation of the ISO 32000-2 algorithm, can be found here.
- The Extended WPF Toolkit can be downloaded here.
- The PDFsharp library can be found here.
- The itextsharp library can be found here.
- The IOC services implementation can be found here.
There are two PDF passwords: user and owner. If the user password is not set, the PDF viewer is expected to check and respect the PDF permissions. However a PDF viewer could completely disregard the PDF permissions and allow all operations. The problem here is that exactly the same data can be used to display the PDF file on the screen or send it to the printer. Therefore the only time when there is some protection is when both the user and owner passwords are set.
In order to create a protected PDF file, I recommend using the Adobe Acrobat X/XI/DC which has a strong key stretching algorithm. Due to the strong key stretching algorithm, a brute force attack on the PDF password is not likely to succeed. Furthermore I recommend setting both the user and owner password when creating a password protected PDF file. It is also important to create a unique password, you can for example use a sentence as a password.
If you are going to use the Extended WPF Toolkit, you need to unblock the Xceed.Wpf.Toolkit.dll after you have downloaded it, as described here.
- May 21st, 2016: Version 1.0.0.0 - Published the article
- June 7th, 2016: Source code added