Last active
March 9, 2018 02:10
-
-
Save jamesnvc/99c168b158259d7cc1e3c7c5f3529878 to your computer and use it in GitHub Desktop.
Enhancement of the default script to also include OTP keys
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Copyright (C) 2014 Tobias V. Langhoff <[email protected]>. All Rights Reserved. | |
# This file is licensed under GPLv2+. Please see COPYING for more information. | |
# | |
# 1Password Importer | |
# | |
# Reads files exported from 1Password and imports them into pass. Supports comma | |
# and tab delimited text files, as well as logins (but not other items) stored | |
# in the 1Password Interchange File (1PIF) format. | |
# | |
# Supports using the title (default) or URL as pass-name, depending on your | |
# preferred organization. Also supports importing metadata, adding them with | |
# `pass insert --multiline`; the username and URL are compatible with | |
# https://github.com/jvenant/passff. | |
require "optparse" | |
require "ostruct" | |
accepted_formats = [".txt", ".1pif"] | |
# Default options | |
options = OpenStruct.new | |
options.force = false | |
options.name = :title | |
options.notes = true | |
options.meta = true | |
optparse = OptionParser.new do |opts| | |
opts.banner = "Usage: #{opts.program_name}.rb [options] filename" | |
opts.on_tail("-h", "--help", "Display this screen") { puts opts; exit } | |
opts.on("-f", "--force", "Overwrite existing passwords") do | |
options.force = true | |
end | |
opts.on("-d", "--default [FOLDER]", "Place passwords into FOLDER") do |group| | |
options.group = group | |
end | |
opts.on("-n", "--name [PASS-NAME]", [:title, :url], | |
"Select field to use as pass-name: title (default) or URL") do |name| | |
options.name = name | |
end | |
opts.on("-m", "--[no-]meta", | |
"Import metadata and insert it below the password") do |meta| | |
options.meta = meta | |
end | |
begin | |
opts.parse! | |
rescue OptionParser::InvalidOption | |
$stderr.puts optparse | |
exit | |
end | |
end | |
# Check for a valid filename | |
filename = ARGV.pop | |
unless filename | |
abort optparse.to_s | |
end | |
unless accepted_formats.include?(File.extname(filename.downcase)) | |
abort "Supported file types: comma/tab delimited .txt files and .1pif files." | |
end | |
passwords = [] | |
# Parse comma or tab delimited text | |
if File.extname(filename) =~ /.txt/i | |
require "csv" | |
# Very simple way to guess the delimiter | |
delimiter = "" | |
File.open(filename) do |file| | |
first_line = file.readline | |
if first_line =~ /,/ | |
delimiter = "," | |
elsif first_line =~ /\t/ | |
delimiter = "\t" | |
else | |
abort "Supported file types: comma/tab delimited .txt files and .1pif files." | |
end | |
end | |
# Import CSV/TSV | |
CSV.foreach(filename, {col_sep: delimiter, headers: true, header_converters: :symbol}) do |entry| | |
pass = {} | |
pass[:name] = "#{(options.group + "/") if options.group}#{entry[options.name]}" | |
pass[:title] = entry[:title] | |
pass[:password] = entry[:password] | |
pass[:login] = entry[:username] | |
pass[:url] = entry[:url] | |
pass[:notes] = entry[:notes] | |
passwords << pass | |
end | |
# Parse 1PIF | |
elsif File.extname(filename) =~ /.1pif/i | |
require "json" | |
options.name = :location if options.name == :url | |
# 1PIF is almost JSON, but not quite. Remove the ***...*** lines | |
# separating records, and then remove the trailing comma | |
pif = File.open(filename).read.gsub(/^\*\*\*.*\*\*\*$/, ",").chomp.chomp(",") | |
# Import 1PIF | |
JSON.parse("[#{pif}]", symbolize_names: true).each do |entry| | |
next unless entry[:typeName] == "webforms.WebForm" | |
next if entry[:secureContents][:fields].nil? | |
next if entry[:secureContents][:fields].empty? | |
pass = {} | |
pass[:name] = "#{(options.group + "/") if options.group}#{entry[options.name]}" | |
pass[:title] = entry[:title] | |
pass[:password] = entry[:secureContents][:fields].detect do |field| | |
field[:designation] == "password" | |
end[:value] | |
username = entry[:secureContents][:fields].detect do |field| | |
field[:designation] == "username" | |
end | |
# might be nil | |
pass[:login] = username[:value] if username | |
if !entry[:secureContents][:sections].nil? | |
fieldSec = entry[:secureContents][:sections].detect do |section| | |
!section[:fields].nil? | |
end | |
if !fieldSec.nil? | |
otp = fieldSec[:fields].detect do |field| | |
field[:v].start_with? "otpauth://" | |
end | |
pass[:otp] = otp[:v] if otp | |
end | |
end | |
pass[:url] = entry[:location] | |
pass[:notes] = entry[:secureContents][:notesPlain] | |
passwords << pass | |
end | |
end | |
puts "Read #{passwords.length} passwords." | |
errors = [] | |
# Save the passwords | |
passwords.each do |pass| | |
IO.popen("pass insert #{"-f " if options.force}-m \"#{pass[:name]}\" > /dev/null", "w") do |io| | |
io.puts pass[:password] | |
if options.meta | |
io.puts "user: #{pass[:login]}" unless pass[:login].to_s.empty? | |
io.puts "url: #{pass[:url]}" unless pass[:url].to_s.empty? | |
io.puts pass[:notes] unless pass[:notes].to_s.empty? | |
io.puts pass[:otp] unless pass[:otp].nil? | |
end | |
end | |
if $? == 0 | |
puts "Imported #{pass[:name]}" | |
else | |
$stderr.puts "ERROR: Failed to import #{pass[:name]}" | |
errors << pass | |
end | |
end | |
if errors.length > 0 | |
$stderr.puts "Failed to import #{errors.map {|e| e[:name]}.join ", "}" | |
$stderr.puts "Check the errors. Make sure these passwords do not already "\ | |
"exist. If you're sure you want to overwrite them with the "\ | |
"new import, try again with --force." | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment