1
0
Fork 0
mirror of https://github.com/nishiki/manage-password.git synced 2025-02-17 08:30:04 +00:00

rewrite mpw for use a new file storage format

This commit is contained in:
nishiki 2016-05-05 20:19:25 +02:00
parent 0268c60484
commit 408ee95bcc
5 changed files with 532 additions and 739 deletions

22
bin/mpw
View file

@ -1,7 +1,6 @@
#!/usr/bin/ruby
# author: nishiki
# mail: nishiki@yaegashi.fr
# info: a simple script who manage your passwords
require 'rubygems'
require 'optparse'
@ -11,7 +10,7 @@ require 'set'
require 'i18n'
require 'mpw/mpw'
require 'mpw/config'
require 'mpw/ui/cli'
require 'mpw/cli'
# --------------------------------------------------------- #
# Set local
@ -77,26 +76,12 @@ OptionParser.new do |opts|
options[:setup] = true
end
opts.on('-p', '--protocol PROTOCOL', I18n.t('option.protocol')) do |protocol|
options[:protocol] = protocol
end
opts.on('-e', '--export FILE', I18n.t('option.export')) do |file|
options[:export] = file
options[:type] = :yaml
end
opts.on('-t', '--type TYPE', I18n.t('option.type')) do |type|
options[:type] = type.to_sym
end
opts.on('-i', '--import FILE', I18n.t('option.import')) do |file|
options[:import] = file
options[:type] = :yaml
end
opts.on('-f', '--force', I18n.t('option.force')) do
options[:force] = true
end
opts.on('-N', '--no-sync', I18n.t('option.no_sync')) do
@ -131,7 +116,6 @@ elsif not config.check_gpg_key?
end
cli.decrypt
cli.sync(options[:sync])
# Display the item's informations
if not options[:show].nil?
@ -156,11 +140,11 @@ elsif not options[:add].nil?
# Export
elsif not options[:export].nil?
cli.export(options[:export], options[:type])
cli.export(options[:export])
# Add a new item
elsif not options[:import].nil?
cli.import(options[:import], options[:type], options[:force])
cli.import(options[:import])
# Interactive mode
end

View file

