File Coverage

blib/lib/Passwd/Keyring/OSXKeychain.pm
Criterion Covered Total %
statement 23 51 45.1
branch 5 18 27.7
condition 0 6 0.0
subroutine 7 14 50.0
pod 5 5 100.0
total 40 94 42.5


line stmt bran cond sub pod time code
1             package Passwd::Keyring::OSXKeychain;
2              
3 11     11   813422 use warnings;
  11         23  
  11         437  
4 11     11   58 use strict;
  11         18  
  11         313  
5              
6 11     11   49 use Carp qw(croak);
  11         21  
  11         674  
7 11     11   8319 use IPC::System::Simple qw(capturex systemx runx);
  11         154341  
  11         1117  
8 11     11   8355 use Capture::Tiny qw(capture_merged);
  11         353905  
  11         1055  
9 11     11   5968 use Passwd::Keyring::OSXKeychain::PasswordTranslate qw(read_security_encoded_passwd);
  11         39  
  11         11539  
10              
11             # TODO: considering we use Capture::Tiny, maybe drop IPC::System::Simple
12             # and move to Capture::Tiny altogether (note that this means
13             # checking exit status and raising exceptions). Or at least
14             # drop all capturex.
15              
16             =head1 NAME
17              
18             Passwd::Keyring::OSXKeychain - Password storage implementation based on OSX/Keychain.
19              
20             =head1 VERSION
21              
22             Version 0.3002
23              
24             =cut
25              
26             our $VERSION = '0.3002';
27              
28             =head1 WARNING
29              
30             I do not have Mac. I wrote the library mimicking actions
31             of some python libraries and tested using mocks, but help
32             of somebody able to test it on true Mac is really needed.
33              
34             =head1 SYNOPSIS
35              
36             OSXKeychain Keyring based implementation of L. Provide secure
37             storage for passwords and similar sensitive data.
38              
39             use Passwd::Keyring::OSXKeychain;
40              
41             my $keyring = Passwd::Keyring::OSXKeychain->new(
42             app=>"blahblah scraper",
43             group=>"Johnny web scrapers",
44             );
45              
46             my $username = "John"; # or get from .ini, or from .argv...
47              
48             my $password = $keyring->get_password($username, "blahblah.com");
49             unless( $password ) {
50             $password = ;
51              
52             # securely save password for future use
53             $keyring->set_password($username, $password, "blahblah.com");
54             }
55              
56             login_somewhere_using($username, $password);
57             if( password_was_wrong ) {
58             $keyring->clear_password($username, "blahblah.com");
59             }
60              
61             Note: see L for detailed comments
62             on keyring method semantics (this document is installed with
63             C package).
64              
65             =head1 SUBROUTINES/METHODS
66              
67             =head2 new(app=>'app name', group=>'passwords folder')
68              
69             Initializes the processing. Croaks if osxkeychain keyring does not
70             seem to be available.
71              
72             Handled named parameters:
73              
74             - app - symbolic application name (not used at the moment, but can be
75             used in future as comment and in prompts, so set sensibly)
76              
77             - group - name for the password group (will be visible in seahorse so
78             can be used by end user to manage passwords, different group means
79             different password set, a few apps may share the same group if they
80             need to use the same passwords set)
81              
82             (OSXKeychain-specific)
83              
84             - security_prog - location of security program (/usr/bin/security by
85             default, possibility to overwrite is mostly needed for testing)
86              
87             - keychain - keychain to use (if not default)
88              
89             =cut
90              
91             sub new {
92 0     0 1 0 my ($cls, %opts) = @_;
93             my $self = {
94             app => $opts{app} || 'Passwd::Keyring',
95             group => $opts{group} || 'Passwd::Keyring unclassified passwords',
96             security => $opts{security_prog} || '/usr/bin/security',
97             keychain => $opts{keychain},
98 0   0     0 };
      0        
      0        
99 0         0 bless $self, $cls;
100              
101 0 0       0 unless( -x $self->{security} ) {
102 0         0 croak("OSXKeychain not available: security program $self->{security} is missing");
103             }
104 0 0       0 if($self->{keychain}) {
105             # Add .keychain suffix if missing
106             $self->{keychain} .= '.keychain'
107 0 0       0 unless $self->{keychain} =~ /\.keychain$/;
108             }
109              
110             # Making some security call to make sure it exists and works
111             # (we should die if Keychain is not available/not working)
112             my $reply = capturex(
113             $self->{security},
114 0         0 "list-keychains");
115             # list-keychains returns quoted, indented by 4 spaces list like:
116             # "/Users/maros/Library/Keychains/login.keychain"
117             # "/Library/Keychains/System.keychain"
118             # So far let's just test whether reply seems to contain anything.
119 0 0       0 unless($reply =~ /\.keychain/) {
120 0         0 croak("OSXKeychain not available: security program $self->{security} seems unaware of any keychains (security list-keychains returned '$reply')\n");
121             }
122              
123             # Another idea is to test specific keychain
124             # -q show-keychain-info «name»
125              
126 0         0 return $self;
127             }
128              
129             # Prepares args by prefixing with command and suffixing with keychain
130             # if specified
131             sub _make_keychainop_cmd {
132 0     0   0 my ($self, @args) = @_;
133 0         0 unshift @args, $self->{security};
134 0 0       0 push @args, $self->{keychain} if $self->{keychain};
135 0         0 return @args;
136             }
137              
138             =head2 set_password(username, password, realm)
139              
140             Sets (stores) password identified by given realm for given user
141              
142             =cut
143              
144             sub set_password {
145 0     0 1 0 my ($self, $user_name, $user_password, $realm) = @_;
146              
147             # TODO: maybe use -l (label) instead of -D
148             systemx($self->_make_keychainop_cmd(
149             "-q", # quiet
150             "add-generic-password",
151             "-a", $user_name,
152             "-s", $realm,
153             "-D", $self->{group}, # "kind", can be used to match so let be
154             "-w", $user_password,
155             "-j", $self->{app}, # comment
156             # "-A", # any app can access (note: alternative is -T app_path, which may be used many times). See issue #3
157 0         0 "-U", # update if present
158             ));
159             }
160              
161             # Parser for "-g" find-generic-password variant
162             sub _parse_password_from_find_output {
163 5     5   118 my ($text) = @_;
164              
165 5 100       109 if($text =~ /^ *password: *"([^"]*)"/m) {
    100          
    50          
