65.9K
CodeProject 正在变化。 阅读更多。
Home

PDF密码恢复工具:智能、暴力破解和字典。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (12投票s)

2016年5月19日

CPOL

13分钟阅读

viewsIcon

34943

downloadIcon

1914

在本文中,我将讨论一个使用Visual Studio 2015的WPF创建的PDF密码恢复工具。

目录

引言

在本文中,我将讨论一个使用Visual Studio 2015的WPF创建的PDF密码恢复工具。在我继续之前,我需要强调的是,此PDF密码恢复工具并非用于破解任何人的PDF密码。该工具的主要目的是恢复丢失的PDF密码。此外,本文应让您了解如何更好地保护您的PDF文档。最新的代码可以在我的Github存储库中找到,在此,ClickOnce部署可以在此启动。

背景

这个密码恢复工具是一个Windows 10桌面应用程序,提供了一种恢复PDF密码的方法。它使用itextsharp库来检索加密的用户密码和所有者密码的哈希值,然后使用这些哈希值来验证可能的密码。应用程序用户可以通过四种不同的方式提供可能的密码:手动点击锁图标,使用字典,正则表达式和暴力破解方法。下面显示了该应用程序的几张屏幕截图。您可以点击图片放大。

图 1:字典编辑器。

图 2:正则表达式编辑器。

图 3:暴力破解编辑器。

PDF安全性

在讨论应用程序架构之前,我将讨论此恢复工具使用的一些PDF安全功能。PDF文件使用从用户密码生成的加密密钥进行解密。当输入正确的密码时,像Adobe Reader这样的程序会计算密钥,并使用它来解密和显示PDF文件。如果输入了无效密码,生成的密钥也将是错误的,并且文件无法解密。因此,要访问受密码保护的PDF,您只需要找到加密密钥。为了找到正确的加密密钥必须测试的最大密钥数量可以通过公式2密钥长度来计算。虽然加密密钥集合是有限的,但直接密钥攻击仅适用于在Acrobat 2-4(RC4 40位密钥长度)中创建的PDF文件。本文讨论的密码恢复工具不使用直接密钥攻击来访问受密码保护的PDF。相反,它将PDF中提取的用户和所有者密码的哈希值与应用程序用户提供的可能密码进行比较。验证可能密码的算法取决于PDF文件的加密类型。表1显示了所有不同的Adobe PDF格式及其相应加密类型的概述。

表 1:不同Adobe PDF版本使用的加密类型。

从表1可以看出,早期版本的Adobe PDF格式使用RC4 40位算法进行加密。
按当今的标准,RC4 40位加密算法具有短的40位密钥,这种弱点允许直接攻击加密密钥。因此,受密码保护的PDF文件的RC4 40位加密密钥可以在一天内恢复。

后续版本的Adobe PDF格式使用了RC4 128位和AES 128位算法进行加密。这些加密算法的主要改进是加密密钥的长度增加到128位。另一个改进是密钥拉伸算法,MD5 + RC4被替换为50xMD5 + 20xRC4。

Adobe Acrobat 9引入了一种新格式,Adobe PDF 1.7 Extension Level 3,具有增强的安全性。加密密钥增加到256位,MD5哈希算法被SHA-256取代。然而,由于缺乏密钥拉伸算法,加密算法容易受到暴力破解攻击,如图1所示。

Adobe PDF格式的最新版本大大提高了加密PDF文件的安全性。Adobe重新引入了密钥拉伸算法,从而增加了验证可能密码的时间。在最新的PDF版本中,加密密钥由SHA-256的单次迭代生成,然后经过使用SHA-256、SHA-384和SHA-512算法的可变密钥变换集。此外,还添加了AES密钥加密。相应的密码验证算法显示在代码片段9中。

应用程序代码

架构

