当今的 Web 应用程序为在线存储、访问和协作提供了许多便利。虽然一些应用程序为用户数据提供了加密,但为数不多。本文提供了添加基本加密支持所需的工具和代码,使您可以在一个最流行的在线日程表上加密用户数据。通过运用 Firefox 扩展和 Gnu Privacy Guard 的巨大灵活性,本文展示如何将加密的事件描述存储到 Google Calendar 应用程序,并且只向拥有解密密匙的用户显示纯文本。
本文从 Elias Torres 的出色的 “Google Calendar Quick Add” 扩展开始,逐步向您介绍如何提取、更改和插入各种组件,为事件进行加密,而不仅仅是使用加密的 TLS 数据通道。按照本文的说明进行操作之后,将会得到下面 图 1 所示的示例,在这里,服务器操作人员看到的是左边的内容,而 Web 浏览者看到的是右边的内容。
图 1. 加密的 Google Calendar
需求
硬件
任何能够提供 2002 年以后的浏览体验的硬件设备,应该能够运行本文所使用的代码。加密算法属于处理器密集型作业,因此,如果要在一个页面上对十几个,甚至上百个日程表事件进行加密,更快的硬件支持能够提供更好的浏览体验。
软件
需要 Firefox 1.5(或更新的版本)和 GnuPG(Gnu Privacy Guard)。具备各种 Firefox 扩展开发工具也是个不错的主意。查看下面的 参考资料 小节获得这些软件包的链接。
虽然本文的开发基于 Ubuntu 7.10 系统,但所用到的概念适用于各种操作环境。着手开发之前,确保您的系统支持 Perl、GnuPG 和 Firefox。
主要的编程方法
本文要求您基本熟悉 Firefox 扩展的开发过程。本文不需要特定的编译器和环境配置,但您必须熟悉软件开发,因为可能需要诊断与设置有关的问题或配置错误。
出于开发的需要,建议创建一个新的 Firefox 配置文件。下面的 参考资料 小节提供了如何创建新的配置文件的信息,同时也提供了有用的关于扩展开发站点的链接。
GnuPG 将用于处理这个扩展的所有加密函数。这个方法提高了 JavaScript 的算法实现效率,同时也提供了健壮的跨平台密匙管理。您需要一个功能齐全的 GnuPG 安装,并且能够访问您选择的加密密匙。此外,还需要 gpg-agent 程序(通常是 GnuPG 包的一部分),用于处理已记住的 passphrase 的临时存储和过期。
有了处理加密的函数性扩展开发环境和 GnuPG 之后,可以通过更改前面开发的扩展的一些代码来修改接口。Elias Torres 开发的 Google Quick Add 扩展为插入加密修改提供了一个很好的开端。在加密阶段,当前输入的事件文本将存储在磁盘上,然后一个外部程序对它进行加密,加密后的文件将被读入并发送到 Google 服务器。在解密阶段,每个事件都被写到磁盘上,然后进行解密,最后读回纯文本并显示给用户。另一种代替方法涉及到编写新的协议,该协议由 Firefox 程序管理。尽管这个过程能够提供更加健壮的数据管道,输出、处理和读取则是更加简单的进程间通信方法,适合于这类扩展。
在继续使用本文提供的代码之前,确保您设置了函数性 Firefox GnuPG 和 gpg-agent。
构建 Google Calendar Quick Add 扩展
Google Calendar Quick Add 扩展利用 Google Calendar SOAP API 从任意页面上获得事件并添加日程表中。本文通过负载截取和过程加密的方式向日程表添加加密事件。一个代替方法是简单地向 Google Calendar 添加经 ASCII 封装的条目,但这里讨论的方法将自动完成这个过程。
首先,创建一个保存扩展目录和代码的目录,比如 mkdir ~/calendarEncrypt。转到这个目录,然后从 参考资料 小节中指定的链接将 Quick Google Calendar Quick Add 扩展 xpi 下载到这个目录。
用以下命令解压缩 xpi:unzip quickgooglecal.xpi。转到刚才创建的 chrome 目录,然后运行命令 unzip quickgooglecal.jar。现在您会看到一个目录树,类似于 清单 1:
清单 1. Google Calendar Quick Add 目录结构
calendarEncrypt/chrome.manifest calendarEncrypt/readme.txt calendarEncrypt/chrome calendarEncrypt/chrome/content calendarEncrypt/chrome/content/hello.xul calendarEncrypt/chrome/content/overlay.xul calendarEncrypt/chrome/content/overlay.js calendarEncrypt/chrome/skin calendarEncrypt/chrome/skin/overlay.css calendarEncrypt/chrome/quickgooglecal.jar calendarEncrypt/chrome/locale calendarEncrypt/chrome/locale/en-US calendarEncrypt/chrome/locale/en-US/hello.dtd calendarEncrypt/chrome/locale/en-US/overlay.dtd calendarEncrypt/install.rdf calendarEncrypt/quickgooglecal.xpi |
转到 ~/calendarEncrypt 目录并编辑 install.rdf 文件。更改标识符和创建者,如 清单 2 所示:
清单 2. 更改 install.rdf 的标识符和创建者
Change the identifier: <em:id>{E31AE5B1-3E5B-4927-9B48-76C0A701F105}</em:id> to: <em:id>calendarEncrypt@devWorks_IBM.com</em:id> Also, change the creator: <em:creator>Elias Torres</em:creator> to: <em:creator>Elias Torres with modifications from devWorks</em:creator> |
编辑 chrome.manifest 文件,将基于 jar 的扩展打包方式更改为更适合开发人员的目录结构打包方式。清单 3 给出了必要的更改。
清单 3. 将 chrome.manifest jar 更改为目录结构
Original chrome.manifest content quickgooglecal jar:chrome/quickgooglecal.jar!/content/ overlay chrome://browser/content/browser.xul chrome://quickgooglecal/content/overlay.xul locale quickgooglecal en-US jar:chrome/quickgooglecal.jar!/locale/en-US/ skin quickgooglecal classic/1.0 jar:chrome/quickgooglecal.jar!/skin/ style chrome://global/content/customizeToolbar.xul chrome://quickgooglecal/skin/overlay.css Change to directory based chrome.manifest: content quickgooglecal chrome/content/ overlay chrome://browser/content/browser.xul chrome://quickgooglecal/content/overlay.xul locale quickgooglecal en-US chrome/locale/en-US/ skin quickgooglecal classic/1.0 chrome/skin/ style chrome://global/content/customizeToolbar.xul chrome://quickgooglecal/skin/overlay.css |
现在,在 Firefox 开发配置文件中为当前的扩展目录设置一个链接。例如,如果您的配置文件是 ~/.mozilla/firefox/b2w2sglj.development,就在 ~/.mozilla/firefox/b2w2sglj.development/extensions/ 创建一个称为 calendarEncrypt@devWorks_IBM.com 的链接。将当前的 Google Quick Add 开发目录路径放置到 calendarEncrypt@devWorks_IBM.com 文件中,在这个示例中是:/home/username/calendarEncrypt。
登录 Google Calendar 然后按 ctrl+;激活 Google Calendar Quick Add 扩展。通过输入 Test unencrypted event tomorrow 15:30 验证可以正确添加事件。验证事件可以正确地显示在日程表上。
Google Calendar Quick Add 扩展就绪之后,现在就可以向扩展插入支持加密和解密的修改了。
修改 Quick Add 扩展,使其支持透明加密
对发送的事件进行加密
本文通过拦截和加密 Quick Add Event 负载的方式,帮助实现自动向日程表添加加密事件。修改现有的扩展使其支持拦截,见下面的 清单 4。编辑 chrome/content/hello.xul 文件并删除第 69 行:var quickAddText = number_html(document.getElementById("quickText").value);。将下面的代码插入到第 69 行:
清单 4. 加密变量,提取日期和时间
var words = document.getElementById("quickText").value.split(' '); var dayVal = words[words.length-2]; var timeVal = words[words.length-1]; var elementData = ""; for( var n=0; n < words.length-2; n++ ){ elementData = elementData + " " + words[n]; } |
以上的代码假设日期和时间通常是快速事件文本中的最后两个单词。为了确保能够在日程表中准确放置,所有后来快速添加的事件的格式必须是 “消息 文本 日期 时间”,日期的格式为 “friday/monday/tomorrow/etc”,时间的格式为 “05:30/10:30/etc”。
将事件描述文本从事件日期和时间分离出来后,接下来应该将事件文本写到磁盘,然后对它进行加密。添加 清单 5 中第 77 行的代码:
清单 5. 将加密文本写到磁盘,然后调用程序
// Write the quick event text to disk var fileOut = Components.classes["@mozilla.org/file/local;1"] .createInstance(Components.interfaces.nsILocalFile); fileOut.initWithPath("/tmp/calendarEvent"); var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"] .createInstance(Components.interfaces.nsIFileOutputStream); foStream.init(fileOut, 0x02 | 0x08 | 0x20, 0666, 0); foStream.write(elementData, elementData.length); foStream.close(); // Run the external encryption process var fileExe = Components.classes["@mozilla.org/file/local;1"] .createInstance(Components.interfaces.nsILocalFile); fileExe.initWithPath("/tmp/CalendarCrypt.pl"); var process = Components.classes["@mozilla.org/process/util;1"] .createInstance(Components.interfaces.nsIProcess); process.init(fileExe); var args = ["encrypt"]; process.run(true, args, args.length); |
清单 5 的第一部分将配置文件 "/tmp/calendarEvent",将拦截到的事件文本写到磁盘。这个文件包含纯文本数据,但这些数据将在加密完成后被分解并删除。清单 5 的第二部分配置了一个文件,使它能够通过推荐的 nsIProcess 组件执行。/tmp/CalendarCrypt.pl 是一个 Perl 程序(将在后面介绍),它处理加密、解密和安全删除函数。对数据进行加密之后,清单 6 中的代码读回加密后的事件文本。将下面的代码置于 chrome/content/hello.xul 的第 98 行。
清单 6. 读回加密文件,然后构建事件文本
// Read the encrypted text back in var fileIn = Components.classes["@mozilla.org/file/local;1"] .createInstance(Components.interfaces.nsILocalFile); fileIn.initWithPath("/tmp/calendarEvent.asc"); var istream = Components.classes["@mozilla.org/network/file-input-stream;1"] .createInstance(Components.interfaces.nsIFileInputStream); istream.init(fileIn, 0x01, 0444, 0); istream.QueryInterface(Components.interfaces.nsILineInputStream); var data = ""; var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"] .createInstance(Components.interfaces.nsIFileInputStream); var sstream = Components.classes["@mozilla.org/scriptableinputstream;1"] .createInstance(Components.\ interfaces.nsIScriptableInputStream); // above line split on \ for formatting, do not include the line break fstream.init(fileIn, -1, 0, 0); sstream.init(fstream); var str = sstream.read(4096); while (str.length > 0) { data += str; str = sstream.read(4096); } sstream.close(); fstream.close(); quickAddText = data + " " + dayVal + " " + timeVal; |
清单 5 的代码表示将文本事件写到一个文件中并运行加密程序。清单 6 中的代码表示读入经 ASCII 封装的加密文本文件(附带了存储日期和时间的信息),然后重新启动快速添加文本的过程。
每个通过 Google Calendar Quick Add 程序添加的事件都被拦截、加密,然后以加密的形式发送到 Google 服务器。
为读取的加密事件修改 overlay.js
完成对 chrome/content/hello.xul 中的事件进行加密的代码后,插入这些代码,解密 chrome/content/overlay.js 中的事件。插入 清单 7 中的行,从文件的末尾开始。
清单 7. 解密事件侦听器,组件定义
window.addEventListener("load", function() { calendarDecryptExtension.init(); }, false); var calendarDecryptExtension = { init: function() { var appcontent = document.getElementById("appcontent"); // browser if(appcontent) appcontent.addEventListener("DOMContentLoaded", this.onPageLoad, true); var messagepane = document.getElementById("messagepane"); // mail if(messagepane) messagepane.addEventListener("load", function () \ { calendarDecryptExtension.onPageLoad(); }, false); // above line split on \ for formatting, do not include the line break }, |
事件显示页面每次加载时,都会通过 addEventListener 调用 calendarDecryptExtension 函数。calendarDecryptionExtension 函数开始是各种定义和挂钩,保证代码在页面装载时开始运行以及函数可以访问正确的数据。将 清单 8 中的代码置于清单 7 中的代码之下,继续文档解密过程:
清单 8. 写入事件的 onPageLoad 函数
onPageLoad: function(aEvent) { dump("pre elemenet \n"); var elementData = "NODATA"; var allSpans = content.document.getElementsByTagName("span"); for (var n = 0; n < allSpans.length; n++){ if( allSpans[n].innerHTML.indexOf("BEGIN PGP") > -1 ){ // file output for encrypted event text elementData = allSpans[n].innerHTML; var fileOut = Components.classes["@mozilla.org/file/local;1"] .createInstance(Components.interfaces.nsILocalFile); fileOut.initWithPath("/tmp/calendarEvent.temp"); var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"] .createInstance(Components. \ interfaces.nsIFileOutputStream); // above line split on \ for formatting, do not include the line break foStream.init(fileOut, 0x02 | 0x08 | 0x20, 0666, 0); foStream.write(elementData, elementData.length); foStream.close(); |
每个日程表条目都存在 HTML 的 span 元素中。临时变量设置完成后,一个 for 循环将处理每个 span 元素,并在文本加密后删除这些内容。下面的 清单 9 调用 CalendarCrypt.pl 程序,使用 "decrypt" 选项,提取事件文本。
清单 9. 运行加密脚本
// create an nsILocalFile for the executable var fileExe = Components.classes["@mozilla.org/file/local;1"] .createInstance(Components.interfaces.nsILocalFile); fileExe.initWithPath("/tmp/CalendarCrypt.pl"); var process = Components.classes["@mozilla.org/process/util;1"] .createInstance(Components.interfaces.nsIProcess); process.init(fileExe); var args = ["decrypt"]; process.run(true, args, args.length); |
将加密后的事件文本写到磁盘,然后运行解密程序,这将创建一个不加密的事件文本文件。添加 清单 10 中的代码将数据读回并更改呈现的信息。
清单 10. 读取解密后的文本
// file input for decrypted event text var data = ""; var fileIn = Components.classes["@mozilla.org/file/local;1"] .createInstance(Components.interfaces.nsILocalFile); fileIn.initWithPath("/tmp/calendarEvent.decrypted"); var istream = Components.classes["@mozilla.org/network/file-input-stream;1"] .createInstance(Components.interfaces.nsIFileInputStream); istream.init(fileIn, 0x01, 0444, 0); istream.QueryInterface(Components.interfaces.nsILineInputStream); var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"] .createInstance(Components.interfaces.nsIFileInputStream); var sstream = Components.classes["@mozilla.org/scriptableinputstream;1"] .createInstance(Components. \ interfaces.nsIScriptableInputStream); // above line split on \ for formatting, do not include the line break fstream.init(fileIn, -1, 0, 0); sstream.init(fstream); var str = sstream.read(4096); while (str.length > 0) { data += str; str = sstream.read(4096); } sstream.close(); fstream.close(); allSpans[n].innerHTML = data; |
从文件读取文本类似于加密阶段中执行的过程。注意,清单 10 中的最后一行,它将当前的 span 数据设置为未加密文本,而不是 “BEGIN PGP...” 原始文本。 添加 清单 11 中的代码,完成解密过程。
清单 11. 分解磁盘上的文本,结束循环
// sanitize the data stored on disk args = ["shred"]; process.run(true, args, args.length); }//if the span item is encrypted }//for each span }//on page load }//calendarDecryptExtension |
将选项更改为 "shred" 并重用 nsiProcess 组件,确保加密后的事件文本安全地从文件系统的临时位置删除。下一部分给出了前面部分调用的 CalendarCrypt.pl 程序。
CalendarCrypt.pl 程序
为了完成加密支持修改,需要根据下面的清单创建 CalendarCrypt.pl 程序。注意,对于典型 GnuPG 用户,给出的实现假设了一些最可能用到的加密/解密设置。如果愿意,可以考虑用 GnuPG::Encrypt 提供的其他选项替换外部程序调用和设置。例如,如果您希望使用多个键,或者 gpg-agent 与您的配置不兼容,GnuPG::Encrypt Perl 模块提供了许多帮助实现环境兼容的选项。首先,通过 清单 12 中的文本将 Perl 程序 calendarEncrypt.pl 置于 /tmp 中。
清单 12. calendarEncrypt.pl header 和加密
#!/usr/bin/perl -w # calendarEncrypt.pl - encrypt/decrypt/shred files use strict; die "specify a mode " unless @ARGV == 1; my $mode = $ARGV[0]; chomp(my $userName = `whoami`); if( $mode eq "encrypt" ) { my $res = `gpg --yes --armor --encrypt -r $userName /tmp/calendarEvent`; $res = `shred /tmp/calendarEvent; rm /tmp/calendarEvent`; |
在检查选项并设置默认用户名之后,将加密 /tmp/calendarEvent 文件。分解并删除原始的纯文本事件文件,确保磁盘上没有遗留敏感的数据。 下面的 清单 13 描述了解密模式:
清单 13. 文件处理,解密
}elsif( $mode eq "decrypt" ) { open(INFILE,"/tmp/calendarEvent.temp") or die "no in file"; open(OUTFILE,"> /tmp/calendarEvent.encrypted" ) or die "no file out"; while(my $line =<INFILE>) { my $begin = substr( $line, 0, 23 ); print OUTFILE "-----$begin\n"; my $version = substr( $line, 23, 34 ); print OUTFILE "$version\n"; print OUTFILE "\n"; my $body = substr( $line, 57 ); $body = substr($body, 0, length($body)-26); my @parts = split " ", $body; for my $piece( @parts ) { print OUTFILE "$piece\n"; } print OUTFILE "-----END PGP MESSAGE-----\n"; }#while each line close(OUTFILE); close(INFILE); my $cmd = qq{ gpg --yes --decrypt /tmp/calendarEvent.encrypted }; $cmd .= qq{ > /tmp/calendarEvent.decrypted }; my $res = `$cmd`; |
在上传、处理或显示日程表事件的某个时刻,会丢失一些原始格式。尤其是 “BEGIN PGP” 消息的前缀 “-----” 以及换行指示,将被去除。在调用解密命令之前,清单 13 中的字符串操作函数将替换丢失的格式。最后,清单 14 中的代码负责安全删除解密文本文件,从而去除任何存储在磁盘上的纯文本信息。
清单 14. 分解纯文本文件
}elsif( $mode eq "shred" ) { my $res = `shred /tmp/calendarEvent.decrypted`; $res = `rm /tmp/calendarEvent.decrypted`; } # EOF |
在将文件保存为 /tmp/CalendarCrypt.pl 之后,要确保该文件在发出以下命令时执行:chmod a+x /tmp/CalendarCrypt.pl。
用例
现在将拦截、加密使用 Google Calendar Quick Add 程序添加的事件,然后以加密的形式将其发布到 Google 服务器。使用 Extension Developer 的 Extension 重新加载所有的 chrome 事件,或重启 Firefox。使用 Ctrl+; 组合键,然后添加一个事件,比如 “Private doctor appointment tomorrow 16:30”。以 “regular” 模式打开您的 Google 日程表,您将看到一个事件描述,类似于 图 1 左边所示的内容。
要显示解密后的事件,请转到链接:http://www.google.com/calendar/htmlembed?src=<yourCalendarName>%40gmail.com,在这里,<yourCalendarName> 是您的帐户名,比如 “developer.works” 或 “bob_smith”。页面装载开始时,您会看到将弹出一个 gpg-agent,请求您的 passphrase。为 CalendarCrypt.pl 程序的第 1 小节中 “uname” 命令识别出的用户输入 passphrase,您的事件将被解密并显示在日程表上。
结束语
使用本文给出的工具和代码,您现在就可以在 Google 日程表中存储加密事件文本。使用 Elias Torres 的 Google Calendar Quick Add Firefox 扩展的修改,对每个添加和查看的事件进行无缝的加密和解密。在重新掌控数据的同时,也获得了 Web 2.0 应用程序带来的好处。
考虑创建一个模糊层(obfuscation layer)来更改事件存储的时间,降低网络流量分析的效率。编写您自己的程序,借助 Google Calendar SOAP API 提取、加密和存储以前和将来的日程表事件。尝试为默认的 Google Calendar 接口创建一个具有 Ajax 风格的透明加密扩展。
(责任编辑:A6)