An unofficial, mostly Bitwarden-compatible API server written in Ruby (Sinatra and ActiveRecord)
at master 135 lines 3.6 kB view raw
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 28require File.realpath(File.dirname(__FILE__) + '/../lib/rubywarden.rb') 29 30require 'csv' 31require 'getoptlong' 32 33def usage 34 puts "usage: #{$PROGRAM_NAME} -f data.csv -u user@example.com" 35 exit 1 36end 37 38def encrypt(str) 39 @u.encrypt_data_with_master_password_key(str, @master_key).to_s 40end 41 42username = nil 43file = nil 44@folders = {} 45 46begin 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 58rescue GetoptLong::InvalidOption 59 usage 60end 61 62usage unless file && username 63 64@u = User.find_by_email(username) 65raise "can't find existing User record for #{username.inspect}" unless @u 66 67print "master password for #{@u.email}: " 68system('stty -echo') if STDIN.tty? 69password = STDIN.gets.chomp 70system('stty echo') if STDIN.tty? 71puts 72 73unless @u.has_password_hash?(Bitwarden.hashPassword(password, @u.email, 74Bitwarden::KDF::TYPES[@u.kdf_type], @u.kdf_iterations)) 75 raise "master password does not match stored hash" 76end 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 84end 85 86to_save = {} 87skipped = 0 88 89CSV.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 107end 108 109puts 110 111to_save.each do |k, v| 112 puts "#{format('% 4d', v.count)} #{Cipher.type_s(k)}" << 113 (v.count == 1 ? '' : 's') 114end 115 116puts "#{format('% 4d', skipped)} skipped" if skipped > 0 117 118print 'ready to import? [Y/n] ' 119exit 1 if STDIN.gets =~ /n/i 120 121imp = 0 122Cipher.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 131end 132 133puts "successfully imported #{imp} item#{imp == 1 ? '' : 's'}" 134 135# EOF