diff --git a/bin/mpw b/bin/mpw index c14c5af..f36bc87 100755 --- a/bin/mpw +++ b/bin/mpw @@ -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 diff --git a/lib/mpw/config.rb b/lib/mpw/config.rb index d2b02a9..dd797d2 100644 --- a/lib/mpw/config.rb +++ b/lib/mpw/config.rb @@ -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 << '' + "\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 << "\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 << '' + "\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 << "\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 diff --git a/lib/mpw/item.rb b/lib/mpw/item.rb index 666d5b3..660f74a 100644 --- a/lib/mpw/item.rb +++ b/lib/mpw/item.rb @@ -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 diff --git a/lib/mpw/mpw.rb b/lib/mpw/mpw.rb index 57a6bce..9fad675 100644 --- a/lib/mpw/mpw.rb +++ b/lib/mpw/mpw.rb @@ -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\/(?.+)\.pub$/ + @keys[match['key']] = f.read + + when /^wallet\/passwords\/(?[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 diff --git a/lib/mpw/ui/cli.rb b/lib/mpw/ui/cli.rb index 3bf9c51..bb6768e 100644 --- a/lib/mpw/ui/cli.rb +++ b/lib/mpw/ui/cli.rb @@ -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