Skip to content

Instantly share code, notes, and snippets.

@tbjgolden
Last active December 30, 2022 23:57
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tbjgolden/7704aa4399b70f14cc4f2fda739a3377 to your computer and use it in GitHub Desktop.
Save tbjgolden/7704aa4399b70f14cc4f2fda739a3377 to your computer and use it in GitHub Desktop.
Import bitwarden export json, interactive deduplicate and export bitwarden import json
const fs = require('fs');
const path = require('path');
const inquirer = require('inquirer');
const uuidv4 = require('uuid/v4');
const owasp = require('owasp-password-strength-test');
const words = new Set([...require('wordlist-english')['english/10'], ...require('wordlist-english')['english/20']]);
const thingsIDontDoAnyMore = require("./thingsIDontDoAnyMore.json");
const { items } = require('./bitwarden_export_file.json');
const oldThingRegex = new RegExp(
thingsIDontDoAnyMore.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join("|"),
"ig"
);
// dead folder
// look at urls, parse chunks and be able to select multiple fragments of each url
// then use this info to merge them
// maybe get rid of dead emails?
const choiceMap = {};
const tlds = ["com","org","edu","gov","co","io","app","site","uk","net","ca","de","jp","fr","au","us","ru","ch","it","nl","se","no","es","mil"];
const subTlds = ["co", "ac", "com"];
const genericSubDomains = ["www", "www1", "secure7", "secure2", "m", "login", "logon", "secure", "signin", "signup", "support", "sso", "my", "mobile", "en", "dashboard", "beta", "auth", "app", "account", "accounts", "android", "androi", "mail", "cloud", "ssl", "uk"];
const regexMap = {};
(async () => {
const dead = [];
const unsafe = [];
const old = [];
const invalid = ["--", "null"];
for (let i = 0; i < items.length; i++) {
const savedData = items[i];
if (invalid.includes(`${savedData.login.username}`)) continue;
if (invalid.includes(`${savedData.login.password}`)) continue;
const url = savedData.login.uris[0].uri;
const isAndroid = !url.indexOf("android://");
const isAndroidApp = !url.indexOf("androidapp://");
const isIOS = /^(ios)?app:\/\//.test(url);
const isIP = /localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.test(url);
const isWeb = /^https?:\/\//.test(url);
const isRegexed = /^\\b.*\\b$/.test(url);
if (!isRegexed && (isAndroid || isAndroidApp || isWeb)) {
let frags;
const withoutProtocol = url.replace(/^https?:\/\//, "");
if (isAndroid) {
frags = url.substring(url.lastIndexOf("@") + 1, url.length - 1).split(".").reverse();
} else if (isAndroidApp || isIOS) {
frags = url.substring(url.lastIndexOf("/") + 1).split(".").reverse();
} else if (isIP) {
frags = [withoutProtocol.substring(0, withoutProtocol.indexOf("/"))];
} else if (isWeb) {
frags = withoutProtocol.substring(0, withoutProtocol.indexOf("/")).split(".");
}
if (genericSubDomains.includes(frags[0])) frags.shift();
let answer;
if (choiceMap[frags.join(".")]) {
answer = choiceMap[frags.join(".")];
} else {
const isNational = subTlds.includes(frags[frags.length - 2]);
const startIndexOfTld = frags.length - (1 + ~~isNational);
const tld = frags.slice(startIndexOfTld).join(".");
const domain = frags[startIndexOfTld - 1];
const subdomain = frags.slice(0, startIndexOfTld - 1).join(".");
let parts = [domain];
const unsure = words.has(domain) || /\d/g.test(domain) || !domain || domain.length < 4;
if (isIP) {
parts = frags;
} else if (unsure) {
const { answer } = await inquirer.prompt([
{
type: "checkbox",
name: "answer",
message: frags.join("."),
default: [domain],
choices: [
tld,
domain,
subdomain,
'!'
].filter(Boolean)
}
]);
if (!answer.length || answer.includes("!")) {
console.log(`skipping ${frags.join(".")}`)
continue;
}
parts = answer.reverse().join(".").split(".");
}
choiceMap[frags.join(".")] = parts;
answer = parts.sort();
}
const regex = (answer.length > 1)
? [answer.join("\\."), answer.reverse().join("\\.")].sort().map(x => `\\b${x}\\b`).join("|")
: `\\b${answer.join("\\.")}\\b`;
let { password } = savedData.login;
if (regexMap[regex] && regexMap[regex][savedData.login.username] && regexMap[regex][savedData.login.username] !== savedData.login.password) {
password = (await inquirer.prompt([
{
type: "list",
name: "password",
message: `${savedData.login.username} @ ${regex.replace(/\\b/g, "")}`,
choices: [
regexMap[regex][savedData.login.username],
savedData.login.password
].filter(Boolean)
}
])).password;
}
regexMap[regex] = {
...(regexMap[regex] || {}),
[savedData.login.username]: password
};
}
}
// Object.entries(regexMap)
// .map([regex, pairs] => Object.entries(pairs).map(pair => [...pair, regex]))
const unsafeUid = `1e555afe${uuidv4().substring(8)}`;
const deadUid = `decea5ed${uuidv4().substring(8)}`;
const pwdTestMap = {};
const bitwardenImportable = {
folders: [
{
id: unsafeUid,
name: "Unsafe"
},
{
id: deadUid,
name: "Dead"
}
],
items: Object.entries(regexMap)
.map(([regex, pairs]) => {
const name = regex.substring(regex.lastIndexOf("|") + 1).replace(/\\\./g, ".");
return Object.entries(pairs).map(([u, p]) => {
const _u = u.replace(/gmail\.com/g, "g")
.replace(/hotmail\.com/g, "hm")
.replace(/live\.com/g, "l")
.replace(/yahoo\.com/g, "y");
return [u, p, regex, `${_u}@${name.substring(2, name.length - 2)}`];
});
})
.reduce((arr, nxt) => arr.concat(nxt), [])
.map(([username, password, regex, name]) => {
pwdTestMap[password] = pwdTestMap[password]
|| owasp.test(password);
const isDead = oldThingRegex.test(username) || oldThingRegex.test(name);
const isUnsafe = !pwdTestMap[password].strong;
return {
id: uuidv4(),
organizationId: null,
folderId: isDead ? deadUid : (isUnsafe ? unsafeUid : null),
type: 1,
name,
notes: null,
favorite: false,
login: {
uris: [
{
match: 4,
uri: regex
}
],
username,
password,
totp: null
},
collectionIds: null
};
})
};
fs.writeFileSync(
path.join(__dirname, "bitwarden_importable.json"),
JSON.stringify(bitwardenImportable)
);
})();
@tbjgolden
Copy link
Author

thingsIDontDoAnyMore.json is an array of strings which are searched for in usernames and domains which match things that you used to do, but don't do any more - like an old school/job, allows it to autosort it into an old category

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment