An unofficial, mostly Bitwarden-compatible API server written in Ruby (Sinatra and ActiveRecord)

Add Chrome import tool (#109)

authored by

Haluk ÜNAL and committed by
GitHub
8ded1067 881815fd

+140
+5
spec/fixtures/chrome_export.csv
··· 1 + name,url,username,password 2 + cloud.digitalocean.com,https://cloud.digitalocean.com/login,root,mySuper.Password1 3 + m.facebook.com,https://m.facebook.com/,admin,mySuper?Password2 4 + github.com,https://github.com/login,supervisor,mySuper%Password3 5 + gitlab.com,https://gitlab.com/users/sign_in,moderator,mySuper-Password4
+135
tools/chrome_import.rb
··· 1 + #!/usr/bin/env ruby 2 + # 3 + # Copyright (c) 2017 joshua stein <jcs@jcs.org> 4 + # Chrome importer by Haluk Unal <admin@halukunal.com> 5 + # 6 + # Permission to use, copy, modify, and distribute this software for any 7 + # purpose with or without fee is hereby granted, provided that the above 8 + # copyright notice and this permission notice appear in all copies. 9 + # 10 + # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 + # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 + # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 + # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 + # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 + # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 + # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 + # 18 + 19 + # 20 + # Read a given Chrome CSV file, ask for the given user's master password, 21 + # then lookup the given user in the bitwarden-ruby SQLite database and 22 + # fetch its key. Import each entry into the bitwarden-ruby database. 23 + # 24 + # No check is done to eliminate duplicates, so this is best used on a fresh 25 + # bitwarden-ruby installation after creating a new account. 26 + # 27 + 28 + require File.realpath(File.dirname(__FILE__) + '/../lib/rubywarden.rb') 29 + 30 + require 'csv' 31 + require 'getoptlong' 32 + 33 + def usage 34 + puts "usage: #{$PROGRAM_NAME} -f data.csv -u user@example.com" 35 + exit 1 36 + end 37 + 38 + def encrypt(str) 39 + @u.encrypt_data_with_master_password_key(str, @master_key).to_s 40 + end 41 + 42 + username = nil 43 + file = nil 44 + @folders = {} 45 + 46 + begin 47 + GetoptLong.new( 48 + ['--file', '-f', GetoptLong::REQUIRED_ARGUMENT], 49 + ['--user', '-u', GetoptLong::REQUIRED_ARGUMENT] 50 + ).each do |opt, arg| 51 + case opt 52 + when '--file' 53 + file = arg 54 + when '--user' 55 + username = arg 56 + end 57 + end 58 + rescue GetoptLong::InvalidOption 59 + usage 60 + end 61 + 62 + usage unless file && username 63 + 64 + @u = User.find_by_email(username) 65 + raise "can't find existing User record for #{username.inspect}" unless @u 66 + 67 + print "master password for #{@u.email}: " 68 + system('stty -echo') if STDIN.tty? 69 + password = STDIN.gets.chomp 70 + system('stty echo') if STDIN.tty? 71 + puts 72 + 73 + unless @u.has_password_hash?(Bitwarden.hashPassword(password, @u.email, 74 + Bitwarden::KDF::TYPES[@u.kdf_type], @u.kdf_iterations)) 75 + raise "master password does not match stored hash" 76 + end 77 + 78 + @master_key = Bitwarden.makeKey(password, @u.email, 79 + Bitwarden::KDF::TYPES[@u.kdf_type], @u.kdf_iterations) 80 + 81 + @u.folders.each do |folder| 82 + folder_name = @u.decrypt_data_with_master_password_key(folder.name, @master_key) 83 + @folders[folder_name] = folder.uuid 84 + end 85 + 86 + to_save = {} 87 + skipped = 0 88 + 89 + CSV.foreach(file, headers: true) do |row| 90 + next if row['name'].blank? 91 + 92 + puts "converting #{row['name']}..." 93 + 94 + c = Cipher.new 95 + c.user_uuid = @u.uuid 96 + c.type = Cipher::TYPE_LOGIN 97 + 98 + cdata = { 'Name' => encrypt(row['name']) } 99 + cdata['Uri'] = encrypt(row['url']) if row['url'].present? 100 + cdata['Username'] = encrypt(row['username']) if row['username'].present? 101 + cdata['Password'] = encrypt(row['password']) if row['password'].present? 102 + 103 + c.data = cdata.to_json 104 + 105 + to_save[c.type] ||= [] 106 + to_save[c.type].push c 107 + end 108 + 109 + puts 110 + 111 + to_save.each do |k, v| 112 + puts "#{format('% 4d', v.count)} #{Cipher.type_s(k)}" << 113 + (v.count == 1 ? '' : 's') 114 + end 115 + 116 + puts "#{format('% 4d', skipped)} skipped" if skipped > 0 117 + 118 + print 'ready to import? [Y/n] ' 119 + exit 1 if STDIN.gets =~ /n/i 120 + 121 + imp = 0 122 + Cipher.transaction do 123 + to_save.each_value do |v| 124 + v.each do |c| 125 + # TODO: convert data to each field natively and call save! on our own 126 + c.migrate_data! 127 + 128 + imp += 1 129 + end 130 + end 131 + end 132 + 133 + puts "successfully imported #{imp} item#{imp == 1 ? '' : 's'}" 134 + 135 + # EOF