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

加密 InfoPath 表单数据

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2013年10月2日

CPOL

9分钟阅读

viewsIcon

22362

downloadIcon

654

一个很棒的库,可用于轻松地为 InfoPath 表单数据添加加密功能。

更新

2013/03/10 - 好的,我发现了一个我在将源代码复制到示例时引入的小错误。它会导致加密在遇到第一个“Group”节点时停止;现已修复。另外,我似乎忘记从 InternalDecryptNode 中删除我的字节数组,这意味着除非您按照我的建议编写自己的 stock 解密代码,否则解密将失败;现已修复。

我还添加了第二个示例供下载(稍后会写一篇关于它的文章,或许是第二篇)。该示例使用按钮单击来加密/解密表单,并要求输入密码。密码使用 Windows 窗体输入,因此输入时会被很好地屏蔽。(这里没有 Wingdings :p)。

关于示例 2 的说明:目前没有处理输入错误密码的错误处理。这将在稍后添加,但如果您需要自己处理,那也很简单。

引言

所以这是我的第一篇文章……请温柔点 :p

在接下来的文章中,我将详细介绍我为 InfoPath 表单构建的加密库。我将提供完整的源代码,供那些希望使用或修改它的人使用。但是,通常的规则适用;未经适当的署名请勿重新分发,诸如此类……

该库当然需要一些编码,但量很少,并且在文章中有很好的文档说明。

背景

最近,负责为我们公司编写 InfoPath 表单的同事被派去编写一份员工评估表。

首先;我们都知道 InfoPath 表单只是 XML(纯文本)文件。理论上,任何有权访问该表单的人都可以阅读其内容,无论是否使用 InfoPath。

其次;该表单将通过电子邮件发送,这意味着表单数据将存储在双方的电子邮件系统中,因此任何人都可以轻松访问,只要他们知道在哪里查找。

起初,IT部门的默认回应当然是……“不行,做不到……总会有办法的”。

你勇敢的作者不这么认为……

当然,有了正确的知识、设备和决心,几乎没有什么安全措施是无法在一段时间内绕过的。你可以从基本的文本编码一直到人类已知的最复杂的加密。最终,你的选择应反映安全性的重要性、实现所需的时间以及试图破解你工作的恶意人员可能具备的专业水平。

在我们的例子中,数据对除员工以外的任何人都不具有至关重要的重要性,而我们员工的平均黑客技能是……嗯……很低。

在我看来,这意味着简单的混淆应该足够了。但是,一如既往,我喜欢为自己的学习以及为事业付出更多。

所以……看我创造的吧!

使用库

InfoPath 表单主要有两个部分:设计器和代码隐藏。

许多开发人员仅使用设计器满足所有需求,但我们的需求超越了这一点,因此我们需要使用代码隐藏。实际上,有多种方法可以实现此库提供的安全措施,但我将重点介绍以下方法:

通过电子邮件提交且作者无法保存,只能由授权用户打开的表单。

注意:您需要设置 Outlook 才能遵循这些说明。如果您没有 Outlook 或不想在此文章中使用它,则可以省略第 2 步,以查看加密工作。

更新:您现在可以尝试示例 2,以查看按下按钮时加密的工作原理。

表单设计

1. 让我们从一个空白的 InfoPath 表单开始。然后添加一些字段和一个按钮。

注意:所有字段的数据类型均为**文本(字符串)**,除了**数字**字段,其数据类型为**整数(整数)**。

2. 您需要设置一个数据连接,将表单提交到电子邮件,命名为“Email Submit”。

3. 设置 **AuthorisedUsers** 角色。

暂时我们不指定任何用户,所以任何打开表单的人都将被授权。如果您想了解更多关于用户角色的信息,请点击这里

 

4. 接下来,我们将按钮设置为**提交**表单。

然后单击“提交选项…”并设置以下选项:

5. 最后,单击“编辑代码…”按钮,加载 VSTA 后,您就可以开始编码了。

注意:如果默认不是 C#,则在单击“编辑代码…”按钮之前,您需要在“工具”>“表单选项”>“编程”下进行设置。

代码

用于提交表单的代码分为两部分。

1. 添加对该库的引用

2. 将以下代码复制到 FormCode.cs 中(替换所有现有代码)。

using Microsoft.Office.InfoPath;
using System;
using System.Windows.Forms;
using System.Xml;
using System.Xml.XPath;
using mshtml;
using InfoPathHelpers;
 
