File Coverage

blib/lib/Passwd/Keyring/KDEWallet.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Passwd::Keyring::KDEWallet;
2              
3 1     1   22915 use warnings;
  1         3  
  1         41  
4 1     1   7 use strict;
  1         1  
  1         43  
5             #use parent 'Keyring';
6 1     1   283 use Net::DBus;
  0            
  0            
7             use Try::Tiny;
8             use Proc::SyncExec qw/sync_exec/;
9             use Carp;
10              
11             =head1 NAME
12              
13             Passwd::Keyring::KDEWallet - Password storage implementation based on KDE Wallet.
14              
15             =head1 VERSION
16              
17             Version 0.60
18              
19             =cut
20              
21             our $VERSION = '0.60';
22              
23             our $APP_NAME = "Passwd::Keyring";
24             our $FOLDER_NAME = "Perl-Passwd-Keyring";
25              
26             # Max time we wait for kwalletd
27             our $KWALLETD_START_TIMEOUT = 2.0;
28             # Frequency of checks
29             our $KWALLETD_CHECK_FREQUENCY = 0.1;
30              
31             =head1 SYNOPSIS
32              
33             KDE Wallet based implementation of L.
34              
35             use Passwd::Keyring::KDEWallet;
36              
37             my $keyring = Passwd::Keyring::KDEWallet->new(
38             app=>"blahblah scraper",
39             group=>"Johnny web scrapers",
40             );
41              
42             my $username = "John"; # or get from .ini, or from .argv...
43              
44             my $password = $keyring->get_password($username, "blahblah.com");
45             unless( $password ) {
46             $password = ;
47              
48             # securely save password for future use
49             $keyring->set_password($username, $password, "blahblah.com");
50             }
51              
52             login_somewhere_using($username, $password);
53             if( password_was_wrong ) {
54             $keyring->clear_password($username, "blahblah.com");
55             }
56              
57             Note: see L for detailed comments
58             on keyring method semantics (this document is installed with
59             C package).
60              
61             =head1 SUBROUTINES/METHODS
62              
63             =head2 new
64              
65             Passwd::Keyring::KDEWallet->new(
66             app=>'app name', group=>'passwords folder');
67              
68             Passwd::Keyring::KDEWallet->new(
69             app=>'app name', group=>'passwords folder',
70             start_kwalletd_if_missing=>1);
71              
72             Initializes the processing. Croaks if kwallet (or d-bus, or anything
73             needed) does not seem to be available.
74              
75             Handled named parameters:
76              
77             =over 4
78              
79             =item app
80              
81             symbolic application name (used in "Application .... is asking
82             to open the wallet" KDE Wallet prompt)
83              
84             =item group
85              
86             name for the password group (used as KDE Wallet folder name)
87              
88             =item dont_start_daemon
89              
90             by default, in case kwalletd service is missing, we try to start
91             it, this option disables this behaviour
92              
93             =item kwalletd_path
94              
95             path to kwalletd binary, used in case we try starting it. Default:
96             C (relative path means searching in C).
97              
98             =back
99              
100             =cut
101              
102             sub new {
103             my ($cls, %args) = @_;
104              
105             my $self = {};
106             $self->{app} = $args{app} || 'Passwd::Keyring::KDEWallet';
107             $self->{group} = $args{group} || 'Passwd::Keyring::default';
108             $self->{dont_start_daemon} = $args{dont_start_daemon} || '';
109             $self->{kwalletd_path} = $args{kwalletd_path} || 'kwalletd';
110             bless $self, $cls;
111              
112             #$self->{bus} = Net::DBus->find()
113             $self->{bus} = Net::DBus->session()
114             or croak("KWallet not available (can't access DBus)");
115              
116             $self->_init_kwallet();
117              
118             $self->_open_if_not_open();
119              
120             unless($self->{kwallet}->hasFolder($self->{handle}, $self->{group}, $self->{app})) {
121             $self->{kwallet}->createFolder($self->{handle}, $self->{group}, $self->{app})
122             or croak("Failed to create or access $self->{group} folder (app $self->{app}).\nDid you reject prompt to open the wallet?\n");
123             }
124              
125             return $self;
126             }
127              
128             # Called from the constructor, setups self->{kwallet} attribute (top level
129             # service object)
130             sub _init_kwallet {
131             my $self = shift;
132              
133             # get_service may fail by itself, if kwalletd is down, in some
134             # cases it fails with
135             #
136             # org.freedesktop.DBus.Error.ServiceUnknown: The name org.kde.kwalletd was not provided by any .service files
137              
138             my $kwallet_svc;
139             my $error;
140             try {
141             $kwallet_svc = $self->{bus}->get_service('org.kde.kwalletd');
142             } catch {
143             $error = $_;
144             chomp($error);
145             };
146              
147             unless($kwallet_svc) {
148             # Mayhaps we are allowed to start kwalletd?
149             if(! $self->{dont_start_daemon}
150             && $error =~ /^org\.freedesktop\.DBus\.Error\.ServiceUnknown:/) {
151             print STDERR "KWallet service not available, attempting to start $self->{kwalletd_path}\n";
152             # spawn kwalletd
153             my $pid = sync_exec
154             sub {
155             # Without this prove (tests) hang
156             close (STDOUT);
157             return 1;
158             },
159             $self->{kwalletd_path};
160             unless($pid) {
161             croak "KWallet not available (not installed or not started)\nand attempt to start it failed\nOriginal error:\n$error" . "Attempted command:\n" . $self->{kwalletd_path} . "\nCommand failure: $!\n";
162             };
163             my $error2;
164             my $DEADLINE = time() + $KWALLETD_START_TIMEOUT;
165             # while($proc->alive && time() <= $DEADLINE) {
166             while(time() <= $DEADLINE) {
167             try {
168             $kwallet_svc = $self->{bus}->get_service('org.kde.kwalletd');
169             } catch {
170             $error2 = $_;
171             chomp($error2);
172             # warn "Still not available: $@\n";
173             };
174             last if $kwallet_svc;
175             sleep($KWALLETD_CHECK_FREQUENCY);
176             }
177             unless($kwallet_svc) {
178             croak "KWallet not available (not installed or not started),\nand attempt to start it did not help\nFirst error:\n$error\nLast error:\n$error2\nAttempted command: $self->{kwalletd_path}\n";
179             }
180             } else {
181             croak "KWallet not available (not installed or not started),\nand we are forbidden to start it ourselves.\nOriginal error:\n$error\n";
182             }
183             }
184              
185             $self->{kwallet} = $kwallet_svc->get_object(
186             '/modules/kwalletd', 'org.kde.KWallet')
187             or croak("Kwallet not available (can't find wallet)");
188             }
189              
190             sub _open_if_not_open {
191             my $self = shift;
192              
193             if($self->{handle}) {
194             if($self->{kwallet}->isOpen($self->{handle})) {
195             return;
196             }
197             }
198             my $net_wallet = $self->{kwallet}->networkWallet()
199             or croak("Kwallet not available (can't access network wallet");
200             $self->{handle} = $self->{kwallet}->open($net_wallet, 0, $self->{app})
201             or croak("Failed to open the KDE wallet");
202             }
203              
204             =head2 set_password(username, password, realm)
205              
206             Sets (stores) password identified by given realm for given user
207              
208             =cut
209              
210             sub set_password {
211             my ($self, $user_name, $user_password, $realm) = @_;
212             $self->_open_if_not_open();
213             my $status = $self->{kwallet}->writePassword(
214             $self->{handle}, $self->{group}, "$realm || $user_name", $user_password, $self->{app});
215             if($status) { # non-zero means failure
216             croak("Failed to save the password (status $status, user name $user_name, realm $realm, handle $self->{handle}, group $self->{group})");
217             }
218             }
219              
220             =head2 get_password($user_name, $realm)
221              
222             Reads previously stored password for given user in given app.
223             If such password can not be found, returns undef.
224              
225             =cut
226              
227             sub get_password {
228             my ($self, $user_name, $realm) = @_;
229             $self->_open_if_not_open();
230             my $reply = $self->{kwallet}->readPassword(
231             $self->{handle}, $self->{group}, "$realm || $user_name", $self->{app});
232             # In case of missing passsword we get empty string. I do not know
233             # whether it is possible to distinguish missing password from empty password,
234             # but empty passwords are exotic enough to ignore.
235             return undef if ! defined($reply) or $reply eq '';
236             return $reply;
237             }
238              
239             =head2 clear_password($user_name, $realm)
240              
241             Removes given password (if present)
242              
243             =cut
244              
245             sub clear_password {
246             my ($self, $user_name, $realm) = @_;
247             $self->_open_if_not_open();
248             my $status = $self->{kwallet}->removeEntry(
249             $self->{handle}, $self->{group}, "$realm || $user_name", $self->{app});
250             if($status == 0) {
251             return 1;
252             } else {
253             # TODO: classify failures
254             return 0;
255             }
256             }
257              
258             =head2 is_persistent
259              
260             Returns info, whether this keyring actually saves passwords persistently.
261              
262             (true in this case)
263              
264             =cut
265              
266             sub is_persistent {
267             my ($self) = @_;
268             return 1;
269             }
270              
271             =head1 AUTHOR
272              
273             Marcin Kasperski
274              
275             Approach inspired by L.
276              
277             =head1 BUGS
278              
279             Please report any bugs or feature requests to
280             issue tracker at L.
281              
282             =head1 SUPPORT
283              
284             You can find documentation for this module with the perldoc command.
285              
286             perldoc Passwd::Keyring::KDEWallet
287              
288             You can also look for information at:
289              
290             L
291              
292             Source code is tracked at:
293              
294             L
295              
296             =head1 LICENSE AND COPYRIGHT
297              
298             Copyright 2012 Marcin Kasperski.
299              
300             This program is free software; you can redistribute it and/or modify it
301             under the terms of either: the GNU General Public License as published
302             by the Free Software Foundation; or the Artistic License.
303              
304             See http://dev.perl.org/licenses/ for more information.
305              
306             =cut
307              
308              
309             1; # End of Passwd::Keyring::KDEWallet