@ -2,230 +2,158 @@
# author: nishiki
# mail: nishiki@yaegashi.fr
require 'rubygems'
require 'gpgme'
require 'yaml'
require 'i18n'
module MPW
class Config
attr_accessor :error_msg
class Config
attr_accessor :key
attr_accessor :share_keys
attr_accessor :lang
attr_accessor :file_gpg
attr_accessor :last_update
attr_accessor :sync_type
attr_accessor :sync_host
attr_accessor :sync_port
attr_accessor :sync_user
attr_accessor :sync_pwd
attr_accessor :sync_path
attr_accessor :last_sync
attr_accessor :dir_config
# Constructor
# @args: file_config -> the specify config file
def initialize(file_config=nil)
@error_msg = nil
attr_accessor :error_msg
if /darwin/ =~ RUBY_PLATFORM
@dir_config = "#{Dir.home}/Library/Preferences/mpw"
elsif /cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM
@dir_config = "#{Dir.home}/AppData/Local/mpw"
else
@dir_config = "#{Dir.home}/.config/mpw"
end
@file_config = "#{@dir_config}/conf/default.cfg"
if not file_config.nil? and not file_config.empty?
@file_config = file_config
end
end
# Create a new config file
# @args: key -> the gpg key to encrypt
# share_keys -> multiple keys to share the password with other people
# lang -> the software language
# file_gpg -> the file who is encrypted
# sync_type -> the type to synchronization
# sync_host -> the server host for synchronization
# sync_port -> the server port for synchronization
# sync_user -> the user for synchronization
# sync_pwd -> the password for synchronization
# sync_suffix -> the suffix file (optionnal)
# @rtrn: true if le config file is create
def setup(key, share_keys, lang, file_gpg, sync_type, sync_host, sync_port, sync_user, sync_pwd, sync_path)
if not key =~ /[a-zA-Z0-9.-_]+\@[a-zA-Z0-9]+\.[a-zA-Z]+/
@error_msg = I18n.t('error.config.key_bad_format')
return false
end
attr_accessor :key
attr_accessor :lang
attr_accessor :config_dir
attr_accessor :wallet_dir
if not check_public_gpg_key(share_keys)
return false
end
if file_gpg.empty?
file_gpg = "#{@dir_config}/db/default.gpg"
end
config = {'config' => {'key' => key,
'share_keys' => share_keys,
'lang' => lang,
'file_gpg' => file_gpg,
'sync_type' => sync_type,
'sync_host' => sync_host,
'sync_port' => sync_port,
'sync_user' => sync_user,
'sync_pwd' => sync_pwd,
'sync_path' => sync_path,
'last_sync' => 0
}
}
Dir.mkdir("#{@config_dir}/conf", 700)
Dir.mkdir("#{@config_dir}/db", 700)
File.open(@file_config, 'w') do |file|
file << config.to_yaml
end
return true
rescue Exception => e
@error_msg = "#{I18n.t('error.config.write')}\n#{e}"
return false
end
# Constructor
# @args: config_file -> the specify config file
def initialize(config_file=nil)
@error_msg = nil
@config_file = config_file
# Setup a new gpg key
# @args: password -> the GPG key password
# name -> the name of user
# length -> length of the GPG key
# expire -> the time of expire to GPG key
# @rtrn: true if the GPG key is create, else false
def setup_gpg_key(password, name, length = 2048, expire = 0)
if name.nil? or name.empty?
@error_msg = "#{I18n.t('error.config.genkey_gpg.name')}"
return false
elsif password.nil? or password.empty?
@error_msg = "#{I18n.t('error.config.genkey_gpg.password')}"
return false
end
param = ''
param << '<GnupgKeyParms format="internal">' + "\n"
param << "Key-Type: DSA\n"
param << "Key-Length: #{length}\n"
param << "Subkey-Type: ELG-E\n"
param << "Subkey-Length: #{length}\n"
param << "Name-Real: #{name}\n"
param << "Name-Comment: #{name}\n"
param << "Name-Email: #{@key}\n"
param << "Expire-Date: #{expire}\n"
param << "Passphrase: #{password}\n"
param << "</GnupgKeyParms>\n"
ctx = GPGME::Ctx.new
ctx.genkey(param, nil, nil)
return true
rescue Exception => e
@error_msg = "#{I18n.t('error.config.genkey_gpg.exception')}\n#{e}"
return false
end
# Check the config file
# @rtrn: true if the config file is correct
def checkconfig
config = YAML::load_file(@file_config)
@key = config['config']['key']
@share_keys = config['config']['share_keys']
@lang = config['config']['lang']
@file_gpg = config['config']['file_gpg']
@sync_type = config['config']['sync_type']
@sync_host = config['config']['sync_host']
@sync_port = config['config']['sync_port']
@sync_user = config['config']['sync_user']
@sync_pwd = config['config']['sync_pwd']
@sync_path = config['config']['sync_path']
@last_sync = config['config']['last_sync'].to_i
if @key.empty? or @file_gpg.empty?
@error_msg = I18n.t('error.config.check')
return false
end
I18n.locale = @lang.to_sym
return true
rescue Exception => e
@error_msg = "#{I18n.t('error.config.check')}\n#{e}"
return false
end
# Check if private key exist
# @rtrn: true if the key exist, else false
def check_gpg_key?
ctx = GPGME::Ctx.new
ctx.each_key(@key, true) do
return true
end
return false
end
# Check if private key exist
# @args: share_keys -> string with all public keys
# @rtrn: true if the key exist, else false
def check_public_gpg_key(share_keys = @share_keys)
ctx = GPGME::Ctx.new
share_keys = share_keys.nil? ? '' : share_keys
if not share_keys.empty?
share_keys.split.each do |k|
if not k =~ /[a-zA-Z0-9.-_]+\@[a-zA-Z0-9]+\.[a-zA-Z]+/
@error_msg = I18n.t('error.config.key_bad_format')
return false
end
ctx.each_key(key, false) do
next
end
@error_msg = I18n.t('error.config.no_key_public', key: k)
return false
end
end
return true
end
# Set the last update when there is a sync
# @rtrn: true is the file has been updated
def set_last_sync
config = {'config' => {'key' => @key,
'share_keys' => @share_keys,
'lang' => @lang,
'file_gpg' => @file_gpg,
'sync_type' => @sync_type,
'sync_host' => @sync_host,
'sync_port' => @sync_port,
'sync_user' => @sync_user,
'sync_pwd' => @sync_pwd,
'sync_path' => @sync_path,
'last_sync' => Time.now.to_i
}
}
File.open(@file_config, 'w') do |file|
file << config.to_yaml
end
return true
rescue Exception => e
@error_msg = "#{I18n.t('error.config.write')}\n#{e}"
return false
if /darwin/ =~ RUBY_PLATFORM
@config_dir = "#{Dir.home}/Library/Preferences/mpw"
elsif /cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM
@config_dir = "#{Dir.home}/AppData/Local/mpw"
else
@config_dir = "#{Dir.home}/.config/mpw"
end
if @config_file.nil? or @config_file.empty?
@config_file = "#{@config_dir}/mpw.cfg"
end
end
# Create a new config file
# @args: key -> the gpg key to encrypt
# lang -> the software language
# wallet_dir -> the directory where are the wallets password
# @rtrn: true if le config file is create
def setup(key, lang, wallet_dir)
if not key =~ /[a-zA-Z0-9.-_]+\@[a-zA-Z0-9]+\.[a-zA-Z]+/
@error_msg = I18n.t('error.config.key_bad_format')
return false
end
if wallet_dir.empty?
wallet_dir = "#{@config_dir}/wallets"
end
config = {'config' => {'key' => key,
'lang' => lang,
'wallet_dir' => wallet_dir,
}
}
Dir.mkdir(wallet_dir, 0700)
File.open(@config_file, 'w') do |file|
file << config.to_yaml
end
return true
rescue Exception => e
@error_msg = "#{I18n.t('error.config.write')}\n#{e}"
return false
end
# Setup a new gpg key
# @args: password -> the GPG key password
# name -> the name of user
# length -> length of the GPG key
# expire -> the time of expire to GPG key
# @rtrn: true if the GPG key is create, else false
def setup_gpg_key(password, name, length = 4096, expire = 0)
if name.nil? or name.empty?
@error_msg = "#{I18n.t('error.config.genkey_gpg.name')}"
return false
elsif password.nil? or password.empty?
@error_msg = "#{I18n.t('error.config.genkey_gpg.password')}"
return false
end
param = ''
param << '<GnupgKeyParms format="internal">' + "\n"
param << "Key-Type: DSA\n"
param << "Key-Length: #{length}\n"
param << "Subkey-Type: ELG-E\n"
param << "Subkey-Length: #{length}\n"
param << "Name-Real: #{name}\n"
param << "Name-Comment: #{name}\n"
param << "Name-Email: #{@key}\n"
param << "Expire-Date: #{expire}\n"
param << "Passphrase: #{password}\n"
param << "</GnupgKeyParms>\n"
ctx = GPGME::Ctx.new
ctx.genkey(param, nil, nil)
return true
rescue Exception => e
@error_msg = "#{I18n.t('error.config.genkey_gpg.exception')}\n#{e}"
return false
end
# Check the config file
# @rtrn: true if the config file is correct
def checkconfig
config = YAML::load_file(@config_file)
@key = config['config']['key']
@lang = config['config']['lang']
@wallet_dir = config['config']['wallet_dir']
if @key.empty? or @wallet_dir.empty?
@error_msg = I18n.t('error.config.check')
return false
end
I18n.locale = @lang.to_sym
return true
rescue Exception => e
@error_msg = "#{I18n.t('error.config.check')}\n#{e}"
return false
end
# Check if private key exist
# @rtrn: true if the key exist, else false
def check_gpg_key?
ctx = GPGME::Ctx.new
ctx.each_key(@key, true) do
return true
end
return false
end
# Set the last update when there is a sync
# @rtrn: true is the file has been updated
def set_last_sync
config = {'config' => {'key' => @key,
'lang' => @lang,
'wallet_dir' => @wallet_dir,
}
}
File.open(@config_file, 'w') do |file|
file << config.to_yaml
end
return true
rescue Exception => e
@error_msg = "#{I18n.t('error.config.write')}\n#{e}"
return false
end
end
end

