内容


用击键力学扩展文本输入选项

使用 xev、Perl 和自定义算法测量字符及其键入方法

Comments

指尖上的螺纹和脊状纹路可以根据您触摸的物体来识别您。触摸方法,尤其是键入方法,同行为一样具有惟一性。击键力学是一个相对较新的领域,这项技术可以通过分析键入方式的统计信息来识别个人。许多商业产品通过分析密码输入的力度以及连续键入监视增强安全性。本文将使用示例代码来演示击键力学如何在验证及连续数据输入环境中增强应用程序安全性。

要求

软件

在配置诸如 Gnome 或 KDE 之类正常工作的 X Window System 时要求使用 Linux®。1999 年以后发布、附带图形桌面的任何发行版都应该提供了预配置环境,可从此环境入手。诸如 xev 之类的程序 —— X 事件查看器 —— 能够有效地捕捉和处理 X Window 事件。查看 参考资料 并下载 xev。即使已经安装或者已在发行版软件包中获得 xev,也需要下载 xev 的源代码。您还需要使用 Perl,目前的 Linux 发行版几乎都提供了 Perl。

硬件

需要配有足够的 RAM 和 CPU 功率的硬件才能显示 X Window System 并成功运行 Linux。需要用直接连接到计算机上的键盘来物理访问计算机。瘦客户机、虚拟网络计算(Virtual Network Computing,VNC)连接和虚拟客户机在理论上可以运行,但是不同级别的处理和网络负载可能会导致反应时间无法预测,这将影响系统的行为。

修改和编译 xev

本文将使用一般性方法演示击键力学的用法,即使用 xev 捕捉所有事件并且把这些事件导入到针对密码创建和确认而自定义的 Perl 程序。

要从头安装 xev,请下载并解压缩 xorg-x11-6.8.1.tar.gz。切换到 xc/programs/xev 目录并键入 xmkmf。xmkmf 程序将使用 Imakefile 文件来生成 makefile。通过键入 make 来构建 xev 程序。make 命令应当顺利完成而不会出现任何错误,但是如果遇到问题,请查询 X.org 文档。如果可以正确构建 xev 程序,则需要执行一项简单更改,从而按照更精确的计时输出击键事件。清单 1 显示了在第 164 行添加了 fflush(stdout); 语句的 xev.c 文件。

清单 1. 经过修改的 xev.c
...
        printf ("    XFilterEvent returns: %s\n",
                XFilterEvent (eventp, e->window) ? "True" : "False");
    }
    fflush(stdout);
}