密码恢复工具使用MVVM和IOC概念来确保代码(应用程序逻辑)与视图有效地解耦。每个视图都有一个相应的ViewModel,其中包含代码。视图和ViewModel之间的通信通过数据绑定完成。此外,ViewModel可以通过ViewModelBase类中引用的各种服务接口与视图进行交互。引用的服务接口是:

  • IFileService允许ViewModel显示CommonOpenFileDialog,让用户选择要打开的一个或多个文件。此对话框存在于Microsoft.WindowsAPICodecPack.Shell程序集中,可作为nuget包使用。
  • IDialogService允许ViewModel显示带有其相应ViewModel的窗口。
  • IFolderBrowserService允许ViewModel显示CommonOpenFileDialog,让用户选择一个目录。

所有ViewModel都继承自ViewModelBase类,因此可以访问定义的这些服务。

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>();
} 

代码片段 1:ViewModelBase类提供的服务。

在下面的代码片段中,IDialogService用于显示PDF属性窗口。此窗口的数据上下文设置为其相应的ViewModel。

private async void OnShowFilePropertiesCmdExecute()
{
    FilePropertiesView filePropertiesView = new FilePropertiesView();
    FilePropertiesViewModel filePropertiesViewModel =
        new FilePropertiesViewModel(filePropertiesView, SelectedFile);
    bool? result = await dialogService.InitDialog(filePropertiesView, filePropertiesViewModel);
    filePropertiesViewModel.Dispose();
}

代码片段 2:IDialogService用于显示PDF属性窗口。

字典、正则表达式和暴力破解编辑器包含在TabControl中。此控件的每个TabItem都绑定到以下ViewModel类型之一:DictionaryManagerViewModel、SmartManagerViewModel或BruteForceManagerViewModel。DataTemplates用于渲染(告诉WPF如何绘制)每个ViewModel及其特定的UserControl。此方法将业务逻辑(ViewModels)与UI(Views)完全分开。TabControl在MainView中定义如下:

<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>

代码片段 3:字典、正则表达式和暴力破解编辑器包含在TabControl中。

TabControl的ItemsSource绑定到一个ViewModel集合。此集合在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); }
}

代码片段 4:创建绑定到TabControl的TabItems。

每个TabItem需要实现ITabViewModel接口。此接口要求ViewModel实现三个属性。HeaderIcon和Header属性用于TabItem的Header属性。PasswordIterator属性用于引用实现IPasswordIterator接口的类,该类可以是以下类之一:Dictionary、Password或RegularExpression类。

public interface ITabViewModel
{
    string Header { get; set; }
    DrawingBrush HeaderIcon { get; set; }
    IPasswordIterator PasswordIterator { get; set; }
}

代码片段 5:ITabViewModel接口。

应用程序用户可以选择应用程序顶部的三个主视图之一:主页、设置和关于视图。当应用程序用户单击任一视图按钮时,会设置一个标志,并将相应的网格可见性设置为可见,如下面的代码片段所示。

<!-- Bottom panel, hidden for views other than the Home view -->
<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>

代码片段 6:SelectedView属性决定哪个网格可见。

使用itextsharp从PDF中提取加密字典

PDF文档包含一个尾部字典,其中包含对重要对象的引用,以及可选的加密字典。如果存在加密字典,则它包含验证可能密码所需的信息。

图 4:PDF尾部和加密字典的示例。

为了从PDF文件中检索加密字典,对itextsharp库进行了修改。如以下代码片段所示,新添加的方法返回验证密码所需的PDF加密数据。修改后的iTextSharp库可以在这里找到。

/**
 * Constructs a new PdfReader.
 * @param filename, Path to PDF File
 */
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);
    }
}

代码片段 7:修改后的iTextSharp库的PdfReader.cs使我们能够检索PDF加密数据。

通过双击PDF文件数据网格中的PDF条目,可以查看从PDF文件中检索到的加密数据。

图 5:使用iTextSharp库从PDF文件中检索到的加密数据。

验证密码

密码验证过程如下:使用专用算法处理新密码,然后将生成的加密密码与上一节所述的从PDF加密字典中提取的加密密码进行比较。如果它们相等,则找到正确的密码,可用于打开解密的PDF文件。用于计算加密密码的专用算法取决于PDF的加密类型,并使用工厂模式获取,如以下代码片段所示。

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
}

代码片段 8:使用工厂模式获取用于计算加密密码的算法。