166 2         13 return $1;
167             }
168             elsif($text =~ /^ *password: *\$([0-9A-Fa-f]*)/m) {
169 2         26 return pack("H*", $1);
170             }
171             elsif($text =~ /^ *password: *$/m) {
172 1         4 return "";
173             }
174             }
175              
176             # Set if we use -w (so handle international passwords), unset if -g
177             our $USING_ENCODED_OUTPUT = 1;
178              
179             =head2 get_password($user_name, $realm)
180              
181             Reads previously stored password for given user in given app.
182             If such password can not be found, returns undef.
183              
184             =cut
185              
186             sub get_password {
187 0     0 1   my ($self, $user_name, $realm) = @_;
188              
189 0 0         if($USING_ENCODED_OUTPUT) {
190             my $reply = capturex(
191             [0, 44],
192             $self->_make_keychainop_cmd(
193             "-q", # quiet
194             "find-generic-password",
195             "-a", $user_name,
196             "-s", $realm,
197             "-D", $self->{group}, # "kind", can be used to match so let be
198 0           "-w", # display (encoded) password only
199             ));
200 0           return read_security_encoded_passwd($reply);
201             }
202             else {
203             my $reply = capture_merged {
204             runx(
205             [0, 44], # Legal exit values. Some CpanTesters report 44 on password not found
206             $self->_make_keychainop_cmd(
207             "-q", # quiet
208             "find-generic-password",
209             "-a", $user_name,
210             "-s", $realm,
211             "-D", $self->{group}, # "kind", can be used to match so let be
212 0     0     "-g", # display the password
213             ));
214 0           };
215 0           return _parse_password_from_find_output($reply);
216             }
217             }
218              
219             =head2 clear_password($user_name, $realm)
220              
221             Removes given password (if present)
222              
223             Returns how many passwords actually were removed
224              
225             =cut
226              
227             sub clear_password {
228 0     0 1   my ($self, $user_name, $realm) = @_;
229              
230             my $reply = systemx($self->_make_keychainop_cmd(
231             "delete-generic-password",
232             "-a", $user_name,
233             "-s", $realm,
234             "-D", $self->{group}, # "kind", can be used to match so let be
235 0           ));
236              
237             }
238              
239             =head2 is_persistent
240              
241             Returns info, whether this keyring actually saves passwords persistently.
242              
243             (true in this case)
244              
245             =cut
246              
247             sub is_persistent {
248 0     0 1   my ($self) = @_;
249 0           return 1;
250             }
251              
252              
253             =head1 AUTHOR
254              
255             Marcin Kasperski
256              
257             =head1 BUGS
258              
259             Please report any bugs or feature requests to
260             issue tracker at L.
261              
262             =head1 SUPPORT
263              
264             You can find documentation for this module with the perldoc command.
265              
266             perldoc Passwd::Keyring::OSXKeychain
267              
268             You can also look for information at:
269              
270             L
271              
272             Source code is tracked at:
273              
274             L
275              
276             =head1 LICENSE AND COPYRIGHT
277              
278             Copyright 2012 Marcin Kasperski.
279              
280             This program is free software; you can redistribute it and/or modify it
281             under the terms of either: the GNU General Public License as published
282             by the Free Software Foundation; or the Artistic License.
283              
284             See http://dev.perl.org/licenses/ for more information.
285              
286             =cut
287              
288              
289             1; # End of Passwd::Keyring::OSXKeychain