summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/EnsureCrypto5KeysStage.java
blob: 5031cf770fbc47fecab666740edc5b8cc0550291 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.sync.stage;

import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.Set;

import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.sync.CollectionKeys;
import org.mozilla.gecko.sync.CryptoRecord;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.InfoCollections;
import org.mozilla.gecko.sync.NoCollectionKeysSetException;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
import org.mozilla.gecko.sync.net.SyncStorageResponse;

public class EnsureCrypto5KeysStage
extends AbstractNonRepositorySyncStage
implements SyncStorageRequestDelegate {

  private static final String LOG_TAG = "EnsureC5KeysStage";
  private static final String CRYPTO_COLLECTION = "crypto";
  protected boolean retrying = false;

  @Override
  public void execute() throws NoSuchStageException {
    InfoCollections infoCollections = session.config.infoCollections;
    if (infoCollections == null) {
      session.abort(null, "No info/collections set in EnsureCrypto5KeysStage.");
      return;
    }

    PersistedCrypto5Keys pck = session.config.persistedCryptoKeys();
    long lastModified = pck.lastModified();
    if (retrying || !infoCollections.updateNeeded(CRYPTO_COLLECTION, lastModified)) {
      // Try to use our local collection keys for this session.
      Logger.debug(LOG_TAG, "Trying to use persisted collection keys for this session.");
      CollectionKeys keys = pck.keys();
      if (keys != null) {
        Logger.trace(LOG_TAG, "Using persisted collection keys for this session.");
        session.config.setCollectionKeys(keys);
        session.advance();
        return;
      }
      Logger.trace(LOG_TAG, "Failed to use persisted collection keys for this session.");
    }

    // We need an update: fetch fresh keys.
    Logger.debug(LOG_TAG, "Fetching fresh collection keys for this session.");
    try {
      SyncStorageRecordRequest request = new SyncStorageRecordRequest(session.wboURI(CRYPTO_COLLECTION, "keys"));
      request.delegate = this;
      request.get();
    } catch (URISyntaxException e) {
      session.abort(e, "Invalid URI.");
    }
  }

  @Override
  public AuthHeaderProvider getAuthHeaderProvider() {
    return session.getAuthHeaderProvider();
  }

  @Override
  public String ifUnmodifiedSince() {
    // TODO: last key time!
    return null;
  }

  protected void setAndPersist(PersistedCrypto5Keys pck, CollectionKeys keys, long timestamp) {
    session.config.setCollectionKeys(keys);
    pck.persistKeys(keys);
    pck.persistLastModified(timestamp);
  }

  /**
   * Return collections where either the individual key has changed, or if the
   * new default key is not the same as the old default key, where the
   * collection is using the default key.
   */
  protected Set<String> collectionsToUpdate(CollectionKeys oldKeys, CollectionKeys newKeys) {
    // These keys have explicitly changed; they definitely need updating.
    Set<String> changedKeys = new HashSet<String>(CollectionKeys.differences(oldKeys, newKeys));

    boolean defaultKeyChanged = true; // Most pessimistic is to assume default key has changed.
    KeyBundle newDefaultKeyBundle = null;
    try {
      KeyBundle oldDefaultKeyBundle = oldKeys.defaultKeyBundle();
      newDefaultKeyBundle = newKeys.defaultKeyBundle();
      defaultKeyChanged = !oldDefaultKeyBundle.equals(newDefaultKeyBundle);
    } catch (NoCollectionKeysSetException e) {
      Logger.warn(LOG_TAG, "NoCollectionKeysSetException in EnsureCrypto5KeysStage.", e);
    }

    if (newDefaultKeyBundle == null) {
      Logger.trace(LOG_TAG, "New default key not provided; returning changed individual keys.");
      return changedKeys;
    }

    if (!defaultKeyChanged) {
      Logger.trace(LOG_TAG, "New default key is the same as old default key; returning changed individual keys.");
      return changedKeys;
    }

    // New keys have a different default/sync key; check known collections against the default key.
    Logger.debug(LOG_TAG, "New default key is not the same as old default key.");
    for (Stage stage : Stage.getNamedStages()) {
      String name = stage.getRepositoryName();
      if (!newKeys.keyBundleForCollectionIsNotDefault(name)) {
        // Default key has changed, so this collection has changed.
        changedKeys.add(name);
      }
    }

    return changedKeys;
  }

  @Override
  public void handleRequestSuccess(SyncStorageResponse response) {
    // Take the timestamp from the response since it is later than the timestamp from info/collections.
    long responseTimestamp = response.normalizedWeaveTimestamp();
    CollectionKeys keys = new CollectionKeys();
    try {
      ExtendedJSONObject body = response.jsonObjectBody();
      if (Logger.LOG_PERSONAL_INFORMATION) {
        Logger.pii(LOG_TAG, "Fetched keys: " + body.toJSONString());
      }
      keys.setKeyPairsFromWBO(CryptoRecord.fromJSONRecord(body), session.config.syncKeyBundle);
    } catch (Exception e) {
      session.abort(e, "Invalid keys WBO.");
      return;
    }

    PersistedCrypto5Keys pck = session.config.persistedCryptoKeys();
    if (!pck.persistedKeysExist()) {
      // New keys, and no old keys! Persist keys and server timestamp.
      Logger.trace(LOG_TAG, "Setting fetched keys for this session; persisting fetched keys and last modified.");
      setAndPersist(pck, keys, responseTimestamp);
      session.advance();
      return;
    }

    // New keys, but we had old keys.  Check for differences.
    CollectionKeys oldKeys = pck.keys();
    Set<String> changedCollections = collectionsToUpdate(oldKeys, keys);
    if (!changedCollections.isEmpty()) {
      // New keys, different from old keys.
      Logger.trace(LOG_TAG, "Fetched keys are not the same as persisted keys; " +
          "setting fetched keys for this session before resetting changed engines.");
      setAndPersist(pck, keys, responseTimestamp);
      session.resetStagesByName(changedCollections);
      session.abort(null, "crypto/keys changed on server.");
      return;
    }

    // New keys don't differ from old keys; persist timestamp and move on.
    Logger.trace(LOG_TAG, "Fetched keys are the same as persisted keys; persisting only last modified.");
    session.config.setCollectionKeys(oldKeys);
    pck.persistLastModified(response.normalizedWeaveTimestamp());
    session.advance();
  }

  @Override
  public void handleRequestFailure(SyncStorageResponse response) {
    if (retrying) {
      // Should happen very rarely -- this means we uploaded our crypto/keys
      // successfully, but failed to re-download.
      session.handleHTTPError(response, "Failure while re-downloading already uploaded keys.");
      return;
    }

    int statusCode = response.getStatusCode();
    if (statusCode == 404) {
      Logger.info(LOG_TAG, "Got 404 fetching keys.  Fresh starting since keys are missing on server.");
      session.freshStart();
      return;
    }
    session.handleHTTPError(response, "Failure fetching keys: got response status code " + statusCode);
  }

  @Override
  public void handleRequestError(Exception ex) {
    session.abort(ex, "Failure fetching keys.");
  }
}