PageRenderTime 136ms CodeModel.GetById 27ms RepoModel.GetById 21ms app.codeStats 1ms

/lib/Passwd/Keyring/PWSafe3.pm

https://bitbucket.org/Mekk/perl-keyring-pwsafe3
Perl | 368 lines | 255 code | 106 blank | 7 comment | 48 complexity | 9d9de4fe45724f6cc29c0e4cc26438fd MD5 | raw file
  1. package Passwd::Keyring::PWSafe3;
  2. use warnings;
  3. use strict;
  4. #use parent 'Keyring';
  5. use Crypt::PWSafe3;
  6. use File::Spec;
  7. use File::Basename;
  8. use Term::ReadKey; # For secure password prompt
  9. use Carp;
  10. =head1 NAME
  11. Passwd::Keyring::PWSafe3 - Password storage based on Password Safe encrypted files
  12. =head1 VERSION
  13. Version 0.61
  14. =cut
  15. our $VERSION = '0.61';
  16. our $APP_NAME = "Passwd::Keyring";
  17. our $FOLDER_NAME = "Perl-Passwd-Keyring";
  18. =head1 SYNOPSIS
  19. Password Safe implementation of L<Passwd::Keyring>. Passwords are
  20. stored in the L<Password Safe|http://passwordsafe.sourceforge.net>
  21. encrypted file.
  22. use Passwd::Keyring::PWSafe3;
  23. my $keyring = Passwd::Keyring::PWSafe3->new(
  24. app=>"blahblah scraper",
  25. group=>"Johnny web scrapers",
  26. file=>"/home/joe/secrets.pwsafe3", # ~/passwd-keyring.pwsafe3 by default
  27. master_password=>"very secret password", # Or callback. See below
  28. );
  29. my $username = "John"; # or get from .ini, or from .argv...
  30. my $password = $keyring->get_password($username, "blahblah.com");
  31. unless( $password ) {
  32. $password = <somehow interactively prompt for password>;
  33. # securely save password for future use
  34. $keyring->set_password($username, $password, "blahblah.com");
  35. }
  36. login_somewhere_using($username, $password);
  37. if( password_was_wrong ) {
  38. $keyring->clear_password($username, "blahblah.com");
  39. }
  40. =head1 DESCRIPTION
  41. This module does not require Password Safe to be installed, and can be
  42. used as generic "store many passwords in file encrypted with single
  43. master password" storage. Password Safe GUI, if installed, may help
  44. the user to review, modify, or delete saved passwords.
  45. =over 4
  46. Official GUIs can be freely downloaded from L<the official
  47. site|http://passwordsafe.sourceforge.net> - both Windows and (beta)
  48. Linux versions are available. Apart from them there exist
  49. various L<compatible tools|http://passwordsafe.sourceforge.net/relatedprojects.shtml>,
  50. for example <Pasaffe for Gnome|https://launchpad.net/pasaffe> or
  51. <PwSafe for Mac|http://passwordsafe.sourceforge.net/relatedprojects.shtml>.
  52. =back
  53. Actual handling of Password Safe format is based on L<Crypt::PWSafe3>
  54. module. Passwd::Keyring::PWSafe3 just wraps it into the interface
  55. compatible with other Passwd::Keyring backends.
  56. See L<Passwd::Keyring::Auto::KeyringAPI> for detailed comments
  57. on keyring methods (this document is installed with
  58. C<Passwd::Keyring::Auto> package).
  59. =head1 CAVEATS
  60. Underlying module (L<Crypt::PWSafe3>) in fact rewrites the whole file
  61. on every save and keeps all passwords cached in memory while active.
  62. This means, that any attempts to use the file paralelly from a few
  63. programs, or from a few objects within one program, are doomed to
  64. cause lost updates. Also, all passwords from the file are kept in
  65. (unprotected) memory while keyring object is active. Therefore, it is
  66. recommended to use separate .psafe3 file for Passwd::Keyring::PWSafe3,
  67. not mixing it with normal Password Safe database, and to keep keyring
  68. object for a short time only, especially if modifications happen.
  69. There are some limitations in L<Crypt::PWSafe3> handling of Password
  70. Safe format. Passwords are read and saved properly and it is possible
  71. to alternate using them from perl, and via Password Safe GUI, but some
  72. less important aspects of the format, like password expiraton policy,
  73. may be ignored. Refer to L<Crypt::PWSafe3> docs for more details.
  74. =head1 DATA MAPPING
  75. Group name is mapped to Password Safe folder.
  76. Realm is mapped as password title.
  77. Username and password are ... well, used as username and password.
  78. =head1 SUBROUTINES/METHODS
  79. =head2 new(app=>'app name', group=>'passwords folder', file=>'pwsafe3 file', master_password=>'secret or callback', lazy_save=>1)
  80. Initializes the processing. Croaks if Crypt::PWSafe3 is not installed or
  81. master password is invalid. May create password file if it is missing.
  82. Handled parameters:
  83. =over 4
  84. =item app
  85. Symbolic application name (used in password notes)
  86. =item group
  87. Name for the password group (used as folder name)
  88. =item file
  89. Location of .pwsafe3 file. If not given, C<passwd-keyring.pwsafe3> in
  90. user home directory is used. Will be created if does not exist. Note:
  91. absolute path is required, relative paths are very error prone.
  92. =item master_password
  93. Password required to unlock the file. Can be given as string, or
  94. as callback returning a string (usually some way of interactively
  95. asking user for the password). The callback gets two parameters: app
  96. and file.
  97. If this param is missing, module will prompt interactively for this
  98. password using console prompt.
  99. =item lazy_save
  100. if given, asks not to save the file after every change (saving is
  101. fairly time consuming), but only when $keyring->save is called or when
  102. keyring is destroyed.
  103. =back
  104. Note: it of course does not make much sense to keep app passwords in
  105. encrypted storage if master password is saved in plain text. The
  106. module most natural usage is to interactively ask for master password
  107. (and use it to protect noticeable number of application-specific
  108. passwords).
  109. Ideas of how to workaround this obstacle are welcome. I loosely
  110. consider either caching master password per desktop session
  111. (implementing sht. similar to ssh-agent/gpg-agent or using one of
  112. those somehow), or integrating the tool with PAM to use actual system
  113. password, or both - but while it seems doable on Linux, cross platform
  114. solution is not so easy.
  115. =cut
  116. sub new {
  117. my ($cls, %args) = @_;
  118. my $self = {};
  119. $self->{app} = $args{app} || 'Passwd::Keyring::PWSafe3';
  120. $self->{group} = $args{group} || 'Passwd::Keyring';
  121. $self->{lazy_save} = $args{lazy_save};
  122. my $file = $args{file} || File::Spec->catfile(File::HomeDir->users_data, "passwd-keyring.pwsafe3");
  123. unless(File::Spec->file_name_is_absolute($file)) {
  124. croak("Absolute path to .pwsafe3 file is required, but relative path '$file' given");
  125. }
  126. my $parent_dir = dirname($file);
  127. unless(-d $parent_dir) {
  128. croak("Directory $parent_dir (parent directory of file $file) does not exist");
  129. }
  130. # TODO: escape group (note that . are used for hierarchy!)
  131. # TODO: some locking or maybe detect gui
  132. bless $self;
  133. my $master = $args{master_password} || \&_prompt_for_password;
  134. if(ref($master) eq 'CODE') {
  135. $master = $master->($self->{app}, $file);
  136. }
  137. $self->{vault} = Crypt::PWSafe3->new(file=>$file, password=>$master);
  138. return $self;
  139. }
  140. sub DESTROY {
  141. my $self = shift;
  142. $self->save();
  143. }
  144. sub _prompt_for_password {
  145. my ($app, $file) = @_;
  146. print "* The applicaton $app is requesting to access\n";
  147. print "* the Password Safe file $file\n";
  148. if (-f $file) {
  149. print "* Enter master password necessary to unlock this file.\n";
  150. } else {
  151. print "* (the file does not exist and will be created on first password save)\n";
  152. print "* Enter master password which will protect this file.\n";
  153. }
  154. while(1) {
  155. print " Master password: ";
  156. ReadMode 'noecho';
  157. my $password = ReadLine 0; chomp($password);
  158. ReadMode 'normal';
  159. print "\n";
  160. return $password if $password;
  161. }
  162. }
  163. # Find data record for given params, or undef if not found
  164. sub _find_record {
  165. my ($self, $username, $realm) = @_;
  166. my $group = $self->{group};
  167. foreach my $record($self->{vault}->getrecords()) {
  168. if( ($record->group || '') eq $group
  169. && ($record->user || '') eq $username
  170. && ($record->title || '') eq $realm) {
  171. return $record;
  172. }
  173. }
  174. return undef;
  175. }
  176. =head2 set_password(username, password, realm)
  177. Sets (stores) password identified by given realm for given user
  178. =cut
  179. sub set_password {
  180. my ($self, $user_name, $user_password, $realm) = @_;
  181. my $rec = $self->_find_record($user_name, $realm);
  182. if($rec) {
  183. $self->{vault}->modifyrecord(
  184. $rec->uuid,
  185. passwd => $user_password,
  186. notes => "Saved by $self->{app}",
  187. );
  188. } else {
  189. $self->{vault}->newrecord(
  190. group => $self->{group},
  191. title => $realm,
  192. user => $user_name,
  193. passwd => $user_password,
  194. notes => "Saved by $self->{app}",
  195. );
  196. }
  197. $self->save() unless $self->{lazy_save};
  198. }
  199. =head2 get_password($user_name, $realm)
  200. Reads previously stored password for given user in given app.
  201. If such password can not be found, returns undef.
  202. =cut
  203. sub get_password {
  204. my ($self, $user_name, $realm) = @_;
  205. my $rec = $self->_find_record($user_name, $realm);
  206. if($rec) {
  207. return $rec->passwd;
  208. }
  209. return undef;
  210. }
  211. =head2 clear_password($user_name, $realm)
  212. Removes given password (if present)
  213. =cut
  214. sub clear_password {
  215. my ($self, $user_name, $realm) = @_;
  216. my $rec = $self->_find_record($user_name, $realm);
  217. if($rec) {
  218. $self->{vault}->deleterecord($rec->uuid);
  219. $self->save() unless $self->{lazy_save};
  220. return 1;
  221. } else {
  222. return 0;
  223. }
  224. }
  225. =head2 save
  226. Saves unsaved changes, if any are present.
  227. Important only when lazy_save was given in constructor.
  228. =cut
  229. sub save {
  230. my ($self) = @_;
  231. # Crypt::PWSafe3 internally keeps track of changes presence,
  232. # and makes this noop if there are no changes. So just call it.
  233. $self->{vault}->save();
  234. }
  235. =head2 is_persistent
  236. Returns info, whether this keyring actually saves passwords persistently.
  237. (true in this case)
  238. =cut
  239. sub is_persistent {
  240. my ($self) = @_;
  241. return 1;
  242. }
  243. =head1 AUTHOR
  244. Marcin Kasperski
  245. =head1 BUGS
  246. Please report any bugs or feature requests to
  247. issue tracker at L<https://bitbucket.org/Mekk/perl-keyring-pwsafe3>.
  248. =head1 SUPPORT
  249. You can find documentation for this module with the perldoc command.
  250. perldoc Passwd::Keyring::PWSafe3
  251. You can also look for information at:
  252. L<http://search.cpan.org/~mekk/Passwd-Keyring-PWSafe3/>
  253. Source code is tracked at:
  254. L<https://bitbucket.org/Mekk/perl-keyring-pwsafe3>
  255. =head1 LICENSE AND COPYRIGHT
  256. Copyright 2012 Marcin Kasperski.
  257. This program is free software; you can redistribute it and/or modify it
  258. under the terms of either: the GNU General Public License as published
  259. by the Free Software Foundation; or the Artistic License.
  260. See http://dev.perl.org/licenses/ for more information.
  261. =cut
  262. 1; # End of Passwd::Keyring::PWSafe3