下面显示了一个用于计算和验证可能密码的专用算法的示例。此算法用于PDF 1.7 Extension Level 5和8,并具有强大的密钥拉伸算法。

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;

    //Take the SHA - 256 hash of the original input to the algorithm 
    //and name the resulting 32 bytes, K.
    byte[] K = sha256.ComputeHash(paddedPassword, 0, paddedPassword.Length);

    //The conditional-OR operator (||) performs a logical - OR 
    //of its bool operands. If the first operand evaluates to true, 
    //the second operand isn't evaluated. If the first operand evaluates 
    //to false, the second operator determines whether the OR expression 
    //as a whole evaluates to true or false.
    while (idx < 64 || E[E.Length - 1] + 32 > idx)
    {
        Array.Copy(K, key, key.Length);
        Array.Copy(K, 16, iv, 0, iv.Length);

        //Make a new string, K1, consisting of 64 repetitions of the sequence: 
        //input password, K, the 48 - byte user key. The 48 byte user key is 
        //only used when checking the owner password or creating the owner key.If
        //checking the user password or creating the user key, K1 is the 
        //concatenation of the input password and K.
        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);
        }

        //Encrypt K1 with the AES-128(CBC, no padding) algorithm, 
        //using the first 16 bytes of K as the key and the second 16 bytes of 
        //K as the initialization vector. The result of this encryption is E.
        E = aes.CreateEncryptor(key, iv).TransformFinalBlock(K1, 0, K1.Length);
        //Now we have to take the first 16 bytes of an unsigned big endian integer... 
        //and compute the remainder modulo 3. Taking the first 16 bytes of E 
        //as an unsigned big-endian integer, compute the remainder, modulo 3.
        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;
}

代码片段 9:PDF 1.7 Extension Level 5和8具有强大的密钥拉伸算法。

如果密码成功通过验证,它将被保存在您在设置视图中指定的位置。

字典编辑器

字典编辑器允许您添加包含密码列表的文本文件。所有添加的文件都放入一个ObservableCollection中,该集合绑定到字典编辑器视图。当解密过程开始时,ObservableCollection中的每个文本文件都通过StreamReader读取并随后进行验证。在设置视图中,您可以配置从字典读取的密码的大小写。例如,您可以将大小写更改为大写、小写或首字母大写。

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;
}

代码片段 10:使用StreamReader从文本文件中读取密码。

正则表达式编辑器

图2显示了正则表达式编辑器。您可以通过单击加号按钮添加新的正则表达式,这会打开如下所示的正则表达式编辑器。此编辑器允许您输入一个正则表达式,然后使用该正则表达式生成匹配该表达式的所有可能字符串。正则表达式编辑器允许您通过按开始按钮来预览正则表达式匹配。

图 6:正则表达式编辑器。

为了生成与正则表达式匹配的所有可能字符串,我使用了Generex库,可以在此下载。不过,有一个小挑战:Generex库需要从Java转换为C#。我使用IKVM.NET框架实现了这一点,该框架可以在此下载。

Generex generex = new Generex("[0-3]([a-c]|[e-g]{1,2})");
Iterator iterator = generex.iterator();
while (iterator.hasNext()) 
{
    System.out.print(iterator.next() + " ");
}

代码片段 11:使用Generex迭代器的示例。

暴力破解编辑器

暴力破解编辑器允许您指定字符集和密码长度。您可以选择指定扫描方向,例如增加或减少密码长度。填写完暴力破解编辑器后,您可以单击预览按钮,然后会显示所用字符集和预期迭代次数的摘要。从图3可以看出,初始密码长度设置为8个字符。此值基于一篇此处找到的密码研究文章。下图说明了Gmail账户的密码长度分布。

图 7:Gmail账户的密码长度分布。

下面显示了暴力破解迭代算法,该算法基于这篇文章

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;
}

代码片段 12:用于生成密码的暴力破解迭代算法。

应用程序用户可以配置暴力破解迭代算法使用的字符集,如图3所示。例如,用户可以通过勾选相应的复选框来选择仅使用数字。或者,用户可以通过选择Unicode单选按钮来指定Unicode字符集。输入的Unicode范围使用以下代码片段中所示的两个正则表达式之一进行验证。
当PDF文件的加密类型为AES256或AES256ISO时,其对应的密码可以由任何Unicode字符组合而成。对于其他PDF加密类型,只允许使用ASCII(U0000-U00FF)字符。

