File Coverage

blib/lib/Crypt/Pwsafe.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Crypt::Pwsafe;
2             # $Id$
3              
4             =head1 NAME
5              
6             Crypt::Pwsafe - Perl extension for decrypting and parsing PasswordSafe V3 data files
7              
8             =cut
9              
10 2     2   56996 use warnings;
  2         7  
  2         70  
11 2     2   13 use strict;
  2         3  
  2         73  
12              
13 2     2   2141 use FileHandle;
  2         27029  
  2         13  
14 2     2   2828 use Term::ReadKey; # comment me out
  0            
  0            
15             #use autouse Term::ReadKey;
16             my $SHA = "Digest::SHA";
17             eval "use $SHA";
18             if ($@) { $SHA .= "::PurePerl"; eval "use $SHA" }
19              
20             my $CIPHER = "Crypt::Twofish";
21             eval "use $CIPHER";
22             if ($@) { $CIPHER .= "_PP"; eval "use $CIPHER" }
23              
24             =head1 VERSION
25              
26             Version 1.2
27              
28             =cut
29              
30             our $VERSION = '1.2';
31              
32             our $DEBUG = 0;
33              
34             =head1 SYNOPSIS
35              
36             use Crypt::Pwsafe;
37             my $file = 'pwsafe.psafe3';
38             my $key = Crypt::Pwsafe::enter_combination();
39             my $pwsafe = Crypt::Pwsafe->new($file, $key);
40              
41             # The password for 'user' at 'host' computer in 'Test group'
42             my $passwd = $pwsafe->{'Test group'}->{'user@host'};
43              
44             =cut
45              
46             my %FieldType = (
47             0 => "None",
48             1 => "UUID",
49             2 => "Group",
50             3 => "Title",
51             4 => "User",
52             5 => "Notes",
53             6 => "Password",
54             7 => "CTime",
55             8 => "PWMTime",
56             9 => "ATime",
57             10 => "LifeTime",
58             11 => "Policy",
59             12 => "RecordMTime",
60             13 => "URL",
61             14 => "AutoType",
62             15 => "PWHistory",
63             255 => "EndofEntry"
64             );
65              
66             # pwsafe3 file header format
67             # V3TAG == "PWS3";
68             # SALT = 32 bytes random
69             # NumHashIters = 32 bit integer (little endian)
70             # Hash = 32 bytes (NumHashIters+1 rounds of SHA256 of Safe combination concatenated with SALT)
71             # B1B2 = mKey encrypted using ECB Twofish with PTag as key
72             # B3B4 = hmac SHA256 key encrypted using ECB Twofish with PTag as key
73             # CBC IV = random 16 bytes
74              
75             # Notes on records
76             # 1. All times are 32-bit little-endian integers
77             # 2. All field values except UUID and times use UTF8
78             # 3. SHA256 HMAC at the end of file is calculated on field values only
79              
80             =head1 DESCRIPTION
81              
82             Crypt::Pwsafe module provide read-only access to database files created by Version 3
83             of PasswordSafe utility available from SourceForge at L.
84              
85             Users of this module should take these notes:
86              
87             1. All passwords will be stored in memory unencrypted (in the form of Perl hashes) once
88             the password file is loaded.
89              
90             2. The module will read the entire content of the password file into memory. This may
91             be a problem for large data files on systems with small amount of memory.
92              
93             3. The modules does not support Version 2 Passwordsafe data files. Please convert
94             them to Version 3 if needed.
95              
96             =cut
97              
98             sub new {
99             my ($class, $file, $pw) = @_;
100             my $fh = new FileHandle $file;
101             die "Failed to open $file\n" unless defined $fh;
102             $pw = enter_combination() unless defined $pw;
103             my $header;
104             my $len = 72;
105             unless ($fh->read($header, $len) == $len) {
106             die "$file has < $len bytes.\n";
107             }
108             $header =~ /^PWS3/ or warn "$file is not a version 3 Password Safe data file.\n";
109             my $salt = substr($header, 4, 32);
110             my $n_iters = unpack('V', substr($header, 36, 4));
111             warn "$file uses < 2048 iterations of hash.\n" if $n_iters < 2048;
112             warn "$file uses $n_iters iterations of hash?\n" if $n_iters > 20480;
113             my $fhash = substr($header, 40, 32);
114             my $ptag = _stretch_key($salt, $n_iters, $fhash, $pw);
115             die "Bad safe combination.\n" unless $ptag;
116             my $crypt = "";
117             # Assume that the whole PWsafe file can comfortably fit into the memory
118             while ($fh->read(my $buf, 0x400000)) {
119             $crypt .= $buf;
120             }
121             $fh->close;
122             my $self = _decrypt($ptag, $crypt);
123             return bless($self, $class);
124             }
125              
126             sub _decrypt {
127             my ($ptag, $crypt) = @_;
128             my $len = length($crypt);
129             die "Data is too short: $len bytes\n" unless $len > 112;
130             die "Data length is not multiple of 16\n" unless $len % 16 == 0;
131             my $term_blk = substr($crypt, -48, 16);
132             $term_blk eq 'PWS3-EOFPWS3-EOF' or warn "Bad terminal block\n";
133             my $hmac_tail = substr($crypt, -32);
134             my ($key, $hmac_key) = _ecb_twofish($ptag, $crypt, 64);
135             return _cbc_twofish($key, substr($crypt, 64, -48), $hmac_key, $hmac_tail);
136             }
137              
138             sub _ecb_twofish {
139             my ($ptag, $crypt, $len) = @_;
140             my $fish = $CIPHER =~ /Twofish_PP/ ?
141             Crypt::Twofish_PP->new($ptag) : Crypt::Twofish->new($ptag);
142             my $bs = $fish->blocksize;
143             my $head = "";
144             for (my $i = 0; $i < $len; $i += $bs) {
145             $head .= $fish->decrypt(substr($crypt, $i, $bs));
146             }
147             return unpack("a32a32", $head);
148             }
149              
150             sub _cbc_twofish {
151             my ($key, $crypt, $hmac_key, $hmac_tail) = @_;
152             my $fish = $CIPHER =~ /Twofish_PP/ ?
153             Crypt::Twofish_PP->new($key) : Crypt::Twofish->new($key);
154             my $bs = $fish->blocksize;
155             my $prev_crypt = substr($crypt, 0, $bs);
156             my $ptr = $bs;
157             my $chain_blocks = sub {
158             my $curr_crypt = substr($crypt, $ptr, $bs);
159             $ptr += $bs;
160             my $curr_plain = $fish->decrypt($curr_crypt) ^ $prev_crypt;
161             $prev_crypt = $curr_crypt;
162             return $curr_plain;
163             };
164             my $plain = "";
165             my $pwsafe = {};
166             my $crypt_len = length($crypt);
167             my ($group, $title, $user);
168             my $entry = {};
169             while($ptr < $crypt_len) {
170             my $curr_plain = $chain_blocks->();
171             # Passwordsafe uses little-endian
172             my ($len, $type) = unpack("VC", $curr_plain);
173             #printf "len=%2d type=%3d ", $len, $type;
174             die "Read negative length from CBC\n" if $len < 0;
175             my $buf_len = $len > 11 ? 11 : $len;
176             my $buf = substr($curr_plain, 5, $buf_len);
177             $len -= $buf_len;
178             while($len > 0) {
179             my $curr_plain = $chain_blocks->();
180             if ($len >= $bs) {
181             $buf .= $curr_plain;
182             $len -= $bs;
183             } else {
184             $buf .= substr($curr_plain, 0, $len);
185             $len = 0;
186             }
187             }
188             $plain .= $buf;
189             #print unpack("H*", $buf), "\n";
190             if ($type == 1) { # UUID
191             $entry->{UUID} = unpack("H*", $buf);
192             print "\tUUID=$entry->{UUID}\n" if $DEBUG;
193             } elsif ($type == 2) { # Group
194             $group = pack("U0C*", unpack("C*", $buf));
195             print "Group=$group\n" if $DEBUG;
196             } elsif ($type == 3) { # Title
197             $title = pack("U0C*", unpack("C*", $buf));
198             print " Title=$title\n" if $DEBUG;
199             } elsif ($type == 4) { # Username
200             $user = pack("U0C*", unpack("C*", $buf));
201             print " User=$user\n" if $DEBUG;
202             } elsif ($type == 0xff) { # End of Entry
203             if (defined($title) and defined($user)) {
204             if (exists $pwsafe->{$group}) {
205             $pwsafe->{$group}->{"$user\@$title"} = $entry;
206             } else {
207             $pwsafe->{$group} = {"$user\@$title" => $entry};
208             }
209             } else {
210             $pwsafe->{$group} = { dummy => $entry};
211             }
212             ($group, $title, $user) = (undef, undef, undef);
213             $entry = {};
214             } else {
215             my $descr = $FieldType{$type};
216             $descr = "Type$type" unless defined $descr;
217             my $value;
218             if ($descr=~/Time/) {
219             $value = unpack("V", $buf);
220             } else {
221             $value = pack("U0C*", unpack("C*", $buf));
222             }
223             $entry->{$descr} = $value;
224             print "\t$descr=$value\n" if $DEBUG;
225             }
226             }
227             my $hmac = Digest::SHA::hmac_sha256($plain, $hmac_key);
228             die "SHA256 HMAC error: data integrity has been compromised.\n" unless $hmac eq $hmac_tail;
229             return $pwsafe;
230             }
231              
232             sub _stretch_key {
233             my ($salt, $n_iters, $fhash, $pw) = @_;
234             my $sha = eval("new $SHA(256)");
235             $sha->add("$pw$salt");
236             my $key = $sha->digest;
237             for(my $i = 0; $i < $n_iters; $i++) {
238             $sha->add($key);
239             $key = $sha->digest;
240             }
241             $sha->add($key);
242             return $key if $sha->digest eq $fhash;
243             }
244              
245             sub enter_combination {
246             print "Enter password safe combination: ";
247             local $SIG{__DIE__} = { ReadMode 0 };
248             ReadMode 2;
249             my $pass = ;
250             ReadMode 0;
251             chomp($pass);
252             print "\n";
253             return $pass;
254             }
255              
256             sub get_password {
257             my ($self, $group, $user_title) = @_;
258             return unless exists $self->{$group};
259             my $gh = $self->{$group};
260             return unless exists $gh->{$user_title};
261             my $uh = $gh->{$user_title};
262             return unless exists $uh->{Password};
263             $uh->{Password};
264             }
265              
266             =head1 AUTHOR
267              
268             Shufeng Tan, C<< >>
269              
270             =head1 BUGS
271              
272             Please report any bugs or feature requests to
273             C, or through the web interface at
274             L.
275             I will be notified, and then you'll automatically be notified of progress on
276             your bug as I make changes.
277              
278             =head1 SUPPORT
279              
280             You can find documentation for this module with the perldoc command.
281              
282             perldoc Crypt::Pwsafe
283              
284             You can also look for information at:
285              
286             =over 4
287              
288             =item * AnnoCPAN: Annotated CPAN documentation
289              
290             L
291              
292             =item * CPAN Ratings
293              
294             L
295              
296             =item * RT: CPAN's request tracker
297              
298             L
299              
300             =item * Search CPAN
301              
302             L
303              
304             =back
305              
306             =head1 ACKNOWLEDGEMENTS
307              
308             PasswordSafe is a password database utility, originally developed by Counterpane Labs.
309             PasswordSafe project is currently administered by Rony Shapiro. The project homepage is
310             located at:
311              
312             L
313              
314             =head1 COPYRIGHT & LICENSE
315              
316             Copyright 2006 Shufeng Tan, all rights reserved.
317              
318             This program is free software; you can redistribute it and/or modify it
319             under the same terms as Perl itself.
320              
321             =cut
322              
323             1