namespace CryptographyExample
{
    public partial class FormCode
    {
        public void InternalStartup()
        {
            EventManager.FormEvents.Submit += new SubmitEventHandler(FormEvents_Submit);
 
            if (!New && UserRole == "AuthorisedUsers")
             {
                 // If the user opening the form is an authorised user then decrypt all data.
                Cryptography.DecryptDocument(this, "key", true);
             }
        }
 
        public void FormEvents_Submit(object sender, SubmitEventArgs e)
        {
            Cryptography.EncryptDocument(this, "key", true);
 
            try
            {
                DataConnections["Email Submit"].Execute();
                e.CancelableArgs.Cancel = false;
            }
            catch (System.Net.WebException ex)
            {
                Cryptography.DecryptDocument(this, "key", true);
 
                if (!ex.Message.Contains("The form was not submitted because the action was canceled."))
                {
                    // If the operation was not cancelled then show the user an error message.
                    MessageBox.Show("The form could not be submitted due to the following error:\r\n\r\n" +
                        ex.Message, "Submission Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }
    }
}
 

测试表单

发布表单,然后打开副本以查看其效果。或者只需预览它,但您将无法通过这种方式测试解密。

几点说明

1. 发布表单时,它需要完全信任或域级别信任。如果您将其发布到共享文件夹,它将正常打开。否则,您必须使用证书对其进行签名并设置完全信任。

2. 您应该注意到,如果您填写表单并按**提交**,所有字段都会被加密……除了数字字段。这是因为加密会创建一个字符串,而数字字段在不是数字时将无效。避免这种情况的方法是将字段设置为字符串。当然,这样您就会失去验证功能,您必须手动验证该字段。我也为此创建了一个库,我希望在以后的文章中写到它。

好的,该库本身通过注释和 XML 文档记录得相当好。但是,我将在下面概述核心原则。

加密

加密相当标准,可以轻松替换为您喜欢的其他方法。但为了记录在案,这是我的尝试。

private static void InternalEncryptNode(XPathNavigator node, string key)
{
    var SymmetricAlgorithm = new RijndaelManaged();
    // Make up your own Byte[].
    var DerivedByteArray = new Rfc2898DeriveBytes(key, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
    SymmetricAlgorithm.Key = DerivedByteArray.GetBytes(SymmetricAlgorithm.KeySize >> 3);
    SymmetricAlgorithm.IV = DerivedByteArray.GetBytes(SymmetricAlgorithm.BlockSize >> 3);
    var Buffer = new MemoryStream();
    byte[] NodeValueBytes = Encoding.UTF8.GetBytes(node.Value);
    using(var CryptoStream = new CryptoStream(Buffer, SymmetricAlgorithm.CreateEncryptor(), CryptoStreamMode.Write))
    {
        CryptoStream.Write(NodeValueBytes, 0, NodeValueBytes.Length);
    }
    node.SetValue(Convert.ToBase64String(Buffer.ToArray()));
}
 

 

此代码仅使用 RijndaelManaged 算法加密节点的值,并将结果转换为 Base64,然后将其作为值设置回节点。

遍历节点树

该库的核心和精髓其实是这里。我们如何知道 XML 的哪些部分需要加密?

为了方便开发人员使用,我决定尽可能自动化。为此,您只需一次调用 `EncryptDocument(this, "key"); ` 即可加密所有字段。

或者,您可以指定要加密的特定节点或节点集。目前,它将加密文档中找到的所有元素和属性。您还可以按名称指定任何您不想加密的节点。

注意:在撰写本文时,我打算为该方法添加标志,以指定诸如 IgnoreAttributes、IsSchemaAware 等选项,使其更具可定制性。但目前,如果您确实需要,您必须手动指定不应加密的属性。

因此,遍历树的代码如下。

private static void ProcessAllNodes(XPathNavigator rootNode, List<string> nodesToIgnore, ProcessNodeDelegate processNode)
{
    if (nodesToIgnore == null)
    {
        nodesToIgnore = new List<string>();
    }
 
    // If the rootNode is null or has no children there is nothing to do.
    if (!rootNode.HasChildren && NodeIsNullOrEmpty(rootNode))
    {
        return;
    }
 
    // We need to use the EndPosition to prevent the navigator from being moved past the
    // current element when processing only a subsection of the document.
    XPathNavigator EndPosition = rootNode.CreateNavigator();
    EndPosition.MoveToNext();
 
    while (rootNode.MoveToFollowing(XPathNodeType.Element, EndPosition))
    {
        // While there are following elements.
        if (nodesToIgnore.Contains(rootNode.LocalName))
        {
            // Nodes with a local name matching one provided in the 
            // nodesToIgnore list should be skipped.
            continue;
        }
 
        if (rootNode.HasAttributes)
        {
            // If the node has attributes move to the first one.
            rootNode.MoveToFirstAttribute();
 
            // Then loop through each one processing them.
            do
            {
                if (rootNode.Prefix == "xsi" ||
                      nodesToIgnore.Contains(rootNode.LocalName) ||
                      NodeIsNullOrEmpty(rootNode))
                {
                    // Nodes with a prefix "xsi" or a local name matching one provided in the 
                    // nodesToIgnore list should be skipped.
                    continue;
                }
 
                processNode(rootNode);
            }
            while (rootNode.MoveToNextAttribute());
 
            // Finally return to the parent node.
            rootNode.MoveToParent();
        }
 
        if (NodeIsGroup(rootNode) || NodeIsNullOrEmpty(rootNode))
        {
            return;
        }
 
        processNode(rootNode);
    }
}
 

 

从顶部开始……

1. 对于带有属性的节点,我们遍历每个属性并对其进行处理,然后返回到根节点。

2. 对于所有其他节点,我们只处理该节点。

3. 组没有文本值,因此不进行处理。

巧妙之处在于

MoveToFollowing(XPathNodeType.Element, EndPosition)

这使我们能够仅迭代元素(忽略空格等),并防止导航器在仅处理文档的一小部分时越过最后一个节点。

模式感知

解谜的最后一部分!这实际上是非常重要的一部分……

我的加密方法接受一个字符串并将其加密为另一个字符串。这在大多数情况下都很好,但对于任何非**文本(字符串)**字段都会引起一些糟糕的问题。有效地加密非字符串数据并不容易,所以我个人不建议这样做。如果您需要加密数据并进行验证,我建议使用代码隐藏中的验证(这很简单)。我写了另一个库来实现这一点,我将在下一篇文章中介绍。

在此期间,如果您有非字符串字段,则必须使用 `EncryptDocument()` 的“模式感知”重载,该重载接受一个布尔值。这将检查文档中每个节点的模式数据类型,并忽略任何不是字符串的内容。这可以防止加密后数据无效,而无需手动指定所有这些字段。

我对这段代码特别满意,所以在此分享。

private static XmlSchemaSet GetSchemaSet(XmlFormHostItem form)
{
    XmlSchema Schema = XmlSchema.Read(form.Template.OpenFileFromPackage("mySchema.xsd"), null);
    XmlSchemaSet SchemaSet = new XmlSchemaSet();
    SchemaSet.Add(Schema);
    SchemaSet.Compile();
    return SchemaSet;
}
 
private static void SchemaAwareProcessNode(XmlSchemaSet schemaSet, XPathNavigator node, ProcessNodeDelegate processNode)
{
    XmlQualifiedName QualifiedName = new XmlQualifiedName(node.LocalName, node.NamespaceURI);
    XmlSchemaType Type = null;
 
    if (node.NodeType == XPathNodeType.Attribute)
    {
        XmlSchemaAttribute Attribute = (XmlSchemaAttribute)schemaSet.GlobalAttributes[QualifiedName];
        Type = Attribute.AttributeSchemaType;
    }
    else
    {
        XmlSchemaElement Element = (XmlSchemaElement)schemaSet.GlobalElements[QualifiedName];
        Type = Element.ElementSchemaType;
    }
 
    if (Type.TypeCode != XmlTypeCode.String)
    {
        return;
    }
    processNode(node);
}

上面的代码从模板加载表单的模式,然后发现文档中节点或属性的类型,然后选择是否处理它们。

最终注释

1. 我不是安全/加密专家!这里可能存在安全问题,其中最轻微的是密钥目前硬编码在表单代码中。如果您发现问题……我不在乎 :p 这不是企业级安全,也从未打算成为企业级安全。它足以防止随意或甚至过于好奇的用户读取提交的表单数据。

2. 该库使用强名称密钥进行签名,因此使用您自己的密钥也未尝不可。它还使用了 AllowPartiallyTustedCallers 属性,如果您处于完全信任环境(并且表单模板已签名),我建议移除它。

3. 我在源代码中包含了我的其他助手,如果您想看看,我稍后会写文章介绍它们。

4. 此库针对 .net 2.0 编写,因为我使用的是 Office 2007,它使用 VSTA 1.0。一旦我升级,我将转向更大更好的东西,并且我将能够最终使用较新版本的 .net 框架重写它,我认为这会使其更好 :p

5. 如果您在使用过程中遇到任何问题,请告诉我,我将更新代码/文章。感谢阅读!

© . All rights reserved.