mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-02 04:43:58 +00:00
Keep what the Bitwarden export actually says (#206)
Four things the first pass threw away, all of them recorded in the mapping notes from the 2026-09-02 hand conversion and all of them silent. Item ids were regenerated. Bitwarden ids are already UUIDs, so keeping them is what makes a re-import idempotent: run the same export twice and the second run reports its items as already present under "skip", rather than duplicating the entire vault. createItem deliberately refuses a caller-supplied id and always mints a fresh one, so the id is adopted after construction, and only when it really is a UUID. Timestamps were restamped to "now". creationDate and revisionDate are the only record of when a password was last rotated, and overwriting them destroys that permanently. The oldest item in a real export dates to 2018; every one of them would have been dated today. Password history was dropped entirely. It is carried now, newest first, mapping lastUsedDate to changedAt and capped at MAX_HISTORY_ENTRIES. 189 items in a real export have history, so this was not a rare case. URI match rules were hardcoded to "domain". Bitwarden stores them as a number -- 0 domain, 1 host, 2 startsWith, 3 exact, 4 regex, 5 never, with null meaning domain -- and flattening them quietly widens a login pinned to an exact URL, which is a security change rather than a cosmetic one. Verified on the same 4,395-item export: all 4,395 ids carried across, stable across two runs, 189 items with history recovered, and the oldest createdAt still 2018-02-22. That export happens to use domain matching throughout, so the match mapping is covered by tests rather than by it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1f9c25fcd2
commit
b873c2bf41
2 changed files with 149 additions and 10 deletions
|
|
@ -172,6 +172,70 @@ describe("reading a Bitwarden JSON export", () => {
|
|||
expect(parsed.skipped[0]?.reason).toBe("Not valid JSON");
|
||||
});
|
||||
|
||||
it("maps Bitwarden's numeric URI match rules instead of assuming domain", () => {
|
||||
// Assuming "domain" for all of them quietly widens a login pinned to an
|
||||
// exact URL, which is a security change, not a cosmetic one.
|
||||
const parsed = parseBitwardenJson(
|
||||
exportOf([
|
||||
{
|
||||
type: 1,
|
||||
name: "x",
|
||||
login: {
|
||||
uris: [
|
||||
{ uri: "https://a.test", match: 3 },
|
||||
{ uri: "https://b.test", match: 5 },
|
||||
{ uri: "https://c.test", match: null },
|
||||
{ uri: "https://d.test" },
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(parsed.items[0]!.login?.uris).toEqual([
|
||||
{ uri: "https://a.test", match: "exact" },
|
||||
{ uri: "https://b.test", match: "never" },
|
||||
{ uri: "https://c.test", match: "domain" },
|
||||
{ uri: "https://d.test", match: "domain" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps Bitwarden's item id, so a re-import dedupes instead of duplicating", () => {
|
||||
const id = "56126b05-485c-44c2-a6fc-acb70001e348";
|
||||
const parsed = parseBitwardenJson(exportOf([{ type: 1, id, name: "x", login: {} }]));
|
||||
expect(parsed.items[0]!.id).toBe(id);
|
||||
});
|
||||
|
||||
it("keeps the original timestamps, the only record of when a password rotated", () => {
|
||||
const parsed = parseBitwardenJson(
|
||||
exportOf([
|
||||
{
|
||||
type: 1,
|
||||
name: "x",
|
||||
login: {},
|
||||
creationDate: "2021-01-21T00:06:52.377Z",
|
||||
revisionDate: "2023-05-02T11:00:00.000Z",
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(parsed.items[0]!.createdAt).toBe("2021-01-21T00:06:52.377Z");
|
||||
expect(parsed.items[0]!.updatedAt).toBe("2023-05-02T11:00:00.000Z");
|
||||
});
|
||||
|
||||
it("carries password history newest first, capped at the spec limit", () => {
|
||||
const history = Array.from({ length: 25 }, (_, i) => ({
|
||||
password: `p${i}`,
|
||||
// Ascending dates, so the newest is the last one written.
|
||||
lastUsedDate: `2024-01-${String(i + 1).padStart(2, "0")}T00:00:00.000Z`,
|
||||
}));
|
||||
const parsed = parseBitwardenJson(
|
||||
exportOf([{ type: 1, name: "x", login: {}, passwordHistory: history }]),
|
||||
);
|
||||
const got = parsed.items[0]!.history ?? [];
|
||||
expect(got).toHaveLength(20);
|
||||
expect(got[0]).toMatchObject({ password: "p24" });
|
||||
expect(got[19]).toMatchObject({ password: "p5" });
|
||||
});
|
||||
|
||||
it("carries favourite through", () => {
|
||||
const parsed = parseBitwardenJson(
|
||||
exportOf([{ type: 1, name: "x", favorite: true, login: {} }]),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue