Created
March 5, 2009 20:57
-
-
Save titanous/74551 to your computer and use it in GitHub Desktop.
Ruby implementation of Steve Gibson's 'Perfect Paper Passwords' v3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Perfect Paper Passwords v3 | |
# For more info see http://grc.com/ppp | |
# | |
# The MIT License | |
# | |
# Copyright (c) 2009 Jonathan Rudenberg | |
# Original version by Gavin Stark (http://is.gd/lYIf) | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in | |
# all copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
# THE SOFTWARE. | |
# | |
require 'rubygems' | |
require 'crypt/rijndael' | |
require 'digest/sha2' | |
# PerfectPaperPasswords can be initialized with the following options: | |
# | |
# Either of the following (required): | |
# :passphrase => textual passphrase to be SHA256 digested | |
# :sequency_key => 32 byte sequence key | |
# | |
# And (optional): | |
# :starting_sequence_number => The first sequence number to use when generating keys | |
# :character_set => The desired character set (string) | |
# :passcode_length => The length of the generated passcodes | |
# | |
# Example: | |
# ppp = PerfectPaperPasswords.new(:passphrase => "zombie", :starting_sequence_number => 0) | |
# puts ppp.next | |
# puts ppp.next(10) | |
# ppp.next(10) { |passcode| puts passcode } | |
class PerfectPaperPasswords | |
attr_reader :sequence_number | |
def initialize(*args) | |
options = args.last.is_a?(Hash) ? args.pop : {} | |
passphrase = options[:passphrase] | |
sequence_key = options[:sequence_key] || Digest::SHA256.digest(passphrase) | |
@sequence_number = options[:starting_sequence_number] || 0 | |
@character_set = (options[:character_set] || '!#%+23456789:=?@ABCDEFGHJKLMNPRSTUVWXYZabcdefghijkmnopqrstuvwxyz').split(//).sort | |
@passcode_length = options[:passcode_length] || 4 | |
raise "Invalid sequence key length" unless sequence_key.length == 32 | |
@cipher = Crypt::Rijndael.new(sequence_key) | |
end | |
# Generates the next passphrase(s) in sequence. | |
# | |
# When called without a parameter or block, returns just a single passcode | |
# ex: ppp.next # => "8N=3" | |
# | |
# When called with a parameter, and without a block, will return an array of passcodes | |
# ex: ppp.next(5) #=> [ "7ucE" "aAg3" "zVv#" "y2Fm" "nGc8" ] | |
# | |
# When called with a block, will yield the specified number of passcodes (default 1) | |
# ex: ppp.next { |passcode| puts passcode } | |
# ex: ppp.next(5) { |passcode| puts passcode } | |
def next(count=nil) | |
# If a block is given we yield the next | |
# passcode the specified number of times. | |
if block_given? | |
count ||= 1 | |
count.times do | |
yield generate_next | |
end | |
return | |
end | |
# If no count is given, just return a single passcode | |
return generate_next if count.nil? | |
# Return an array of the number of passcodes requested | |
return (1..count).collect { generate_next } | |
end | |
private | |
def generate_next | |
packed_sequence_number = pack128(@sequence_number) | |
packed_sequence_number << ("\000" * (16-packed_sequence_number.length)) | |
cipher_code = unpack128(@cipher.encrypt_block(packed_sequence_number)) | |
passcode = '' | |
@passcode_length.times do | |
passcode += @character_set[cipher_code % @character_set.length] | |
cipher_code = cipher_code / @character_set.length | |
end | |
@sequence_number += 1 | |
return passcode | |
end | |
def pack128(integer) | |
packed_integer = '' | |
while integer != 0 do | |
packed_integer += (integer%256).chr | |
integer = integer / 256 | |
end | |
return packed_integer | |
end | |
def unpack128(packed_integer) | |
integer = 0 | |
packed_integer.reverse.each_byte do |c| | |
integer = (integer*256)+c | |
end | |
return integer | |
end | |
end | |
# Test suite for PerfectPaperPasswords | |
require 'test/unit' | |
class PerfectPaperPasswordsTest < Test::Unit::TestCase | |
def setup | |
@passphrase = 'zombie' | |
@starting_sequence_number = 0 | |
@ppp = PerfectPaperPasswords.new(:passphrase => @passphrase, :starting_sequence_number => @starting_sequence_number) | |
end | |
def test_next_without_parameter_produces_single_passcode | |
assert_equal "4p=#", @ppp.next | |
end | |
def test_next_with_parameter_produces_array_of_passcodes | |
assert_equal %w{4p=#}, @ppp.next(1) | |
assert_equal %w{eXk? z=ao c!Zb T!Lb S8Ex fGF5 t8u+ ps4p qSxM}, @ppp.next(9) | |
end | |
def test_next_with_block_only_yields_single_passcode | |
results = [] | |
@ppp.next do |passcode| | |
results << passcode | |
end | |
assert_equal %w{4p=#}, results | |
end | |
def test_next_with_parameter_and_block_yields_multiple_passcodes | |
results = [] | |
@ppp.next(10) do |passcode| | |
results << passcode | |
end | |
assert_equal %w{4p=# eXk? z=ao c!Zb T!Lb S8Ex fGF5 t8u+ ps4p qSxM}, results | |
end | |
def test_passcode_from_steves_example | |
# From: http://www.grc.com/ppp/algorithm.htm | |
assert_equal %w{4p=# eXk? z=ao c!Zb T!Lb S8Ex fGF5 t8u+ ps4p qSxM}, @ppp.next(10) | |
end | |
def test_first_500 | |
# generated by http://www.jgc.org/blog/2008/02/ppp3-in-java-and-c.html | |
expected_codes = %w{ 4p=# eXk? z=ao c!Zb T!Lb S8Ex fGF5 t8u+ ps4p qSxM | |
TX#Y bz!b +gR= jo2r PPEh PD#x uEox U6ZS eW42 zrci | |
54yL HaiP gejs 48Vd 6nzo ztKn PVJ: sNnU Lp#F 3qWs | |
bnR5 6dAf XBmH 8=9# iLEL ==nE ?hHA uNd@ V#G5 zFz4 | |
7Cxc 5TU+ zceN !5Yc YVvL W3Sa kKnw :bAT vpJ5 HMS6 | |
km%G hRz6 ypKb hN39 hG+Z uxGJ BZWq 3rEH kRfv ig?U | |
qYif E?G5 qDrL V:aw ezjM %x@n NEzg nyj7 zR@9 =Gf6 | |
npkH gzyo rU8K 2:g9 k:?x cJA5 WNUN r6D6 Ayj7 #i3R | |
9vvV WdU7 ZLGt Yr7i P8ps F#78 iBFJ =6Vg jP7% p+b: | |
whWF CPyU DBM% Lnha GYLx uvzy 2CWk %b2S GAL? wh?S | |
Fqc2 cs7@ NG9V KVxs Hbfj XAU6 6jta 6ufa xKUN R83L | |
mjTw 7Nhj zqHd MJJW uFLa d473 5C%E hv99 mWdP oZb4 | |
tZww D7rt VAc+ m3YD pCT: z9LM aA+S #wE9 varJ k:hB | |
iFXm Zf!W CefM Dyyp ec+w +@SA kA2P DqHJ 3si% LY+i | |
rkET 3Gch jX%5 4owk #7CM iyoq eedh emde Ta?e WWNZ | |
ZWku i?z= qB!k Atg+ e%b5 yKG: F3pM MCKB 68Mw Dcm+ | |
qv:J f6G= UPj3 waMv @ran :Pkg eiVE omXN #D2d PbNy | |
%uGH 8BUH KrK: r?EU eNM3 =gHU mA3@ +guS 5xZy ofvm | |
A%2= W4f= @=gy 3W2d ysW% mjF8 Ax?o JZTK 5GBT gTq? | |
q?g? Ci6% GJT# Rcgg i5H7 TV62 L!A@ pHZe tdJ4 dd?? | |
T3Af GVYH KozW eSG= %5oX YCCj oJyE jj?o J#mZ 5!GY | |
aCDC E#UW Ffyf 8t3A U7N5 Be84 dvta #gG% =?iT mKSf | |
2RF! T3it Xjaa @b+? m+yo 87MD ?fZa Sksp F9Rb M?Z% | |
W%m7 ucFD @goG ?!b% UdRG og4@ 4MbH Ajoh UeHe b5Mw | |
En5H p4zB g4wB ZaDd f?u: TFCr of2+ 9cC! vn5b kTUp | |
@%we a6zM !Rox U7S8 C?Pt 4mNL cGP5 PAPq po=s rV4a | |
ezss n8fc WEk4 gapS hLET h#9= A@3: A45t gY9n 2%vJ | |
swev wXfd agcc tecv moya u6mj Gp4T NF5d 8wDL G9UF | |
KqX# BSC5 #uz6 Cd:D L4H7 a22R +Se@ z67f Grur E9H5 | |
!d6n @Hnx JYHV NbdT SJPP X2Wt pPS9 a9q8 2%#F 9ASZ | |
Saon q5gv 8X4F bcPs hFMz 5g4n %Vyc aiiP WMk+ mwuu | |
4Yaj CWe6 Yrt% +qbE oUdH 8bvJ Au:D 4yHg PbcR ig:h | |
EXKT u!wR csrB Zx6S 44nS 34J% RjGt Stnn cMtX JTq@ | |
TN7X tdJ= ij8M soUV wVpa %j+e u69Y Kjun Ai3B FuJS | |
5+rM 59AA t!9a CmX+ sMuH APZb PCch 3@:A p9gx %UMk | |
dDK2 X+rS s4Lx 4=MY uV#= :Vgg SVNs qZnL Gyt3 6nx4 | |
XFaV 7%GL PPdx JXXf oHGH Czkw 8RAp s5aK dxun seSi | |
dAty x%+v pG5E i269 2N95 FqcN Bf3? H5MJ jNqi p?LL | |
2+3B Zzy= 8d43 RmsE w25S M#dL moyU TGCJ U46U orZU | |
#Wv? onnv gs:g :pjL sZSn j8Pv Sya% +fKK 2ad+ VHb6 | |
b4#F 3Wxe gai4 ?+7s R9T? LZSi yLqZ qxYc zSez JXkN | |
P?Wp fP+F 3fvt %e2D T4Xz iV=S hyoY M6PS Z77X s@yq | |
aR7y q2!q @kia kPfx !s9m HNj3 G6:? N:As 9zCY D=Zy | |
@J@b pgL? 5S=: HMWE 2dfT sjwP 8=eK atN= kDTP NoLB | |
bKVX =!Rn sDUe RNKv j!ww VsV= G7KM 9?ez =s?t !d?y | |
Yg2G 46ap ?aYL e!+M %%q? Kn@s xJNH gUeb 8JF! ijip | |
:@CT 9CZk M6bB aw86 C@tj PBkf x4j% DkV7 54Kp uiHp | |
zsom ?+?c =7EK znoy A5pE 6g8! kRuB %CWM U2bU trMN | |
Tn2p q7qM dYRS %o:J vFg: @@Mw ##pf n5z! n84j R9Up | |
bNy+ 7SY6 iDaA =C=x Dt!t 98pX +uF8 K!4G v2UT Ji7F } | |
assert_equal expected_codes, @ppp.next(500) | |
end | |
def test_sequence_number_is_initialized | |
assert_equal @starting_sequence_number, @ppp.sequence_number | |
end | |
def test_sequence_number_is_incremented_when_necessary | |
@ppp.next | |
assert_equal @starting_sequence_number + 1, @ppp.sequence_number | |
@ppp.next(5) | |
assert_equal @starting_sequence_number + 6, @ppp.sequence_number | |
end | |
def test_character_set_is_changeable | |
ppp = PerfectPaperPasswords.new(:passphrase=> @passphrase, :starting_sequence_number => @starting_sequence_number, :character_set => '0123456789') | |
assert_equal %w{4392 3448 7028 9846 0291}, ppp.next(5) | |
end | |
def test_length_is_changeable | |
ppp = PerfectPaperPasswords.new(:passphrase=> @passphrase, :starting_sequence_number => @starting_sequence_number, :passcode_length => 6) | |
assert_equal %w{4p=#3W eXk?oE z=ao?9 c!ZbRp T!LbaU}, ppp.next(5) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment