使用 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 场景的设备。


