Skip to content

Instantly share code, notes, and snippets.

@pjones
Last active Aug 29, 2015
Embed
What would you like to do?
Read a tab-separated file generated by PasswordSafe and use the pass(1) tool to record new passwords.
#!/usr/bin/env ruby
################################################################################
# Read a tab-separated file generated by PasswordSafe and use the
# pass(1) tool to record new passwords.
#
# To use this script:
#
# 1. Create and mount an encrypted disk image (cryptsetup).
#
# 2. Export your passwords from PasswordSafe into the encrypted disk
# image in tab-separated value format with the default settings.
#
# 3. Initialize your password store (pass init).
#
# 4. Run this script, passing the exported TSV file as the first
# argument.
#
# 5. Enjoy.
#
require("open3")
require("optparse")
require("ostruct")
################################################################################
class Driver
##############################################################################
DEFAULT_OPTIONS = {
:downcase => false,
}
##############################################################################
PASS_STORE = File.expand_path("~/.password-store")
##############################################################################
attr_reader(:options, :file)
##############################################################################
def initialize
@options = OpenStruct.new(DEFAULT_OPTIONS)
OptionParser.new do |p|
p.banner = "Usage: convert.rb [options] file"
p.on("-h", "--help", "This message.") {$stdout.puts(p); exit}
p.on("-d", "Downcase groups and titles") {|t| options.downcase = t}
end.parse!(ARGV)
if (@file = ARGV.shift).nil? || !File.exist?(@file)
raise("please give a valid TSV file name")
end
end
##############################################################################
def run
# Can't use the CSV library because the PasswordSafe files are not
# properly encoded.
File.open(file) do |f|
@header = nil
f.each do |line|
row = line.chomp.split("\t")
(@header = row; next) if @header.nil?
groups = get(row, "Group/Title").split(".")
group = groups.map {|g| decode(g.gsub("/", "-"), true)}.join("/")
group.downcase! if options.downcase
if File.exist?(File.join(PASS_STORE, group))
raise("entry already exists: #{group}")
end
command = ["pass", "insert", "-m", group]
Open3::popen2e(*command) do |stdin, stdout, wait|
export(row, stdin)
stdin.close
status = wait.value
if !status.exitstatus.zero?
raise("failed import of password item #{group}")
end
end
puts group
end
end
end
##############################################################################
private
################################################################################
def export (row, io)
io.puts(get(row, "Password"))
io.print("\n")
maybe(row, io, "Username:", "Username")
maybe(row, io, "URL:", "URL")
maybe(row, io, "Email:", "e-mail")
notes = decode(get(row, "Notes").to_s).gsub(/(^"|"$)/, '').strip
if !notes.empty?
io.print("\n")
io.puts("Notes:")
# Notes need a bit of love.
notes.split("\n").each do |line|
io.print(" ")
io.print("- ") if !line.match(/^\s*-/) && !line.match(/^\s*$/)
io.puts(line.strip)
end
end
end
##############################################################################
def maybe (row, io, title, header)
value = get(row, header)
if value && !value.strip.empty?
io.puts(title + " " + value.strip)
end
end
##############################################################################
def decode (str, title=false)
str.gsub("\u00BB", title ? "." : "\n")
end
##############################################################################
def get (row, name)
index = @header.index(name)
index && row[index]
end
end
################################################################################
begin
Driver.new.run
rescue RuntimeError => e
$stderr.puts(File.basename($0) + ": ERROR: #{e}")
exit(1)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment