#!/usr/bin/env ruby # # Copyright (c) 2017 joshua stein # # 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 1Password Interchange Format (1pif) 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 1Password password 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 'uri' def usage puts "usage: #{$0} -f data.1pif -u user@example.com" exit 1 end username = nil file = nil 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) def encrypt(str) @u.encrypt_data_with_master_password_key(str, @master_key).to_s end to_save = {} skipped = 0 def save_field(cdata, field) field['v'] = field['v'].to_s if field['v'] return if field['value'].blank? && field['v'].blank? field['t'] = 'unnamed field' if field['t'].blank? case field['designation'] when 'username' cdata["Username"] = encrypt(field['value']) return when 'password' @current_password = field['value'] cdata['Password'] = encrypt(field['value']) return end case field['k'] when 'string' cdata['Fields'].push("Type" => 0, "Name" => encrypt(field['t']), "Value" => encrypt(field['v'])) when 'concealed' if field['n'] =~ /^TOTP/ totp_secret = if field['v'] =~ %r{^otpauth://} URI.decode_www_form(URI.parse(field['v']).query).assoc('secret').last else field['v'] end cdata['Totp'] = encrypt(totp_secret) else # some other password return if field['v'] == @current_password cdata['Fields'].push("Type" => 1, "Name" => encrypt(field['t']), "Value" => encrypt(field['v'])) end end end File.read(file).split("\n").each do |line| next if line[0] != "{" i = JSON.parse(line) c = Cipher.new c.user_uuid = @u.uuid c.type = Cipher::TYPE_LOGIN c.favorite = !!(i["openContents"] && i["openContents"]["faveIndex"]) cdata = { "Name" => encrypt(i["title"].blank? ? "--" : i["title"]), } if i["createdAt"] c.created_at = Time.at(i["createdAt"].to_i) end if i["updatedAt"] c.updated_at = Time.at(i["updatedAt"].to_i) end case i["typeName"] when "passwords.Password" if i["location"].present? cdata["Uri"] = encrypt(i["location"]) end when "securenotes.SecureNote" c.type = Cipher::TYPE_NOTE cdata["SecureNote"] = { "Type" => 0 } when "wallet.computer.Router" next if i["secureContents"]["wireless_password"].nil? cdata["Password"] = encrypt(i["secureContents"]["wireless_password"]) when "wallet.financial.CreditCard" c.type = Cipher::TYPE_CARD if i["secureContents"]["cardholder"].present? cdata["CardholderName"] = encrypt(i["secureContents"]["cardholder"]) end if i["secureContents"]["type"].present? cdata["Brand"] = encrypt(i["secureContents"]["type"]) end if i["secureContents"]["ccnum"].present? cdata["Number"] = encrypt(i["secureContents"]["ccnum"]) end if i["secureContents"]["expiry_mm"].present? cdata["ExpMonth"] = encrypt(i["secureContents"]["expiry_mm"]) end if i["secureContents"]["expiry_yy"].present? cdata["ExpYear"] = encrypt(i["secureContents"]["expiry_yy"]) end if i["secureContents"]["cvv"].present? cdata["Code"] = encrypt(i["secureContents"]["cvv"]) end when "webforms.WebForm" if i["location"].present? cdata["Uri"] = encrypt(i["location"]) end when "identities.Identity", "system.folder.Regular", "system.folder.SavedSearch", "wallet.computer.License", "wallet.computer.UnixServer", "wallet.computer.License", "wallet.government.SsnUS", "wallet.government.Passport", "wallet.financial.BankAccountUS", "wallet.government.DriversLicense", "wallet.membership.Membership", "wallet.onlineservices.Email.v2", "wallet.computer.Database" puts "skipping #{i["typeName"]} #{i["title"]}" skipped += 1 next else raise "unimplemented: #{i["typeName"].inspect}" end puts "converting #{Cipher.type_s(c.type)} #{i["title"]}... " if i["secureContents"] @current_password = nil if i["secureContents"]["notesPlain"].present? cdata["Notes"] = encrypt(i["secureContents"]["notesPlain"]) end if i["secureContents"]["password"].present? @current_password = cdata["Password"] = encrypt(i["secureContents"]["password"]) end cdata["Fields"] = [] (i["secureContents"]["fields"] || []).each do |field| save_field(cdata, field) end (i['secureContents']['sections'] || []).map { |x| x['fields'] }.compact.flatten.each do |field| save_field(cdata, field) 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 |_, 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"}"