password-store

Simple password manager using gpg and ordinary unix directories
git clone https://git.zx2c4.com/password-store
Log | Files | Refs | README | LICENSE

1password2pass.rb (4759B)


      1 #!/usr/bin/env ruby
      2 
      3 # Copyright (C) 2014 Tobias V. Langhoff <tobias@langhoff.no>. All Rights Reserved.
      4 # This file is licensed under GPLv2+. Please see COPYING for more information.
      5 #
      6 # 1Password Importer
      7 #
      8 # Reads files exported from 1Password and imports them into pass. Supports comma
      9 # and tab delimited text files, as well as logins (but not other items) stored
     10 # in the 1Password Interchange File (1PIF) format.
     11 #
     12 # Supports using the title (default) or URL as pass-name, depending on your
     13 # preferred organization. Also supports importing metadata, adding them with
     14 # `pass insert --multiline`; the username and URL are compatible with
     15 # https://github.com/jvenant/passff.
     16 
     17 require "optparse"
     18 require "ostruct"
     19 
     20 accepted_formats = [".txt", ".1pif"]
     21 
     22 # Default options
     23 options = OpenStruct.new
     24 options.force = false
     25 options.name = :title
     26 options.notes = true
     27 options.meta = true
     28 
     29 optparse = OptionParser.new do |opts|
     30   opts.banner = "Usage: #{opts.program_name}.rb [options] filename"
     31   opts.on_tail("-h", "--help", "Display this screen") { puts opts; exit }
     32   opts.on("-f", "--force", "Overwrite existing passwords") do
     33     options.force = true
     34   end
     35   opts.on("-d", "--default [FOLDER]", "Place passwords into FOLDER") do |group|
     36     options.group = group
     37   end
     38   opts.on("-n", "--name [PASS-NAME]", [:title, :url],
     39           "Select field to use as pass-name: title (default) or URL") do |name|
     40     options.name = name
     41   end
     42   opts.on("-m", "--[no-]meta",
     43           "Import metadata and insert it below the password") do |meta|
     44     options.meta = meta
     45   end
     46 
     47   begin
     48     opts.parse!
     49   rescue OptionParser::InvalidOption
     50     $stderr.puts optparse
     51     exit
     52   end
     53 end
     54 
     55 # Check for a valid filename
     56 filename = ARGV.pop
     57 unless filename
     58   abort optparse.to_s
     59 end
     60 unless accepted_formats.include?(File.extname(filename.downcase))
     61   abort "Supported file types: comma/tab delimited .txt files and .1pif files."
     62 end
     63 
     64 passwords = []
     65 
     66 # Parse comma or tab delimited text
     67 if File.extname(filename) =~ /.txt/i
     68   require "csv"
     69 
     70   # Very simple way to guess the delimiter
     71   delimiter = ""
     72   File.open(filename) do |file|
     73     first_line = file.readline
     74     if first_line =~ /,/
     75       delimiter = ","
     76     elsif first_line =~ /\t/
     77       delimiter = "\t"
     78     else
     79       abort "Supported file types: comma/tab delimited .txt files and .1pif files."
     80     end
     81   end
     82 
     83   # Import CSV/TSV
     84   CSV.foreach(filename, {col_sep: delimiter, headers: true, header_converters: :symbol}) do |entry|
     85     pass = {}
     86     pass[:name] = "#{(options.group + "/") if options.group}#{entry[options.name]}"
     87     pass[:title] = entry[:title]
     88     pass[:password] = entry[:password]
     89     pass[:login] = entry[:username]
     90     pass[:url] = entry[:url]
     91     pass[:notes] = entry[:notes]
     92     passwords << pass
     93   end
     94 # Parse 1PIF
     95 elsif File.extname(filename) =~ /.1pif/i
     96   require "json"
     97 
     98   options.name = :location if options.name == :url
     99 
    100   # 1PIF is almost JSON, but not quite.  Remove the ***...*** lines
    101   # separating records, and then remove the trailing comma
    102   pif = File.open(filename).read.gsub(/^\*\*\*.*\*\*\*$/, ",").chomp.chomp(",")
    103 
    104   # Import 1PIF
    105   JSON.parse("[#{pif}]", symbolize_names: true).each do |entry|
    106     next unless entry[:typeName] == "webforms.WebForm"
    107     next if entry[:secureContents][:fields].nil?
    108 
    109     pass = {}
    110 
    111     pass[:name] = "#{(options.group + "/") if options.group}#{entry[options.name]}"
    112 
    113     pass[:title] = entry[:title]
    114 
    115     pass[:password] = entry[:secureContents][:fields].detect do |field|
    116       field[:designation] == "password"
    117     end[:value]
    118 
    119     username = entry[:secureContents][:fields].detect do |field|
    120       field[:designation] == "username"
    121     end
    122     # might be nil
    123     pass[:login] = username[:value] if username
    124 
    125     pass[:url] = entry[:location]
    126     pass[:notes] = entry[:secureContents][:notesPlain]
    127     passwords << pass
    128   end
    129 end
    130 
    131 puts "Read #{passwords.length} passwords."
    132 
    133 errors = []
    134 # Save the passwords
    135 passwords.each do |pass|
    136   IO.popen("pass insert #{"-f " if options.force}-m \"#{pass[:name]}\" > /dev/null", "w") do |io|
    137     io.puts pass[:password]
    138     if options.meta
    139       io.puts "login: #{pass[:login]}" unless pass[:login].to_s.empty?
    140       io.puts "url: #{pass[:url]}" unless pass[:url].to_s.empty?
    141       io.puts pass[:notes] unless pass[:notes].to_s.empty?
    142     end
    143   end
    144   if $? == 0
    145     puts "Imported #{pass[:name]}"
    146   else
    147     $stderr.puts "ERROR: Failed to import #{pass[:name]}"
    148     errors << pass
    149   end
    150 end
    151 
    152 if errors.length > 0
    153   $stderr.puts "Failed to import #{errors.map {|e| e[:name]}.join ", "}"
    154   $stderr.puts "Check the errors. Make sure these passwords do not already "\
    155                "exist. If you're sure you want to overwrite them with the "\
    156                "new import, try again with --force."
    157 end