Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  passwords.sys.mjs   Sprache: unbekannt

 
/* 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/. */

import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";

import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
import { CollectionValidator } from "resource://services-sync/collection_validator.sys.mjs";
import {
  Changeset,
  Store,
  SyncEngine,
  Tracker,
} from "resource://services-sync/engines.sys.mjs";
import { Svc, Utils } from "resource://services-sync/util.sys.mjs";

// These are valid fields the server could have for a logins record
// we mainly use this to detect if there are any unknownFields and
// store (but don't process) those fields to roundtrip them back
const VALID_LOGIN_FIELDS = [
  "id",
  "displayOrigin",
  "formSubmitURL",
  "formActionOrigin",
  "httpRealm",
  "hostname",
  "origin",
  "password",
  "passwordField",
  "timeCreated",
  "timeLastUsed",
  "timePasswordChanged",
  "timesUsed",
  "username",
  "usernameField",
  "everSynced",
  "syncCounter",
  "unknownFields",
];

import { LoginManagerStorage } from "resource://passwordmgr/passwordstorage.sys.mjs";

// Sync and many tests rely on having an time that is rounded to the nearest
// 100th of a second otherwise tests can fail intermittently.
function roundTimeForSync(time) {
  return Math.round(time / 10) / 100;
}

export function LoginRec(collection, id) {
  CryptoWrapper.call(this, collection, id);
}

LoginRec.prototype = {
  _logName: "Sync.Record.Login",

  cleartextToString() {
    let o = Object.assign({}, this.cleartext);
    if (o.password) {
      o.password = "X".repeat(o.password.length);
    }
    return JSON.stringify(o);
  },
};
Object.setPrototypeOf(LoginRec.prototype, CryptoWrapper.prototype);

Utils.deferGetSet(LoginRec, "cleartext", [
  "hostname",
  "formSubmitURL",
  "httpRealm",
  "username",
  "password",
  "usernameField",
  "passwordField",
  "timeCreated",
  "timePasswordChanged",
]);

export function PasswordEngine(service) {
  SyncEngine.call(this, "Passwords", service);
}

PasswordEngine.prototype = {
  _storeObj: PasswordStore,
  _trackerObj: PasswordTracker,
  _recordObj: LoginRec,

  syncPriority: 2,

  emptyChangeset() {
    return new PasswordsChangeset();
  },

  async ensureCurrentSyncID(newSyncID) {
    return Services.logins.ensureCurrentSyncID(newSyncID);
  },

  async getLastSync() {
    let legacyValue = await super.getLastSync();
    if (legacyValue) {
      await this.setLastSync(legacyValue);
      Svc.PrefBranch.clearUserPref(this.name + ".lastSync");
      this._log.debug(
        `migrated timestamp of ${legacyValue} to the logins store`
      );
      return legacyValue;
    }
    return this._store.storage.getLastSync();
  },

  async setLastSync(timestamp) {
    await this._store.storage.setLastSync(timestamp);
  },

  // Testing function to emulate that a login has been synced.
  async markSynced(guid) {
    this._store.storage.resetSyncCounter(guid, 0);
  },

  async pullAllChanges() {
    return this._getChangedIDs(true);
  },

  async getChangedIDs() {
    return this._getChangedIDs(false);
  },

  async _getChangedIDs(getAll) {
    let changes = {};

    let logins = await this._store.storage.getAllLogins(true);
    for (let login of logins) {
      if (getAll || login.syncCounter > 0) {
        if (Utils.getSyncCredentialsHosts().has(login.origin)) {
          continue;
        }

        changes[login.guid] = {
          counter: login.syncCounter, // record the initial counter value
          modified: roundTimeForSync(login.timePasswordChanged),
          deleted: this._store.storage.loginIsDeleted(login.guid),
        };
      }
    }

    return changes;
  },

  async trackRemainingChanges() {
    // Reset the syncCounter on the items that were changed.
    for (let [guid, { counter, synced }] of Object.entries(
      this._modified.changes
    )) {
      if (synced) {
        this._store.storage.resetSyncCounter(guid, counter);
      }
    }
  },

  async _findDupe(item) {
    let login = this._store._nsLoginInfoFromRecord(item);
    if (!login) {
      return null;
    }

    let logins = await this._store.storage.searchLoginsAsync({
      origin: login.origin,
      formActionOrigin: login.formActionOrigin,
      httpRealm: login.httpRealm,
    });

    // Look for existing logins that match the origin, but ignore the password.
    for (let local of logins) {
      if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo) {
        return local.guid;
      }
    }

    return null;
  },

  _deleteId(id) {
    this._noteDeletedId(id);
  },

  getValidator() {
    return new PasswordValidator();
  },
};
Object.setPrototypeOf(PasswordEngine.prototype, SyncEngine.prototype);

