[pass] [PATCH] Importer for 1Password
Tobias V. Langhoff
tobias at langhoff.no
Sun Apr 13 14:58:44 CEST 2014
Hey, I wrote an importer script for 1Password. It supports 1Password's text exports
(CSV or TSV) and its 1PIF file format (pseudo-JSON). In addition to the passwords
it imports notes, as well as the username and URL which it stores in passff-
compatible format (it can also use either the title or the URL itself as pass-name).
Great work on pass and congrats on 1.5; I hope it'll get a lot of new users in
these Heartbleed times :)
---
contrib/importers/1password2pass.rb | 149 ++++++++++++++++++++++++++++++++++++
1 file changed, 149 insertions(+)
create mode 100755 contrib/importers/1password2pass.rb
diff --git a/contrib/importers/1password2pass.rb b/contrib/importers/1password2pass.rb
new file mode 100755
index 0000000..d26d0ef
--- /dev/null
+++ b/contrib/importers/1password2pass.rb
@@ -0,0 +1,149 @@
+#!/usr/bin/env ruby
+
+# Copyright (C) 2014 Tobias V. Langhoff <tobias at langhoff.no>. 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"
+
+ # 1PIF is almost JSON, but not quite
+ pif = "[#{File.open(filename).read}]"
+ pif.gsub!(/^\*\*\*.*\*\*\*$/, ",")
+ pif = JSON.parse(pif, {symbolize_names: true})
+
+ options.name = :location if options.name == :url
+
+ # Import 1PIF
+ pif.each do |entry|
+ next unless entry[:typeName] == "webforms.WebForm"
+ pass = {}
+ pass[:name] = "#{(options.group + "/") if options.group}#{entry[options.name]}"
+ pass[:title] = entry[:title]
+ pass[:password] = entry[:secureContents][:fields].detect do |field|
+ field[:name] == "password"
+ end[:value]
+ pass[:login] = entry[:secureContents][:fields].detect do |field|
+ field[:name] == "username"
+ end[:value]
+ 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 "login: #{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?
+ 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
--
Tobias V. Langhoff
More information about the Password-Store
mailing list