-
-
Save ignlg/0a349712a973cb94be67c8c4a3e3a196 to your computer and use it in GitHub Desktop.
Export Bitwarden to KeePass 2 XML format, with custom banned fields to clean things up
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
#!python | |
""" | |
Exports a Bitwarden database into a KeePass file (kdbx) including file attachments, custom fields and folder structure. | |
It can also export an unencrypted XML KeePass file conforming to KeePass 2 XML format. | |
It requires keepassxc-cli, if not available it can still export KeePass 2 XML | |
- https://github.com/keepassxreboot/keepassxc | |
Usage: bw_export_kp.py [-h] [-x] [-d] [-bw-password None] [-kee-password None] | |
bw_user output_file | |
positional arguments: | |
bw_user | |
output_file | |
optional arguments: | |
-h, --help show this help message and exit | |
-x, --xml-output saves an UNENCRYPTED KeePass 2 XML file | |
-d, --diff-pass different passwords for Bitwarden and KeePass file | |
-bw-password None Bitwarden password (prompted if not provided) | |
-kee-password None KeePass password (prompted if not provided) | |
References: | |
- Bitwarden CLI: https://help.bitwarden.com/article/cli/ | |
- KeePass 2 XML: https://github.com/keepassxreboot/keepassxc-specs/blob/master/kdbx-xml/rfc.txt | |
""" | |
from __future__ import print_function | |
import os | |
import getpass | |
import xmltodict | |
import uuid | |
import json | |
import subprocess | |
import base64 | |
import tempfile | |
import contextlib | |
import shutil | |
import errno | |
import sys | |
found = [] | |
banned = ['02U', | |
'02W', | |
'02Y', | |
'030', | |
'0_cart_quantity_select', | |
'1_cart_quantity_select', | |
'2_cart_quantity_select', | |
'2b54dd7995c52288df0bc7c36e7f1725840e3122', | |
'3_cart_quantity_select', | |
'4_cart_quantity_select', | |
'5_cart_quantity_select', | |
'6035d4db3b4cbb3e34acc605977169d7cefb1d52', | |
'_fmu.un._0.e', | |
'_fmu.uni-._0.l', | |
'_fmu.uni-regi._0.c', | |
'_fmu.uni-register-a._0.a', | |
'_fmu.uni-register-aut._0.a', | |
'_fmu.uni-register-auth._0.a', | |
'_fmu.uni-register-authcom._0.a', | |
'_fmu.uni-register-me._0.m', | |
'_fmu.uni-register-mem._0.m', | |
'_fmu.uni-register-memb._0.m', | |
'_fmu.uni-register-p._0.p', | |
'_fmu.uni-register-pa._0.pa', | |
'_fmu.uni-register-ph._0.p', | |
'_fmu.uni-register-pho._0.p', | |
'_fmu.uni-register._0.t', | |
'_fmu.uni._0.f', | |
'accept_terms', | |
'accept_terms_use', | |
'AcceptPrivacyPolicy', | |
'account.Password2', | |
'aceptado', | |
'acepto', | |
'acepto1', | |
'acepto2', | |
'aceptoCond', | |
'aceptocookies', | |
'acpo', | |
'action', | |
'address1', | |
'address[city]', | |
'address[country]', | |
'address[name]', | |
'address[postal_code]', | |
'address[province]', | |
'address[street]', | |
'address[surname]', | |
'admin_color', | |
'age', | |
'agree', | |
'agree_tos', | |
'allow_email', | |
'anno', | |
'answer1', | |
'apellido1', | |
'apellido2', | |
'approve_terms', | |
'authCompanyStateInput', | |
'AuthenticationRequestDto.EmailAddress', | |
'AuthenticationRequestDto.Password', | |
'ayoNac', | |
'b-entrar', | |
'B1', | |
'bbsbbstopics', | |
'billing[address]', | |
'billing[city]', | |
'billing[name]', | |
'billing[phone]', | |
'billing[state]', | |
'billing[zip]', | |
'billing_zip', | |
'BirthDay', | |
'birthday.day', | |
'birthday.month', | |
'birthday.year', | |
'BirthYear', | |
'blog_public', | |
'borrower_address', | |
'borrower_city', | |
'borrower_country', | |
'borrower_dateofbirth', | |
'borrower_email_repeat', | |
'borrower_firstname', | |
'borrower_nif', | |
'borrower_phone', | |
'borrower_sex', | |
'borrower_state', | |
'borrower_streetnumber', | |
'borrower_surname', | |
'borrower_zipcode', | |
'boton', | |
'btn_continue', | |
'btnChangpassword', | |
'btnLogin', | |
'business', | |
'button', | |
'callingCode', | |
'cancelbutton', | |
'candidate[address]', | |
'candidate[name]', | |
'candidate[time_zone]', | |
'Captcha', | |
'captchaAnswer', | |
'cargo_password2', | |
'carpeta', | |
'cart_quantity[]', | |
'CestaForm[aceptar]', | |
'CestaForm[apellidos]', | |
'CestaForm[codigo_postal_empresa]', | |
'CestaForm[confirmar_contrasena]', | |
'CestaForm[direccion_empresa]', | |
'CestaForm[nombre]', | |
'CestaForm[poblacion_empresa]', | |
'CestaForm[provincia_empresa]', | |
'CestaForm[telefono]', | |
'cgv', | |
'changepassword', | |
'check_password', | |
'china_payment_selection', | |
'City', | |
'city', | |
'clave', | |
'clicked_element', | |
'co', | |
'cod_pos_cas', | |
'commentshours', | |
'commercialInfo', | |
'commit', | |
'company name', | |
'company', | |
'company-name', | |
'company_size', | |
'companyCountry', | |
'condiciones', | |
'condiciones1step', | |
'conditions', | |
'confirm', | |
'confirm-password', | |
'confirm_email_address', | |
'confirmaEmail', | |
'confirmaPass', | |
'confirmation', | |
'confirmationemail', | |
'ConfirmEmail', | |
'ConfirmPassword', | |
'confirmpassword', | |
'confirmPassword', | |
'contrasenha_repeat', | |
'cookielength', | |
'cookieuser', | |
'country', | |
'Country', | |
'country_code', | |
'country_id', | |
'country_key', | |
'country_name', | |
'country_prefix_phone_1', | |
'countryId', | |
'CountryID', | |
'cp', | |
'createuser', | |
'Credentials.UsernameRepeat', | |
'credit limit', | |
'csdb', | |
'ctl00$bCPH$tabContainer$loginPanel$loginControl$LoginButton', | |
'ctl00$bCPH$tabContainer$loginPanel$loginControl$Password', | |
'ctl00$bCPH$tabContainer$newUserPanel$createUserWizard$__CustomNav0$StepNextButtonButton', | |
'ctl00$bCPH$tabContainer$newUserPanel$createUserWizard$CreateUserStepContainer$ConfirmEmail', | |
'ctl00$bCPH$tabContainer$newUserPanel$createUserWizard$CreateUserStepContainer$ConfirmPassword', | |
'ctl00$bCPH$tabContainer$newUserPanel$createUserWizard$CreateUserStepContainer$Email', | |
'ctl00$ContentPlaceHolder1$uc_RegisterForm$', | |
'ctl00$ContentPlaceHolder1$uc_RegisterForm$btn_login', | |
'ctl00$ContentPlaceHolder1$uc_RegisterForm$btn_register', | |
'ctl00$ContentPlaceHolder1$uc_RegisterForm$frm_remember_me', | |
'ctl00$ContentPlaceHolder1$uc_RegisterForm$frm_remember_me_new_user', | |
'ctl00$ContentPlaceHolderOficinaVirtual$btCancelar', | |
'ctl00$ContentPlaceHolderOficinaVirtual$btEnviar', | |
'ctl00$ctl00$ctl00$ctl00$base_content$web_base_content$home_content$page_content_left$ctl01$CreateUserButton', | |
'ctl00$ctl00$ctl00$ctl00$base_content$web_base_content$home_content$page_content_left$ctl01$Email', | |
'ctl00$ctl00$ctl00$ctl00$base_content$web_base_content$home_content$page_content_left$ctl01$FirstName', | |
'ctl00$ctl00$ctl00$ctl00$base_content$web_base_content$home_content$page_content_left$ctl01$LastName', | |
'ctl00$ctl00$ctl00$ctl00$base_content$web_base_content$home_content$page_content_left$ctl01$SignupPWord', | |
'ctl00$PagePlaceHolder$btnPreRegistro', | |
'ctl00$PagePlaceHolder$btnReturn', | |
'currency', | |
'custom_fields[custom2]', | |
'customer_firstname', | |
'customer_lastname', | |
'customer_privacy', | |
'customerName', | |
'CustomerType', | |
'data.Password2', | |
'DataPrivacyAcceptedCheckBox', | |
'dataProtectionSpecific', | |
'days', | |
'default_newsletters', | |
'delivery_option[0]', | |
'department', | |
'departureAirportName', | |
'description', | |
'diaNac', | |
'direccion', | |
'DistributionID', | |
'DNI', | |
'dni', | |
'dob', | |
'dob_Day', | |
'dob_Month', | |
'dob_Year', | |
'doLogin', | |
'donde', | |
'doOrder', | |
'doRegister', | |
'doReset', | |
'dst', | |
'email1', | |
'email2', | |
'email_confirm', | |
'email_newsletter3', | |
'email_repeat', | |
'email_verify', | |
'emailAsAliasBuff', | |
'EmailB', | |
'emailConfirm', | |
'emailconfirm', | |
'emailConfirmation', | |
'emailok', | |
'EMAILTYPE', | |
'enterprise-size', | |
'entra', | |
'Enviar', | |
'enviar', | |
'event_address', | |
'event_city', | |
'event_name', | |
'event_start_date_time', | |
'event_start_date_time_date', | |
'event_start_date_time_time', | |
'event_url', | |
'event_venue_capacity', | |
'event_venue_name', | |
'f-a94e32ce9d2f414b4c3b-email', | |
'f__camp-llocs_poi', | |
'f__camp-llocs_poi1', | |
'f__camp-llocs_poi2', | |
'f__camp-llocs_poi_d', | |
'f__checkbox__1', | |
'f__checkbox__3', | |
'f__checkbox__4', | |
'f__checkbox__5', | |
'f__checkbox__8', | |
'f_submit_login', | |
'facebookLink', | |
'fechaNac', | |
'first name', | |
'first', | |
'first-name', | |
'first_name', | |
'first_name-4', | |
'firstName', | |
'FirstName', | |
'firstname', | |
'FiscalCode', | |
'flight-search-type-option', | |
'fname', | |
'form1:ape1PER', | |
'form1:ape2PER', | |
'form1:confirmacionEmail', | |
'form1:confirmPass', | |
'form1:documentoPER', | |
'form1:email', | |
'form1:idioma', | |
'form1:j_id_jsp_1264618786_582pc5', | |
'form1:nombrePER', | |
'form1:nombreUsr', | |
'form1:nombreViaPER', | |
'form1:numeroViaPER', | |
'form1:pisoPER', | |
'form1:portalPER', | |
'form1:preguntaRecordatorio', | |
'form1:puertaPER', | |
'form1:respuestaRecordatorio', | |
'form1:tipoViaFacturaPER', | |
'form1:tipoViaPER', | |
'form1:usoCuenta', | |
'form_address_key', | |
'free', | |
'fsurname', | |
'fttkhcjf', | |
'full_name', | |
'fullname', | |
'gamer_account', | |
'gender', | |
'Github', | |
'GmailAddress', | |
'Go', | |
'googleLink', | |
'group1', | |
'group[10469][1]', | |
'hide_password', | |
'hide_register[password][second]', | |
'http_passwd', | |
'http_passwdConfirm', | |
'i_agree_check', | |
'iagree', | |
'iagreebutton', | |
'id_country', | |
'id_country_invoice', | |
'id_gender', | |
'id_state', | |
'id_state_invoice', | |
'idioma', | |
'ignoreEmailDup', | |
'im_id', | |
'image_type', | |
'image_verify_code', | |
'indexbbstopics', | |
'indexlatestadded', | |
'indexlatestcomments', | |
'indexlatestparties', | |
'indexlatestreleased', | |
'indexojnews', | |
'indexoneliner', | |
'indextopglops', | |
'indextopkeops', | |
'indextopprods', | |
'indexwatchlist', | |
'india_payment_selection', | |
'inf_field_City', | |
'inf_field_Email', | |
'inf_field_FirstName', | |
'inf_field_LastName', | |
'input_10', | |
'input_12', | |
'input_14', | |
'input_16.1', | |
'input_1_2', | |
'input_2.3', | |
'input_2.6', | |
'input_6.6', | |
'input_9.1', | |
'inputEmail', | |
'inputLanguage', | |
'inputLocation', | |
'inputPass2', | |
'interval', | |
'is_subscribed', | |
'itemsPerPageInTable', | |
'j_id105:checkboxAceptarCondiciones', | |
'j_id105:checkboxAceptarLOPD', | |
'j_id105:j_id253', | |
'j_id105:pais', | |
'j_id105:resPartnerAddresscif', | |
'j_id105:resPartnerAddressCity', | |
'j_id105:resPartnerAddresspasswordConfirma', | |
'j_id105:resPartnerAddressphone', | |
'j_id105:resPartnerAddressProvincia', | |
'j_id105:resPartnerAddressstreet', | |
'j_id105:resPartnerAddresszip', | |
'j_id105:resPartnerEmail', | |
'j_id105:resPartnername', | |
'j_id105:resPartnersurname', | |
'j_id105:resPartnersurname2', | |
'jabber', | |
'job_title_level', | |
'jobTitle', | |
'jrlsolej', | |
'keep_logged_in', | |
'keepMeSignInOption', | |
'keyval', | |
'keywords', | |
'lang', | |
'lang-chooser', | |
'language', | |
'language-selector', | |
'LanguageSelect', | |
'last name', | |
'last', | |
'last-name', | |
'last_name', | |
'last_name-4', | |
'lastname', | |
'LastName', | |
'lastName', | |
'LinkedIn', | |
'lname', | |
'lnshdven', | |
'locale', | |
'localidad', | |
'location', | |
'location_check_1', | |
'location_check_2', | |
'log_nombre', | |
'log_persistent', | |
'log_pwd', | |
'login', | |
'Login', | |
'LOGIN', | |
'login_submit_dummy', | |
'login_threadless', | |
'loginButton', | |
'loginForm.remember', | |
'LoginForm[email]', | |
'LoginForm[password]', | |
'logonId', | |
'logonPasswordVerify', | |
'lugar', | |
'lugar_instalacion', | |
'mail', | |
'mailinglist', | |
'member ID (additional)', | |
'member name', | |
'member since', | |
'member[password_confirmation]', | |
'membernext', | |
'mesNac', | |
'mobile', | |
'mobile_country', | |
'mobile_intl', | |
'mobilePhone', | |
'months', | |
'móvil', | |
'n_pass2', | |
'nationalId', | |
'nationalIdentificationNumber', | |
'nationality', | |
'new_password', | |
'new_password1', | |
'new_password2', | |
'NewPasswordConfirm', | |
'newPinCheck', | |
'newsletter', | |
'newsletters[317]', | |
'NewUserContactDetails.ConfirmPassword', | |
'NewUserContactDetails.Email', | |
'NewUserContactDetails.FirstName', | |
'NewUserContactDetails.LastName', | |
'NewUserLogin.NewEmail', | |
'nick_name', | |
'nickname', | |
'no_name', | |
'nombre', | |
'NOMBRE', | |
'nombreVia', | |
'numDoc', | |
'objecttype', | |
'op', | |
'options', | |
'options[adminemail]', | |
'original email', | |
'original mail', | |
'p2', | |
'pais', | |
'pApellido', | |
'pasPassw_2', | |
'pass', | |
'pass1-text', | |
'PASS2', | |
'pass2', | |
'pass[pass2]', | |
'passwd2', | |
'PasswdAgain', | |
'password-confirm', | |
'Password2', | |
'password2', | |
'password_2', | |
'password_again', | |
'password_answer', | |
'password_confirm', | |
'password_confirmar', | |
'password_confirmation', | |
'password_confirmation_again', | |
'password_question', | |
'password_repeat', | |
'password_two', | |
'password_verify', | |
'passwordAgain', | |
'passwordCheck', | |
'PasswordConf', | |
'passwordconfirm', | |
'passwordConfirm', | |
'passwordConfirmation', | |
'passwordResetRequest[password][second]', | |
'passwrd', | |
'passwrd1', | |
'passwrd2', | |
'payment_type', | |
'PC759$AddressCtl$tb_AddressLine', | |
'PC759$AddressCtl$tb_CityUK', | |
'PC759$AddressCtl$tb_ZipUK', | |
'PC759$btnRegRequest', | |
'PC759$dpBirthDate', | |
'PC759$txtConfirmPwdR', | |
'PC759$txtEmailRR', | |
'PC759$txtFirstName', | |
'PC759$txtLastName', | |
'PC759$txtPhone', | |
'persistent', | |
'PersistentCookie', | |
'personTitle', | |
'phone', | |
'Phone', | |
'phone1', | |
'phone_1', | |
'phone_intl', | |
'phone_mobile', | |
'phone_number', | |
'phoneNumber', | |
'phpcan_action', | |
'piso', | |
'poblacion', | |
'policies', | |
'policy', | |
'portal', | |
'postal_code', | |
'postcode', | |
'prefix_phone_1', | |
'primaryRole', | |
'primaryUse', | |
'privacy', | |
'prodlistprods', | |
'profile.contactme', | |
'Profile.FirstName', | |
'profile.gender', | |
'Profile.Gender', | |
'Profile.LastName', | |
'profile_perfil_p[field_primer_apellido][und][0][value]', | |
'profile_perfil_p[field_usuario_nombre][und][0][value]', | |
'profileFirstName', | |
'profileLastName', | |
'profilephone_number', | |
'promotion_code', | |
'provideProducts', | |
'provider', | |
'province', | |
'provincia', | |
'pt[]', | |
'puerta', | |
'pw1', | |
'pw2', | |
'pwd', | |
'qa_answer', | |
'qs', | |
'quantity_1142_0_0_0', | |
'quantity_2668_609_0_0', | |
'quantity_3800_0_0_0', | |
'quantity_3804_979_0_0', | |
'quantity_5081_0_0_0', | |
'quantity_744_0_0_0', | |
'query', | |
'QuestionCode1', | |
'r_address/1.company_name', | |
'radioReConsider', | |
'recaptcha_response_field', | |
'recognize', | |
'RecoveryPhoneNumber', | |
'reenter_email', | |
'reenter_password', | |
'regenerar', | |
'region_id', | |
'register', | |
'register.birth.day', | |
'register.birth.month', | |
'register.birth.year', | |
'register_vv[code]', | |
'register_vv[q][1]', | |
'register_vv[q][337]', | |
'register_vv[q][358]', | |
'registerambit', | |
'registercarrera', | |
'registercheck', | |
'registercognoms', | |
'registerform.deliverycountry', | |
'registerform.deliverytitle', | |
'registerform.paymentaddress1', | |
'registerform.paymentcountry', | |
'registerform.paymentdayphone', | |
'registerform.paymentfirstname', | |
'registerform.paymentmobilephone', | |
'registerform.paymentsurname', | |
'registerform.paymenttitle', | |
'registerform.paymenttown', | |
'registerform.paymentzip', | |
'registerform.repeat', | |
'registerform.selectasdelivery', | |
'registernom', | |
'registerpass', | |
'registeruser', | |
'registrarse', | |
'regSubmit', | |
'remember', | |
'remember_me', | |
'rememberme', | |
'rememberMe', | |
'rememberMe:checkbox', | |
'rememberPassword', | |
'reminder answer', | |
'reminder question', | |
'repassword', | |
'repeat_user_password', | |
'repeatEmail', | |
'repeatPassword', | |
'RetypePassword', | |
'ringkosk', | |
'rNewPassword', | |
'role', | |
'rsa', | |
'sa', | |
'sApellido', | |
'savePassword', | |
'search', | |
'searcharea', | |
'searchGranularity', | |
'searchin', | |
'searchprods', | |
'searchType', | |
'searchwords', | |
'secretQuestion', | |
'Security questions', | |
'segments_select[]', | |
'select-type', | |
'select_language', | |
'selector-currency', | |
'selIsClass', | |
'selIsWantFace', | |
'selIsWantTextChat', | |
'selX3', | |
'selX3a', | |
'selX4', | |
'selX6', | |
'selX6a', | |
'selX6b', | |
'send', | |
'send_user_notification', | |
'sex', | |
'sexo', | |
'sgnBt', | |
'ShippingCountryID', | |
'signIn', | |
'signin', | |
'SignUp', | |
'signup-name', | |
'signup[terms_of_service]', | |
'signup[username]', | |
'signup_email_mask', | |
'signup_name', | |
'signup_password_mask', | |
'SignupForm[firstName]', | |
'SignupForm[lastName]', | |
'skype', | |
'slengpung', | |
'spareEmail', | |
'spree_user[password_confirmation]', | |
'sq', | |
'ssh.Password2', | |
'state', | |
'state_tf', | |
'street', | |
'Street', | |
'street_address', | |
'styleid', | |
'subdomain', | |
'Submit', | |
'submit', | |
'submit-30887', | |
'submit-button', | |
'submit1', | |
'submit_button', | |
'submit_search', | |
'submitAccount', | |
'submitAddDiscount', | |
'submitbutton', | |
'submitContext', | |
'SubmitLogin', | |
'submitNewsletter', | |
'subscribe', | |
'subscribe[]', | |
'subscribe_to_email_communication', | |
'subscribed_to_newsletter', | |
'surname', | |
'surnames', | |
'swapSize', | |
'telefono', | |
'telefono2', | |
'telekom_sitereportbundle_user[password][Repeat]', | |
'telekom_sitereportbundle_user[whoWill]', | |
'Template$FormControl$Button1', | |
'Template$FormControl$txtCodigoActual', | |
'Template$FormControl$txtNuevoCodigo', | |
'term', | |
'terms', | |
'terms[term]', | |
'terms_of_use', | |
'termsAgree', | |
'termsChecked', | |
'TermsOfService', | |
'TestForApplications', | |
'time_zone', | |
'timezone', | |
'timezoneoffset', | |
'tip_doc', | |
'tipoDoc', | |
'tipoVia', | |
'title_key', | |
'titulo', | |
'topicposts', | |
'tos', | |
'tos_agree', | |
'tosPrivacy', | |
'toua_consent', | |
'Twitter', | |
'txtEmail2', | |
'type_account', | |
'unlimited', | |
'up_email', | |
'up_password', | |
'up_password_', | |
'up_user', | |
'updates', | |
'usage-type', | |
'user-type', | |
'user[account_type]', | |
'user[country]', | |
'user[country_code]', | |
'user[email]', | |
'user[email_confirmation]', | |
'user[email_volume]', | |
'user[first_name]', | |
'user[firstname]', | |
'user[full_name]', | |
'user[last_name]', | |
'user[lastname]', | |
'user[location]', | |
'user[name]', | |
'user[organisation][name]', | |
'user[over_13_and_accept_terms_and_conditions]', | |
'user[password]', | |
'user[password_confirm]', | |
'user[password_confirmation]', | |
'user[registration_company]', | |
'user[registration_role]', | |
'user[role]', | |
'user[terms_of_service]', | |
'user[time_zone]', | |
'user[weekly_newsletter]', | |
'user_email', | |
'user_email2', | |
'user_login', | |
'user_login-4', | |
'user_name', | |
'user_pass2', | |
'user_password-4', | |
'user_password_confirmation', | |
'user_registered', | |
'userAcceptsTermsOfService', | |
'usercomments', | |
'UserDto.UserCredential.ChallengeAnswer', | |
'UserDto.UserCredential.ChallengeQuestionId', | |
'UserDto.UserCredential.ConfirmPassword', | |
'usergroups', | |
'userlistusers', | |
'userlogos', | |
'usernfos', | |
'userparties', | |
'userPrincipal##alias', | |
'userPrincipal##confirmEmail', | |
'userPrincipal##confirmPassword', | |
'userPrincipal##secretAnswer', | |
'userPrincipal##secretQuestion', | |
'userPrincipal##spamBlocker', | |
'userprods', | |
'userrulez', | |
'userscreenshots', | |
'usersucks', | |
'usuarios[apodo]', | |
'usuarios[nombre]', | |
'usuarios[sexo]', | |
'vb_login_password', | |
'Vencemento', | |
'verify', | |
'vp', | |
'web_user[password_confirmation]', | |
'weblog_title', | |
'wp-submit', | |
'wpLoginattempt', | |
'wpLoginAttempt', | |
'wpMailmypassword', | |
'wpRemember', | |
'wt-msisdn', | |
'years', | |
'yt0', | |
'yt1', | |
'za-signup-btn', | |
'zero', | |
'zip', | |
'zipcode', | |
'Zipcode', | |
'zone_id', | |
'zxdemo', 'mes', 'card_expiration_year', 'card_expiration_month', 'num', 'card_number', 'mes', 'birth', 'birth date', 'birth_date', 'card_code', 'password', 'user_password', 'passwd', 'contrasena', 'login_passwd', 'cpassword', 'login_password', 'admin_password', 'username', 'usuario', 'Passwd'] | |
def eprint(*args, **kwargs): | |
print(*args, file=sys.stderr, **kwargs) | |
def is_tool(name): | |
"""Check whether `name` is on PATH and marked as executable.""" | |
# from whichcraft import which | |
from shutil import which | |
return which(name) is not None | |
def static_vars(**kwargs): | |
""" | |
Decorates a function with static variables and initializes them. | |
Usage: | |
Add before function declaration | |
@static_vars(var1=value, var2=value, ...) | |
""" | |
def decorate(func): | |
for k in kwargs: | |
setattr(func, k, kwargs[k]) | |
return func | |
return decorate | |
@contextlib.contextmanager | |
def write_open(filename=None): | |
if filename and filename != '-': | |
dirname, basename = os.path.split(filename) | |
temp, tempPath = tempfile.mkstemp(prefix=basename, dir=dirname) | |
fh = open(temp, 'w') | |
else: | |
fh = sys.stdout | |
try: | |
yield fh | |
finally: | |
if fh is not sys.stdout: | |
fh.close() | |
shutil.move(tempPath, filename) | |
def safe_move(src, dst): | |
"""Rename a file from ``src`` to ``dst``. | |
* Moves must be atomic. ``shutil.move()`` is not atomic. | |
Note that multiple threads may try to write to the cache at once, | |
so atomicity is required to ensure the serving on one thread doesn't | |
pick up a partially saved image from another thread. | |
* Moves must work across filesystems. Often temp directories and the | |
cache directories live on different filesystems. ``os.rename()`` can | |
throw errors if run across filesystems. | |
So we try ``os.rename()``, but if we detect a cross-filesystem copy, we | |
switch to ``shutil.move()`` with some wrappers to make it atomic. | |
""" | |
try: | |
os.rename(src, dst) | |
except OSError as err: | |
if err.errno == errno.EXDEV: | |
# Generate a unique ID, and copy `<src>` to the target directory | |
# with a temporary name `<dst>.<ID>.tmp`. Because we're copying | |
# across a filesystem boundary, this initial copy may not be | |
# atomic. We intersperse a random UUID so if different processes | |
# are copying into `<dst>`, they don't overlap in their tmp copies. | |
copy_id = uuid.uuid4() | |
tmp_dst = "%s.%s.tmp" % (dst, copy_id) | |
shutil.copyfile(src, tmp_dst) | |
# Then do an atomic rename onto the new name, and clean up the | |
# source image. | |
os.rename(tmp_dst, dst) | |
os.unlink(src) | |
else: | |
raise | |
def get_uuid(name): | |
""" | |
Computes the UUID of the given string as required by KeePass XML standard | |
https://github.com/keepassxreboot/keepassxc-specs/blob/master/kdbx-xml/rfc.txt | |
""" | |
uid = uuid.uuid5(uuid.NAMESPACE_DNS, name) | |
return base64.b64encode(uid.bytes).decode("utf-8") | |
def get_folder(f): | |
""" | |
Returns a dict of the input folder JSON structure returned by Bitwarden. | |
""" | |
return dict(UUID=get_uuid(f['name']), | |
Name=f['name']) | |
def get_protected_value(v): | |
""" | |
Returns a Value element that is "memory protected" in KeePass | |
(useful for Passwords and sensitive custom fields/strings). | |
""" | |
return {'#text': v, '@ProtectInMemory': 'True'} | |
def get_fields(subitem, protected=[]): | |
""" | |
Returns the components of subitem as a fields array, | |
protecting the items in protected list | |
""" | |
fields = [] | |
for k, v in subitem.items(): | |
if v is not None: | |
# check if it's protected | |
k = get_correct_name(k) | |
if k in protected: | |
v = get_protected_value(v) | |
fields.append(dict(Key=k, Value=v)) | |
return fields | |
def get_correct_name(name): | |
lower = name.lower() | |
if lower in ['username', 'usuario', 'alias']: | |
name = 'Alias' | |
if lower in ['name', 'accountname']: | |
name = 'Name' | |
elif lower in ['email', 'user_email', 'email_address', 'login_email']: | |
name = 'Email' | |
elif lower in ['address']: | |
name = 'Address' | |
elif lower in ['numero', 'number', ]: | |
name = 'Number' | |
elif lower in ['bank name', ]: | |
name = 'BankName' | |
elif lower in ['name on account', ]: | |
name = 'NameOnAccount' | |
elif lower in ['cardholder name', 'cardholderName']: | |
name = 'CardholderName' | |
elif lower in ['verification number', ]: | |
name = 'VerificationNumber' | |
elif lower in ['website', 'url']: | |
name = 'Website' | |
elif lower in ['type', 'brand']: | |
name = 'Brand' | |
elif lower == 'pubilc key' or lower == 'public key': | |
name = 'PublicKey' | |
elif lower == 'pirvate key' or lower == 'private key': | |
name = 'PrivateKey' | |
return name | |
@static_vars(binary_id=0, binaries=[]) | |
def get_entry(e): | |
""" | |
Returns a dict of the input entry (item from Bitwarden) | |
Parses the title, username, password, urls, notes, and custom fields. | |
""" | |
fields = [] | |
done = [] | |
# Parse custom fields, protecting as necessary | |
if 'fields' in e: | |
for f in e['fields']: | |
if f['name'] not in banned and f['value'] is not None: | |
# get value | |
value = f['value'] | |
# get key | |
key = get_correct_name(f['name']) | |
if key not in done: | |
done.append(key) | |
found.append(key) | |
# if protected? | |
if f['type'] == 1 or key in ['PrivateKey', 'VerificationNumber', 'storePassword', 'PIN']: | |
value = get_protected_value(value) | |
# put together | |
fields.append(dict(Key=key, Value=value)) | |
# default values | |
urls = '' | |
username, password = '', '' | |
notes = e['notes'] if e['notes'] is not None else '' | |
# read username, password, and url if a login item | |
if 'login' in e: | |
login = e['login'] | |
if 'uris' in login: | |
urls = [u['uri'] for u in login['uris']] | |
urls = ','.join(urls) | |
# get username and password | |
username = login['username'] | |
password = login['password'] | |
# add totop to fields as protected | |
fields.append(dict(Key='totp', | |
Value=get_protected_value(login['totp']))) | |
# Parse Card items | |
if 'card' in e: | |
# Make number a protected field | |
fields.extend(get_fields(e['card'], protected=[ | |
'number', 'Number', 'VerificationNumber', 'verification number', 'PIN', 'code', 'Code'])) | |
# Parse Identity items | |
if 'identity' in e: | |
fields.extend(get_fields(e['identity'])) | |
# Parse attachments | |
attachments = [] | |
if 'attachments' in e: | |
for a in e['attachments']: | |
if a['id'] is not None: | |
# append attachment reference | |
attachments.append( | |
dict( | |
Key=a['fileName'], | |
Value={'@Ref': get_entry.binary_id} | |
) | |
) | |
# add binary data to function static list and update static counter | |
att = get_bw_attachment(a['id'], e['id']) | |
get_entry.binaries.append( | |
dict({'@ID': get_entry.binary_id, | |
'@Compressed': 'False', '#text': att}) | |
) | |
get_entry.binary_id += 1 | |
# Check it's not None | |
username = username or '' | |
password = password or '' | |
# assemble the entry into a dict with a UUID | |
entry = dict(UUID=get_uuid(e['name']), | |
String=[dict(Key='Title', Value=e['name']), | |
dict(Key='UserName', Value=username), | |
dict(Key='Password', Value=get_protected_value(password)), | |
dict(Key='URL', Value=urls), | |
dict(Key='Notes', Value=notes) | |
] + fields) | |
if (attachments): | |
entry.update(dict(Binary=attachments)) | |
return entry | |
def get_cmd_output(cmd): | |
""" | |
Returns the output of the given command | |
""" | |
status, output = subprocess.getstatusoutput(cmd) | |
if status != 0: | |
eprint("Error running command:", cmd) | |
raise Exception | |
return output | |
def bw_logout(): | |
""" | |
Bitwarden logout | |
""" | |
cmd = 'bw logout --raw' | |
subprocess.run(cmd, shell=True, capture_output=True) | |
def get_bw_data(): | |
""" | |
Gets the folders and items from Bitwarden CLI | |
""" | |
# get folders | |
cmd = 'bw list folders --session '+main.bw_session | |
folders = json.loads(get_cmd_output(cmd)) | |
# get items | |
cmd = 'bw list items --session '+main.bw_session | |
items = json.loads(get_cmd_output(cmd)) | |
return folders, items | |
def secure_delete(path, passes=1): | |
""" | |
Safely delete a file by overwriting it with random data | |
""" | |
with open(path, "ba+") as delfile: | |
length = delfile.tell() | |
for i in range(passes): | |
delfile.seek(0) | |
delfile.write(os.urandom(length)) | |
os.remove(path) | |
def get_bw_attachment(id, itemid): | |
""" | |
Gets an attachment from Bitwarden CLI | |
""" | |
with tempfile.TemporaryDirectory() as tmpdir: | |
cmd = 'bw get attachment --itemid '+itemid+' '+id + \ | |
' --output '+tmpdir+'/ --raw --session '+main.bw_session | |
path = get_cmd_output(cmd) | |
if not os.path.isfile(path): | |
eprint("Error downloading attachment:", id) | |
raise Exception | |
with open(path, "rb") as f: | |
encoded_file = base64.b64encode(f.read()).decode("utf-8") | |
secure_delete(path) | |
return encoded_file | |
@static_vars(bw_session='') | |
def main(bw_user, output_file, | |
xml_output: ('saves an UNENCRYPTED KeePass 2 XML file', 'flag', 'x'), | |
diff_pass: ('different passwords for Bitwarden and KeePass file', 'flag', 'd'), | |
bw_password: ('Bitwarden password (prompted if not provided)', 'option') = None, | |
kee_password: ( | |
'KeePass password (prompted if not provided)', 'option') = None | |
): | |
""" | |
Main function | |
""" | |
if not is_tool('bw'): | |
eprint("Bitwarden cli not found") | |
raise Exception | |
# Log out any existing session | |
bw_logout() | |
# Bitwarden login | |
if bw_password is None: | |
bw_password = getpass.getpass(prompt='Bitwarden password: ') | |
cmd = 'bw --raw login '+str(bw_user) | |
res = subprocess.run(cmd, input=bytearray( | |
bw_password, 'utf-8'), shell=True, capture_output=True) | |
if res.returncode != 0: | |
eprint("Wrong password") | |
raise SystemExit | |
main.bw_session = res.stdout.decode() | |
del res # delete result object which has the password as a cmd argument | |
# set keepass password | |
if kee_password is None: | |
if diff_pass: | |
kee_password = getpass.getpass(prompt='KeePass password: ') | |
else: | |
kee_password = bw_password | |
del bw_password | |
# get data from bw | |
bw_folders, bw_items = get_bw_data() | |
# parse all entries | |
entries = [get_entry(e) for e in bw_items] | |
# loop over folders | |
# bw_folders = d['folders'] | |
folders = [] | |
root_entries = [] | |
for f in bw_folders: | |
# parse the folder | |
folder = get_folder(f) | |
folder_id = f['id'] | |
# loop on entries in this folder | |
folder_entries = [] | |
for entry, item in zip(entries, bw_items): | |
if item['folderId'] == folder_id: | |
folder_entries.append(entry) | |
# NoFolder (with None id) | |
if folder_id is None: | |
root_entries = folder_entries | |
# Normal folder | |
else: | |
if len(folder_entries) > 0: | |
folder['Entry'] = folder_entries | |
# add to output folder | |
folders.append(folder) | |
# Root group | |
root_group = get_folder(dict(name='Root')) | |
root_group['Group'] = folders | |
# add items to root folder | |
if len(root_entries) > 0: | |
root_group['Entry'] = root_entries | |
# Root element | |
root = dict(Group=root_group) | |
# Meta element | |
meta = dict(Generator='bitwarden export', | |
MasterKeyChangeForce=-1, MasterKeyChangeRec=-1) | |
# add binary files from attachments | |
if (get_entry.binaries): | |
meta.update(dict(Binaries=dict(Binary=get_entry.binaries))) | |
# xml document contents | |
xml = dict(KeePassFile=dict(Meta=meta, Root=root)) | |
found.sort() | |
print(found) | |
if xml_output: | |
# export unencrypted XML | |
with write_open(output_file) as out: | |
out.write(xmltodict.unparse(xml, pretty=True)) | |
out.close() | |
raise SystemExit | |
if not is_tool('keepassxc-cli'): | |
eprint("keepassxc-cli not found") | |
raise Exception | |
# write XML and export to keepass | |
with tempfile.NamedTemporaryFile() as xml_out: | |
xml_out.write(bytearray(xmltodict.unparse(xml, pretty=True), 'utf-8')) | |
xml_out.flush() | |
with tempfile.TemporaryDirectory() as tmpdir: | |
output_tempfile = tmpdir + '/tmp' | |
cmd = 'keepassxc-cli import '+xml_out.name+' '+output_tempfile | |
res = subprocess.run(cmd, input=bytearray( | |
kee_password, 'utf-8'), shell=True, capture_output=True) | |
if res.returncode != 0: | |
eprint("Error exporting KeePass file") | |
raise SystemExit | |
del res | |
safe_move(output_tempfile, output_file) | |
# cleanup and exit | |
del kee_password | |
bw_logout() | |
sys.exit(0) | |
if __name__ == "__main__": | |
import plac | |
try: | |
plac.call(main) | |
except SystemExit: | |
bw_logout() | |
sys.exit(0) | |
except: | |
import traceback | |
print("Unexpected error:", sys.exc_info()) | |
print(traceback.format_exc()) | |
bw_logout() | |
sys.exit(1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment