Last active
December 30, 2022 23:57
-
-
Save tbjgolden/7704aa4399b70f14cc4f2fda739a3377 to your computer and use it in GitHub Desktop.
Import bitwarden export json, interactive deduplicate and export bitwarden import json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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