PageRenderTime 60ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Passwd/Keyring/OSXKeychain.pm

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