//Unicode \u0000-\uFFFF
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};?$";

//ASCII \u0000-\u00FF
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};?$";

代码片段 13:用于验证输入的Unicode字符集的正则表达式。

配置完暴力破解编辑器后,您可以单击暴力破解编辑器中的预览按钮,然后会显示所用字符集和预期迭代次数的摘要视图。在下面的示例中,所用字符集仅包含数字,密码长度在1到15之间。从下面的摘要视图可以看到,密码迭代序列是对称的,这意味着迭代从8位数字密码开始,然后是9位数字密码,接着是7位数字密码。

图 8:暴力破解迭代摘要视图。

密码验证速度

如前所述,密码验证速度在很大程度上取决于使用的加密算法。
下表总结了不同加密算法的密码验证速度。从下表中可以看出,PDF密码恢复工具在处理Adobe PDF 1.7 Extension Level 3文件时达到最高的验证速度。您可以点击图表放大。

图表 1:速度测试(Intel T4200,@2.00 GHz),每分钟可验证的用户密码数量。

此外,图表显示,Adobe PDF格式的最新版本比早期版本提供了更多的保护。增加的保护是由新引入的密钥拉伸算法引起的,该算法降低了验证速度。

SVG(可缩放矢量图形)图标

PDF密码恢复工具中使用的图标源自矢量图标。如果您在互联网上搜索“SVG Icons”,您会找到许多提供免费或付费SVG图标的网站。我用来下载大多数图标的网站可以在这里找到。下载SVG图标后,我使用ViewerSvg将它们转换为DrawingBrush对象。使用DrawingBrush对象的一个优点是易于自定义颜色。

<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>

代码片段 14:使用ViewerSvg将SVG图标转换为DrawingBrush对象。

<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>

代码片段 15:Start DrawingBrush可以作为静态或动态资源引用。

使用NUnit参数化测试的单元测试

我使用NUnit框架为密码恢复工具创建了单元测试。此框架的一个优点是它允许参数化测试。参数化测试只是通过方法参数而不是在方法内部硬编码值来将值传递到测试方法。如您在下面的代码片段中所见,您可以在单个单元测试中添加多个TestCase属性。通过添加更多具有适当值的TestCase属性,可以增加测试提供的覆盖范围。

[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);
}

代码片段 16:参数化单元测试。

参考列表

  • NUnit参数化测试文章
  • Generex库的替代方案是Rex,可以在此下载。
  • Fare是Generex库的另一个替代方案,可以在此下载。
  • CPDF是一个免费工具,可用于生成用于测试的加密PDF。
  • Mozilla实现的ISO 32000-2算法可以在这里找到。
  • Extended WPF Toolkit可以在此下载。
  • PDFsharp库可以在这里找到。
  • itextsharp库可以在这里找到。
  • IOC服务实现可以在这里找到。

关注点

有两个PDF密码:用户密码和所有者密码。如果未设置用户密码,则PDF查看器应检查并遵守PDF权限。但是,PDF查看器可能会完全忽略PDF权限并允许所有操作。这里的问题是,显示PDF文件在屏幕上或将其发送到打印机可以使用完全相同的数据。因此,唯一有保护的时候是当用户密码和所有者密码都设置时。

为了创建受保护的PDF文件,我建议使用Adobe Acrobat X/XI/DC,它具有强大的密钥拉伸算法。由于强大的密钥拉伸算法,对PDF密码进行暴力破解攻击可能不会成功。此外,我建议在创建受密码保护的PDF文件时同时设置用户密码和所有者密码。创建唯一的密码也很重要,例如,您可以使用一个句子作为密码。

如果您将使用Extended WPF Toolkit,则在下载Xceed.Wpf.Toolkit.dll后需要将其解除阻止,如此处所述。

历史

  • 2016年5月21日:版本1.0.0.0 - 发布文章
  • 2016年6月7日:添加源代码
© . All rights reserved.