Last active
August 29, 2015 14:26
-
-
Save pjones/e743cfa03b751068e02d to your computer and use it in GitHub Desktop.
Read a tab-separated file generated by PasswordSafe and use the pass(1) tool to record new passwords.
This file contains 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 | |
################################################################################ | |
# 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