static void
do_KeyRelease (XEvent *eventp)
{   
...

保存更新后的文件并且再次运行 make 来构建新版本的 xev。将 xev 复制到当前工作目录中,以供密码输入程序使用。要测试 xev 设置,请打开一个 xterm 窗口并键入 xwininfo 命令。单击当前的 xterm 窗口并查看输出。您应当会看到类似 “xwininfo: Window id: 0x32000012 nathan@kinakuta:~” 的一行。获取用于表示 xterm 的 Window ID 的十六进制代码并运行 ./xev -id 0x32000012 命令。现在将输出 xterm 窗口内发生的所有事件。尝试按不同的键或者移动鼠标,然后按 Ctrl+C 组合键退出。

处理键盘事件以获得总输入时间

许多软件开发人员都几乎按照平常的键入速度来输入最常用的密码。如果其他人要输入您的密码,他们的键入速度很可能会慢得多。下列代码的第一部分将展示一个简单的密码输入和确认程序,它将计算输入密码的完整时间。其他代码将展示您的密码输入通常以最快的速度键入或者具有稳定的节奏。清单 2 将通过主逻辑循环引入 xevKeyDyn.pl 程序。

清单 2. 主 xevKeyDyn.pl 程序逻辑
#!/usr/bin/perl -w 
# xevKeyDyn.pl  keystroke dynamics password demonstrator using xev
use strict;
$|=1; #to ensure timely display of non-printable equivalent strings

die "specify a window id to read xev events from" if( @ARGV != 1 );

my $cmd = "./xev -id $ARGV[0]";
my $maxTimeDiff = 500;  # one half second
my $maxKeyDiff = 100;   # one tenth second
my $passwordOK = 0;

my @keysOne = ();  # first password array of "key event time" items
my @keysTwo = ();  # confim password array

my @nonPrintable = (
'Control_','Alt_','Shift_','Up','Left','Right','Down','BackSpace','Print',
'_Lock','Prior','Next','Home','End','Insert','Delete','Pause','Break' );

# main program loop

while( $passwordOK == 0 )
{ 
  readPassword( \@keysOne, "Enter a");
  printPassword( \@keysOne );
  
  readPassword( \@keysTwo, "Confirm the");
  printPassword( \@keysTwo );
  
  my $catch = <STDIN>;  #initial password catch
     $catch = <STDIN>;  #confirmation password catch

  next unless( charactersMatch() );
  next unless( totalTimeOK() );
  
  $passwordOK = 1;

}#while passwordOk =0;

主程序逻辑首先关闭了缓冲输出。这项功能稍后将用于对不可打印的字符的管理。在检查确保已经指定了 X Window ID 之后,将为密码输入总时间和键事件输入时间设置一些变量。这些时间均以毫秒为单位,应该能够提供足够的精确度,但是打字速度严重缺乏规律的人除外。keysOnekeysTwo 变量将在输入时保存密码,而 nonPrintable 数组将包含支持的各个不可打印字符的最重要部分。程序将不断循环,直至输入匹配字符的有效密码和总输入时间。清单 3 显示了 readPassword 子例程。

清单 3. readPassword 子例程
sub readPassword
{
  my $arrRef = $_[0];  # looks less like line noise 
  @{$arrRef} = ();     # clear the hash

  shift;
  print "@_ password:\n";

  open( XEV, " $cmd |" ) or die "can't open xev";
  while( my $inLine = <XEV> )
  {
    if( $inLine =~ /KeyPress event/ || $inLine =~ /KeyRelease event/ )
    { 
      my $keyType = substr($inLine, 0, index($inLine,","));

      # get the time entry
      my $currTime = <XEV>
         $currTime = substr( $currTime, index($currTime,"time ")+5);
         $currTime = substr( $currTime, 0, index($currTime,","));

      # get the key name 
      my $currKey = <XEV>
         $currKey = substr( $currKey, index($currKey,"keysym ")+7);
         $currKey = substr( $currKey, 0, index($currKey,"),"));
         $currKey = substr( $currKey, index($currKey, ", ")+2);


      # echo the non-printable key names to the screen on key release only
      if( $keyType =~ /KeyRelease/ )
      { 
        map { print "<$currKey>" if( $currKey =~ /$_/ ) } @nonPrintable;
      }

      # continue to read if return pressed and no keys read, exit loop if return pressed
      # after keys have been entered
      next if( $currKey =~ /Return/ && @{$arrRef} == 0 );
      last if( $currKey =~ /Return/ && @{$arrRef} != 0 );

      # add the "time key type" to the array
      push @{$arrRef}, "$currTime $currKey $keyType";

    }#if a key press or release

  }#while xev in

  close(XEV);

}#readPassword

从上面执行的 xev 测试可以看到,单个按键或释放事件将导致 xev 程序打印四行或五行输出。readPassword 将侦听与键相关的事件,然后提取各个键名(如 JCtrl+L)。键名及其时间和类型随后将被添加到当前密码数组中以供稍后处理。如果至少已输入了一个字符,则在按下 Enter 键后,子例程将退出。清单 4 显示了 printPassword 子例程,该子例程用于输出密码键和计时以供参考。

清单 4. printPassword 子例程
sub printPassword
{
  my $arrRef = $_[0];  

  for my $keyStr ( @{$arrRef} )
  {
    my( $time, $key, $type ) = split " ", $keyStr;
    print "Key: $key at $time Type: $type\n";
  }

  print "Total time: ", getTotalTime( $arrRef );
  print "\n\n";
  
}#printPassword

使用上面列出的主程序代码和两个子例程,xevKeyDyn.pl 可以读取和输出两个密码。为确保密码字符和字符顺序匹配,我们将添加 charactersMatch 子例程,如清单 5 所示:

清单 5. charactersMatch 子例程
sub charactersMatch
{   
  # the hold a key and press enter check, or you type so fast you press enter
  # before releasing your previous key
  if( ($#keysOne+1) % 2 != 0 || ($#keysTwo+1) % 2 != 0 )
  { 
    print "Key event missed, please try again.\n";
    return(0);
  }
  
  if( $#keysOne != $#keysTwo )
  { 
    print "Passwords are not equal length\n";
    return(0);
  }
  
  my $count = 0;
  while( $count < $#keysOne )
  { 
    my( undef, $key1, undef ) = split " ", $keysOne[$count];
    my( undef, $key2, undef ) = split " ", $keysTwo[$count];
    last if( $key1 ne $key2 );
    $count++;
  }#while each key entry
  
  return(1) if( $count == $#keysOne );
  
  print "Password keys do not match. \n";
  return(0);

}#charactersMatch

执行的第一个检查将确保记录给定输入的按键和释放事件。试用 xevKeyDyn.pl 程序时,您可能会注意到某些击键趋于重叠,尤其是那些必须使用双手输入的击键。也就是说,在左手键入 a s d f 时,右手小手指挪去按 Enter 键,您通常会在释放 f 键的瞬间按下 Enter 键。这第一个检查将确保记录每次击键的按下和释放操作。

在经过简单的检查确保两个密码长度相等之后,将在整个密码中匹配各个键 —— 例如,确保两个密码中的第五个字符是零。如果收集了所有必需的按键和释放操作的密码长度都相同并且包含相同的字符,则找到一个匹配并且子例程成功退出。清单 6 将添加第一个基本的击键力度相关的检查子例程:密码输入总时间。

清单 6. totalTimegetTotalTime 检查子例程
sub totalTimeOK
{ 
  return(1) if( abs(getTotalTime(\@keysOne) - getTotalTime(\@keysTwo)) < $maxTimeDiff);
  print "Total length 1 is: ", getTotalTime( \@keysOne ), "\n";
  print "Total length 2 is: ", getTotalTime( \@keysTwo ), "\n";
  print "Passwords are too far apart in total length \n";
  return(0);
}#totalTimeOK

sub getTotalTime
{ 
  # get the first and last times, and return the difference
  my $arrRef = $_[0];
  my ($strTime, undef, undef ) = split " ", ${$arrRef}[0];  # first array item
  my ($endTime, undef, undef ) = split " ", ${$arrRef}[$#{$arrRef}]; # last array item
  return( $endTime - $strTime );
}#getTotalTime

子例程 getTotalTime 将用第一次击键时间减去最后一次击键时间(以毫秒为单位)。如果总时间差值小于 maxTimeDiff(500 毫秒),则 totalTimeOK 子例程认为密码及其确认有效。

用命令 perl xevKeyDyn.pl 0x32000012 运行程序。在这里尝试键入各种密码并且注意输入这些密码的总时间是如何记录的。养成一个不变的节奏,然后您可以把 maxTimeDiff 变量缩小到十分之一秒,对于高级用户,还可以缩小到更短的时间。

使用不可打印字符

将密码隐藏在视线触及不到的位置是增加安全方案模糊度的常用策略。使用不可打印的字符(例如退格键或暂停)可以提高密码的模糊度。将 xev 程序作为事件获取程序,允许无缝地添加不可打印字符要求。在启用了此功能后,即使您将密码中的字符告诉给朋友,他们仍然不能使用该密码。清单 7 显示了 hasNonPrintable 子例程,它可以针对 @nonPrintable 数组中的一个预期前缀检查密码。

清单 7. hasNonPrintable
sub hasNonPrintable
{ 
  for my $keyStr ( @keysOne )
  { 
    my( undef, $key, undef ) = split " ", $keyStr;
    map { return(1) if( $key =~ /$_/ ) } @nonPrintable;
  }

  print "A non-printable character is required.\n";
  return(0);

}#hasNonPrintable

通过向主程序循环中添加清单 8 中所示的行来启用 hasNonPrintable 检查。

清单 8. 向主程序循环中添加 hasNonPrintable
  next unless( charactersMatch() );
  next unless( totalTimeOK() );
  next unless( hasNonPrintable() ); # add at line 35

捕捉并要求使用不可打印字符是将密码隐藏在视线之外的一种方法。即使受到监视,使用后退键或者其他明显破坏密码字符串显示和顺序的字符,都可以使偶然的攻击者无所适从。

按键的时间

对密码模糊度和测量总计时的补充是击键需求之间的精度。如果快速键入密码的第一部分,但是由于键入数字或切换大小写而导致第二部分的键入速度减慢,则将测量连贯性。此外,如果打字者以不同的速度慢速输入字母,但是快速输入数字(可能使用了数字小键盘),individualKeyTimingsOK 子例程将检测到这些差异。

清单 9. individualKeyTimingsOK
sub individualKeyTimingsOK 
{ 

  my $count = 0;
  while( $count < ($#keysOne-1) )
  {
    my( $time1, undef, undef) = split " ", $keysOne[$count];
    my( $time2, undef, undef) = split " ", $keysOne[$count+1];

    my $timeDiff_0 = $time2 - $time1;

    ( $time1, undef, undef) = split " ", $keysTwo[$count];
    ( $time2, undef, undef) = split " ", $keysTwo[$count+1];

    my $timeDiff_1 = $time2 - $time1;

    if( abs($timeDiff_0 - $timeDiff_1) > $maxKeyDiff )
    {
      print "Intra-key timings invalid at position [$count] for " .
            "$timeDiff_0 and $timeDiff_1, please try again.\n";
      return(0);
    }
    $count++;

  }#for each character

  return(1);

}#individualKeyTimingsOK

清单 9 显示了 individualKeyTimingsOK 子例程。对于本示例,仅测量击键与紧随其后的击键之间的时间。如果密码之间的击键差异小于 maxKeyDiff 阈值(100 毫秒),则认为计时是匹配的。把阈值降低到小于十分之一秒的值,实现特定于打字者的某些匹配。作者过去选择密码的习惯是可以用单手在键盘上的不同位置键入。运行 xevKeyDyn.pl 程序后发现这些密码的总键入时间非常短,并且某些特定按键事件花费的时间都不超过 20 毫秒。即使对于最快的打字者来说,某些特定击键也很难实现 0.02 秒的重复精度,在知道正确的密码字符后就更不用进行猜测了。

通过向主程序循环中添加清单 10 中所示的行来启用 individualKeyTimingsOK 检查。

清单 10. 向主程序循环中添加 individualKeyTimingsOK
  next unless( charactersMatch() );
  next unless( totalTimeOK() );
  next unless( hasNonPrintable() ); 
  next unless( individualKeyTimingsOK() ); # add at line 36

当不可打印字符要求和键检查之间的各次计时就绪后,请用 perl xevKeyDyn.pl 0x32000012 运行程序。尝试不同的字符和不可打印字符的组合。请使用不同的速度按键或输入密码的各个部分来测试文本输入的新方式。

示例实现

考虑清单 11 中所示的示例密码。用新的方法输入的字符是 's-<backspace>-e-c-r-3-t:按住 s 键,直至按下并释放了密码中的最后一个键之后。xev 记录键盘事件的简单但强大的方法将无缝引入这项新功能。对于开发过简单游戏或实时用户界面的开发人员,应该非常熟悉人机交互中的重叠按键/释放事件。这项功能经常被忽略,并且密码文本框只记录按键事件而忽略释放事件。在这个监视击键力度的应用程序中,测量击键的按下和释放时间对于添加更健壮的识别功能非常重要。

清单 11. “secr3t” 的示例密码
nathan:$ perl xevKeyDyn.pl 0x32000012
Enter a password: 
s<BackSpace>cr3t             # note that the 'e' has been erased
Key: s at 9813585 Type: KeyPress   # note no immediate release for 's' key
Key: e at 9813832 Type: KeyPress
Key: e at 9813951 Type: KeyRelease
Key: BackSpace at 9814160 Type: KeyPress
Key: BackSpace at 9814264 Type: KeyRelease
Key: c at 9814483 Type: KeyPress
Key: c at 9814586 Type: KeyRelease
Key: r at 9814809 Type: KeyPress
Key: r at 9814880 Type: KeyRelease
Key: 3 at 9815078 Type: KeyPress
Key: 3 at 9815221 Type: KeyRelease
Key: t at 9815356 Type: KeyPress
Key: t at 9815451 Type: KeyRelease
Key: s at 9815649 Type: KeyRelease  # 's' key release

许多用户发现密码的特定输入时间要求不会给验证过程带来太多困扰。通过适当的说服工作,增加身份验证方案的安全性可以像对当前密码输入代码应用控制算法一样简单。

远程访问和实现说明

远程身份验证方案(例如 Ajax 登录页面或运行服务器托管的 X 会话的瘦客户机)需要进行特殊考虑。输入会话之间甚至每次击键之间的键事件计时精确性可能极为不同,因为网络反应时间和计算机处理时间可能会受到各种各样的影响。注意发生意外计算机状态(例如消耗额外资源的高网络负载或出现故障的磁盘驱动器)时,实现击键测量系统不会妨碍身份验证。

另请注意,实现击键力学要求用户能够精确地输入字符和击键。如果实现本文中所述的技术,则释放时间和初始按键时间必须精确。您可以修改本文所示的代码以仅支持按键事件。但是,在按下其他键时将丢失按键和持键信息。

结束语

您可以使用本文中所述的技术来增强现有 Perl 应用程序的身份验证方式,也可以根据这些概念修改任何程序。此外,击键力学是使用生物测量学进行连续身份验证的少数领域之一。考虑修改您的应用程序来使用本文中所述的一些技术以检测和跟踪常见输入错误、几乎同时按下的按键以及您的应用程序用户特有的其他键入模式。通过在 Web 身份验证应用程序中测量击键按下和释放时间,进一步推动 captcha 竞赛。


下载资源


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • 获得 xev 的最简单方法之一是下载完整的 X11 源代码
  • 从位于 Perl.org 的资源中获取 Perl
  • 从维基百科 (Wikipedia) 中获得关于 击键力学 的更多信息。
  • 使用 XML-RPC 为 C++ 应用程序启用 Web 服务” 是将 C++ 方法公开为服务的分步指南。
  • 收听针对软件开发人员的有趣访谈和讨论,一定要访问 developerWorks podcast
  • 访问 developerWorks 开放源码专区,获得丰富的 how-to 信息、工具和项目更新,帮助您用开放源码技术进行开发,并与 IBM 产品结合使用。
  • 使用 IBM 试用软件 改进您的下一个开发项目,这些软件可以通过下载或从 DVD 中获得。
  • 下载 IBM 产品评估版,并开始使用 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=281312
ArticleTitle=用击键力学扩展文本输入选项
publish-date=01102008