function PasswordStore(name, engine) {
  Store.call(this, name, engine);
  this._nsLoginInfo = new Components.Constructor(
    "@mozilla.org/login-manager/loginInfo;1",
    Ci.nsILoginInfo,
    "init"
  );
  this.storage = LoginManagerStorage.create();
}
PasswordStore.prototype = {
  _newPropertyBag() {
    return Cc["@mozilla.org/hash-property-bag;1"].createInstance(
      Ci.nsIWritablePropertyBag2
    );
  },

  // Returns an stringified object of any fields not "known" by this client
  // mainly used to to prevent data loss for other clients by roundtripping
  // these fields without processing them
  _processUnknownFields(record) {
    let unknownFields = {};
    let keys = Object.keys(record);
    keys
      .filter(key => !VALID_LOGIN_FIELDS.includes(key))
      .forEach(key => {
        unknownFields[key] = record[key];
      });
    // If we found some unknown fields, we stringify it to be able
    // to properly encrypt it for roundtripping since we can't know if
    // it contained sensitive fields or not
    if (Object.keys(unknownFields).length) {
      return JSON.stringify(unknownFields);
    }
    return null;
  },

  /**
   * Return an instance of nsILoginInfo (and, implicitly, nsILoginMetaInfo).
   */
  _nsLoginInfoFromRecord(record) {
    function nullUndefined(x) {
      return x == undefined ? null : x;
    }

    function stringifyNullUndefined(x) {
      return x == undefined || x == null ? "" : x;
    }

    if (record.formSubmitURL && record.httpRealm) {
      this._log.warn(
        "Record " +
          record.id +
          " has both formSubmitURL and httpRealm. Skipping."
      );
      return null;
    }

    // Passing in "undefined" results in an empty string, which later
    // counts as a value. Explicitly `|| null` these fields according to JS
    // truthiness. Records with empty strings or null will be unmolested.
    let info = new this._nsLoginInfo(
      record.hostname,
      nullUndefined(record.formSubmitURL),
      nullUndefined(record.httpRealm),
      stringifyNullUndefined(record.username),
      record.password,
      record.usernameField,
      record.passwordField
    );

    info.QueryInterface(Ci.nsILoginMetaInfo);
    info.guid = record.id;
    if (record.timeCreated && !isNaN(new Date(record.timeCreated).getTime())) {
      info.timeCreated = record.timeCreated;
    }
    if (
      record.timePasswordChanged &&
      !isNaN(new Date(record.timePasswordChanged).getTime())
    ) {
      info.timePasswordChanged = record.timePasswordChanged;
    }

    // Check the record if there are any unknown fields from other clients
    // that we want to roundtrip during sync to prevent data loss
    let unknownFields = this._processUnknownFields(record.cleartext);
    if (unknownFields) {
      info.unknownFields = unknownFields;
    }
    return info;
  },

  async _getLoginFromGUID(guid) {
    let logins = await this.storage.searchLoginsAsync({ guid }, true);
    if (logins.length) {
      this._log.trace(logins.length + " items matching " + guid + " found.");
      return logins[0];
    }

    this._log.trace("No items matching " + guid + " found. Ignoring");
    return null;
  },

  async applyIncoming(record) {
    if (record.deleted) {
      // Need to supply the sourceSync flag.
      await this.remove(record, { sourceSync: true });
      return;
    }

    await super.applyIncoming(record);
  },

  async getAllIDs() {
    let items = {};
    let logins = await this.storage.getAllLogins(true);

    for (let i = 0; i < logins.length; i++) {
      // Skip over Weave password/passphrase entries.
      let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo);
      if (Utils.getSyncCredentialsHosts().has(metaInfo.origin)) {
        continue;
      }

      items[metaInfo.guid] = metaInfo;
    }

    return items;
  },

  async changeItemID(oldID, newID) {
    this._log.trace("Changing item ID: " + oldID + " to " + newID);

    if (!(await this.itemExists(oldID))) {
      this._log.trace("Can't change item ID: item doesn't exist");
      return;
    }
    if (await this._getLoginFromGUID(newID)) {
      this._log.trace("Can't change item ID: new ID already in use");
      return;
    }

    let prop = this._newPropertyBag();
    prop.setPropertyAsAUTF8String("guid", newID);

    let oldLogin = await this._getLoginFromGUID(oldID);
    this.storage.modifyLogin(oldLogin, prop, true);
  },

  async itemExists(id) {
    let login = await this._getLoginFromGUID(id);
    return login && !this.storage.loginIsDeleted(id);
  },

  async createRecord(id, collection) {
    let record = new LoginRec(collection, id);
    let login = await this._getLoginFromGUID(id);

    if (!login || this.storage.loginIsDeleted(id)) {
      record.deleted = true;
      return record;
    }

    record.hostname = login.origin;
    record.formSubmitURL = login.formActionOrigin;
    record.httpRealm = login.httpRealm;
    record.username = login.username;
    record.password = login.password;
    record.usernameField = login.usernameField;
    record.passwordField = login.passwordField;

    // Optional fields.
    login.QueryInterface(Ci.nsILoginMetaInfo);
    record.timeCreated = login.timeCreated;
    record.timePasswordChanged = login.timePasswordChanged;

    // put the unknown fields back to the top-level record
    // during upload
    if (login.unknownFields) {
      let unknownFields = JSON.parse(login.unknownFields);
      if (unknownFields) {
        Object.keys(unknownFields).forEach(key => {
          // We have to manually add it to the cleartext since that's
          // what gets processed during upload
          record.cleartext[key] = unknownFields[key];
        });
      }
    }

    return record;
  },

  async create(record) {
    let login = this._nsLoginInfoFromRecord(record);
    if (!login) {
      return;
    }

    login.everSynced = true;

    this._log.trace("Adding login for " + record.hostname);
    this._log.trace(
      "httpRealm: " +
        JSON.stringify(login.httpRealm) +
        "; " +
        "formSubmitURL: " +
        JSON.stringify(login.formActionOrigin)
    );
    await Services.logins.addLoginAsync(login);
  },

  async remove(record, { sourceSync = false } = {}) {
    this._log.trace("Removing login " + record.id);

    let loginItem = await this._getLoginFromGUID(record.id);
    if (!loginItem) {
      this._log.trace("Asked to remove record that doesn't exist, ignoring");
      return;
    }

    this.storage.removeLogin(loginItem, sourceSync);
  },

  async update(record) {
    let loginItem = await this._getLoginFromGUID(record.id);
    if (!loginItem || this.storage.loginIsDeleted(record.id)) {
      this._log.trace("Skipping update for unknown item: " + record.hostname);
      return;
    }

    this._log.trace("Updating " + record.hostname);
    let newinfo = this._nsLoginInfoFromRecord(record);
    if (!newinfo) {
      return;
    }

    loginItem.everSynced = true;

    this.storage.modifyLogin(loginItem, newinfo, true);
  },

  async wipe() {
    this.storage.removeAllUserFacingLogins(true);
  },
};
Object.setPrototypeOf(PasswordStore.prototype, Store.prototype);

