Skip to content

Commit

Permalink
Support Firefox Lockwise CSV file format for export and import
Browse files Browse the repository at this point in the history
  • Loading branch information
JustOff committed Jun 20, 2021
1 parent a5be28a commit 4638b96
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 18 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Well known paths to Chromium based browsers passwords database:

Firefox profile folder can be found using the menu "Help" -> "Troubleshooting Information".

Starting from version 1.4.0, [Firefox Lockwise](https://support.mozilla.org/en-US/kb/password-manager-remember-delete-edit-logins) CSV file format is supported directly, both for export and import.

**Warning**

When selecting to "obfuscate" your usernames and passwords, note that this only makes the data not readable to the human eye, but is easily reversed. Even with this option selected, ANYONE CAN EASILY DISCOVER YOUR PASSWORDS. Please take this into account when exporting your passwords and never place your exported file(s) where others could access them.
88 changes: 70 additions & 18 deletions chrome/pwdex-loginmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,18 @@ var passwordExporterLoginMgr = {
masterPassword = passwordExporter.showMasterPasswordPrompt();

if (masterPassword && passwordExporter.accepted == true) {
// Whether to encrypt the passwords
var encrypt = document.getElementById('pwdex-encrypt').checked;

var picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
picker.init(window, passwordExporter.getString("passwordexporter.filepicker-title"), picker.modeSave);
picker.defaultString = "password-export-" + passwordExporter.getDateString() + ".xml";
picker.defaultExtension = "xml";
picker.appendFilter("XML", "*.xml");
picker.appendFilter("CSV", "*.csv");
if (!encrypt) {
picker.appendFilter("Firefox CSV", "*.csv");
}

if (picker.returnCancel != picker.show()) {
var result = { file : picker.file, type : picker.filterIndex };
Expand All @@ -72,8 +78,6 @@ var passwordExporterLoginMgr = {
result.file.create(result.file.NORMAL_FILE_TYPE, parseInt("0666", 8));
ostream.init(result.file, 0x02, 0x200, null);

// Whether to encrypt the passwords
var encrypt = document.getElementById('pwdex-encrypt').checked;
var content = "";
// do export
switch (result.type) {
Expand All @@ -83,6 +87,9 @@ var passwordExporterLoginMgr = {
case 1:
content = this.export('csv', encrypt);
break;
case 2:
content = this.export('fcsv', false);
break;
}

var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
Expand Down Expand Up @@ -122,6 +129,9 @@ var passwordExporterLoginMgr = {
this.currentExport = '# Generated by ' + expEngine + '; Export format ' + expPwdVer + '; Encrypted: ' + encrypt + passwordExporter.linebreak;
this.currentExport += '"hostname","username","password","formSubmitURL","httpRealm","usernameField","passwordField"' + passwordExporter.linebreak;
}
else if (type == 'fcsv') {
this.currentExport += '"url","username","password","httpRealm","formActionOrigin","guid","timeCreated","timeLastUsed","timePasswordChanged"' + passwordExporter.linebreak;
}

this.count = 0;
this.errorCount = 0;
Expand All @@ -139,6 +149,10 @@ var passwordExporterLoginMgr = {
this.entryToCSV(logins[i].hostname, logins[i].formSubmitURL, logins[i].httpRealm, logins[i].username,
logins[i].usernameField, logins[i].password, logins[i].passwordField, encrypt);
}
else if (type == 'fcsv') {
this.entryToFCSV(logins[i].hostname, logins[i].username, logins[i].password,
logins[i].httpRealm, logins[i].formSubmitURL);
}
}

if (type == 'xml') {
Expand Down Expand Up @@ -209,6 +223,29 @@ var passwordExporterLoginMgr = {
}
},

// Records an nsILoginInfo entry to Firefox CSV
entryToFCSV: function(hostname, username, password, httpRealm, formSubmitURL) {

try {
var fcsv = '"' + this.escapeCVS(hostname) + '",';
fcsv += '"' + this.escapeCVS(username) + '",';
fcsv += '"' + this.escapeCVS(password) + '",';

fcsv += '"' + (httpRealm ? this.escapeCVS(httpRealm) : '') + '",';
fcsv += '"' + (formSubmitURL ? this.escapeCVS(formSubmitURL) : '') + '"';

fcsv += passwordExporter.linebreak;

this.currentExport += fcsv;
this.count++;
} catch (e) {
this.errorCount++;
try {
this.failed += hostname + passwordExporter.linebreak;
} catch (e) { }
}
},

// escapes special characters so that it will parse correctly in CSV
escapeCVS: function(string) {
string = string.replace(/"/g, '""');
Expand Down Expand Up @@ -319,6 +356,12 @@ var passwordExporterLoginMgr = {
// Previously, the header was in CSV form in the first line
header = /(.+?),(.{3,6}),(true|false)/i.exec(input);
}
if (!header) {
// If Firefox 79+ exported login data
if (/^"url","username","password","httpRealm","formActionOrigin"/.test(input)) {
header = [null, "Firefox", "79", false];
}
}
if (!header) {
// If we still can't read header, there's a problem with the file
alert(passwordExporter.getString('passwordexporter.alert-cannot-import'));
Expand Down Expand Up @@ -396,7 +439,7 @@ var passwordExporterLoginMgr = {
// Validates import file and parses it
import: function (type, properties, entries, key) {
// Make sure this is a Password Backup Tool or Password Exporter export file
if (properties.extension != expEngine && properties.extension != oldEngine) {
if (properties.extension != expEngine && properties.extension != oldEngine && properties.extension != "Firefox") {
alert(passwordExporter.getString('passwordexporter.alert-cannot-import'));
return;
}
Expand All @@ -408,8 +451,9 @@ var passwordExporterLoginMgr = {
}

// Make sure this was exported from a version supported (not a future version)
if ((properties.extension == oldEngine && properties.importversion in {'1.0.2':'', '1.0.4':'', '1.1':''}) ||
(properties.extension == expEngine && properties.importversion == "2.0")) {
if (properties.extension == oldEngine && properties.importversion in {'1.0.2':'', '1.0.4':'', '1.1':''} ||
properties.extension == expEngine && properties.importversion == "2.0" ||
properties.extension == "Firefox") {
// Import
var logins = [];
this.totalCount = 0;
Expand Down Expand Up @@ -466,22 +510,18 @@ var passwordExporterLoginMgr = {
}
}
else if (type == 'csv') {
if (/\r\n/i.test(entries))
var entryArray = entries.split("\r\n");
else if (/\r/i.test(entries))
var entryArray = entries.split("\r");
else
var entryArray = entries.split("\n");

// Prior to version 1.1, we only had one line of header
entries = entries.replace(/(\r\n|\n|\r)+$/, '');
var entryArray = entries.split(/\r\n|\n|\r/);

// Prior to version 1.1, we only had one line of header, the same is true for Firefox CSV
// After 1.1, there was a header comment and a labels line
if (properties.extension == oldEngine && (properties.importversion == '1.0.2' || properties.importversion == '1.0.4')) {
if (properties.extension == oldEngine && (properties.importversion == '1.0.2' || properties.importversion == '1.0.4') || properties.extension == "Firefox") {
var start = 1;
} else {
var start = 2;
}

for (var i = start; i < (entryArray.length - 1); i++) {
for (var i = start; i < entryArray.length; i++) {
if (properties.extension == oldEngine) {
if (properties.importversion == '1.0.2' || properties.importversion == '1.0.4') {
// Before version 1.1, csv didn't have quotes
Expand All @@ -502,7 +542,7 @@ var passwordExporterLoginMgr = {

var loginInfo = new nsLoginInfo(
(fields[0] == '"' ? null : unescape(fields[0].replace('"', ''))), // hostname
(fields[3] == '' ? null : unescape(fields[3])), // formSubmitURL
(fields[3] == '' ? (fields[4] == '' ? '' : null) : unescape(fields[3])), // formSubmitURL
(fields[4] == '' ? null : unescape(fields[4])), // httpRealm
unescape(fields[1]), // username
unescape(fields[2]), // password
Expand All @@ -528,15 +568,27 @@ var passwordExporterLoginMgr = {
fields[n] += l;
p = l;
}
var loginInfo = new nsLoginInfo(
if (properties.extension == "Firefox") {
var loginInfo = new nsLoginInfo(
(fields[0] == '' ? null : fields[0]), // hostname
(fields[4] == '' ? (fields[3] == '' ? '' : null) : fields[4]), // formSubmitURL
(fields[3] == '' ? null : fields[3]), // httpRealm
fields[1], // username
fields[2], // password
"", // usernameField
"" // passwordField
);
} else {
var loginInfo = new nsLoginInfo(
(fields[0] == '' ? null : fields[0]), // hostname
(fields[3] == '' ? null : fields[3]), // formSubmitURL
(fields[3] == '' ? (fields[4] == '' ? '' : null) : fields[3]), // formSubmitURL
(fields[4] == '' ? null : fields[4]), // httpRealm
fields[1], // username
fields[2], // password
fields[5], // usernameField
fields[6] // passwordField
);
}
}

var formattedLogins = this.getFormattedLogin(properties, loginInfo);
Expand Down

0 comments on commit 4638b96

Please sign in to comment.