An unofficial, mostly Bitwarden-compatible API server written in Ruby (Sinatra and ActiveRecord)
at master 202 lines 5.2 kB view raw
1#!/usr/bin/env ruby 2# 3# Copyright (c) 2018 joshua stein <jcs@jcs.org> 4# Keepass importer by Martin Gross <martin@pc-coholic.de> 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 Keepass kdbx file, optional keyfile, ask for the given user's 21# master password, then lookup the given user in the bitwarden-ruby SQLite 22# database and fetch its key. Each Keepass password entry is encrypted and 23# inserted into the database. 24# 25# No check is done to eliminate duplicates, so this is best used on a fresh 26# bitwarden-ruby installation after creating a new account. 27# 28 29require File.realpath(File.dirname(__FILE__) + "/../lib/rubywarden.rb") 30require "getoptlong" 31require "rubeepass" 32 33def usage 34 puts "usage: #{$0} -f example.kdb [-k keyfile] -u user@example.com" 35 exit 1 36end 37 38username = nil 39file = nil 40keyfile = nil 41@folders = {} 42 43begin 44 GetoptLong.new( 45 [ "--file", "-f", GetoptLong::REQUIRED_ARGUMENT ], 46 [ "--keyfile", "-k", GetoptLong::OPTIONAL_ARGUMENT ], 47 [ "--user", "-u", GetoptLong::REQUIRED_ARGUMENT ], 48 ).each do |opt,arg| 49 case opt 50 when "--file" 51 file = arg 52 53 when "--user" 54 username = arg 55 56 when "--keyfile" 57 keyfile = arg 58 end 59 end 60 61rescue GetoptLong::InvalidOption 62 usage 63end 64 65if !file || !username 66 usage 67end 68 69@u = User.find_by_email(username) 70if !@u 71 raise "can't find existing User record for #{username.inspect}" 72end 73 74print "master password for #{@u.email}: " 75system("stty -echo") if STDIN.tty? 76password = STDIN.gets.chomp 77system("stty echo") if STDIN.tty? 78print "\n" 79 80unless @u.has_password_hash?(Bitwarden.hashPassword(password, @u.email, 81Bitwarden::KDF::TYPES[@u.kdf_type], @u.kdf_iterations)) 82 raise "master password does not match stored hash" 83end 84 85@master_key = Bitwarden.makeKey(password, @u.email, 86 Bitwarden::KDF::TYPES[@u.kdf_type], @u.kdf_iterations) 87 88@u.folders.each do |folder| 89 folder_name = @u.decrypt_data_with_master_password_key(folder.name, @master_key) 90 @folders[folder_name] = folder.uuid 91end 92 93def encrypt(str) 94 @u.encrypt_data_with_master_password_key(str, @master_key).to_s 95end 96 97def get_or_create_folder_uuid(str) 98 return @folders[str] if @folders.key? str 99 100 f = Folder.new 101 f.user_uuid = @u.uuid 102 f.name = encrypt(str).to_s 103 104 Folder.transaction do 105 return validation_error('error creating folder') unless f.save 106 end 107 108 @folders[str] = f.uuid 109 f.uuid 110end 111 112@to_save = {} 113 114print "master password for #{file}: " 115system("stty -echo") 116keepasspass = STDIN.gets.chomp 117system("stty echo") 118print "\n" 119 120@keepass = RubeePass.new(file, keepasspass, keyfile).open 121@db = @keepass.db 122 123def getEntries(db) 124 if db.entries.any? 125 db.entries.each do |entry| 126 c = Cipher.new 127 c.user_uuid = @u.uuid 128 c.type = Cipher::TYPE_LOGIN 129 130 cdata = { 131 "Name" => encrypt(entry[1].title.blank? ? "--" : entry[1].title), 132 } 133 134 puts "converting #{Cipher.type_s(c.type)} #{entry[1].title}... " 135 136 if entry[1].group.path != "/" 137 c.folder_uuid = get_or_create_folder_uuid(entry[1].group.path[1..-1]) 138 end 139 140 cdata['Uri'] = encrypt(entry[1].url) if entry[1].url.present? 141 cdata['Username'] = encrypt(entry[1].username) if entry[1].username.present? 142 cdata['Password'] = encrypt(entry[1].password) if entry[1].password.present? 143 cdata['Notes'] = encrypt(entry[1].notes) if entry[1].notes.present? 144 145 if entry[1].attachments.any? 146 puts "This entry has an attachment - but it won't be converted as rubywarden does not support attachments yet." 147 end 148 149 if entry[1].additional_attributes.any? 150 cdata['Fields'] = [] 151 entry[1].additional_attributes.each_pair do |k, v| 152 cdata['Fields'].push( 153 'Type' => 0, # 0 = text, 1 = hidden, 2 = boolean 154 'Name' => encrypt(k), 155 'Value' => encrypt(v) 156 ) 157 end 158 end 159 160 c.data = cdata.to_json 161 162 @to_save[c.type] ||= [] 163 @to_save[c.type].push c 164 165 next 166 end 167 end 168 if db.groups.any? 169 db.groups.each do |group| 170 getEntries(group[1]) 171 next 172 end 173 end 174end 175 176@to_save.each do |k,v| 177 puts "#{sprintf("% 4d", v.count)} #{Cipher.type_s(k)}" << 178 (v.count == 1 ? "" : "s") 179end 180 181puts "" 182 183getEntries(@db) 184 185print "ready to import? [Y/n] " 186if STDIN.gets.to_s.match(/n/i) 187 exit 1 188end 189 190imp = 0 191Cipher.transaction do 192 @to_save.each do |_, v| 193 v.each do |c| 194 # TODO: convert data to each field natively 195 c.migrate_data! 196 197 imp += 1 198 end 199 end 200end 201 202puts "successfully imported #{imp} item#{imp == 1 ? "" : "s"}"