View file

@ -2,108 +2,104 @@
# author: nishiki
# mail: nishiki@yaegashi.fr
require 'rubygems'
require 'i18n'
module MPW
class Item
class Item
attr_accessor :error_msg
attr_accessor :error_msg
attr_accessor :id
attr_accessor :name
attr_accessor :group
attr_accessor :host
attr_accessor :protocol
attr_accessor :user
attr_accessor :password
attr_accessor :port
attr_accessor :comment
attr_accessor :last_edit
attr_accessor :last_sync
attr_accessor :created
attr_accessor :id
attr_accessor :name
attr_accessor :group
attr_accessor :host
attr_accessor :protocol
attr_accessor :user
attr_accessor :port
attr_accessor :comment
attr_accessor :last_edit
attr_accessor :last_sync
attr_accessor :created
# Constructor
# Create a new item
# @args: options -> a hash of parameter
# raise an error if the hash hasn't the key name
def initialize(options={})
if not options.has_key?(:name) or options[:name].to_s.empty?
@error_msg = I18n.t('error.update.name_empty')
raise @error_msg
end
if not options.has_key?(:id) or options[:id].to_s.empty? or not options.has_key?(:created) or options[:created].to_s.empty?
@id = generate_id
@created = Time.now.to_i
else
@id = options[:id]
@created = options[:created]
@last_edit = options[:last_edit]
options[:no_update_last_edit] = true
end
update(options)
# Constructor
# Create a new item
# @args: options -> a hash of parameter
# raise an error if the hash hasn't the key name
def initialize(options={})
if not options.has_key?(:name) or options[:name].to_s.empty?
@error_msg = I18n.t('error.update.name_empty')
raise @error_msg
end
# Update the item
# @args: options -> a hash of parameter
# @rtrn: true if the item is update
def update(options={})
if options.has_key?(:name) and options[:name].to_s.empty?
@error_msg = I18n.t('error.update.name_empty')
return false
end
@name = options[:name] if options.has_key?(:name)
@group = options[:group] if options.has_key?(:group)
@host = options[:host] if options.has_key?(:host)
@protocol = options[:protocol] if options.has_key?(:protocol)
@user = options[:user] if options.has_key?(:user)
@password = options[:password] if options.has_key?(:password)
@port = options[:port].to_i if options.has_key?(:port) and not options[:port].to_s.empty?
@comment = options[:comment] if options.has_key?(:comment)
@last_edit = Time.now.to_i if not options.has_key?(:no_update_last_edit)
return true
if not options.has_key?(:id) or options[:id].to_s.empty? or not options.has_key?(:created) or options[:created].to_s.empty?
@id = generate_id
@created = Time.now.to_i
else
@id = options[:id]
@created = options[:created]
@last_edit = options[:last_edit]
options[:no_update_last_edit] = true
end
# Update last_sync
def set_last_sync
@last_sync = Time.now.to_i
end
update(options)
end
# Delete all data
# @rtrn: true
def delete
@id = nil
@name = nil
@group = nil
@host = nil
@protocol = nil
@user = nil
@password = nil
@port = nil
@comment = nil
@created = nil
@last_edit = nil
@last_sync = nil
return true
end
def empty?
return @name.to_s.empty?
end
def nil?
# Update the item
# @args: options -> a hash of parameter
# @rtrn: true if the item is update
def update(options={})
if options.has_key?(:name) and options[:name].to_s.empty?
@error_msg = I18n.t('error.update.name_empty')
return false
end
# Generate an random id
private
def generate_id
return ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(16).join
end
@name = options[:name] if options.has_key?(:name)
@group = options[:group] if options.has_key?(:group)
@host = options[:host] if options.has_key?(:host)
@protocol = options[:protocol] if options.has_key?(:protocol)
@user = options[:user] if options.has_key?(:user)
@port = options[:port].to_i if options.has_key?(:port) and not options[:port].to_s.empty?
@comment = options[:comment] if options.has_key?(:comment)
@last_edit = Time.now.to_i if not options.has_key?(:no_update_last_edit)
return true
end
# Update last_sync
def set_last_sync
@last_sync = Time.now.to_i
end
# Delete all data
# @rtrn: true
def delete
@id = nil
@name = nil
@group = nil
@host = nil
@protocol = nil
@user = nil
@port = nil
@comment = nil
@created = nil
@last_edit = nil
@last_sync = nil
return true
end
def empty?
return @name.to_s.empty?
end
def nil?
return false
end
# Generate an random id
private
def generate_id
return ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(16).join
end
end
end

