Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
267 lines (220 sloc) 7.02 KB
#!/usr/bin/env ruby
#
# Copyright (c) 2017 joshua stein <jcs@jcs.org>
#
# 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"}"