使用 JavaScript 和 HTML5 进行双因素身份验证





5.00/5 (4投票s)
用 JavaScript 编写的紧凑型一次性密码生成器(RFC6238)。
引言
在我之前的一篇文章中,我曾简要地演示过 OTP 值是如何计算的
(https://codeproject.org.cn/Articles/502240/Mysterious-google-two-step-authentication-in-debug) 以及分享了一套紧凑的 PHP 类和库,用于在服务器端代码中计算 OTP (https://github.com/Voronenko/PHPOTP)。这种方法的前提是您希望您的客户使用谷歌身份验证器工具来获取 OTP 值。例如,LastPass 密码服务就采用了这种方法。但如果您想要自定义 OTP 令牌生成器的 UI 呢?您可能希望此 UI 具有您应用程序/服务的皮肤等品牌元素……
本文将介绍如何处理这个问题。
背景
OTP 令牌生成器通常是某些移动设备上的应用程序:IOS 或 Android。这两个平台都很好地支持 HTML5,这使得我们能够以纯 HTML/Javascript 的方式实现我们的 OTP 生成器,作为一个单页应用程序。
需要解决的挑战
- 在 JavaScript 中实现 OTP 令牌生成
- 实现 UI 和逻辑,每 30 秒更改一次代码
- 确保实现的解决方案能够离线工作。
让我们一步一步来。
在 JavaScript 中生成 OTP 令牌。
正如您可能还记得我之前的一篇文章,我们的算法需要以下要素
- base32 转换库,
- sha1 加密算法实现
- HMAC 和 OTP 算法实现(如果存在)。
我们偏好使用 MIT 或 LGPL 许可证的库,以便能够将我们的解决方案许可为免费商用。对于 base32 实现,我强烈推荐 nibbler 库:http://www.tumuski.com/2010/04/nibbler/。它在填充方面有一些小问题,但这种情况很少见,并且项目页面上提供了社区补丁。
对于 JavaScript 中的 Sha1 算法和其他加密算法,我推荐谷歌的 CryptoJS 库 http://code.google.com/p/crypto-js/。CryptoJS 是使用最佳实践和模式用 JavaScript 实现的标准和安全加密算法的集合。它们速度快,并且具有一致且简单的接口。该库目前仍在支持和开发中。我们可以在其中找到 sha1 和 hmac 的实现 - 非常棒!
OTP 算法:JavaScript 现在非常流行:例如,我们可以以此 NodeJS 模块为基础 https://github.com/guyht/notp/。问题是该模块是专门为 NodeJS 环境设计的,因此需要消除所有不重要的依赖项,以便该模块能在浏览器环境中工作。MIT 许可证允许我们进行此类修改。
在这种情况下,我不得不移植 Buffer 对象,使用 nibbler 实现 base32,并模拟 NodeJS crypto 模块 (https://node.org.cn/api/crypto.html) 来计算 HMAC,如下所示:
var cryptoFAKE = {
<pre> createHmac:function(algorithm, key) {
var _key = key.value();
return new HMacBasicImpl(_key);
}
};
结果是我们采用了一个 NOTP
类,它提供了计算一次性密码的方法
Notp.getTOTP (args, err, cb)
参数:一个包含必需字段 K 的对象 - 私钥字符串
UI
对于 UI,我们必须回答以下问题:
- 我们将把密钥(在本节中称为 CLUE)存储在哪里?
- 我们将如何编程 UI。
幸运的是,HTML5 允许网页在客户端设备上持久化其数据 -
DOM Storage https://mdn.org.cn/en-US/docs/DOM/Storage。
var CLUE= localStorage.getItem('CLUE');
if (typeof(CLUE)=="undefined") {
CLUE=null;
}
对于单页应用程序,我最喜欢的库是 KnockoutJS。它允许我们专注于开发逻辑,
并将绑定到 HTML 元素的工作外包给 Knockout 标记。
模型:具有三个属性:clue(密钥)、current token 和一个布尔属性,该属性指示 clue 是否存在。只有一个方法 - UpdateToken
- 用于计算 OTP 并更新模型属性。
var Model = {
existsclue:ko.observable((CLUE!=null)),
clue: ko.observable(CLUE),
token: ko.observable('XXXXXXX'),
notp: new Notp(),
UpdateTokenCallback: function(code) {
this.token(code);
},
UpdateToken: function(){
var args = {
K : CLUE
};
this.notp.getTOTP(args,
function(err) { alert(err); },
Model.UpdateTokenCallback.bind(Model)
);
}
}
视图
好消息是,您在设计上不受限制。您可以更改 OTP 应用程序的外观和感觉,使用图像、HTML 和 CSS:添加您的公司 Logo、企业字体等。
<header aria="company logo">
<div class="center"><img src="im/logo.gif"/></div>
</header>
<div id="main" role="main" class="center">
<p data-bind="text:token" id="code">LOADING...</p>
<p data-bind="text:clue" id="clue">CLUE</p>(<span data-bind="text:existsclue"></span>)
<p data-bind="visible:(!existsclue())" id="syncro">
<a href="setup.php">Please navigate to this link to setup your device!</a>
</p>
<p>
<a href="#" onclick="window.applicationCache.update()">Debug: cache.swapCache()</a>
</p>
</div>
我们检测本地存储中是否存在 CLUE,如果不存在 - 则提示客户进行设置(“请导航到此链接设置您的设备”)。在实际场景中,我们可能希望用户通过某种安全方法登录,但为了演示目的,我们采用简单的方法:将 clue 放入会话中并显示二维码,客户端设备可以扫描该二维码 - 即客户只需扫描二维码即可配置您的 OTP 应用程序。
<?php
require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR .'rfc6238/base32static.php');
session_start();
$secretcode = '12345678901234567890';
$_SESSION['secretcode'] = $secretcode;
;
$url = "http://".$_SERVER["HTTP_HOST"].str_replace(basename($_SERVER["SCRIPT_NAME"]),"",$_SERVER["SCRIPT_NAME"])."setupinitdevice.php?PHPSESSID=".$_COOKIE["PHPSESSID"];
?>
<h1> Please navigate by link below to setup 2 factor auth </h1>
<img src="setupqrcodeimage.php?PHPSESSID=<?php print $_COOKIE["PHPSESSID"]?>" />
<br/>
<a href="<?php print $url?>">This is the same link for debug</a>
一旦通过二维码或其他方式在设备上打开链接,设备上的应用程序就可以使用了。
<?php session_start(); $secretcode = $_SESSION['secretcode']; if (empty($secretcode)) { die('Sorry, device is not supported /'.$_COOKIE["PHPSESSID"].'/ while'.session_id(). ' AND #'.$_SESSION['secretcode'].'#'); } $url = "http://".$_SERVER["HTTP_HOST"].str_replace(basename($_SERVER["SCRIPT_NAME"]),"",$_SERVER["SCRIPT_NAME"])."index.html"; ?> <html> <head> <meta http-equiv="refresh" content="2;url=<?php print $url?>"> <script type="text/javascript"> if (!window.localStorage) { alert('Sorry! this device is not supported'); } localStorage.setItem('CLUE', '<?php print $secretcode?>'); alert(localStorage.getItem('CLUE')); </script> </head> <body> <a href="<?php print $url?>">If this page did not redirect you, press here</a> </body> </html>
离线模式
我们的客户不应该每次需要 OTP 值时都必须访问互联网。这是地方
我们利用另一项 HTML5 技术:离线缓存 https://mdn.org.cn/en-US/docs/HTML/Using_the_application_cache。通过声明 manifest 来启用我们的应用程序离线使用
<html class="no-js" lang="en" manifest="appcache.php">
在实际场景中,您可能希望 manifest 文件紧凑,但为了演示目的,让我们将所有项目脚本包含在离线模式中。
<?php
header('Content-Type: text/cache-manifest');
echo "CACHE MANIFEST\n";
$hashes = "";
$dir = new RecursiveDirectoryIterator(".");
foreach(new RecursiveIteratorIterator($dir) as $file) {
if ($file->IsFile() &&
($file != "./appcache.php") &&
(pathinfo($file, PATHINFO_EXTENSION)!='appcache') &&
(substr($file->getFilename(), 0, 1) != ".")
)
{
echo $file . "\n";
$hashes .= md5_file($file);
}
}
echo "# Hash: " . md5($hashes) . "\n";
?>
代码实战
我将通过一系列截图来演示代码。
在离线模式下运行演示的重要说明
请确保缓存 manifest 以正确的 MIME 类型提供
AddType text/cache-manifest appcache
AddType text/cache-manifest .appcache
如果您克隆了存储库 - 请调整 appcache.php 代码,或删除 .git 文件夹及其子文件夹。
代码可从 GitHub 下载:https://github.com/Voronenko/JSOTP
摘要
我真心希望更安全的双因素身份验证将在网站上得到广泛应用。本文分享的想法可以使开发人员更好地控制客户 OTP 应用程序的外观和感觉,并能够针对更多能够运行 HTML5 场景的设备。