Skip to content

Instantly share code, notes, and snippets.

@titanous
Created March 5, 2009 20:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save titanous/74551 to your computer and use it in GitHub Desktop.
Save titanous/74551 to your computer and use it in GitHub Desktop.
Ruby implementation of Steve Gibson's 'Perfect Paper Passwords' v3
# 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