Skip to content

Instantly share code, notes, and snippets.

@ignlg
Forked from mpalet/bw_export_kp.py
Last active March 31, 2020 18:20
Show Gist options
  • Save ignlg/0a349712a973cb94be67c8c4a3e3a196 to your computer and use it in GitHub Desktop.
Save ignlg/0a349712a973cb94be67c8c4a3e3a196 to your computer and use it in GitHub Desktop.

Revisions

  1. ignlg revised this gist Mar 31, 2020. 1 changed file with 932 additions and 73 deletions.
    1,005 changes: 932 additions & 73 deletions bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -28,22 +28,825 @@
    """

    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)

    import os
    import errno
    import shutil
    import contextlib
    import tempfile
    import base64
    import subprocess
    import json
    import uuid
    import xmltodict
    import getpass

    def is_tool(name):
    """Check whether `name` is on PATH and marked as executable."""
    @@ -53,6 +856,7 @@ def is_tool(name):

    return which(name) is not None


    def static_vars(**kwargs):
    """
    Decorates a function with static variables and initializes them.
    @@ -66,6 +870,7 @@ def decorate(func):
    return func
    return decorate


    @contextlib.contextmanager
    def write_open(filename=None):
    if filename and filename != '-':
    @@ -82,6 +887,7 @@ def write_open(filename=None):
    fh.close()
    shutil.move(tempPath, filename)


    def safe_move(src, dst):
    """Rename a file from ``src`` to ``dst``.
    @@ -118,14 +924,14 @@ def safe_move(src, dst):
    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
    """
    #name = name.encode('ascii', 'ignore')
    uid = uuid.uuid5(uuid.NAMESPACE_DNS, name)
    return base64.b64encode(uid.bytes).decode("utf-8")
    return base64.b64encode(uid.bytes).decode("utf-8")


    def get_folder(f):
    @@ -134,7 +940,7 @@ def get_folder(f):
    """
    return dict(UUID=get_uuid(f['name']),
    Name=f['name'])


    def get_protected_value(v):
    """
    @@ -152,32 +958,72 @@ def get_fields(subitem, protected=[]):
    fields = []

    for k, v in subitem.items():
    # check if it's protected
    if k in protected:
    v = get_protected_value(v)
    fields.append(dict(Key=k, Value=v))
    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.
    """
    # Parse custom fields, protecting as necessary
    fields = []
    done = []
    # Parse custom fields, protecting as necessary
    if 'fields' in e:
    for f in e['fields']:
    if f['name'] is not None:
    if f['name'] not in banned and f['value'] is not None:
    # get value
    value = f['value']
    # if protected?
    if f['type'] == 1:
    value = get_protected_value(value)
    # put together
    fields.append(dict(Key=f['name'], Value=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 = '', ''
    @@ -189,19 +1035,20 @@ def get_entry(e):
    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']
    username = login['username']
    password = login['password']

    # add totop to fields as protected
    fields.append(dict(Key='totp',
    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']))
    fields.extend(get_fields(e['card'], protected=[
    'number', 'Number', 'VerificationNumber', 'verification number', 'PIN', 'code', 'Code']))

    # Parse Identity items
    if 'identity' in e:
    @@ -211,35 +1058,36 @@ def get_entry(e):
    attachments = []
    if 'attachments' in e:
    for a in e['attachments']:
    if a['id'] is not None:
    #append attachment reference
    if a['id'] is not None:
    # append attachment reference
    attachments.append(
    dict(
    Key=a['fileName'],
    Value={'@Ref': get_entry.binary_id }
    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'])
    # 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})
    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)
    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))

    @@ -257,13 +1105,15 @@ def get_cmd_output(cmd):

    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
    @@ -278,6 +1128,7 @@ def get_bw_data():

    return folders, items


    def secure_delete(path, passes=1):
    """
    Safely delete a file by overwriting it with random data
    @@ -289,14 +1140,16 @@ def secure_delete(path, passes=1):
    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
    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
    @@ -310,12 +1163,13 @@ def get_bw_attachment(id, itemid):


    @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
    ):
    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
    """
    @@ -324,23 +1178,24 @@ def main(bw_user, output_file,
    eprint("Bitwarden cli not found")
    raise Exception

    #Log out any existing session
    # 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)
    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
    del res # delete result object which has the password as a cmd argument

    #set keepass password
    # set keepass password
    if kee_password is None:
    if diff_pass:
    kee_password = getpass.getpass(prompt='KeePass password: ')
    @@ -350,11 +1205,10 @@ def main(bw_user, output_file,

    # 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 = []
    @@ -363,45 +1217,49 @@ def main(bw_user, output_file,
    # 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
    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)
    root = dict(Group=root_group)

    # Meta element
    meta = dict(Generator='bitwarden export', MasterKeyChangeForce=-1, MasterKeyChangeRec=-1)
    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:
    @@ -413,28 +1271,29 @@ def main(bw_user, output_file,
    eprint("keepassxc-cli not found")
    raise Exception

    #write XML and export to keepass
    # 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)
    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
    # cleanup and exit
    del kee_password
    bw_logout()
    sys.exit(0)


    if __name__ == "__main__":
    import plac;
    import plac
    try:
    plac.call(main)
    except SystemExit:
  2. @mpalet mpalet revised this gist Nov 12, 2019. 1 changed file with 188 additions and 45 deletions.
    233 changes: 188 additions & 45 deletions bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -1,43 +1,122 @@
    #!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 sys

    def eprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)

    import os
    import errno
    import shutil
    import contextlib
    import tempfile
    import base64
    import subprocess
    import json
    import uuid
    import xmltodict
    import getpass

    def is_tool(name):
    """Check whether `name` is on PATH and marked as executable."""

    """
    Exports a Bitwraden database into an XML file conforming to KeePass 2 XML Format.
    The advantage of the XML format, is that it supports importing custom fields from
    Bitwarden into their own custom fields in KeePass 2, which is not currently supported
    in the Bitwarden CSV import function.
    Usage:
    # 1. log into bw
    $ bw login
    # from whichcraft import which
    from shutil import which

    # 2. export xml
    $ python bw_export_kp.py > passwords.xml
    return which(name) is not None

    # 3. import the passwords.xml file into KeePass 2 (or other KeePass clones that
    # support importing KeePass2 XML formats)
    # 4. delete passwords.xml
    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

    References:
    - Bitwarden CLI: https://help.bitwarden.com/article/cli/
    - KeePass 2 XML: https://github.com/keepassxreboot/keepassxc-specs/blob/master/kdbx-xml/rfc.txt
    """
    @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):
    """
    @@ -80,19 +159,6 @@ def get_fields(subitem, protected=[]):

    return fields

    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

    @static_vars(binary_id=0, binaries=[])
    def get_entry(e):
    """
    @@ -186,22 +252,28 @@ def get_cmd_output(cmd):
    """
    status, output = subprocess.getstatusoutput(cmd)
    if status != 0:
    print("Error running command:", cmd)
    sys.exit(1)
    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'
    cmd = 'bw list folders --session '+main.bw_session
    folders = json.loads(get_cmd_output(cmd))

    # get items
    cmd = 'bw list items'
    cmd = 'bw list items --session '+main.bw_session
    items = json.loads(get_cmd_output(cmd))

    return folders, items
    @@ -222,12 +294,12 @@ 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'
    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):
    print("Error downloading attachment:", id)
    sys.exit(1)
    eprint("Error downloading attachment:", id)
    raise Exception

    with open(path, "rb") as f:
    encoded_file = base64.b64encode(f.read()).decode("utf-8")
    @@ -237,12 +309,45 @@ def get_bw_attachment(id, itemid):
    return encoded_file



    def main():
    @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()

    @@ -297,9 +402,47 @@ def main():
    # xml document contents
    xml = dict(KeePassFile=dict(Meta=meta, Root=root))

    # write XML document to stdout
    print(xmltodict.unparse(xml, pretty=True))
    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__":
    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)
  3. @mpalet mpalet revised this gist Nov 9, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -288,7 +288,7 @@ def main():
    root=dict(Group=root_group)

    # Meta element
    meta = dict(Generator='bitwarden export', MasterKeyChangeForce=-1)
    meta = dict(Generator='bitwarden export', MasterKeyChangeForce=-1, MasterKeyChangeRec=-1)

    # add binary files from attachments
    if (get_entry.binaries):
  4. @mpalet mpalet revised this gist Nov 9, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -288,7 +288,7 @@ def main():
    root=dict(Group=root_group)

    # Meta element
    meta = dict(DatabaseName=, Generator='bitwarden export', MasterKeyChangeForce=-1, )
    meta = dict(Generator='bitwarden export', MasterKeyChangeForce=-1)

    # add binary files from attachments
    if (get_entry.binaries):
  5. @mpalet mpalet revised this gist Nov 9, 2019. 1 changed file with 92 additions and 11 deletions.
    103 changes: 92 additions & 11 deletions bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -1,13 +1,20 @@
    #!/usr/bin/python
    #!python

    from __future__ import print_function
    import sys

    def eprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)

    import os
    import tempfile
    import base64
    import commands
    import subprocess
    import json
    import sys
    import uuid
    import xmltodict


    """
    Exports a Bitwraden database into an XML file conforming to KeePass 2 XML Format.
    The advantage of the XML format, is that it supports importing custom fields from
    @@ -37,9 +44,9 @@ 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
    """
    name = name.encode('ascii', 'ignore')
    #name = name.encode('ascii', 'ignore')
    uid = uuid.uuid5(uuid.NAMESPACE_DNS, name)
    return base64.b64encode(uid.bytes)
    return base64.b64encode(uid.bytes).decode("utf-8")


    def get_folder(f):
    @@ -65,14 +72,28 @@ def get_fields(subitem, protected=[]):
    """
    fields = []

    for k, v in subitem.iteritems():
    for k, v in subitem.items():
    # check if it's protected
    if k in protected:
    v = get_protected_value(v)
    fields.append(dict(Key=k, Value=v))

    return fields

    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

    @static_vars(binary_id=0, binaries=[])
    def get_entry(e):
    """
    Returns a dict of the input entry (item from Bitwarden)
    @@ -119,6 +140,26 @@ def get_entry(e):
    # 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 ''
    @@ -132,15 +173,18 @@ def get_entry(e):
    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 = commands.getstatusoutput(cmd)
    status, output = subprocess.getstatusoutput(cmd)
    if status != 0:
    print("Error running command:", cmd)
    sys.exit(1)
    @@ -162,19 +206,49 @@ def get_bw_data():

    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'
    path = get_cmd_output(cmd)

    if not os.path.isfile(path):
    print("Error downloading attachment:", id)
    sys.exit(1)

    with open(path, "rb") as f:
    encoded_file = base64.b64encode(f.read()).decode("utf-8")

    secure_delete(path)

    return encoded_file



    def main():
    """
    Main function
    """

    # get data from bw
    bw_folders, bw_items = get_bw_data()

    # parse all entries
    entries = [get_entry(e) for e in bw_items]

    # Meta element
    meta = dict()

    # loop over folders
    # bw_folders = d['folders']
    @@ -212,7 +286,14 @@ def main():

    # Root element
    root=dict(Group=root_group)


    # Meta element
    meta = dict(DatabaseName=, Generator='bitwarden export', MasterKeyChangeForce=-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))

  6. @mohamedadaly mohamedadaly revised this gist Nov 8, 2018. 1 changed file with 15 additions and 7 deletions.
    22 changes: 15 additions & 7 deletions bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -179,6 +179,7 @@ def main():
    # loop over folders
    # bw_folders = d['folders']
    folders = []
    root_entries = []
    for f in bw_folders:
    # parse the folder
    folder = get_folder(f)
    @@ -190,17 +191,24 @@ def main():
    if item['folderId'] == folder_id:
    folder_entries.append(entry)

    # add if there is something
    if len(entries) > 0:
    folder['Entry'] = folder_entries

    # add to output folder
    folders.append(folder)
    # 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
    # root_group['Entry'] = entries

    # add items to root folder
    if len(root_entries) > 0:
    root_group['Entry'] = root_entries

    # Root element
    root=dict(Group=root_group)
  7. @mohamedadaly mohamedadaly revised this gist Nov 8, 2018. 1 changed file with 44 additions and 9 deletions.
    53 changes: 44 additions & 9 deletions bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -56,19 +56,40 @@ def get_protected_value(v):
    (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.iteritems():
    # check if it's protected
    if k in protected:
    v = get_protected_value(v)
    fields.append(dict(Key=k, Value=v))

    return fields

    def get_entry(e):
    """
    Returns a dict of the input entry (item from Bitwarden)
    Parses the title, username, password, urls, notes, and custom fields.
    """
    # Parse custom fields into protected values
    # Parse custom fields, protecting as necessary
    fields = []
    if 'fields' in e:
    for f in e['fields']:
    if f['name'] is not None:
    fields.append(dict(Key=f['name'],
    Value=get_protected_value(f['value'])))
    # get value
    value = f['value']
    # if protected?
    if f['type'] == 1:
    value = get_protected_value(value)
    # put together
    fields.append(dict(Key=f['name'], Value=value))

    # default values
    urls = ''
    @@ -77,13 +98,27 @@ def get_entry(e):

    # read username, password, and url if a login item
    if 'login' in e:
    if 'uris' in e['login']:
    urls = [u['uri'] for u in e['login']['uris']]
    login = e['login']
    if 'uris' in login:
    urls = [u['uri'] for u in login['uris']]
    urls = ','.join(urls)


    username = e['login']['username']
    password = e['login']['password']
    # 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']))

    # Parse Identity items
    if 'identity' in e:
    fields.extend(get_fields(e['identity']))

    # Check it's not None
    username = username or ''
  8. @mohamedadaly mohamedadaly revised this gist Nov 8, 2018. 1 changed file with 43 additions and 32 deletions.
    75 changes: 43 additions & 32 deletions bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -12,20 +12,20 @@
    Exports a Bitwraden database into an XML file conforming to KeePass 2 XML Format.
    The advantage of the XML format, is that it supports importing custom fields from
    Bitwarden into their own custom fields in KeePass 2, which is not currently supported
    in the CSV import function.
    in the Bitwarden CSV import function.
    Usage:
    # 1. log into bw
    $bw login
    $ bw login
    # 2. export xml
    $python bw_export_kp.py > passwords.xml
    $ python bw_export_kp.py > passwords.xml
    # 3. import the passwords.xml file into KeePass 2 (or other KeePass clones that
    # support importing KeePass2 XML formats)
    # 4. delete passwords.xml.
    # 4. delete passwords.xml
    References:
    - Bitwarden CLI: https://help.bitwarden.com/article/cli/
    @@ -98,63 +98,73 @@ def get_entry(e):
    dict(Key='Notes', Value=notes)
    ] + fields)

    # print(entry)
    return entry


    def main():
    def get_cmd_output(cmd):
    """
    Main function
    Returns the output of the given command
    """
    # get output from bw CLI by listing all folders and items
    # and returning a JSON object with
    # 'folders': list of folders
    # 'items': list of items
    cmd = "(bw list folders | jq '{folders: .}' ; bw list items | jq '{items: .}') | cat | jq -s '. | add'"
    status, output = commands.getstatusoutput(cmd)
    # read stdin
    # stdin = sys.stdin.read()
    if status != 0:
    print("Error ...")
    print(output)
    print("Error running command:", cmd)
    sys.exit(1)

    # parse input json to a dict structure
    d = json.loads(output)
    #print(d['folders'][0]['name'])
    # print(d['items'][0])
    # print(get_entry(d['items'][0]))

    return output


    def get_bw_data():
    """
    Gets the folders and items from Bitwarden CLI
    """
    # get folders
    cmd = 'bw list folders'
    folders = json.loads(get_cmd_output(cmd))

    # get items
    cmd = 'bw list items'
    items = json.loads(get_cmd_output(cmd))

    return folders, items


    def main():
    """
    Main function
    """
    # get data from bw
    bw_folders, bw_items = get_bw_data()

    # parse all entries
    in_entries = [get_entry(e) for e in d['items']]
    entries = [get_entry(e) for e in bw_items]

    # Meta element
    meta = dict()

    # loop over folders
    in_folders = d['folders']
    out_folders = []
    for f in in_folders:
    # bw_folders = d['folders']
    folders = []
    for f in bw_folders:
    # parse the folder
    folder = get_folder(f)
    folder_id = f['id']

    # loop on entries in this folder
    entries = []
    for entry, item in zip(in_entries, d['items']):
    folder_entries = []
    for entry, item in zip(entries, bw_items):
    if item['folderId'] == folder_id:
    entries.append(entry)
    folder_entries.append(entry)

    # add if there is something
    if len(entries) > 0:
    folder['Entry'] = entries
    folder['Entry'] = folder_entries

    # add to output folder
    out_folders.append(folder)
    folders.append(folder)

    # Root group
    root_group = get_folder(dict(name='Root'))
    root_group['Group'] = out_folders
    root_group['Group'] = folders
    # root_group['Entry'] = entries

    # Root element
    @@ -166,5 +176,6 @@ def main():
    # write XML document to stdout
    print(xmltodict.unparse(xml, pretty=True))


    if __name__ == "__main__":
    main()
  9. @mohamedadaly mohamedadaly revised this gist Nov 7, 2018. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -10,7 +10,9 @@

    """
    Exports a Bitwraden database into an XML file conforming to KeePass 2 XML Format.
    The advantage of the XML format, is that it supports importing custom fields from
    Bitwarden into their own custom fields in KeePass 2, which is not currently supported
    in the CSV import function.
    Usage:
  10. @mohamedadaly mohamedadaly revised this gist Nov 7, 2018. 1 changed file with 17 additions and 3 deletions.
    20 changes: 17 additions & 3 deletions bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -1,16 +1,30 @@
    #!/usr/bin/python

    from __future__ import print_function
    import xmltodict
    import base64
    import commands
    import json
    import sys
    import commands
    import uuid
    import base64
    import xmltodict

    """
    Exports a Bitwraden database into an XML file conforming to KeePass 2 XML Format.
    Usage:
    # 1. log into bw
    $bw login
    # 2. export xml
    $python bw_export_kp.py > passwords.xml
    # 3. import the passwords.xml file into KeePass 2 (or other KeePass clones that
    # support importing KeePass2 XML formats)
    # 4. delete passwords.xml.
    References:
    - Bitwarden CLI: https://help.bitwarden.com/article/cli/
    - KeePass 2 XML: https://github.com/keepassxreboot/keepassxc-specs/blob/master/kdbx-xml/rfc.txt
  11. @mohamedadaly mohamedadaly revised this gist Nov 7, 2018. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -8,6 +8,14 @@
    import uuid
    import base64

    """
    Exports a Bitwraden database into an XML file conforming to KeePass 2 XML Format.
    References:
    - Bitwarden CLI: https://help.bitwarden.com/article/cli/
    - KeePass 2 XML: https://github.com/keepassxreboot/keepassxc-specs/blob/master/kdbx-xml/rfc.txt
    """

    def get_uuid(name):
    """
    Computes the UUID of the given string as required by KeePass XML standard
  12. @mohamedadaly mohamedadaly revised this gist Nov 7, 2018. 1 changed file with 42 additions and 12 deletions.
    54 changes: 42 additions & 12 deletions bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -9,32 +9,49 @@
    import base64

    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
    """
    name = name.encode('ascii', 'ignore')
    uid = uuid.uuid5(uuid.NAMESPACE_DNS, name)
    return base64.b64encode(uid.bytes)

    return base64.b64encode(uid.bytes)


    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_entry(e):
    """
    Returns a dict of the input entry (item from Bitwarden)
    Parses the title, username, password, urls, notes, and custom fields.
    """
    # Parse custom fields into protected values
    fields = []
    if 'fields' in e:
    for f in e['fields']:
    if f['name'] is not None:
    fields.append(dict(Key=f['name'],
    Value=get_protected_value(f['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:
    if 'uris' in e['login']:
    urls = [u['uri'] for u in e['login']['uris']]
    @@ -44,9 +61,11 @@ def get_entry(e):
    username = e['login']['username']
    password = e['login']['password']

    # 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),
    @@ -58,8 +77,15 @@ def get_entry(e):
    # print(entry)
    return entry


    def main():
    # get output from bw
    """
    Main function
    """
    # get output from bw CLI by listing all folders and items
    # and returning a JSON object with
    # 'folders': list of folders
    # 'items': list of items
    cmd = "(bw list folders | jq '{folders: .}' ; bw list items | jq '{items: .}') | cat | jq -s '. | add'"
    status, output = commands.getstatusoutput(cmd)
    # read stdin
    @@ -69,13 +95,14 @@ def main():
    print(output)
    sys.exit(1)

    # parse input to json
    # parse input json to a dict structure
    d = json.loads(output)
    #print(d['folders'][0]['name'])
    # print(d['items'][0])
    # print(get_entry(d['items'][0]))

    entries = [get_entry(e) for e in d['items']]
    # parse all entries
    in_entries = [get_entry(e) for e in d['items']]

    # Meta element
    meta = dict()
    @@ -84,20 +111,21 @@ def main():
    in_folders = d['folders']
    out_folders = []
    for f in in_folders:
    # parse the folder
    folder = get_folder(f)
    folder_id = f['id']

    # loop on entries in this folder
    out_entries = []
    for entry, item in zip(entries, d['items']):
    entries = []
    for entry, item in zip(in_entries, d['items']):
    if item['folderId'] == folder_id:
    out_entries.append(entry)
    entries.append(entry)

    # add if there is something
    if len(out_entries) > 0:
    folder['Entry'] = out_entries
    if len(entries) > 0:
    folder['Entry'] = entries

    # add to output
    # add to output folder
    out_folders.append(folder)

    # Root group
    @@ -108,8 +136,10 @@ def main():
    # Root element
    root=dict(Group=root_group)

    # xml contents
    # xml document contents
    xml = dict(KeePassFile=dict(Meta=meta, Root=root))

    # write XML document to stdout
    print(xmltodict.unparse(xml, pretty=True))

    if __name__ == "__main__":
  13. @mohamedadaly mohamedadaly created this gist Nov 7, 2018.
    116 changes: 116 additions & 0 deletions bw_export_kp.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,116 @@
    #!/usr/bin/python

    from __future__ import print_function
    import xmltodict
    import json
    import sys
    import commands
    import uuid
    import base64

    def get_uuid(name):
    name = name.encode('ascii', 'ignore')
    uid = uuid.uuid5(uuid.NAMESPACE_DNS, name)
    return base64.b64encode(uid.bytes)



    def get_folder(f):
    return dict(UUID=get_uuid(f['name']),
    Name=f['name'])


    def get_protected_value(v):
    return {'#text': v, '@ProtectInMemory': 'True'}

    def get_entry(e):
    fields = []
    if 'fields' in e:
    for f in e['fields']:
    if f['name'] is not None:
    fields.append(dict(Key=f['name'],
    Value=get_protected_value(f['value'])))

    urls = ''
    username, password = '', ''
    notes = e['notes'] if e['notes'] is not None else ''

    if 'login' in e:
    if 'uris' in e['login']:
    urls = [u['uri'] for u in e['login']['uris']]
    urls = ','.join(urls)


    username = e['login']['username']
    password = e['login']['password']

    username = username or ''
    password = password or ''

    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)

    # print(entry)
    return entry

    def main():
    # get output from bw
    cmd = "(bw list folders | jq '{folders: .}' ; bw list items | jq '{items: .}') | cat | jq -s '. | add'"
    status, output = commands.getstatusoutput(cmd)
    # read stdin
    # stdin = sys.stdin.read()
    if status != 0:
    print("Error ...")
    print(output)
    sys.exit(1)

    # parse input to json
    d = json.loads(output)
    #print(d['folders'][0]['name'])
    # print(d['items'][0])
    # print(get_entry(d['items'][0]))

    entries = [get_entry(e) for e in d['items']]

    # Meta element
    meta = dict()

    # loop over folders
    in_folders = d['folders']
    out_folders = []
    for f in in_folders:
    folder = get_folder(f)
    folder_id = f['id']

    # loop on entries in this folder
    out_entries = []
    for entry, item in zip(entries, d['items']):
    if item['folderId'] == folder_id:
    out_entries.append(entry)

    # add if there is something
    if len(out_entries) > 0:
    folder['Entry'] = out_entries

    # add to output
    out_folders.append(folder)

    # Root group
    root_group = get_folder(dict(name='Root'))
    root_group['Group'] = out_folders
    # root_group['Entry'] = entries

    # Root element
    root=dict(Group=root_group)

    # xml contents
    xml = dict(KeePassFile=dict(Meta=meta, Root=root))
    print(xmltodict.unparse(xml, pretty=True))

    if __name__ == "__main__":
    main()