1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00

[PM-22440] [PM-22114] Parse RfFieldsV2 Roboform Fields (#15099)

* add support for RfFieldsV2

* add unit tests for totp and custom fields

* update empty-folders data for new unit tests

* ignore User ID$, Password$ and Script$

* refactor: extract parsing logic for Rf_fields and RfFieldsV2 into separate methods and don't ignore User ID$, Password$ or Script$

* Fixed linting issue by executing npm run prettier

---------

Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Joel
2025-07-14 12:00:48 +01:00
committed by GitHub
parent 4df5ef155d
commit 537b75cb1f
3 changed files with 92 additions and 20 deletions

View File

@@ -16,7 +16,7 @@ describe("Roboform CSV Importer", () => {
expect(result != null).toBe(true);
expect(result.folders.length).toBe(0);
expect(result.ciphers.length).toBe(5);
expect(result.ciphers.length).toBe(4);
expect(result.ciphers[0].name).toBe("Bitwarden");
expect(result.ciphers[0].login.username).toBe("user@bitwarden.com");
expect(result.ciphers[0].login.password).toBe("password");
@@ -31,13 +31,32 @@ describe("Roboform CSV Importer", () => {
expect(result.ciphers.length).toBe(5);
});
it("should parse CSV data totp", async () => {
const importer = new RoboFormCsvImporter();
const result = await importer.parse(dataNoFolder);
expect(result != null).toBe(true);
expect(result.ciphers[2].login.totp).toBe("totpKeyValue");
});
it("should parse CSV data custom fields", async () => {
const importer = new RoboFormCsvImporter();
const result = await importer.parse(dataNoFolder);
expect(result != null).toBe(true);
expect(result.ciphers[1].fields[0].name).toBe("Custom Field 1");
expect(result.ciphers[1].fields[0].value).toBe("Custom Field 1 Value");
expect(result.ciphers[1].fields[1].name).toBe("Custom Field 2");
expect(result.ciphers[1].fields[1].value).toBe("Custom Field 2 Value");
});
it("should parse CSV data secure note", async () => {
const importer = new RoboFormCsvImporter();
const result = await importer.parse(dataNoFolder);
expect(result != null).toBe(true);
expect(result.ciphers[4].type).toBe(CipherType.SecureNote);
expect(result.ciphers[4].notes).toBe("This is a safe note");
expect(result.ciphers[4].name).toBe("note - 2023-03-31");
expect(result.ciphers[3].type).toBe(CipherType.SecureNote);
expect(result.ciphers[3].notes).toBe("This is a safe note");
expect(result.ciphers[3].name).toBe("note - 2023-03-31");
});
it("should parse CSV data with folder hierarchy", async () => {

View File

@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FieldType } from "@bitwarden/common/vault/enums";
import { ImportResult } from "../models/import-result";
import { BaseImporter } from "./base-importer";
@@ -31,19 +33,9 @@ export class RoboFormCsvImporter extends BaseImporter implements Importer {
cipher.login.uris = this.makeUriArray(value.Url);
if (!this.isNullOrWhitespace(value.Rf_fields)) {
let fields: string[] = [value.Rf_fields];
if (value.__parsed_extra != null && value.__parsed_extra.length > 0) {
fields = fields.concat(value.__parsed_extra);
}
fields.forEach((field: string) => {
const parts = field.split(":");
if (parts.length < 3) {
return;
}
const key = parts[0] === "-no-name-" ? null : parts[0];
const val = parts.length === 4 && parts[2] === "rck" ? parts[1] : parts[2];
this.processKvp(cipher, key, val);
});
this.parseRfFields(cipher, value);
} else if (!this.isNullOrWhitespace(value.RfFieldsV2)) {
this.parseRfFieldsV2(cipher, value);
}
this.convertToNoteIfNeeded(cipher);
@@ -68,4 +60,66 @@ export class RoboFormCsvImporter extends BaseImporter implements Importer {
result.success = true;
return Promise.resolve(result);
}
private parseRfFields(cipher: any, value: any): void {
let fields: string[] = [value.Rf_fields];
if (value.__parsed_extra != null && value.__parsed_extra.length > 0) {
fields = fields.concat(value.__parsed_extra);
}
fields.forEach((field: string) => {
const parts = field.split(":");
if (parts.length < 3) {
return;
}
const key = parts[0] === "-no-name-" ? null : parts[0];
const val = parts.length === 4 && parts[2] === "rck" ? parts[1] : parts[2];
this.processKvp(cipher, key, val);
});
}
private parseRfFieldsV2(cipher: any, value: any): void {
let fields: string[] = [value.RfFieldsV2];
if (value.__parsed_extra != null && value.__parsed_extra.length > 0) {
fields = fields.concat(value.__parsed_extra);
}
let userIdCount = 1;
let passwordCount = 1;
fields.forEach((field: string) => {
const parts = field.split(",");
if (parts.length < 5) {
return;
}
const key = parts[0] === "-no-name-" ? null : parts[0];
const type = parts[3] === "pwd" ? FieldType.Hidden : FieldType.Text;
const val = parts[4];
if (key === "TOTP KEY$") {
cipher.login.totp = val;
return;
}
// Skip if value matches login fields
if (key === "User ID$" && val === cipher.login.username) {
return;
}
if (key === "Password$" && val === cipher.login.password) {
return;
}
// Index any extra User IDs or Passwords
let displayKey = key;
if (key === "User ID$") {
displayKey = `Alternate User ID ${userIdCount++}`;
} else if (key === "Password$") {
displayKey = `Alternate Password ${passwordCount++}`;
}
this.processKvp(cipher, displayKey, val, type);
});
}
}

View File

@@ -1,6 +1,5 @@
export const data = `Name,Url,MatchUrl,Login,Pwd,Note,Folder,RfFieldsV2
Bitwarden,https://bitwarden.com,https://bitwarden.com,user@bitwarden.com,password,,,"User ID$,,,txt,user@bitwarden.com","Password$,,,pwd,password"
Test,https://www.test.com/,https://www.test.com/,test@gmail.com,:testPassword,test,,"User ID$,,,txt,test@gmail.com","Password$,,,pwd,:testPassword"
LoginWebsite,https://login.Website.com/,https://login.Website.com/,test@outlook.com,123password,,,"User ID$,,,txt,test@outlook.com","Password$,,,pwd,123password"
Website,https://signin.website.com/,https://signin.website.com/,user@bitwarden.com,password123,Website ,,"User ID$,,,txt,user@bitwarden.com","Password$,,,pwd,password123"
Customfields,https://www.customfields.com,https://www.customfields.com,customfields@gmail.com,customfieldsPassword,,,"User ID$,,,txt,customfields@gmail.com","Password$,,,pwd,customfieldsPassword","Custom Field 1,,,txt,Custom Field 1 Value","Custom Field 2,,,txt,Custom Field 2 Value"
Totpwebsite,https://www.totpwebsite.com,https://www.totpwebsite.com,totp@gmail.com,totpPassword,,,"User ID$,,,txt,totp@gmail.com","Password$,,,pwd,totpPassword","TOTP KEY$,,,txt,totpKeyValue"
note - 2023-03-31,,,,,This is a safe note,`;