#!/usr/bin/env ruby # # Copyright (c) 2017 joshua stein # LastPass importer by Simon Cantem # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # # # Read a given LastPass CSV file, ask for the given user's master password, # then lookup the given user in the bitwarden-ruby SQLite database and # fetch its key. Each LastPass password & secure note entry is encrypted # and inserted into the database. # # No check is done to eliminate duplicates, so this is best used on a fresh # bitwarden-ruby installation after creating a new account. # require File.realpath(File.dirname(__FILE__) + "/../lib/rubywarden.rb") require "getoptlong" require "csv" def usage puts "usage: #{$0} -f data.csv -u user@example.com" exit 1 end username = nil file = nil @folders = {} begin GetoptLong.new( [ "--file", "-f", GetoptLong::REQUIRED_ARGUMENT ], [ "--user", "-u", GetoptLong::REQUIRED_ARGUMENT ], ).each do |opt,arg| case opt when "--file" file = arg when "--user" username = arg end end rescue GetoptLong::InvalidOption usage end if !file || !username usage end @u = User.find_by_email(username) if !@u raise "can't find existing User record for #{username.inspect}" end print "master password for #{@u.email}: " system("stty -echo") if STDIN.tty? password = STDIN.gets.chomp system("stty echo") if STDIN.tty? print "\n" unless @u.has_password_hash?(Bitwarden.hashPassword(password, @u.email, Bitwarden::KDF::TYPES[@u.kdf_type], @u.kdf_iterations)) raise "master password does not match stored hash" end @master_key = Bitwarden.makeKey(password, @u.email, Bitwarden::KDF::TYPES[@u.kdf_type], @u.kdf_iterations) @u.folders.each do |folder| folder_name = @u.decrypt_data_with_master_password_key(folder.name, @master_key) @folders[folder_name] = folder.uuid end def encrypt(str) @u.encrypt_data_with_master_password_key(str, @master_key).to_s end def get_or_create_folder_uuid(str) if @folders.has_key? str return @folders[str] end f = Folder.new f.user_uuid = @u.uuid f.name = encrypt(str).to_s Folder.transaction do if !f.save return validation_error("error creating folder") end end @folders[str] = f.uuid return f.uuid end to_save = {} skipped = 0 CSV.foreach(file, headers: true) do |row| next if row["name"].blank? puts "converting #{row["name"]}..." c = Cipher.new c.user_uuid = @u.uuid c.type = Cipher::TYPE_LOGIN c.favorite = (row["fav"].to_i == 1) cdata = { "Name" => encrypt(row["name"]), } if !row["grouping"].blank? c.folder_uuid = get_or_create_folder_uuid(row["grouping"]) end # http://sn means it's a secure note if row["url"] == "http://sn" c.type = Cipher::TYPE_NOTE cdata["SecureNote"] = { "Type" => 0 } if !row["extra"].blank? cdata["Notes"] = encrypt(row["extra"]) end else if !row["url"].blank? cdata["Uri"] = encrypt(row["url"]) end if !row["password"].blank? cdata["Password"] = encrypt(row["password"]) end if !row["username"].blank? cdata["Username"] = encrypt(row["username"]) end if !row["extra"].blank? cdata["Notes"] = encrypt(row["extra"]) end end c.data = cdata.to_json to_save[c.type] ||= [] to_save[c.type].push c end puts "" to_save.each do |k,v| puts "#{sprintf("% 4d", v.count)} #{Cipher.type_s(k)}" << (v.count == 1 ? "" : "s") end if skipped > 0 puts "#{sprintf("% 4d", skipped)} skipped" end print "ready to import? [Y/n] " if STDIN.gets.to_s.match(/n/i) exit 1 end imp = 0 Cipher.transaction do to_save.each do |k,v| v.each do |c| # TODO: convert data to each field natively and call save! on our own c.migrate_data! imp += 1 end end end puts "successfully imported #{imp} item#{imp == 1 ? "" : "s"}"