View file

@ -2,331 +2,288 @@
# author: nishiki
# mail: nishiki@yaegashi.fr
require 'rubygems'
require 'rubygems/package'
require 'gpgme'
require 'csv'
require 'i18n'
require 'fileutils'
require 'yaml'
require 'mpw/item'
module MPW
class MPW
attr_accessor :error_msg
# Constructor
def initialize(file_gpg, key, share_keys='')
@error_msg = nil
@file_gpg = file_gpg
@key = key
@share_keys = share_keys
@data = []
end
# Decrypt a gpg file
# @args: password -> the GPG key password
# @rtrn: true if data has been decrypted
def decrypt(password=nil)
@data = []
class MPW
if File.exist?(@file_gpg) and not File.zero?(@file_gpg)
crypto = GPGME::Crypto.new(armor: true)
data_decrypt = crypto.decrypt(IO.read(@file_gpg), password: password).read.force_encoding('utf-8')
attr_accessor :error_msg
# Constructor
def initialize(key, wallet_file, gpg_pass=nil)
@error_msg = nil
@key = key
@gpg_pass = gpg_pass
@wallet_file = wallet_file
end
if not data_decrypt.to_s.empty?
YAML.load(data_decrypt).each_value do |d|
@data.push(Item.new(id: d['id'],
name: d['name'],
group: d['group'],
host: d['host'],
protocol: d['protocol'],
user: d['user'],
password: d['password'],
port: d['port'],
comment: d['comment'],
last_edit: d['last_edit'],
created: d['created'],
)
)
end
# Decrypt a gpg file
# @args: password -> the GPG key password
# @rtrn: true if data has been decrypted
def read_data
@config = nil
@keys = []
@data = []
@passwords = {}
data = nil
return if not File.exists?(@wallet_file)
Gem::Package::TarReader.new(File.open(@wallet_file)) do |tar|
tar.each do |f|
case f.full_name
when 'wallet/config.yml'
@config = YAML.load(f.read)
check_config
when 'wallet/meta.gpg'
data = decrypt(f.read)
when /^wallet\/keys\/(?<key>.+)\.pub$/
@keys[match['key']] = f.read
when /^wallet\/passwords\/(?<id>[a-zA-Z0-9]+)\.gpg$/
@passwords[Regexp.last_match('id')] = f.read
else
next
end
end
return true
rescue Exception => e
@error_msg = "#{I18n.t('error.gpg_file.decrypt')}\n#{e}"
return false
end
# Encrypt a file
# @rtrn: true if the file has been encrypted
def encrypt
FileUtils.cp(@file_gpg, "#{@file_gpg}.bk") if File.exist?(@file_gpg)
data_to_encrypt = {}
@data.each do |item|
next if item.empty?
data_to_encrypt.merge!(item.id => {'id' => item.id,
'name' => item.name,
'group' => item.group,
'host' => item.host,
'protocol' => item.protocol,
'user' => item.user,
'password' => item.password,
'port' => item.port,
'comment' => item.comment,
'last_edit' => item.last_edit,
'created' => item.created,
}
)
end
recipients = []
recipients.push(@key)
if not @share_keys.nil?
@share_keys.split.each { |k| recipients.push(k) }
end
crypto = GPGME::Crypto.new(armor: true)
file_gpg = File.open(@file_gpg, 'w+')
crypto.encrypt(data_to_encrypt.to_yaml, recipients: recipients, output: file_gpg)
file_gpg.close
FileUtils.rm("#{@file_gpg}.bk") if File.exist?("#{@file_gpg}.bk")
return true
rescue Exception => e
@error_msg = "#{I18n.t('error.gpg_file.encrypt')}\n#{e}"
FileUtils.mv("#{@file_gpg}.bk", @file_gpg) if File.exist?("#{@file_gpg}.bk")
return false
end
# Add a new item
# @args: item -> Object MPW::Item
# @rtrn: true if add item
def add(item)
if not item.instance_of?(Item)
@error_msg = I18n.t('error.bad_class')
return false
elsif item.empty?
@error_msg = I18n.t('error.add.empty')
return false
else
@data.push(item)
return true
end
end
# Search in some csv data
# @args: options -> a hash with paramaters
# @rtrn: a list with the resultat of the search
def list(options={})
result = []
search = options[:search].to_s.downcase
group = options[:group].to_s.downcase
protocol = options[:protocol].to_s.downcase
if not data.nil? and not data.empty?
YAML.load(data).each_value do |d|
@data.push(Item.new(id: d['id'],
name: d['name'],
group: d['group'],
host: d['host'],
protocol: d['protocol'],
user: d['user'],
port: d['port'],
comment: d['comment'],
last_edit: d['last_edit'],
created: d['created'],
)
)
end
end
end
@data.each do |item|
next if item.empty?
# Encrypt a file
# @rtrn: true if the file has been encrypted
# TODO export key pub
def write_data
data = {}
next if not group.empty? and not group.eql?(item.group.downcase)
next if not protocol.empty? and not protocol.eql?(item.protocol.downcase)
name = item.name.to_s.downcase
host = item.host.to_s.downcase
comment = item.comment.to_s.downcase
@data.each do |item|
next if item.empty?
if not name =~ /^.*#{search}.*$/ and not host =~ /^.*#{search}.*$/ and not comment =~ /^.*#{search}.*$/
next
data.merge!(item.id => {'id' => item.id,
'name' => item.name,
'group' => item.group,
'host' => item.host,
'protocol' => item.protocol,
'user' => item.user,
'port' => item.port,
'comment' => item.comment,
'last_edit' => item.last_edit,
'created' => item.created,
}
)
end
Gem::Package::TarWriter.new(File.open(@wallet_file, 'w+')) do |tar|
data_encrypt = encrypt(YAML::dump(data))
tar.add_file_simple('wallet/meta.gpg', 0400, data_encrypt.length) do |io|
io.write(data_encrypt)
end
@passwords.each do |id, password|
tar.add_file_simple("wallet/passwords/#{id}.gpg", 0400, password.length) do |io|
io.write(password)
end
result.push(item)
end
return result
end
# Search in some csv data
# @args: id -> the id item
# @rtrn: a row with the result of the search
def search_by_id(id)
@data.each do |item|
return item if item.id == id
end
return nil
end
# Export to csv
# @args: file -> file where you export the data
# type -> udata type
# @rtrn: true if export work
def export(file, type=:yaml)
case type
when :csv
CSV.open(file, 'w', write_headers: true,
headers: ['name', 'group', 'protocol', 'host', 'user', 'password', 'port', 'comment']) do |csv|
@data.each do |item|
csv << [item.name, item.group, item.protocol, item.host, item.user, item.password, item.port, item.comment]
end
end
when :yaml
data = {}
@data.each do |item|
data.merge!(item.id => {'id' => item.id,
'name' => item.name,
'group' => item.group,
'host' => item.host,
'protocol' => item.protocol,
'user' => item.user,
'password' => item.password,
'port' => item.port,
'comment' => item.comment,
'last_edit' => item.last_edit,
'created' => item.created,
}
)
end
File.open(file, 'w') {|f| f << data.to_yaml}
else
@error_msg = "#{I18n.t('error.export.unknown_type', type: type)}"
return false
end
return true
rescue Exception => e
@error_msg = "#{I18n.t('error.export.write', file: file)}\n#{e}"
return false
end
# Import to csv
# @args: file -> path to file import
# type -> udata type
# @rtrn: true if the import work
def import(file, type=:yaml)
case type
when :csv
CSV.foreach(file, {headers: true}) do |row|
item = Item.new(name: row['name'],
group: row['group'],
host: row['host'],
protocol: row['protocol'],
user: row['user'],
password: row['password'],
port: row['port'],
comment: row['comment'],
)
return false if item.empty?
@data.push(item)
end
when :yaml
YAML::load_file(file).each_value do |row|
item = Item.new(name: row['name'],
group: row['group'],
host: row['host'],
protocol: row['protocol'],
user: row['user'],
password: row['password'],
port: row['port'],
comment: row['comment'],
)
return false if item.empty?
@data.push(item)
end
else
@error_msg = "#{I18n.t('error.export.unknown_type', type: type)}"
return false
end
return true
rescue Exception => e
@error_msg = "#{I18n.t('error.import.read', file: file)}\n#{e}"
return false
end
# Return a preview import
# @args: file -> path to file import
# @rtrn: a hash with the items to import, if there is an error return false
def import_preview(file, type=:yaml)
data = []
case type
when :csv
CSV.foreach(file, {headers: true}) do |row|
item = Item.new(name: row['name'],
group: row['group'],
host: row['host'],
protocol: row['protocol'],
user: row['user'],
password: row['password'],
port: row['port'],
comment: row['comment'],
)
return false if item.empty?
data.push(item)
end
when :yaml
YAML::load_file(file).each_value do |row|
item = Item.new(name: row['name'],
group: row['group'],
host: row['host'],
protocol: row['protocol'],
user: row['user'],
password: row['password'],
port: row['port'],
comment: row['comment'],
)
return false if item.empty?
data.push(item)
end
else
@error_msg = "#{I18n.t('error.export.unknown_type', type: type)}"
return false
end
return data
rescue Exception => e
@error_msg = "#{I18n.t('error.import.read', file: file)}\n#{e}"
return false
end
# Generate a random password
# @args: length -> the length password
# @rtrn: a random string
def self.password(length=8)
if length.to_i <= 0
length = 8
else
length = length.to_i
end
result = ''
while length > 62 do
result << ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(62).join
length -= 62
end
result << ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(length).join
return result
end
end
# TODO comment
def get_password(id)
return decrypt(@passwords[id])
end
# TODO comment
def set_password(id, password)
@passwords[id] = encrypt(password)
end
# TODO
def check_config
if false
raise 'ERROR'
end
end
# Add a new item
# @args: item -> Object MPW::Item
# @rtrn: true if add item
# TODO add password
def add(item)
if not item.instance_of?(Item)
raise I18n.t('error.bad_class')
elsif item.empty?
raise I18n.t('error.add.empty')
else
@data.push(item)
end
end
# Search in some csv data
# @args: options -> a hash with paramaters
# @rtrn: a list with the resultat of the search
def list(options={})
result = []
search = options[:search].to_s.downcase
group = options[:group].to_s.downcase
protocol = options[:protocol].to_s.downcase
@data.each do |item|
next if item.empty?
next if not group.empty? and not group.eql?(item.group.downcase)
next if not protocol.empty? and not protocol.eql?(item.protocol.downcase)
name = item.name.to_s.downcase
host = item.host.to_s.downcase
comment = item.comment.to_s.downcase
if not name =~ /^.*#{search}.*$/ and not host =~ /^.*#{search}.*$/ and not comment =~ /^.*#{search}.*$/
next
end
result.push(item)
end
return result
end
# Search in some csv data
# @args: id -> the id item
# @rtrn: a row with the result of the search
def search_by_id(id)
@data.each do |item|
return item if item.id == id
end
return nil
end
# Export to csv
# @args: file -> file where you export the data
def export(file)
data = {}
@data.each do |item|
data.merge!(item.id => {'id' => item.id,
'name' => item.name,
'group' => item.group,
'host' => item.host,
'protocol' => item.protocol,
'user' => item.user,
'password' => get_password(item.id),
'port' => item.port,
'comment' => item.comment,
'last_edit' => item.last_edit,
'created' => item.created,
}
)
end
File.open(file, 'w') {|f| f << data.to_yaml}
rescue Exception => e
raise "#{I18n.t('error.export.write', file: file)}\n#{e}"
end
# Import to yaml
# @args: file -> path to file import
# TODO raise
def import(file)
YAML::load_file(file).each_value do |row|
item = Item.new(name: row['name'],
group: row['group'],
host: row['host'],
protocol: row['protocol'],
user: row['user'],
port: row['port'],
comment: row['comment'],
)
raise 'Item is empty' if item.empty?
@data.push(item)
set_password(item.id, row['password'])
end
rescue Exception => e
raise "#{I18n.t('error.import.read', file: file)}\n#{e}"
end
# Generate a random password
# @args: length -> the length password
# @rtrn: a random string
def self.password(length=8)
if length.to_i <= 0
length = 8
else
length = length.to_i
end
result = ''
while length > 62 do
result << ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(62).join
length -= 62
end
result << ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(length).join
return result
end
# Decrypt a gpg file
# @args: password -> the GPG key password
# @rtrn: true if data has been decrypted
private
def decrypt(data)
crypto = GPGME::Crypto.new(armor: true)
return crypto.decrypt(data, password: @gpg_pass).read.force_encoding('utf-8')
rescue Exception => e
raise "#{I18n.t('error.gpg_file.decrypt')}\n#{e}"
end
# Encrypt a file
# @rtrn: true if the file has been encrypted
private
def encrypt(data)
recipients = []
crypto = GPGME::Crypto.new(armor: true)
# @config['keys'].each do |key|
# recipients.push(key)
# end
recipients.push(@key)
return crypto.encrypt(data, recipients: recipients).read
rescue Exception => e
raise "#{I18n.t('error.gpg_file.encrypt')}\n#{e}"
end
end
end

