diff --git a/VERSION b/VERSION index 2cd3aa1..3eebe6a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 - stable +1.1.0 - dev diff --git a/i18n/en.yml b/i18n/en.yml index 6eecd3c..7805f04 100644 --- a/i18n/en.yml +++ b/i18n/en.yml @@ -1,8 +1,6 @@ --- en: error: - add: - name_empty: "You must define a name!" config: write: "Can't write the config file!" check: "Checkconfig failed!" @@ -18,7 +16,12 @@ en: bad_format: "Can't import, the file is badly formated!" read: "Can't import, unable to read %{file}!" update: - id_no_exist: "Can't update the item, the item %{id} doesn't exist!" + name_empty: "You must define a name!" + sync: + connection: "Connection fail!" + no_data: "Nothing data!" + not_authorized: "You haven't the access to remote file!" + unknown: "An unknown error is occured!" cli: option: usage: "Usage" @@ -101,6 +104,8 @@ en: port: "Port" protocol: "Protocol" server: "Server" + sync: + not_connect: "The server connection fail!" ssh: option: usage: "Usage" @@ -111,6 +116,25 @@ en: display: connect: "Connection to:" nothing: "Nothing result!" + server: + option: + usage: "Usage" + config: "Specifie the configuration file" + checkconfig: "Check the configuration" + setup: "Setup a new configuration file" + help: "Show this message help" + checkconfig: + fail: "Checkconfig failed:!" + empty: "ERROR: an importe option is missing!" + datadir: "ERROR: le data directory doesn't exist!" + form: + setup: + title: "Serveur configuration" + host: "IP listen: " + port: "Port listen: " + data_dir: "Data directory: " + timeout: "Timeout to second: " + not_valid: "ERROR: Impossible to write the configuration file!" formats: default: ! '%Y-%m-%d' long: ! '%B %d, %Y' diff --git a/i18n/fr.yml b/i18n/fr.yml index ba433fb..9005238 100644 --- a/i18n/fr.yml +++ b/i18n/fr.yml @@ -1,8 +1,6 @@ --- fr: error: - add: - name_empty: "Vous devez définir un nom!" config: write: "Impossible d'écrire le fichier de configuration!" check: "Le fichier de configuration est invalide!" @@ -18,7 +16,12 @@ fr: bad_format: "Impossible d'importer le fichier car son format est incorrect!" read: "Impossible d'importer le fichier %{file}, car il n'est pas lisible!" update: - id_no_exist: "Impossible de mettre à jour l'élément %{id}, car il n'existe pas!" + name_empty: "Vous devez définir un nom!" + sync: + connection: "La connexion n'a pu être établie" + no_data: "Aucune data!" + not_authorized: "Vous n'avez pas les autorisations d'accès au fichier distant!" + unknown: "Une erreur inconnue est survenue!" cli: option: usage: "Utilisation" @@ -101,6 +104,8 @@ fr: port: "Port" protocol: "Protocol" server: "Serveur" + sync: + not_connect: "La connexion au serveur n'a pu être établie!" ssh: option: usage: "Utilisation" @@ -111,6 +116,25 @@ fr: display: connect: "Connexion à:" nothing: "Aucun résultat!" + server: + option: + usage: "Utilisation" + config: "Spécifie le fichier de configuration" + checkconfig: "Vérifie le fichier de configuration" + setup: "Permet de générer un nouveau fichier de configuration" + help: "Affiche ce message d'aide" + checkconfig: + fail: "Le fichier de configuration est invalide!" + empty: "ERREUR: Une option importante est manquante!" + datadir: "ERREUR: Le répertoire des données n'existe pas!" + form: + setup: + title: "Configuration du serveur" + host: "IP d'écoute: " + port: "Port d'écoute: " + data_dir: "Répertoire des données: " + timeout: "Timeout en seconde: " + not_valid: "ERREUR: Impossible d'écire le fichier de configuration!" formats: default: ! '%Y-%m-%d' long: ! '%B %d, %Y' diff --git a/lib/Cli.rb b/lib/Cli.rb index cc67cd1..31c4a57 100644 --- a/lib/Cli.rb +++ b/lib/Cli.rb @@ -8,24 +8,47 @@ require 'highline/import' require 'pathname' require 'readline' require 'i18n' +require 'yaml' require "#{APP_ROOT}/lib/MPW.rb" +require "#{APP_ROOT}/lib/MPWConfig.rb" +require "#{APP_ROOT}/lib/Sync.rb" class Cli # Constructor # @args: lang -> the operating system language # config_file -> a specify config file - def initialize(lang, config_file=nil) - @m = MPW.new(config_file) - - if not @m.checkconfig() - self.setup(lang) + def initialize(lang, config) + @config = config + + @mpw = MPW.new(@config.file_gpg, @config.key) + if not decrypt() + puts "#{I18n.t('cli.display.error')}: #{@mpw.error_msg}" + exit 2 end - if not self.decrypt() - puts "#{I18n.t('cli.display.error')}: #{@m.error_msg}" - exit 2 + @sync = Sync.new() + if @config.sync_host.nil? || @config.sync_port.nil? + @sync.disable() + elsif !@sync.connect(@config.sync_host, @config.sync_port, @config.key, @config.sync_pwd, @config.sync_suffix) + puts "#{I18n.t('cli.sync.not_connect')}:\n#{@sync.error_msg}" + end + end + + # Destructor + def finalize() + @sync.close() + end + + # Sync the data with the server + def sync() + begin + @mpw.sync(@sync.get(@passwd), @config.last_update) + @sync.update(File.open(@config.file_gpg).read) + @config.setLastUpdate() + rescue Exception => e + puts "#{I18n.t('cli.sync.error')}:\n#{e}" end end @@ -44,35 +67,35 @@ class Cli end I18n.locale = language.to_sym - if @m.setup(key, language, file_gpg, timeout_pwd) + if @config.setup(key, language, file_gpg, timeout_pwd) puts I18n.t('cli.form.setup.valid') else - puts "#{I18n.t('cli.display.error')}: #{@m.error_msg}" + puts "#{I18n.t('cli.display.error')}: #{@config.error_msg}" end - if not @m.checkconfig() - puts "#{I18n.t('cli.display.error')}: #{@m.error_msg}" + if not @config.checkconfig() + puts "#{I18n.t('cli.display.error')}: #{@config.error_msg}" end end # Request the GPG password and decrypt the file def decrypt() @passwd = ask(I18n.t('cli.display.gpg_password')) {|q| q.echo = false} - return @m.decrypt(@passwd) + return @mpw.decrypt(@passwd) end # Display the query's result # @args: search -> the string to search # protocol -> search from a particular protocol def display(search, protocol=nil, group=nil, format=nil) - result = @m.search(search, group, protocol) + result = @mpw.search(search, group, protocol) if not result.empty? result.each do |r| if format.nil? || !format - self.displayFormat(r) + displayFormat(r) else - self.displayFormatAlt(r) + displayFormatAlt(r) end end else @@ -125,21 +148,21 @@ class Cli port = ask(I18n.t('cli.form.add.port')).to_s comment = ask(I18n.t('cli.form.add.comment')).to_s - if @m.add(name, group, server, protocol, login, passwd, port, comment) - if @m.encrypt() + if @mpw.update(name, group, server, protocol, login, passwd, port, comment) + if @mpw.encrypt() puts I18n.t('cli.form.add.valid') else - puts "#{I18n.t('cli.display.error')}: #{@m.error_msg}" + puts "#{I18n.t('cli.display.error')}: #{@mpw.error_msg}" end else - puts "#{I18n.t('cli.display.error')}: #{@m.error_msg}" + puts "#{I18n.t('cli.display.error')}: #{@mpw.error_msg}" end end # Update an item # @args: id -> the item's id def update(id) - row = @m.searchById(id) + row = @mpw.searchById(id) if not row.empty? puts I18n.t('cli.form.update.title') @@ -153,14 +176,14 @@ class Cli port = ask(I18n.t('cli.form.update.port' , :port => row[MPW::PORT])).to_s comment = ask(I18n.t('cli.form.update.comment' , :comment => row[MPW::COMMENT])).to_s - if @m.update(id, name, group, server, protocol, login, passwd, port, comment) - if @m.encrypt() + if @mpw.update(name, group, server, protocol, login, passwd, port, comment, id) + if @mpw.encrypt() puts I18n.t('cli.form.update.valid') else - puts "#{I18n.t('cli.display.error')}: #{@m.error_msg}" + puts "#{I18n.t('cli.display.error')}: #{@mpw.error_msg}" end else - puts "#{I18n.t('cli.display.error')}: #{@m.error_msg}" + puts "#{I18n.t('cli.display.error')}: #{@mpw.error_msg}" end else puts I18n.t('cli.display.nothing') @@ -172,10 +195,10 @@ class Cli # force -> no resquest a validation def remove(id, force=false) if not force - result = @m.searchById(id) + result = @mpw.searchById(id) if result.length > 0 - self.displayFormat(result) + displayFormat(result) confirm = ask("#{I18n.t('cli.form.delete.ask', :id => id)} (y/N) ").to_s if confirm =~ /^(y|yes|YES|Yes|Y)$/ @@ -187,11 +210,11 @@ class Cli end if force - if @m.remove(id) - if @m.encrypt() + if @mpw.remove(id) + if @mpw.encrypt() puts I18n.t('cli.form.delete.valid', :id => id) else - puts "#{I18n.t('cli.display.error')}: #{@m.error_msg}" + puts "#{I18n.t('cli.display.error')}: #{@mpw.error_msg}" end else puts I18n.t('cli.form.delete.not_valid') @@ -202,10 +225,10 @@ class Cli # Export the items in a CSV file # @args: file -> the destination file def export(file) - if @m.export(file) + if @mpw.export(file) puts "The export in #{file} is succesfull!" else - puts "#{I18n.t('cli.display.error')}: #{@m.error_msg}" + puts "#{I18n.t('cli.display.error')}: #{@mpw.error_msg}" end end @@ -214,12 +237,12 @@ class Cli # @args: file -> the import file # force -> no resquest a validation def import(file, force=false) - result = @m.importPreview(file) + result = @mpw.importPreview(file) if not force if result.is_a?(Array) && !result.empty? result.each do |r| - self.displayFormat(r) + displayFormat(r) end confirm = ask("#{I18n.t('cli.form.import.ask', :file => file)} (y/N) ").to_s @@ -232,10 +255,10 @@ class Cli end if force - if @m.import(file) && @m.encrypt() + if @mpw.import(file) && @mpw.encrypt() puts I18n.t('cli.form.import.valid') else - puts "#{I18n.t('cli.display.error')}: #{@m.error_msg}" + puts "#{I18n.t('cli.display.error')}: #{@mpw.error_msg}" end end end @@ -247,7 +270,7 @@ class Cli while buf = Readline.readline(' ', true) - if @m.timeout_pwd < Time.now.to_i - last_access + if @config.timeout_pwd < Time.now.to_i - last_access passwd_confirm = ask(I18n.t('cli.interactive.ask_password')) {|q| q.echo = false} if @passwd.eql?(passwd_confirm) @@ -265,17 +288,17 @@ class Cli case command[0] when 'display', 'show', 'd', 's' if !command[1].nil? && !command[1].empty? - self.display(command[1], group, command[2]) + display(command[1], group, command[2]) end when 'add', 'a' add() when 'update', 'u' if !command[1].nil? && !command[1].empty? - self.update(command[1]) + update(command[1]) end when 'remove', 'delete', 'r', 'd' if !command[1].nil? && !command[1].empty? - self.remove(command[1]) + remove(command[1]) end when 'group', 'g' if !command[1].nil? && !command[1].empty? diff --git a/lib/MPW.rb b/lib/MPW.rb index f09f438..6886b85 100644 --- a/lib/MPW.rb +++ b/lib/MPW.rb @@ -6,7 +6,6 @@ require 'rubygems' require 'gpgme' require 'csv' -require 'yaml' require 'i18n' class MPW @@ -20,80 +19,15 @@ class MPW PASSWORD = 6 PORT = 7 COMMENT = 8 + DATE = 9 attr_accessor :error_msg - attr_accessor :timeout_pwd - + # 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 - # @rtrn: true if le config file is create - def setup(key, lang, file_gpg, timeout_pwd) - - 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.empty? ? (timeout_pwd = 60) : (timeout_pwd = timeout_pwd.to_i) - - config = {'config' => {'key' => key, - 'lang' => lang, - 'file_gpg' => file_gpg, - 'timeout_pwd' => timeout_pwd}} - - 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 - - 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 + def initialize(file_gpg, key=nil) + @error_msg = nil + @file_gpg = file_gpg + @key = key end # Decrypt a gpg file @@ -107,10 +41,8 @@ class MPW crypto = GPGME::Crypto.new(:armor => true) data_decrypt = crypto.decrypt(IO.read(@file_gpg), :password => passwd).read - id = 0 data_decrypt.lines do |line| - @data[id] = line.parse_csv.unshift(id) - id += 1; + @data.push(line.parse_csv) end end @@ -130,7 +62,7 @@ class MPW data_to_encrypt = '' @data.each do |row| - data_to_encrypt << row.drop(1).to_csv + data_to_encrypt << row.to_csv end crypto.encrypt(data_to_encrypt, :recipients => @key, :output => file_gpg) @@ -147,7 +79,7 @@ class MPW # @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) + def search(search='', group=nil, protocol=nil) result = Array.new() if !search.nil? @@ -174,57 +106,15 @@ class MPW # @args: id -> the id item # @rtrn: a row with the resultat of the search def searchById(id) - if not @data[id.to_i].nil? - return @data[id.to_i] - else - return Array.new + @data.each do |row| + if row[ID] == id + return row + end end + + return Array.new() end - # Add a new item - # @args: name -> the item name - # group -> the item group - # server -> the ip or server - # protocol -> the protocol - # login -> the login - # passwd -> the password - # port -> the port - # comment -> a comment - # @rtrn: true if it works - def add(name, group=nil, server=nil, protocol=nil, login=nil, passwd=nil, port=nil, comment=nil) - row = Array.new() - - if name.nil? || name.empty? - @error_msg = I18n.t('error.add.name_empty') - return false - end - - if port.to_i <= 0 - port = nil - end - - if not @data.last.nil? - id = @data.last - id = id[ID].to_i + 1 - else - id = 0 - end - - row[ID] = id - row[PORT] = port - row[NAME] = name.force_encoding('ASCII-8BIT') - group.nil? || group.empty? ? (row[GROUP] = nil) : (row[GROUP] = group.force_encoding('ASCII-8BIT')) - server.nil? || server.empty? ? (row[SERVER] = nil) : (row[SERVER] = server.force_encoding('ASCII-8BIT')) - protocol.nil? || protocol.empty? ? (row[PROTOCOL] = nil) : (row[PROTOCOL] = protocol.force_encoding('ASCII-8BIT')) - login.nil? || login.empty? ? (row[LOGIN] = nil) : (row[LOGIN] = login.force_encoding('ASCII-8BIT')) - passwd.nil? || passwd.empty? ? (row[PASSWORD] = nil) : (row[PASSWORD] = passwd.force_encoding('ASCII-8BIT')) - comment.nil? || comment.empty? ? (row[COMMENT] = nil) : (row[COMMENT] = comment.force_encoding('ASCII-8BIT')) - - @data[id] = row - - return true - end - # Update an item # @args: id -> the item's identifiant # name -> the item name @@ -236,46 +126,66 @@ class MPW # port -> the port # comment -> a comment # @rtrn: true if the item has been updated - def update(id, name=nil, group=nil, server=nil, protocol=nil, login=nil, passwd=nil, port=nil, comment=nil) - id = id.to_i + def update(name, group, server, protocol, login, passwd, port, comment, id=nil) + row = Array.new() + update = false - if not @data[id].nil? - - if port.to_i <= 0 - port = nil + i = 0 + @data.each do |r| + if r[ID] == id + row = r + update = true + break end + i += 1 + end - row = @data[id] - row_update = Array.new() + if port.to_i <= 0 + port = nil + end - name.nil? || name.empty? ? (row_update[NAME] = row[NAME]) : (row_update[NAME] = name) - group.nil? || group.empty? ? (row_update[GROUP] = row[GROUP]) : (row_update[GROUP] = group) - server.nil? || server.empty? ? (row_update[SERVER] = row[SERVER]) : (row_update[SERVER] = server) - protocol.nil? || protocol.empty? ? (row_update[PROTOCOL] = row[PROTOCOL]) : (row_update[PROTOCOL] = protocol) - login.nil? || login.empty? ? (row_update[LOGIN] = row[LOGIN]) : (row_update[LOGIN] = login) - passwd.nil? || passwd.empty? ? (row_update[PASSWORD] = row[PASSWORD]) : (row_update[PASSWORD] = passwd) - port.nil? || port.empty? ? (row_update[PORT] = row[PORT]) : (row_update[PORT] = port) - comment.nil? || comment.empty? ? (row_update[COMMENT] = row[COMMENT]) : (row_update[COMMENT] = comment) - - @data[id] = row_update + row_update = Array.new() + row_update[DATE] = Time.now.to_i - return true - else - @error_msg = I18n.t('error.update.id_no_exist', :id => id) + id.nil? || id.empty? ? (row_update[ID] = MPW.generatePassword(16)) : (row_update[ID] = row[ID]) + name.nil? || name.empty? ? (row_update[NAME] = row[NAME]) : (row_update[NAME] = name) + group.nil? || group.empty? ? (row_update[GROUP] = row[GROUP]) : (row_update[GROUP] = group) + server.nil? || server.empty? ? (row_update[SERVER] = row[SERVER]) : (row_update[SERVER] = server) + protocol.nil? || protocol.empty? ? (row_update[PROTOCOL] = row[PROTOCOL]) : (row_update[PROTOCOL] = protocol) + login.nil? || login.empty? ? (row_update[LOGIN] = row[LOGIN]) : (row_update[LOGIN] = login) + passwd.nil? || passwd.empty? ? (row_update[PASSWORD] = row[PASSWORD]) : (row_update[PASSWORD] = passwd) + port.nil? || port.empty? ? (row_update[PORT] = row[PORT]) : (row_update[PORT] = port) + comment.nil? || comment.empty? ? (row_update[COMMENT] = row[COMMENT]) : (row_update[COMMENT] = comment) + + 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) - if not @data.delete_at(id.to_i).nil? - return true - else - @error_msg = I18n.t('error.delete.id_no_exist', :id => id) - return false + 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 @@ -285,7 +195,7 @@ class MPW begin File.open(file, 'w+') do |file| @data.each do |row| - row.delete_at(ID) + row.delete_at(ID).delete_at(DATE) file << row.to_csv end end @@ -309,7 +219,7 @@ class MPW return false else row = line.parse_csv.unshift(0) - if not add(row[NAME], row[GROUP], row[SERVER], row[PROTOCOL], row[LOGIN], row[PASSWORD], row[PORT], row[COMMENT]) + if not update(row[NAME], row[GROUP], row[SERVER], row[PROTOCOL], row[LOGIN], row[PASSWORD], row[PORT], row[COMMENT]) return false end end @@ -322,7 +232,7 @@ class MPW end end - # Return + # 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 importPreview(file) @@ -349,6 +259,50 @@ class MPW 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 + + encrypt() + + return true + end + # Generate a random password # @args: length -> the length password # @rtrn: a random string @@ -367,7 +321,6 @@ class MPW result << ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(length).join return result - #return ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]-%w(0 1 I O l i o)).sample(length).join end - + end diff --git a/lib/MPWConfig.rb b/lib/MPWConfig.rb new file mode 100644 index 0000000..d68d0c7 --- /dev/null +++ b/lib/MPWConfig.rb @@ -0,0 +1,130 @@ +#!/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 + # @rtrn: true if le config file is create + def setup(key, lang, file_gpg, timeout_pwd) + + 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.empty? ? (timeout_pwd = 60) : (timeout_pwd = timeout_pwd.to_i) + + config = {'config' => {'key' => key, + 'lang' => lang, + 'file_gpg' => file_gpg, + 'timeout_pwd' => timeout_pwd, + 'sync_host' => host, + 'sync_port' => port, + 'sync_pwd' => password, + 'sync_suffix' => 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 + + def setLastUpdate() + 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/Server.rb b/lib/Server.rb new file mode 100644 index 0000000..5b76de7 --- /dev/null +++ b/lib/Server.rb @@ -0,0 +1,295 @@ +#!/usr/bin/ruby + +require 'socket' +require 'json' +require 'highline/import' +require 'digest' + +require "#{APP_ROOT}/lib/MPW.rb" + +class Server + + attr_accessor :error_msg + + # Constructor + def initialize() + YAML::ENGINE.yamler='syck' + end + + # Start the server + def start() + server = TCPServer.open(@host, @port) + loop do + Thread.start(server.accept) do |client| + while true do + msg = getClientMessage(client) + + if !msg + next + end + + if msg['gpg_key'].nil? || msg['gpg_key'].empty? || msg['password'].nil? || msg['password'].empty? + closeConnection(client) + next + end + + case msg['action'] + when 'get' + client.puts getFile(msg) + when 'update' + client.puts updateFile(msg) + when 'delete' + client.puts deleteFile(msg) + when 'close' + closeConnection(client) + else + client.puts 'Unknown command' + closeConnection(client) + end + end + end + end + end + + # Get a gpg file + # @args: msg -> message puts by the client + # @rtrn: json message + def getFile(msg) + gpg_key = msg['gpg_key'].sub('@', '_') + + if msg['suffix'].nil? || msg['suffix'].empty? + file_gpg = "#{@data_dir}/#{gpg_key}.yml" + else + file_gpg = "#{@data_dir}/#{gpg_key}-#{msg['suffix']}.yml" + end + + if File.exist?(file_gpg) + gpg_data = YAML::load_file(file_gpg) + salt = gpg_data['gpg']['salt'] + hash = gpg_data['gpg']['hash'] + data = gpg_data['gpg']['data'] + + if isAuthorized?(msg['password'], salt, hash) + send_msg = {:action => 'get', + :gpg_key => msg['gpg_key'], + :msg => 'done', + :data => data} + else + send_msg = {:action => 'get', + :gpg_key => msg['gpg_key'], + :msg => 'fail', + :error => 'not_authorized'} + end + else + send_msg = {:action => 'get', + :gpg_key => msg['gpg_key'], + :data => '', + :msg => 'fail', + :error => 'file_not_exist'} + end + + return send_msg.to_json + end + + # Update a file + # @args: msg -> message puts by the client + # @rtrn: json message + def updateFile(msg) + gpg_key = msg['gpg_key'].sub('@', '_') + data = msg['data'] + + if data.nil? || data.empty? + send_msg = {:action => 'update', + :gpg_key => msg['gpg_key'], + :msg => 'fail', + :error => 'no_data'} + + return send_msg.to_json + end + + if msg['suffix'].nil? || msg['suffix'].empty? + file_gpg = "#{@data_dir}/#{gpg_key}.yml" + else + file_gpg = "#{@data_dir}/#{gpg_key}-#{msg['suffix']}.yml" + end + + if File.exist?(file_gpg) + gpg_data = YAML::load_file(file_gpg) + salt = gpg_data['gpg']['salt'] + hash = gpg_data['gpg']['hash'] + + else + salt = MPW.generatePassword(4) + hash = Digest::SHA256.hexdigest(salt + msg['password']) + end + + if isAuthorized?(msg['password'], salt, hash) + begin + config = {'gpg' => {'salt' => salt, + 'hash' => hash, + 'data' => data}} + + File.open(file_gpg, 'w+') do |file| + file << config.to_yaml + end + + send_msg = {:action => 'update', + :gpg_key => msg['gpg_key'], + :msg => 'done'} + rescue Exception => e + send_msg = {:action => 'update', + :gpg_key => msg['gpg_key'], + :msg => 'fail', + :error => 'server_error'} + end + else + send_msg = {:action => 'update', + :gpg_key => msg['gpg_key'], + :msg => 'fail', + :error => 'not_autorized'} + end + + return send_msg.to_json + end + + # Remove a gpg file + # @args: msg -> message puts by the client + # @rtrn: json message + def deleteFile(msg) + gpg_key = msg['gpg_key'].sub('@', '_') + + if msg['suffix'].nil? || msg['suffix'].empty? + file_gpg = "#{@data_dir}/#{gpg_key}.yml" + else + file_gpg = "#{@data_dir}/#{gpg_key}-#{msg['suffix']}.yml" + end + + if !File.exist?(file_gpg) + send_msg = {:action => 'delete', + :gpg_key => msg['gpg_key'], + :msg => 'delete_fail', + :error => 'file_not_exist'} + + return send_msg.to_json + end + + gpg_data = YAML::load_file(file_gpg) + salt = gpg_data['gpg']['salt'] + hash = gpg_data['gpg']['hash'] + + if isAuthorized?(msg['password'], salt, hash) + begin + File.unlink(file_gpg) + + send_msg = {:action => 'delete', + :gpg_key => msg['gpg_key'], + :msg => 'delete_done'} + rescue Exception => e + send_msg = {:action => 'delete', + :gpg_key => msg['gpg_key'], + :msg => 'delete_fail', + :error => e} + end + else + send_msg = {:action => 'delete', + :gpg_key => msg['gpg_key'], + :msg => 'delete_fail', + :error => 'not_autorized'} + end + + return send_msg.to_json + end + + # Check is the hash equal the password with the salt + # @args: password -> the user password + # salt -> the salt + # hash -> the hash of the password with the salt + # @rtrn: true is is good, else false + def isAuthorized?(password, salt, hash) + if hash == Digest::SHA256.hexdigest(salt + password) + return true + else + return false + end + end + + # Get message to client + # @args: client -> client connection + # @rtrn: array of the json string, or false if isn't json message + def getClientMessage(client) + begin + msg = client.gets + return JSON.parse(msg) + rescue + closeConnection(client) + return false + end + end + + # Close the client connection + # @args: client -> client connection + def closeConnection(client) + client.puts "Closing the connection. Bye!" + client.close + end + + # Check the config file + # @args: file_config -> the configuration file + # @rtrn: true if the config file is correct + def checkconfig(file_config) + begin + config = YAML::load_file(file_config) + @host = config['config']['host'] + @port = config['config']['port'].to_i + @data_dir = config['config']['data_dir'] + @timeout = config['config']['timeout'].to_i + + if @host.empty? || @port <= 0 || @data_dir.empty? + puts I18n.t('server.checkconfig.fail') + puts I18n.t('server.checkconfig.empty') + return false + end + + if !Dir.exist?(@data_dir) + puts I18n.t('server.checkconfig.fail') + puts I18n.t('server.checkconfig.datadir') + return false + end + + rescue Exception => e + puts "#{I18n.t('server.checkconfig.fail')}\n#{e}" + return false + end + + return true + end + + # Create a new config file + # @args: file_config -> the configuration file + # @rtrn: true if le config file is create + def setup(file_config) + puts I18n.t('server.form.setup.title') + puts '--------------------' + host = ask(I18n.t('server.form.setup.host')).to_s + port = ask(I18n.t('server.form.setup.port')).to_s + data_dir = ask(I18n.t('server.form.setup.data_dir')).to_s + timeout = ask(I18n.t('server.form.setup.timeout')).to_s + + config = {'config' => {'host' => host, + 'port' => port, + 'data_dir' => data_dir, + 'timeout' => timeout}} + + begin + File.open(file_config, 'w') do |file| + file << config.to_yaml + end + rescue Exception => e + puts "#{I18n.t('server.formsetup.not_valid')}\n#{e}" + return false + end + + return true + end + +end diff --git a/lib/Sync.rb b/lib/Sync.rb new file mode 100644 index 0000000..0c3ed31 --- /dev/null +++ b/lib/Sync.rb @@ -0,0 +1,133 @@ +#!/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 + + # Constructor + def initialize() + @error_msg = nil + end + + # Disable the sync + def disable() + @sync = 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) + @sync = true + rescue Exception => e + @error_msg = "#{I18n.t('error.sync.connection')}\n#{e}" + @sync = false + end + + return @sync + end + + # Get data on server + # @args: gpg_password -> the gpg password + # @rtrn: nil if nothing data or error + def get(gpg_password) + if !@sync + 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) + + case msg['error'] + when nil, 'file_not_exist' + tmp_file = "/tmp/mpw-#{MPW.generatePassword()}.gpg" + File.open(tmp_file, 'w') do |file| + file << msg['data'] + end + + @mpw = MPW.new(tmp_file) + if !@mpw.decrypt(gpg_password) + return nil + end + + File.unlink(tmp_file) + + return @mpw.search() + when 'not_authorized' + @error_msg = "#{I18n.t('error.sync.not_authorized')}\n#{e}" + else + @error_msg = "#{I18n.t('error.sync.unknown')}\n#{e}" + end + + return nil + end + + # Update the remote data + # @args: data -> the data to send on server + # @rtrn: false if there is a problem + def update(data) + if !@sync + 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) + + case msg['error'] + when nil + return true + when 'not_authorized' + @error_msg = "#{I18n.t('error.sync.not_authorized')}\n#{e}" + when 'no_data' + @error_msg = "#{I18n.t('error.sync.no_data')}\n#{e}" + else + @error_msg = "#{I18n.t('error.sync.unknown')}\n#{e}" + end + + return false + end + + def delete() + end + + # Close the connection + def close() + if !@sync + return + end + + send_msg = {:action => 'close'} + @socket.puts send_msg.to_json + end +end diff --git a/mpw b/mpw index a313ed7..80f3344 100755 --- a/mpw +++ b/mpw @@ -93,13 +93,17 @@ OptionParser.new do |opts| end end.parse! +config = MPWConfig.new(options[:config]) +check_error = config.checkconfig() -cli = Cli.new(lang, options[:config]) +cli = Cli.new(lang, config) +cli.sync() + +# Setup a new config +if !check_error || !options[:setup].nil? + cli.setup(lang) # Display the item's informations -if not options[:setup].nil? - cli.setup() - elsif not options[:display].nil? cli.display(options[:display], options[:group], options[:type], options[:format]) @@ -125,7 +129,16 @@ elsif not options[:import].nil? # Interactive mode else - cli.interactive + begin + cli.interactive() + rescue SystemExit, Interrupt + cli.sync() + cli = nil + return 1 + end end +cli.sync() +cli = nil + exit 0 diff --git a/mpw-server b/mpw-server new file mode 100755 index 0000000..3143b59 --- /dev/null +++ b/mpw-server @@ -0,0 +1,60 @@ +#!/usr/bin/ruby +# author: nishiki +# mail: nishiki@yaegashi.fr +# info: a simple script who manage your passwords + +require 'rubygems' +require 'optparse' +require 'pathname' +require 'locale' +require 'i18n' + +APP_ROOT = File.dirname(Pathname.new(__FILE__).realpath) +require "#{APP_ROOT}/lib/Server.rb" + +lang = Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1] + +I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks) +I18n.load_path = Dir["#{APP_ROOT}/i18n/*.yml"] +I18n.default_locale = :en +I18n.locale = lang.to_sym + +options = {} +OptionParser.new do |opts| + opts.banner = "#{I18n.t('server.option.usage')}: mpw-server -c CONFIG [options]" + + opts.on("-c", "--config CONFIG", I18n.t('server.option.config')) do |config| + options[:config] = config + end + + opts.on("-t", "--checkconfig", I18n.t('server.option.checkconfig')) do |b| + options[:checkconfig] = b + end + + opts.on("-s", "--setup", I18n.t('server.option.setup')) do |b| + options[:setup] = b + end + + opts.on("-h", "--help", I18n.t('server.option.help')) do |b| + puts opts + exit 0 + end +end.parse! + +if options[:config].nil? || options[:config].empty? + puts "#{I18n.t('server.option.usage')}: mpw-server -c CONFIG [options]" + exit 2 +end + +server = Server.new + +if options[:checkconfig] + server.checkconfig(options[:config]) +elsif options[:setup] + server.setup(options[:config]) +else + server.checkconfig(options[:config]) + server.start() +end + +exit 0