An unofficial, mostly Bitwarden-compatible API server written in Ruby (Sinatra and ActiveRecord)
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