function PasswordTracker(name, engine) {
  Tracker.call(this, name, engine);
}
PasswordTracker.prototype = {
  onStart() {
    Svc.Obs.add("passwordmgr-storage-changed", this.asyncObserver);
  },

  onStop() {
    Svc.Obs.remove("passwordmgr-storage-changed", this.asyncObserver);
  },

  async observe(subject, topic, data) {
    if (this.ignoreAll) {
      return;
    }

    switch (data) {
      case "modifyLogin":
        // The syncCounter should have been incremented only for
        // those items that need to be sycned.
        if (
          subject.QueryInterface(Ci.nsIArrayExtensions).GetElementAt(1)
            .syncCounter > 0
        ) {
          this.score += SCORE_INCREMENT_XLARGE;
        }
        break;

      case "addLogin":
      case "removeLogin":
      case "importLogins":
        this.score += SCORE_INCREMENT_XLARGE;
        break;

      case "removeAllLogins":
        this.score +=
          SCORE_INCREMENT_XLARGE *
          (subject.QueryInterface(Ci.nsIArrayExtensions).Count() + 1);
        break;
    }
  },
};
Object.setPrototypeOf(PasswordTracker.prototype, Tracker.prototype);

export class PasswordValidator extends CollectionValidator {
  constructor() {
    super("passwords", "id", [
      "hostname",
      "formSubmitURL",
      "httpRealm",
      "password",
      "passwordField",
      "username",
      "usernameField",
    ]);
  }

  async getClientItems() {
    let logins = await Services.logins.getAllLogins();
    let syncHosts = Utils.getSyncCredentialsHosts();
    let result = logins
      .map(l => l.QueryInterface(Ci.nsILoginMetaInfo))
      .filter(l => !syncHosts.has(l.origin));
    return Promise.resolve(result);
  }

  normalizeClientItem(item) {
    return {
      id: item.guid,
      guid: item.guid,
      hostname: item.hostname,
      formSubmitURL: item.formSubmitURL,
      httpRealm: item.httpRealm,
      password: item.password,
      passwordField: item.passwordField,
      username: item.username,
      usernameField: item.usernameField,
      original: item,
    };
  }

  async normalizeServerItem(item) {
    return Object.assign({ guid: item.id }, item);
  }
}

export class PasswordsChangeset extends Changeset {
  getModifiedTimestamp(id) {
    return this.changes[id].modified;
  }

  has(id) {
    let change = this.changes[id];
    if (change) {
      return !change.synced;
    }
    return false;
  }

  delete(id) {
    let change = this.changes[id];
    if (change) {
      // Mark the change as synced without removing it from the set.
      // This allows the sync counter to be reset when sync is complete
      // within trackRemainingChanges.
      change.synced = true;
    }
  }
}

[ Dauer der Verarbeitung: 0.27 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge