Skip to content
Snippets Groups Projects
lastpass2pass.rb 3.42 KiB
Newer Older
  • Learn to ignore specific revisions
  • Alex Sayers's avatar
    Alex Sayers committed
    # Copyright (C) 2012 Alex Sayers <alex.sayers@gmail.com>. All Rights Reserved.
    # This file is licensed under the GPLv2+. Please see COPYING for more information.
    
    
    # LastPass Importer
    #
    # Reads CSV files exported from LastPass and imports them into pass.
    
    # Usage:
    #
    # Go to lastpass.com and sign in. Next click on your username in the top-right
    # corner. In the drop-down meny that appears, click "Export". After filling in
    # your details again, copy the text and save it somewhere on your disk. Make sure
    # you copy the whole thing, and resist the temptation to "Save Page As" - the
    # script doesn't like HTML.
    #
    # Fire up a terminal and run the script, passing the file you saved as an argument.
    # It should look something like this:
    #
    
    Alex Sayers's avatar
    Alex Sayers committed
    #$ ./lastpass2pass.rb path/to/passwords_file.csv
    
    Alex Sayers's avatar
    Alex Sayers committed
    # Parse flags
    require 'optparse'
    optparse = OptionParser.new do |opts|
      opts.banner = "Usage: #{$0} [options] filename"
    
      FORCE = false
      opts.on("-f", "--force", "Overwrite existing records") { FORCE = true }
      DEFAULT_GROUP = ""
      opts.on("-d", "--default GROUP", "Place uncategorised records into GROUP") { |group| DEFAULT_GROUP = group }
      opts.on("-h", "--help", "Display this screen") { puts opts; exit }
    
      opts.parse!
    end
    
    # Check for a filename
    if ARGV.empty?
      puts optparse
      exit 0
    end
    
    # Get filename of csv file
    filename = ARGV.join(" ")
    puts "Reading '#{filename}'..."
    
    
    
    class Record
      def initialize name, url, username, password, extra, grouping, fav
        @name, @url, @username, @password, @extra, @grouping, @fav = name, url, username, password, extra, grouping, fav
      end
    
      def name
        s = ""
        s << @grouping + "/" unless @grouping.empty?
        s << @name
        s.gsub(/ /, "_").gsub(/'/, "")
      end
    
      def to_s
        s = ""
        s << "#{@password}\n---\n"
        s << "#{@grouping} / " unless @grouping.empty?
        s << "#{@name}\n"
        s << "username: #{@username}\n" unless @username.empty?
        s << "password: #{@password}\n" unless @password.empty?
        s << "url: #{@url}\n" unless @url == "http://sn"
        s << "#{@extra}\n" unless @extra.nil?
        return s
      end
    end
    
    # Extract individual records
    entries = []
    entry = ""
    begin
      file = File.open(filename)
      file.each do |line|
        if line =~ /^http/
          entries.push(entry)
          entry = ""
        end
        entry += line
      end
      entries.push(entry)
      entries.shift
      puts "#{entries.length} records found!"
    rescue
      puts "Couldn't find #{filename}!"
      exit 1
    end
    
    # Parse records and create Record objects
    records = []
    entries.each do |e|
      args = e.split(",")
      url = args.shift
      username = args.shift
      password = args.shift
      fav = args.pop
      grouping = args.pop
      grouping = DEFAULT_GROUP if grouping.empty?
      name = args.pop
      extra = args.join(",")[1...-1]
      
      records << Record.new(name, url, username, password, extra, grouping, fav)
    end
    puts "Records parsed: #{records.length}"
    
    successful = 0
    errors = []
    records.each do |r|
      print "Creating record #{r.name}..."
    
    Alex Sayers's avatar
    Alex Sayers committed
      IO.popen("pass insert -m#{"f" if FORCE} '#{r.name}' > /dev/null", 'w') do |io|
    
        io.puts r
      end
      if $? == 0
        puts " done!"
        successful += 1
      else
        puts " error!"
        errors << r
      end
    end
    puts "#{successful} records successfully imported!"
    
    if errors
      puts "There were #{errors.length} errors:"
      errors.each { |e| print e.name + (e == errors.last ? ".\n" : ", ")}
      puts "These probably occurred because an identically-named record already existed, or because there were multiple entries with the same name in the csv file."
    end