View file

@ -1,15 +1,11 @@
#!/usr/bin/ruby
# author: nishiki
# mail: nishiki@yaegashi.fr
# info: a simple script who m your passwords
require 'rubygems'
require 'highline/import'
require 'pathname'
require 'readline'
require 'i18n'
require 'colorize'
require 'mpw/sync'
require 'highline/import'
require 'mpw/mpw'
require 'mpw/item'
@ -18,29 +14,10 @@ class Cli
# Constructor
# @args: lang -> the operating system language
# config_file -> a specify config file
# TODO
def initialize(config)
@config = config
end
# Sync the data with the server
# @args: allow_sync -> allow or disable sync (boolean)
# @rtnr: true if the synchro is finish
def sync(allow_sync=nil)
if not allow_sync.nil?
@allow_sync = allow_sync
end
return true if not @allow_sync
@sync = MPW::Sync.new(@config, @mpw, @password)
raise(@sync.error_msg) if not @sync.get_remote
raise(@sync.error_msg) if not @sync.sync
return true
rescue Exception => e
puts "#{I18n.t('display.error')} #7: #{e}".red
return false
@wallet_file = "#{@config.wallet_dir}/test.mpw"
end
# Create a new config file
@ -48,33 +25,16 @@ class Cli
def setup(lang)
puts I18n.t('form.setup.title')
puts '--------------------'
language = ask(I18n.t('form.setup.lang', lang: lang)).to_s
key = ask(I18n.t('form.setup.gpg_key')).to_s
share_keys = ask(I18n.t('form.setup.share_gpg_keys')).to_s
file_gpg = ask(I18n.t('form.setup.gpg_file', home: @conf.dir_config)).to_s
sync_type = ask(I18n.t('form.setup.sync_type')).to_s
language = ask(I18n.t('form.setup.lang', lang: lang)).to_s
key = ask(I18n.t('form.setup.gpg_key')).to_s
wallet_dir = ask(I18n.t('form.setup.wallet_dir')).to_s
if ['ssh', 'ftp', 'mpw'].include?(sync_type)
sync_host = ask(I18n.t('form.setup.sync_host')).to_s
sync_port = ask(I18n.t('form.setup.sync_port')).to_s
sync_user = ask(I18n.t('form.setup.sync_user')).to_s
sync_pwd = ask(I18n.t('form.setup.sync_pwd')).to_s
sync_path = ask(I18n.t('form.setup.sync_path')).to_s
end
if language.nil? or language.empty?
language = lang
end
I18n.locale = language.to_sym
sync_type = sync_type.nil? or sync_type.empty? ? nil : sync_type
sync_host = sync_host.nil? or sync_host.empty? ? nil : sync_host
sync_port = sync_port.nil? or sync_port.empty? ? nil : sync_port.to_i
sync_user = sync_user.nil? or sync_user.empty? ? nil : sync_user
sync_pwd = sync_pwd.nil? or sync_pwd.empty? ? nil : sync_pwd
sync_path = sync_path.nil? or sync_path.empty? ? nil : sync_path
if @config.setup(key, share_keys, language, file_gpg, sync_type, sync_host, sync_port, sync_user, sync_pwd, sync_path)
if @config.setup(key, lang, wallet_dir)
puts "#{I18n.t('form.setup.valid')}".green
else
puts "#{I18n.t('display.error')} #8: #{@config.error_msg}".red
@ -86,7 +46,7 @@ class Cli
exit 2
end
end
# Setup a new GPG key
def setup_gpg_key
puts I18n.t('form.setup_gpg_key.title')
@ -124,17 +84,15 @@ class Cli
end
end
# Request the GPG password and decrypt the file
def decrypt
if not defined?(@mpw)
@mpw = MPW::MPW.new(@config.file_gpg, @config.key, @config.share_keys)
@password = ask(I18n.t('display.gpg_password')) {|q| q.echo = false}
@mpw = MPW::MPW.new(@config.key, @wallet_file, @password)
end
@password = ask(I18n.t('display.gpg_password')) {|q| q.echo = false}
if not @mpw.decrypt(@password)
puts "#{I18n.t('display.error')} #11: #{@mpw.error_msg}".red
exit 2
end
@mpw.read_data
end
# Display the query's result
@ -185,7 +143,7 @@ class Cli
print "#{I18n.t('display.login')}: ".cyan
puts item.user
print "#{I18n.t('display.password')}: ".cyan
puts item.password
puts @mpw.get_password(item.id)
print "#{I18n.t('display.port')}: ".cyan
puts item.port
print "#{I18n.t('display.comment')}: ".cyan
@ -203,21 +161,17 @@ class Cli
options[:host] = ask(I18n.t('form.add.server')).to_s
options[:protocol] = ask(I18n.t('form.add.protocol')).to_s
options[:user] = ask(I18n.t('form.add.login')).to_s
options[:password] = ask(I18n.t('form.add.password')).to_s
password = ask(I18n.t('form.add.password')).to_s
options[:port] = ask(I18n.t('form.add.port')).to_s
options[:comment] = ask(I18n.t('form.add.comment')).to_s
item = MPW::Item.new(options)
if @mpw.add(item)
if @mpw.encrypt
sync
puts "#{I18n.t('form.add.valid')}".green
else
puts "#{I18n.t('display.error')} #12: #{@mpw.error_msg}".red
end
else
puts "#{I18n.t('display.error')} #13: #{item.error_msg}".red
end
@mpw.add(item)
@mpw.set_password(item.id, password)
@mpw.write_data
puts "#{I18n.t('form.add.valid')}".green
end
# Update an item
@ -235,22 +189,17 @@ class Cli
options[:host] = ask(I18n.t('form.update.server' , server: item.host)).to_s
options[:protocol] = ask(I18n.t('form.update.protocol', protocol: item.protocol)).to_s
options[:user] = ask(I18n.t('form.update.login' , login: item.user)).to_s
options[:password] = ask(I18n.t('form.update.password')).to_s
password = ask(I18n.t('form.update.password')).to_s
options[:port] = ask(I18n.t('form.update.port' , port: item.port)).to_s
options[:comment] = ask(I18n.t('form.update.comment' , comment: item.comment)).to_s
options.delete_if { |k,v| v.empty? }
if item.update(options)
if @mpw.encrypt
sync
puts "#{I18n.t('form.update.valid')}".green
else
puts "#{I18n.t('display.error')} #14: #{@mpw.error_msg}".red
end
else
puts "#{I18n.t('display.error')} #15: #{item.error_msg}".red
end
item.update(options)
@mpw.encrypt
@mpw.write_data
puts "#{I18n.t('form.update.valid')}".green
else
puts I18n.t('display.nothing')
end
@ -289,43 +238,22 @@ class Cli
# Export the items in a CSV file
# @args: file -> the destination file
def export(file, type=:yaml)
if @mpw.export(file, type)
puts "#{I18n.t('export.valid', file)}".green
else
puts "#{I18n.t('display.error')} #17: #{@mpw.error_msg}".red
end
def export(file)
@mpw.export(file)
puts "#{I18n.t('export.valid', file)}".green
rescue Exception => e
puts "#{I18n.t('display.error')} #17: #{e}".red
end
# Import items from a CSV file
# Import items from a YAML file
# @args: file -> the import file
# force -> no resquest a validation
def import(file, type=:yaml, force=false)
def import(file)
@mpw.import(file)
@mpw.write_data
if not force
result = @mpw.import_preview(file, type)
if result.is_a?(Array) and not result.empty?
result.each do |r|
display_item(r)
end
confirm = ask("#{I18n.t('form.import.ask', file: file)} (y/N) ").to_s
if confirm =~ /^(y|yes|YES|Yes|Y)$/
force = true
end
else
puts I18n.t('form.import.not_valid')
end
end
if force
if @mpw.import(file, type) and @mpw.encrypt
sync
puts "#{I18n.t('form.import.valid')}".green
else
puts "#{I18n.t('display.error')} #18: #{@mpw.error_msg}".red
end
end
puts "#{I18n.t('form.import.valid')}".green
rescue Exception => e
puts "#{I18n.t('display.error')} #18: #{e}".red
end
end