From 96fe71e07d6c67f72492cb8a5ef036cc23da3b29 Mon Sep 17 00:00:00 2001 From: nishiki Date: Wed, 29 Jan 2014 20:49:39 +0100 Subject: [PATCH] add module MPW --- MPW/Config.rb | 140 ++++++++++++++++ MPW/MPW.rb | 335 ++++++++++++++++++++++++++++++++++++++ MPW/Sync/MPW.rb | 130 +++++++++++++++ {lib => MPW/UI}/Cli.rb | 53 +++--- {lib => MPW/UI}/CliSSH.rb | 10 +- lib/MPW.rb | 331 ------------------------------------- lib/MPWConfig.rb | 136 ---------------- lib/Sync.rb | 128 --------------- mpw | 6 +- mpw-ssh | 5 +- 10 files changed, 642 insertions(+), 632 deletions(-) create mode 100644 MPW/Config.rb create mode 100644 MPW/MPW.rb create mode 100644 MPW/Sync/MPW.rb rename {lib => MPW/UI}/Cli.rb (86%) rename {lib => MPW/UI}/CliSSH.rb (74%) delete mode 100644 lib/MPW.rb delete mode 100644 lib/MPWConfig.rb delete mode 100644 lib/Sync.rb diff --git a/MPW/Config.rb b/MPW/Config.rb new file mode 100644 index 0000000..74d309c --- /dev/null +++ b/MPW/Config.rb @@ -0,0 +1,140 @@ +#!/usr/bin/ruby +# author: nishiki +# mail: nishiki@yaegashi.fr +# info: a simple script who manage your passwords + +module MPW + + require 'rubygems' + require 'yaml' + require 'i18n' + + class Config + + attr_accessor :error_msg + + attr_accessor :key + attr_accessor :lang + attr_accessor :file_gpg + attr_accessor :timeout_pwd + attr_accessor :last_update + attr_accessor :sync_host + attr_accessor :sync_port + attr_accessor :sync_pwd + attr_accessor :sync_suffix + attr_accessor :last_update + + # Constructor + # @args: file_config -> the specify config file + def initialize(file_config=nil) + @error_msg = nil + @file_config = "#{Dir.home()}/.mpw.cfg" + + if !file_config.nil? && !file_config.empty? + @file_config = file_config + end + end + + # Create a new config file + # @args: key -> the gpg key to encrypt + # lang -> the software language + # file_gpg -> the file who is encrypted + # timeout_pwd -> time to save the password + # sync_host -> the server host for synchronization + # sync_port -> the server port 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, lang, file_gpg, timeout_pwd, sync_host=nil, sync_port=nil, sync_pwd=nil, sync_suffix=nil) + + 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 file_gpg.empty? + file_gpg = "#{Dir.home()}/.mpw.gpg" + end + + timeout_pwd = timeout_pwd.empty? ? 60 : timeout_pwd.to_i + + config = {'config' => {'key' => key, + 'lang' => lang, + 'file_gpg' => file_gpg, + 'timeout_pwd' => timeout_pwd, + 'sync_host' => sync_host, + 'sync_port' => sync_port, + 'sync_pwd' => sync_pwd, + 'sync_suffix' => sync_suffix, + 'last_update' => 0 }} + + begin + File.open(@file_config, 'w') do |file| + file << config.to_yaml + end + rescue Exception => e + @error_msg = "#{I18n.t('error.config.write')}\n#{e}" + return false + end + + return true + end + + # Check the config file + # @rtrn: true if the config file is correct + def checkconfig() + begin + config = YAML::load_file(@file_config) + @key = config['config']['key'] + @lang = config['config']['lang'] + @file_gpg = config['config']['file_gpg'] + @timeout_pwd = config['config']['timeout_pwd'].to_i + @sync_host = config['config']['sync_host'] + @sync_port = config['config']['sync_port'] + @sync_pwd = config['config']['sync_pwd'] + @sync_suffix = config['config']['sync_suffix'] + @last_update = config['config']['last_update'].to_i + + if @key.empty? || @file_gpg.empty? + @error_msg = I18n.t('error.config.check') + return false + end + + I18n.locale = @lang.to_sym + + rescue Exception => e + @error_msg = "#{I18n.t('error.config.check')}\n#{e}" + return false + end + + return true + end + + # Set the last update when there is a sync + # @rtrn: true is the file has been updated + def set_last_update() + config = {'config' => {'key' => @key, + 'lang' => @lang, + 'file_gpg' => @file_gpg, + 'timeout_pwd' => @timeout_pwd, + 'sync_host' => @sync_host, + 'sync_port' => @sync_port, + 'sync_pwd' => @sync_pwd, + 'sync_suffix' => @sync_suffix, + 'last_update' => Time.now.to_i }} + + begin + File.open(@file_config, 'w') do |file| + file << config.to_yaml + end + rescue Exception => e + @error_msg = "#{I18n.t('error.config.write')}\n#{e}" + return false + end + + return true + end + + end + +end diff --git a/MPW/MPW.rb b/MPW/MPW.rb new file mode 100644 index 0000000..1f84400 --- /dev/null +++ b/MPW/MPW.rb @@ -0,0 +1,335 @@ +#!/usr/bin/ruby +# author: nishiki +# mail: nishiki@yaegashi.fr +# info: a simple script who manage your passwords + +module MPW + + require 'rubygems' + require 'gpgme' + require 'csv' + require 'i18n' + + class MPW + + ID = 0 + NAME = 1 + GROUP = 2 + PROTOCOL = 3 + SERVER = 4 + LOGIN = 5 + PASSWORD = 6 + PORT = 7 + COMMENT = 8 + DATE = 9 + + attr_accessor :error_msg + + # Constructor + def initialize(file_gpg, key=nil) + @error_msg = nil + @file_gpg = file_gpg + @key = key + end + + # Decrypt a gpg file + # @args: password -> the GPG key password + # @rtrn: true if data has been decrypted + def decrypt(passwd=nil) + @data = Array.new + + begin + if File.exist?(@file_gpg) + crypto = GPGME::Crypto.new(:armor => true) + data_decrypt = crypto.decrypt(IO.read(@file_gpg), :password => passwd).read + + data_decrypt.lines do |line| + @data.push(line.parse_csv) + end + end + + return true + rescue Exception => e + @error_msg = "#{I18n.t('error.gpg_file.decrypt')}\n#{e}" + return false + end + end + + # Encrypt a file + # @rtrn: true if the file has been encrypted + def encrypt() + begin + crypto = GPGME::Crypto.new(:armor => true) + file_gpg = File.open(@file_gpg, 'w+') + + data_to_encrypt = '' + @data.each do |row| + data_to_encrypt << row.to_csv + end + + crypto.encrypt(data_to_encrypt, :recipients => @key, :output => file_gpg) + file_gpg.close + + return true + rescue Exception => e + @error_msg = "#{I18n.t('error.gpg_file.encrypt')}\n#{e}" + return false + end + end + + # Search in some csv data + # @args: search -> the string to search + # protocol -> the connection protocol (ssh, web, other) + # @rtrn: a list with the resultat of the search + def search(search='', group=nil, protocol=nil) + result = Array.new() + + if !search.nil? + search = search.downcase + end + search = search.force_encoding('ASCII-8BIT') + + @data.each do |row| + name = row[NAME].nil? ? nil : row[NAME].downcase + server = row[SERVER].nil? ? nil : row[SERVER].downcase + comment = row[COMMENT].nil? ? nil : row[COMMENT].downcase + + if name =~ /^.*#{search}.*$/ || server =~ /^.*#{search}.*$/ || comment =~ /^.*#{search}.*$/ + if (protocol.nil? || protocol.eql?(row[PROTOCOL])) && (group.nil? || group.eql?(row[GROUP])) + result.push(row) + end + end + end + + return result + end + + # Search in some csv data + # @args: id -> the id item + # @rtrn: a row with the resultat of the search + def search_by_id(id) + @data.each do |row| + if row[ID] == id + return row + end + end + + return Array.new() + end + + # Update an item + # @args: id -> the item's identifiant + # name -> the item name + # group -> the item group + # server -> the ip or hostname + # protocol -> the protocol + # login -> the login + # passwd -> the password + # port -> the port + # comment -> a comment + # @rtrn: true if the item has been updated + def update(name, group, server, protocol, login, passwd, port, comment, id=nil) + row = Array.new() + update = false + + i = 0 + @data.each do |r| + if r[ID] == id + row = r + update = true + break + end + i += 1 + end + + if port.to_i <= 0 + port = nil + end + + row_update = Array.new() + row_update[DATE] = Time.now.to_i + + row_update[ID] = id.nil? || id.empty? ? MPW.password(16) : id + row_update[NAME] = name.nil? || name.empty? ? row[NAME] : name + row_update[GROUP] = group.nil? || group.empty? ? row[GROUP] : group + row_update[SERVER] = server.nil? || server.empty? ? row[SERVER] : server + row_update[PROTOCOL] = protocol.nil? || protocol.empty? ? row[PROTOCOL] : protocol + row_update[LOGIN] = login.nil? || login.empty? ? row[LOGIN] : login + row_update[PASSWORD] = passwd.nil? || passwd.empty? ? row[PASSWORD] : passwd + row_update[PORT] = port.nil? || port.empty? ? row[PORT] : port + row_update[COMMENT] = comment.nil? || comment.empty? ? row[COMMENT] : comment + + row_update[] = row_update[NAME].nil? ? nil : row_update[NAME].force_encoding('ASCII-8BIT') + row_update[] = row_update[GROUP].nil? ? nil : row_update[GROUP].force_encoding('ASCII-8BIT') + row_update[] = row_update[SERVER].nil? ? nil : row_update[SERVER].force_encoding('ASCII-8BIT') + row_update[] = row_update[PROTOCOL].nil? ? nil : row_update[PROTOCOL].force_encoding('ASCII-8BIT') + row_update[] = row_update[LOGIN].nil? ? nil : row_update[LOGIN].force_encoding('ASCII-8BIT') + row_update[] = row_update[PASSWORD].nil? ? nil : row_update[PASSWORD].force_encoding('ASCII-8BIT') + row_update[] = row_update[COMMENT].nil? ? nil : row_update[COMMENT].force_encoding('ASCII-8BIT') + + if row_update[NAME].nil? || row_update[NAME].empty? + @error_msg = I18n.t('error.update.name_empty') + return false + end + + if update + @data[i] = row_update + else + @data.push(row_update) + end + + return true + end + + # Remove an item + # @args: id -> the item's identifiant + # @rtrn: true if the item has been deleted + def remove(id) + i = 0 + @data.each do |row| + if row[ID] == id + @data.delete_at(i) + return true + end + i += 1 + end + + @error_msg = I18n.t('error.delete.id_no_exist', :id => id) + return false + end + + # Export to csv + # @args: file -> a string to match + # @rtrn: true if export work + def export(file) + begin + File.open(file, 'w+') do |file| + @data.each do |row| + row.delete_at(ID).delete_at(DATE) + file << row.to_csv + end + end + + return true + rescue Exception => e + @error_msg = "#{I18n.t('error.export.write', :file => file)}\n#{e}" + return false + end + end + + # Import to csv + # @args: file -> path to file import + # @rtrn: true if the import work + def import(file) + begin + data_new = IO.read(file) + data_new.lines do |line| + if not line =~ /(.*,){6}/ + @error_msg = I18n.t('error.import.bad_format') + return false + else + row = line.parse_csv.unshift(0) + if not update(row[NAME], row[GROUP], row[SERVER], row[PROTOCOL], row[LOGIN], row[PASSWORD], row[PORT], row[COMMENT]) + return false + end + end + end + + return true + rescue Exception => e + @error_msg = "#{I18n.t('error.import.read', :file => file)}\n#{e}" + return false + end + end + + # Return a preview import + # @args: file -> path to file import + # @rtrn: an array with the items to import, if there is an error return false + def import_preview(file) + begin + result = Array.new() + id = 0 + + data = IO.read(file) + data.lines do |line| + if not line =~ /(.*,){6}/ + @error_msg = I18n.t('error.import.bad_format') + return false + else + result.push(line.parse_csv.unshift(id)) + end + + id += 1 + end + + return result + rescue Exception => e + @error_msg = "#{I18n.t('error.import.read', :file => file)}\n#{e}" + return false + end + end + + # Sync remote data and local data + # @args: data_remote -> array with the data remote + # last_update -> last update + # @rtrn: false if data_remote is nil + def sync(data_remote, last_update) + if !data_remote.instance_of?(Array) + return false + end + + @data.each do |l| + j = 0 + update = false + + # Update item + data_remote.each do |r| + if l[ID] == r[ID] + if l[DATE].to_i < r[DATE].to_i + update(r[NAME], r[GROUP], r[SERVER], r[PROTOCOL], r[LOGIN], r[PASSWORD], r[PORT], r[COMMENT], l[ID]) + end + update = true + data_remote.delete_at(j) + break + end + j += 1 + end + + # Delete an old item + if !update && l[DATE].to_i < last_update + remove(l[ID]) + end + end + + # Add item + data_remote.each do |r| + if r[DATE].to_i > last_update + update(r[NAME], r[GROUP], r[SERVER], r[PROTOCOL], r[LOGIN], r[PASSWORD], r[PORT], r[COMMENT], r[ID]) + end + end + + return encrypt() + 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 + +end diff --git a/MPW/Sync/MPW.rb b/MPW/Sync/MPW.rb new file mode 100644 index 0000000..da72e43 --- /dev/null +++ b/MPW/Sync/MPW.rb @@ -0,0 +1,130 @@ +#!/usr/bin/ruby +# author: nishiki +# mail: nishiki@yaegashi.fr +# info: a simple script who manage your passwords + +module MPW + + module Sync + + require 'rubygems' + require 'i18n' + require 'socket' + require 'json' + + class MPW + + attr_accessor :error_msg + attr_accessor :enable + + # Constructor + def initialize() + @error_msg = nil + @enable = false + end + + # Connect to server + # @args: host -> the server host + # port -> ther connection port + # gpg_key -> the gpg key + # password -> the remote password + # suffix -> the suffix file + # @rtrn: false if the connection fail + def connect(host, port, gpg_key, password, suffix=nil) + @gpg_key = gpg_key + @password = password + @suffix = suffix + + begin + @socket = TCPSocket.new(host, port) + @enable = true + rescue Exception => e + @error_msg = "#{I18n.t('error.sync.connection')}\n#{e}" + @enable = false + end + + return @enable + end + + # Get data on server + # @args: gpg_password -> the gpg password + # @rtrn: nil if nothing data or error + def get(gpg_password) + if !@enable + return nil + end + + send_msg = {:action => 'get', + :gpg_key => @gpg_key, + :password => @password, + :suffix => @suffix} + + @socket.puts send_msg.to_json + msg = JSON.parse(@socket.gets) + + if !defined?(msg['error']) + @error_msg = I18n.t('error.sync.communication') + return nil + elsif msg['error'].nil? + tmp_file = "/tmp/mpw-#{MPW.password()}.gpg" + File.open(tmp_file, 'w') do |file| + file << msg['data'] + end + + @mpw = MPW.new(tmp_file) + if !@mpw.decrypt(gpg_password) + puts @mpw.error_msg + return nil + end + + File.unlink(tmp_file) + return @mpw.search() + else + @error_msg = I18n.t(msg['error']) + return nil + end + + end + + # Update the remote data + # @args: data -> the data to send on server + # @rtrn: false if there is a problem + def update(data) + if !@enable + return true + end + + send_msg = {:action => 'update', + :gpg_key => @gpg_key, + :password => @password, + :suffix => @suffix, + :data => data} + + @socket.puts send_msg.to_json + msg = JSON.parse(@socket.gets) + + if !defined?(msg['error']) + @error_msg = I18n.t('error.sync.communication') + return false + elsif msg['error'].nil? + return true + else + @error_msg = I18n.t(msg['error']) + return false + end + end + + # Close the connection + def close() + if !@enable + return + end + + send_msg = {:action => 'close'} + @socket.puts send_msg.to_json + end + end + + end + +end diff --git a/lib/Cli.rb b/MPW/UI/Cli.rb similarity index 86% rename from lib/Cli.rb rename to MPW/UI/Cli.rb index 152f3bf..870c9a2 100644 --- a/lib/Cli.rb +++ b/MPW/UI/Cli.rb @@ -10,9 +10,8 @@ require 'readline' require 'i18n' require 'yaml' -require "#{APP_ROOT}/lib/MPW.rb" -require "#{APP_ROOT}/lib/MPWConfig.rb" -require "#{APP_ROOT}/lib/Sync.rb" +require "#{APP_ROOT}/MPW/MPW" +require "#{APP_ROOT}/MPW/Sync/MPW" class Cli @@ -32,7 +31,7 @@ class Cli # @rtnr: true if the synchro is finish def sync() if !defined?(@sync) - @sync = Sync.new() + @sync = MPW::Sync::MPW.new if !@config.sync_host.nil? && !@config.sync_port.nil? if !@sync.connect(@config.sync_host, @config.sync_port, @config.key, @config.sync_pwd, @config.sync_suffix) @@ -99,7 +98,7 @@ class Cli # Request the GPG password and decrypt the file def decrypt() if !defined?(@mpw) - @mpw = MPW.new(@config.file_gpg, @config.key) + @mpw = MPW::MPW.new(@config.file_gpg, @config.key) end @passwd = ask(I18n.t('display.gpg_password')) {|q| q.echo = false} @@ -132,30 +131,30 @@ class Cli # @args: item -> an array with the item information def displayFormat(item) puts '--------------------' - puts "Id: #{item[MPW::ID]}" - puts "#{I18n.t('display.name')}: #{item[MPW::NAME]}" - puts "#{I18n.t('display.group')}: #{item[MPW::GROUP]}" - puts "#{I18n.t('display.server')}: #{item[MPW::SERVER]}" - puts "#{I18n.t('display.protocol')}: #{item[MPW::PROTOCOL]}" - puts "#{I18n.t('display.login')}: #{item[MPW::LOGIN]}" - puts "#{I18n.t('display.password')}: #{item[MPW::PASSWORD]}" - puts "#{I18n.t('display.port')}: #{item[MPW::PORT]}" - puts "#{I18n.t('display.comment')}: #{item[MPW::COMMENT]}" + puts "Id: #{item[MPW::MPW::ID]}" + puts "#{I18n.t('display.name')}: #{item[MPW::MPW::NAME]}" + puts "#{I18n.t('display.group')}: #{item[MPW::MPW::GROUP]}" + puts "#{I18n.t('display.server')}: #{item[MPW::MPW::SERVER]}" + puts "#{I18n.t('display.protocol')}: #{item[MPW::MPW::PROTOCOL]}" + puts "#{I18n.t('display.login')}: #{item[MPW::MPW::LOGIN]}" + puts "#{I18n.t('display.password')}: #{item[MPW::MPW::PASSWORD]}" + puts "#{I18n.t('display.port')}: #{item[MPW::MPW::PORT]}" + puts "#{I18n.t('display.comment')}: #{item[MPW::MPW::COMMENT]}" end # Display an item in the alternative format # @args: item -> an array with the item information def displayFormatAlt(item) - port = item[MPW::PORT].nil? ? '' : ":#{item[MPW::PORT]}" + port = item[MPW::MPW::PORT].nil? ? '' : ":#{item[MPW::MPW::PORT]}" - if item[MPW::PASSWORD].nil? || item[MPW::PASSWORD].empty? - if item[MPW::LOGIN].include('@') - puts "# #{item[MPW::ID]} #{item[MPW::PROTOCOL]}://#{item[MPW::LOGIN]}@#{item[MPW::SERVER]}#{port}" + if item[MPW::MPW::PASSWORD].nil? || item[MPW::MPW::PASSWORD].empty? + if item[MPW::MPW::LOGIN].include('@') + puts "# #{item[MPW::MPW::ID]} #{item[MPW::MPW::PROTOCOL]}://#{item[MPW::MPW::LOGIN]}@#{item[MPW::MPW::SERVER]}#{port}" else - puts "# #{item[MPW::ID]} #{item[MPW::PROTOCOL]}://{#{item[MPW::LOGIN]}}@#{item[MPW::SERVER]}#{port}" + puts "# #{item[MPW::MPW::ID]} #{item[MPW::MPW::PROTOCOL]}://{#{item[MPW::MPW::LOGIN]}}@#{item[MPW::MPW::SERVER]}#{port}" end else - puts "# #{item[MPW::ID]} #{item[MPW::PROTOCOL]}://{#{item[MPW::LOGIN]}:#{item[MPW::PASSWORD]}}@#{item[MPW::SERVER]}#{port}" + puts "# #{item[MPW::MPW::ID]} #{item[MPW::MPW::PROTOCOL]}://{#{item[MPW::MPW::LOGIN]}:#{item[MPW::MPW::PASSWORD]}}@#{item[MPW::MPW::SERVER]}#{port}" end end @@ -193,14 +192,14 @@ class Cli if not row.empty? puts I18n.t('form.update.title') puts '--------------------' - name = ask(I18n.t('form.update.name' , :name => row[MPW::NAME])).to_s - group = ask(I18n.t('form.update.group' , :group => row[MPW::GROUP])).to_s - server = ask(I18n.t('form.update.server' , :server => row[MPW::SERVER])).to_s - protocol = ask(I18n.t('form.update.protocol', :protocol => row[MPW::PROTOCOL])).to_s - login = ask(I18n.t('form.update.login' , :login => row[MPW::LOGIN])).to_s + name = ask(I18n.t('form.update.name' , :name => row[MPW::MPW::NAME])).to_s + group = ask(I18n.t('form.update.group' , :group => row[MPW::MPW::GROUP])).to_s + server = ask(I18n.t('form.update.server' , :server => row[MPW::MPW::SERVER])).to_s + protocol = ask(I18n.t('form.update.protocol', :protocol => row[MPW::MPW::PROTOCOL])).to_s + login = ask(I18n.t('form.update.login' , :login => row[MPW::MPW::LOGIN])).to_s passwd = ask(I18n.t('form.update.password')).to_s - port = ask(I18n.t('form.update.port' , :port => row[MPW::PORT])).to_s - comment = ask(I18n.t('form.update.comment' , :comment => row[MPW::COMMENT])).to_s + port = ask(I18n.t('form.update.port' , :port => row[MPW::MPW::PORT])).to_s + comment = ask(I18n.t('form.update.comment' , :comment => row[MPW::MPW::COMMENT])).to_s if @mpw.update(name, group, server, protocol, login, passwd, port, comment, id) if @mpw.encrypt() diff --git a/lib/CliSSH.rb b/MPW/UI/CliSSH.rb similarity index 74% rename from lib/CliSSH.rb rename to MPW/UI/CliSSH.rb index ebcea04..9292ca8 100644 --- a/lib/CliSSH.rb +++ b/MPW/UI/CliSSH.rb @@ -3,7 +3,7 @@ # mail: nishiki@yaegashi.fr # info: a simple script who manage your passwords -require "#{APP_ROOT}/lib/Cli.rb" +require "#{APP_ROOT}/MPW/UI/Cli" class CliSSH < Cli @@ -16,11 +16,11 @@ class CliSSH < Cli if result.length > 0 result.each do |r| - server = @server.nil? ? r[MPW::SERVER] : @server - port = @port.nil? ? r[MPW::PORT] : @port - login = @login.nil? ? r[MPW::LOGIN] : @login + server = @server.nil? ? r[MPW::MPW::SERVER] : @server + port = @port.nil? ? r[MPW::MPW::PORT] : @port + login = @login.nil? ? r[MPW::MPW::LOGIN] : @login - passwd = r[MPW::PASSWORD] + passwd = r[MPW::MPW::PASSWORD] if port.nil? || port.empty? port = 22 diff --git a/lib/MPW.rb b/lib/MPW.rb deleted file mode 100644 index f82fcc2..0000000 --- a/lib/MPW.rb +++ /dev/null @@ -1,331 +0,0 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr -# info: a simple script who manage your passwords - -require 'rubygems' -require 'gpgme' -require 'csv' -require 'i18n' - -class MPW - - ID = 0 - NAME = 1 - GROUP = 2 - PROTOCOL = 3 - SERVER = 4 - LOGIN = 5 - PASSWORD = 6 - PORT = 7 - COMMENT = 8 - DATE = 9 - - attr_accessor :error_msg - - # Constructor - def initialize(file_gpg, key=nil) - @error_msg = nil - @file_gpg = file_gpg - @key = key - end - - # Decrypt a gpg file - # @args: password -> the GPG key password - # @rtrn: true if data has been decrypted - def decrypt(passwd=nil) - @data = Array.new - - begin - if File.exist?(@file_gpg) - crypto = GPGME::Crypto.new(:armor => true) - data_decrypt = crypto.decrypt(IO.read(@file_gpg), :password => passwd).read - - data_decrypt.lines do |line| - @data.push(line.parse_csv) - end - end - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.gpg_file.decrypt')}\n#{e}" - return false - end - end - - # Encrypt a file - # @rtrn: true if the file has been encrypted - def encrypt() - begin - crypto = GPGME::Crypto.new(:armor => true) - file_gpg = File.open(@file_gpg, 'w+') - - data_to_encrypt = '' - @data.each do |row| - data_to_encrypt << row.to_csv - end - - crypto.encrypt(data_to_encrypt, :recipients => @key, :output => file_gpg) - file_gpg.close - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.gpg_file.encrypt')}\n#{e}" - return false - end - end - - # Search in some csv data - # @args: search -> the string to search - # protocol -> the connection protocol (ssh, web, other) - # @rtrn: a list with the resultat of the search - def search(search='', group=nil, protocol=nil) - result = Array.new() - - if !search.nil? - search = search.downcase - end - search = search.force_encoding('ASCII-8BIT') - - @data.each do |row| - name = row[NAME].nil? ? nil : row[NAME].downcase - server = row[SERVER].nil? ? nil : row[SERVER].downcase - comment = row[COMMENT].nil? ? nil : row[COMMENT].downcase - - if name =~ /^.*#{search}.*$/ || server =~ /^.*#{search}.*$/ || comment =~ /^.*#{search}.*$/ - if (protocol.nil? || protocol.eql?(row[PROTOCOL])) && (group.nil? || group.eql?(row[GROUP])) - result.push(row) - end - end - end - - return result - end - - # Search in some csv data - # @args: id -> the id item - # @rtrn: a row with the resultat of the search - def search_by_id(id) - @data.each do |row| - if row[ID] == id - return row - end - end - - return Array.new() - end - - # Update an item - # @args: id -> the item's identifiant - # name -> the item name - # group -> the item group - # server -> the ip or hostname - # protocol -> the protocol - # login -> the login - # passwd -> the password - # port -> the port - # comment -> a comment - # @rtrn: true if the item has been updated - def update(name, group, server, protocol, login, passwd, port, comment, id=nil) - row = Array.new() - update = false - - i = 0 - @data.each do |r| - if r[ID] == id - row = r - update = true - break - end - i += 1 - end - - if port.to_i <= 0 - port = nil - end - - row_update = Array.new() - row_update[DATE] = Time.now.to_i - - row_update[ID] = id.nil? || id.empty? ? MPW.password(16) : id - row_update[NAME] = name.nil? || name.empty? ? row[NAME] : name - row_update[GROUP] = group.nil? || group.empty? ? row[GROUP] : group - row_update[SERVER] = server.nil? || server.empty? ? row[SERVER] : server - row_update[PROTOCOL] = protocol.nil? || protocol.empty? ? row[PROTOCOL] : protocol - row_update[LOGIN] = login.nil? || login.empty? ? row[LOGIN] : login - row_update[PASSWORD] = passwd.nil? || passwd.empty? ? row[PASSWORD] : passwd - row_update[PORT] = port.nil? || port.empty? ? row[PORT] : port - row_update[COMMENT] = comment.nil? || comment.empty? ? row[COMMENT] : comment - - row_update[] = row_update[NAME].nil? ? nil : row_update[NAME].force_encoding('ASCII-8BIT') - row_update[] = row_update[GROUP].nil? ? nil : row_update[GROUP].force_encoding('ASCII-8BIT') - row_update[] = row_update[SERVER].nil? ? nil : row_update[SERVER].force_encoding('ASCII-8BIT') - row_update[] = row_update[PROTOCOL].nil? ? nil : row_update[PROTOCOL].force_encoding('ASCII-8BIT') - row_update[] = row_update[LOGIN].nil? ? nil : row_update[LOGIN].force_encoding('ASCII-8BIT') - row_update[] = row_update[PASSWORD].nil? ? nil : row_update[PASSWORD].force_encoding('ASCII-8BIT') - row_update[] = row_update[COMMENT].nil? ? nil : row_update[COMMENT].force_encoding('ASCII-8BIT') - - if row_update[NAME].nil? || row_update[NAME].empty? - @error_msg = I18n.t('error.update.name_empty') - return false - end - - if update - @data[i] = row_update - else - @data.push(row_update) - end - - return true - end - - # Remove an item - # @args: id -> the item's identifiant - # @rtrn: true if the item has been deleted - def remove(id) - i = 0 - @data.each do |row| - if row[ID] == id - @data.delete_at(i) - return true - end - i += 1 - end - - @error_msg = I18n.t('error.delete.id_no_exist', :id => id) - return false - end - - # Export to csv - # @args: file -> a string to match - # @rtrn: true if export work - def export(file) - begin - File.open(file, 'w+') do |file| - @data.each do |row| - row.delete_at(ID).delete_at(DATE) - file << row.to_csv - end - end - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.export.write', :file => file)}\n#{e}" - return false - end - end - - # Import to csv - # @args: file -> path to file import - # @rtrn: true if the import work - def import(file) - begin - data_new = IO.read(file) - data_new.lines do |line| - if not line =~ /(.*,){6}/ - @error_msg = I18n.t('error.import.bad_format') - return false - else - row = line.parse_csv.unshift(0) - if not update(row[NAME], row[GROUP], row[SERVER], row[PROTOCOL], row[LOGIN], row[PASSWORD], row[PORT], row[COMMENT]) - return false - end - end - end - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.import.read', :file => file)}\n#{e}" - return false - end - end - - # Return a preview import - # @args: file -> path to file import - # @rtrn: an array with the items to import, if there is an error return false - def import_preview(file) - begin - result = Array.new() - id = 0 - - data = IO.read(file) - data.lines do |line| - if not line =~ /(.*,){6}/ - @error_msg = I18n.t('error.import.bad_format') - return false - else - result.push(line.parse_csv.unshift(id)) - end - - id += 1 - end - - return result - rescue Exception => e - @error_msg = "#{I18n.t('error.import.read', :file => file)}\n#{e}" - return false - end - end - - # Sync remote data and local data - # @args: data_remote -> array with the data remote - # last_update -> last update - # @rtrn: false if data_remote is nil - def sync(data_remote, last_update) - if !data_remote.instance_of?(Array) - return false - end - - @data.each do |l| - j = 0 - update = false - - # Update item - data_remote.each do |r| - if l[ID] == r[ID] - if l[DATE].to_i < r[DATE].to_i - update(r[NAME], r[GROUP], r[SERVER], r[PROTOCOL], r[LOGIN], r[PASSWORD], r[PORT], r[COMMENT], l[ID]) - end - update = true - data_remote.delete_at(j) - break - end - j += 1 - end - - # Delete an old item - if !update && l[DATE].to_i < last_update - remove(l[ID]) - end - end - - # Add item - data_remote.each do |r| - if r[DATE].to_i > last_update - update(r[NAME], r[GROUP], r[SERVER], r[PROTOCOL], r[LOGIN], r[PASSWORD], r[PORT], r[COMMENT], r[ID]) - end - end - - return encrypt() - 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 diff --git a/lib/MPWConfig.rb b/lib/MPWConfig.rb deleted file mode 100644 index bce49e6..0000000 --- a/lib/MPWConfig.rb +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr -# info: a simple script who manage your passwords - -require 'rubygems' -require 'yaml' -require 'i18n' - -class MPWConfig - - attr_accessor :error_msg - - attr_accessor :key - attr_accessor :lang - attr_accessor :file_gpg - attr_accessor :timeout_pwd - attr_accessor :last_update - attr_accessor :sync_host - attr_accessor :sync_port - attr_accessor :sync_pwd - attr_accessor :sync_suffix - attr_accessor :last_update - - # Constructor - # @args: file_config -> the specify config file - def initialize(file_config=nil) - @error_msg = nil - @file_config = "#{Dir.home()}/.mpw.cfg" - - if !file_config.nil? && !file_config.empty? - @file_config = file_config - end - end - - # Create a new config file - # @args: key -> the gpg key to encrypt - # lang -> the software language - # file_gpg -> the file who is encrypted - # timeout_pwd -> time to save the password - # sync_host -> the server host for synchronization - # sync_port -> the server port 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, lang, file_gpg, timeout_pwd, sync_host=nil, sync_port=nil, sync_pwd=nil, sync_suffix=nil) - - 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 file_gpg.empty? - file_gpg = "#{Dir.home()}/.mpw.gpg" - end - - timeout_pwd = timeout_pwd.empty? ? 60 : timeout_pwd.to_i - - config = {'config' => {'key' => key, - 'lang' => lang, - 'file_gpg' => file_gpg, - 'timeout_pwd' => timeout_pwd, - 'sync_host' => sync_host, - 'sync_port' => sync_port, - 'sync_pwd' => sync_pwd, - 'sync_suffix' => sync_suffix, - 'last_update' => 0 }} - - begin - File.open(@file_config, 'w') do |file| - file << config.to_yaml - end - rescue Exception => e - @error_msg = "#{I18n.t('error.config.write')}\n#{e}" - return false - end - - return true - end - - # Check the config file - # @rtrn: true if the config file is correct - def checkconfig() - begin - config = YAML::load_file(@file_config) - @key = config['config']['key'] - @lang = config['config']['lang'] - @file_gpg = config['config']['file_gpg'] - @timeout_pwd = config['config']['timeout_pwd'].to_i - @sync_host = config['config']['sync_host'] - @sync_port = config['config']['sync_port'] - @sync_pwd = config['config']['sync_pwd'] - @sync_suffix = config['config']['sync_suffix'] - @last_update = config['config']['last_update'].to_i - - if @key.empty? || @file_gpg.empty? - @error_msg = I18n.t('error.config.check') - return false - end - - I18n.locale = @lang.to_sym - - rescue Exception => e - @error_msg = "#{I18n.t('error.config.check')}\n#{e}" - return false - end - - return true - end - - # Set the last update when there is a sync - # @rtrn: true is the file has been updated - def set_last_update() - config = {'config' => {'key' => @key, - 'lang' => @lang, - 'file_gpg' => @file_gpg, - 'timeout_pwd' => @timeout_pwd, - 'sync_host' => @sync_host, - 'sync_port' => @sync_port, - 'sync_pwd' => @sync_pwd, - 'sync_suffix' => @sync_suffix, - 'last_update' => Time.now.to_i }} - - begin - File.open(@file_config, 'w') do |file| - file << config.to_yaml - end - rescue Exception => e - @error_msg = "#{I18n.t('error.config.write')}\n#{e}" - return false - end - - return true - end - -end diff --git a/lib/Sync.rb b/lib/Sync.rb deleted file mode 100644 index e85b0ce..0000000 --- a/lib/Sync.rb +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr -# info: a simple script who manage your passwords - -require 'rubygems' -require 'i18n' -require 'socket' -require 'json' - -require "#{APP_ROOT}/lib/MPW.rb" - -class Sync - - attr_accessor :error_msg - attr_accessor :enable - - # Constructor - def initialize() - @error_msg = nil - @enable = false - end - - # Connect to server - # @args: host -> the server host - # port -> ther connection port - # gpg_key -> the gpg key - # password -> the remote password - # suffix -> the suffix file - # @rtrn: false if the connection fail - def connect(host, port, gpg_key, password, suffix=nil) - @gpg_key = gpg_key - @password = password - @suffix = suffix - - begin - @socket = TCPSocket.new(host, port) - @enable = true - rescue Exception => e - @error_msg = "#{I18n.t('error.sync.connection')}\n#{e}" - @enable = false - end - - return @enable - end - - # Get data on server - # @args: gpg_password -> the gpg password - # @rtrn: nil if nothing data or error - def get(gpg_password) - if !@enable - return nil - end - - send_msg = {:action => 'get', - :gpg_key => @gpg_key, - :password => @password, - :suffix => @suffix} - - @socket.puts send_msg.to_json - msg = JSON.parse(@socket.gets) - - if !defined?(msg['error']) - @error_msg = I18n.t('error.sync.communication') - return nil - elsif msg['error'].nil? - tmp_file = "/tmp/mpw-#{MPW.password()}.gpg" - File.open(tmp_file, 'w') do |file| - file << msg['data'] - end - - @mpw = MPW.new(tmp_file) - if !@mpw.decrypt(gpg_password) - puts @mpw.error_msg - return nil - end - - File.unlink(tmp_file) - return @mpw.search() - else - @error_msg = I18n.t(msg['error']) - return nil - end - - end - - # Update the remote data - # @args: data -> the data to send on server - # @rtrn: false if there is a problem - def update(data) - if !@enable - return true - end - - send_msg = {:action => 'update', - :gpg_key => @gpg_key, - :password => @password, - :suffix => @suffix, - :data => data} - - @socket.puts send_msg.to_json - msg = JSON.parse(@socket.gets) - - if !defined?(msg['error']) - @error_msg = I18n.t('error.sync.communication') - return false - elsif msg['error'].nil? - return true - else - @error_msg = I18n.t(msg['error']) - return false - end - end - - # TODO - def delete() - end - - # Close the connection - def close() - if !@enable - return - end - - send_msg = {:action => 'close'} - @socket.puts send_msg.to_json - end -end diff --git a/mpw b/mpw index ff27575..ae59416 100755 --- a/mpw +++ b/mpw @@ -10,8 +10,8 @@ require 'locale' require 'i18n' APP_ROOT = File.dirname(Pathname.new(__FILE__).realpath) -require "#{APP_ROOT}/lib/Cli.rb" -require "#{APP_ROOT}/lib/MPW.rb" +require "#{APP_ROOT}/MPW/UI/Cli" +require "#{APP_ROOT}/MPW/Config" lang = Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1] @@ -93,7 +93,7 @@ OptionParser.new do |opts| end end.parse! -config = MPWConfig.new(options[:config]) +config = MPW::Config.new(options[:config]) check_error = config.checkconfig() cli = Cli.new(lang, config) diff --git a/mpw-ssh b/mpw-ssh index 39fe1b3..9ba3791 100755 --- a/mpw-ssh +++ b/mpw-ssh @@ -10,7 +10,8 @@ require 'locale' require 'i18n' APP_ROOT = File.dirname(Pathname.new(__FILE__).realpath) -require "#{APP_ROOT}/lib/CliSSH.rb" +require "#{APP_ROOT}/MPW/UI/CliSSH" +require "#{APP_ROOT}/MPW/Config" lang = Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1] @@ -45,7 +46,7 @@ OptionParser.new do |opts| end end.parse! -config = MPWConfig.new(options[:config]) +config = MPW::Config.new(options[:config]) check_error = config.checkconfig() cli = CliSSH.new(lang, config)