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