1 <?php
2 /**
3 * Keychain classes for Alphred
4 *
5 * PHP version 5
6 *
7 * @package Alphred
8 * @copyright Shawn Patrick Rice 2014
9 * @license http://opensource.org/licenses/MIT MIT
10 * @version 1.0.0
11 * @author Shawn Patrick Rice <rice@shawnrice.org>
12 * @link http://www.github.com/shawnrice/alphred
13 * @link http://shawnrice.github.io/alphred
14 * @since File available since Release 1.0.0
15 *
16 */
17
18 namespace Alphred;
19
20 /**
21 * Enables easy access to parts of the Keychain for secure password storage / retrieval
22 *
23 * Uses the `security` command in order to add / retrieve / delete passwords. Note: we use
24 * only the "generic" password functions and not the "internet" password functions.
25 *
26 * @see https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/security.1.html security(1)
27 *
28 */
29 class Keychain {
30
31 /**
32 * Throws an exception if you try to instantiate it
33 *
34 * @throws UseOnlyAsStatic if you try to institate a Globals object
35 */
36 public function __construct() {
37 throw new UseOnlyAsStatic( 'The Keychain class is to be used statically only.', 2 );
38 }
39
40 /**
41 * Saves a password to the keychain
42 *
43 * @throws PasswordExists (indirectly )when trying to add a password that already exists
44 * without specifying 'update'
45 *
46 * @param string $account the name of the account
47 * @param string $password the new password
48 * @param boolean $update whether or not to update an old password (defaults to `true`)
49 * @param string $service optional: defaults to the bundleid of the workflow (if set)
50 *
51 * @return boolean whether or not it was successful (usually true)
52 */
53 public static function save_password( $account, $password, $update = true, $service = null ) {
54 if ( $update ) {
55 $update = ' -U';
56 } else {
57 $update = '';
58 }
59 return self::call_security( 'add-generic-password', $service, $account, "{$update} -w '{$password}'" );
60 }
61
62
63 /**
64 * Retrieves a password from the keychain
65 *
66 * @throws InvalidKeychainAccount on an empty account
67 *
68 * @param string $account the name of an account
69 * @param string $service optional: defaults to the bundleid of the workflow (if set)
70 *
71 * @return string the password
72 */
73 public static function find_password( $account, $service = null ) {
74 // Make sure that the account is something other than whitespace
75 if ( empty( trim( $account ) ) ) {
76 throw new InvalidKeychainAccount( 'You must specify an account to get a password', 3 );
77 }
78
79 return self::call_security( 'find-generic-password', $service, $account, '-w' );
80 }
81
82
83 /**
84 * Deletes a password from the keychain
85 *
86 * @param string $account the name of the account
87 * @param string $service optional: defaults to the bundleid of the workflow (if set)
88 * @return boolean success of command
89 */
90 public static function delete_password( $account, $service = null ) {
91 if ( empty( trim( $account ) ) ) {
92 throw new InvalidKeychainAccount(
93 'The action you just attempted will delete the entire keychain; please specify the account', 3
94 );
95 }
96 return self::call_security( 'delete-generic-password', $service, $account, '' );
97 }
98
99
100 /**
101 * Interfaces directly with the `security` command
102 *
103 * @throws PasswordExists when trying to add a password that already exists without specifying 'update'
104 * @throws PasswordNotFound when trying to find a password that does not exist
105 * @throws UnknownSecurityException when something weird happens
106 *
107 * @param string $action one of 'add-', 'delete-', or 'find-generic-password'
108 * @param string $service the "owner" of the action; usually the bundle id
109 * @param string $account the "account" of the password
110 * @param string $args extra arguments for the security command
111 * @return string|boolean either a found password or true
112 */
113 private function call_security( $action, $service, $account, $args ) {
114 if ( ! in_array( $action, [ 'add-generic-password', 'delete-generic-password', 'find-generic-password' ] ) ) {
115 throw new InvalidSecurityAction( "{$action} is not valid.", 4 );
116
117 // So, if, for some reason, the thing is caught, we can't really go on. So we'll exit anyway.
118 return false;
119 }
120 $service = self::set_service( $service );
121
122 // Note: $args needs to be escaped in the function that calls this one
123 $command = "security {$action} -s '{$service}' -a '{$account}' {$args}";
124 exec( $command, $output, $return_code );
125 if ( 45 == $return_code ) {
126 // raise exception because password already exists
127 throw new PasswordExists( 'Password Already Exists, did you mean to update it?', 2 );
128 } else if ( 44 == $return_code ) {
129 // raise exception because password does not exist
130 throw new PasswordNotFound( "Password for '{$account}' does not exist", 3 );
131 } else if ( 0 == $return_code ) {
132 // Do nothing here. For now.
133 // @todo Do something here.
134 } else {
135 throw new UnknownSecurityException(
136 'An unanticipated error has happened when trying to call the security command', 4
137 );
138 }
139
140 if ( 'find-generic-password' === $action ) {
141 /**
142 * @todo Test that this is exactly what we need to return
143 */
144 return $output[0];
145 }
146 return true;
147 }
148
149 /**
150 * Sets the service appropriately, usually to the bundle id of the workflow
151 */
152 private function set_service( $service ) {
153
154 // The service has not been set, so let's set it to the bundle id of the workflow
155 if ( is_null( $service ) ) {
156 if ( Globals::bundle() ) {
157 $service = Globals::bundle();
158 }
159 }
160 return $service;
161 }
162
163
164 }