1/* 2 * Copyright 2016 Software Freedom Conservancy Inc. 3 * 4 * This software is licensed under the GNU Lesser General Public License 5 * (version 2.1 or later). See the COPYING file in this distribution. 6 */ 7 8/** LibSecret password adapter. */ 9public class SecretMediator : Geary.CredentialsMediator, Object { 10 11 private const string ATTR_LOGIN = "login"; 12 private const string ATTR_HOST = "host"; 13 private const string ATTR_PROTO = "proto"; 14 15 private static Secret.Schema schema = new Secret.Schema( 16 Application.Client.SCHEMA_ID, 17 Secret.SchemaFlags.NONE, 18 ATTR_LOGIN, Secret.SchemaAttributeType.STRING, 19 ATTR_HOST, Secret.SchemaAttributeType.STRING, 20 ATTR_PROTO, Secret.SchemaAttributeType.STRING, 21 null 22 ); 23 24 // See Bug 697681 25 private static Secret.Schema compat_schema = new Secret.Schema( 26 "org.gnome.keyring.NetworkPassword", 27 Secret.SchemaFlags.NONE, 28 "user", Secret.SchemaAttributeType.STRING, 29 "domain", Secret.SchemaAttributeType.STRING, 30 "object", Secret.SchemaAttributeType.STRING, 31 "protocol", Secret.SchemaAttributeType.STRING, 32 "port", Secret.SchemaAttributeType.INTEGER, 33 "server", Secret.SchemaAttributeType.STRING, 34 "authtype", Secret.SchemaAttributeType.STRING, 35 null 36 ); 37 38 39 public async SecretMediator(GLib.Cancellable? cancellable) 40 throws GLib.Error { 41 yield check_unlocked(cancellable); 42 } 43 44 public virtual async bool load_token(Geary.AccountInformation account, 45 Geary.ServiceInformation service, 46 Cancellable? cancellable) 47 throws GLib.Error { 48 bool loaded = false; 49 if (service.credentials != null) { 50 if (service.remember_password) { 51 string? password = yield Secret.password_lookupv( 52 SecretMediator.schema, new_attrs(service), cancellable 53 ); 54 55 if (password == null) { 56 password = yield migrate_old_password(service, cancellable); 57 } 58 59 if (password != null) { 60 service.credentials = 61 service.credentials.copy_with_token(password); 62 loaded = true; 63 } 64 } else { 65 // Not remembering the password, so just make sure it 66 // has been filled in 67 loaded = service.credentials.is_complete(); 68 } 69 } 70 71 return loaded; 72 } 73 74 public async void update_token(Geary.AccountInformation account, 75 Geary.ServiceInformation service, 76 Cancellable? cancellable) 77 throws GLib.Error { 78 if (service.credentials != null) { 79 yield do_store(service, service.credentials.token, cancellable); 80 } 81 } 82 83 public async void clear_token(Geary.AccountInformation account, 84 Geary.ServiceInformation service, 85 Cancellable? cancellable) 86 throws Error { 87 if (service.credentials != null) { 88 yield Secret.password_clearv(SecretMediator.schema, 89 new_attrs(service), 90 cancellable); 91 92 // Remove legacy formats 93 // <= 0.11 94 yield Secret.password_clear( 95 compat_schema, 96 cancellable, 97 "user", get_legacy_user(service, account.primary_mailbox.address) 98 ); 99 // <= 0.6 100 yield Secret.password_clear( 101 compat_schema, 102 cancellable, 103 "user", get_legacy_user(service, service.credentials.user) 104 ); 105 } 106 } 107 108 // Ensure the default collection unlocked. Try to unlock it since 109 // the user may be running in a limited environment and it would 110 // prevent us from prompting the user multiple times in one 111 // session. See Bug 784300. 112 private async void check_unlocked(Cancellable? cancellable) 113 throws Error { 114 Secret.Service service = yield Secret.Service.get( 115 Secret.ServiceFlags.OPEN_SESSION, cancellable 116 ); 117 Secret.Collection? collection = yield Secret.Collection.for_alias( 118 service, 119 Secret.COLLECTION_DEFAULT, 120 Secret.CollectionFlags.NONE, 121 cancellable 122 ); 123 124 // For custom desktop setups, it is possible that the current 125 // session has a service responding on DBus but no password 126 // keyring. There's no much we can do in this case except just 127 // check for the collection being null so we don't crash. See 128 // Bug 795328. 129 if (collection != null && collection.get_locked()) { 130 List<Secret.Collection> to_lock = new List<Secret.Collection>(); 131 to_lock.append(collection); 132 List<DBusProxy> unlocked; 133 yield service.unlock(to_lock, cancellable, out unlocked); 134 if (unlocked.length() != 0) { 135 // XXX 136 } 137 } 138 } 139 140 private async void do_store(Geary.ServiceInformation service, 141 string password, 142 Cancellable? cancellable) 143 throws Error { 144 yield Secret.password_storev( 145 SecretMediator.schema, 146 new_attrs(service), 147 Secret.COLLECTION_DEFAULT, 148 "Geary %s password".printf(to_proto_value(service.protocol)), 149 password, 150 cancellable 151 ); 152 } 153 154 private HashTable<string,string> new_attrs(Geary.ServiceInformation service) { 155 HashTable<string,string> table = new HashTable<string,string>( 156 str_hash, str_equal 157 ); 158 table.insert(ATTR_PROTO, to_proto_value(service.protocol)); 159 table.insert(ATTR_HOST, service.host); 160 table.insert(ATTR_LOGIN, service.credentials.user); 161 return table; 162 } 163 164 private inline string to_proto_value(Geary.Protocol protocol) { 165 return protocol.to_value().ascii_up(); 166 } 167 168 private async string? migrate_old_password(Geary.ServiceInformation service, 169 GLib.Cancellable? cancellable) 170 throws GLib.Error { 171 // <= 0.11 172 string user = get_legacy_user(service, service.credentials.user); 173 string? password = yield Secret.password_lookup( 174 compat_schema, 175 cancellable, 176 "user", user 177 ); 178 179 if (password != null) { 180 // Clear the old password 181 yield Secret.password_clear( 182 compat_schema, 183 cancellable, 184 "user", user 185 ); 186 187 // Store it in the new format 188 yield do_store(service, password, cancellable); 189 } 190 191 return password; 192 } 193 194 private string get_legacy_user(Geary.ServiceInformation service, string user) { 195 switch (service.protocol) { 196 case Geary.Protocol.IMAP: 197 return "org.yorba.geary imap_username:" + user; 198 case Geary.Protocol.SMTP: 199 return "org.yorba.geary smtp_username:" + user; 200 default: 201 warning("Unknown service type"); 202 return ""; 203 } 204 } 205 206} 207