to copy the password"
otp_code: "Press pour copier le mot de passe"
otp_code: "Pressez to quit"
delete_key:
valid: "Key has been deleted!"
delete_item:
- ask: "Are you sure you want to remove the item %{id} ?"
- valid: "The item %{id} has been removed!"
- not_valid: "The item %{id} hasn't been removed, because it doesn't exist!"
+ ask: "Are you sure you want to remove the item ?"
+ valid: "The item has been removed!"
import:
ask: "Are you sure you want to import this file %{file} ?"
valid: "The import is succesfull!"
@@ -127,16 +135,15 @@ en:
wait: "Please waiting during the GPG key generate, this process can take few minutes."
valid: "Your GPG key has been created ;-)"
update_item:
- title: "Update an item"
- name: "Enter the name [%{name}]: "
- group: "Enter the group [%{group}]: "
- server: "Enter the hostname or ip [%{server}]: "
- protocol: "Enter the protocol of the connection [%{protocol}]: "
- login: "Enter the login connection [%{login}]: "
- password: "Enter the the password: "
- port: "Enter the connection port [%{port}]: "
- comment: "Enter a comment [%{comment}]: "
- otp_key: "Enter the otp secret (base32 secret key): "
+ name: "The item's name (mandatory"
+ group: "The group's name"
+ host: "The hostname or ip"
+ protocol: "The protocol of the connection (ssh, http, ...)"
+ login: "The login of connection"
+ password: "The password (leave empty if you don't want change)"
+ port: "The connection port"
+ comment: "A comment"
+ otp_key: "The OTP secret (leave empty if you don't want change"
valid: "Item has been updated!"
export:
valid: "The export in %{file} is succesfull!"
diff --git a/i18n/fr.yml b/i18n/fr.yml
index 41cac4d..d2cae5b 100644
--- a/i18n/fr.yml
+++ b/i18n/fr.yml
@@ -3,15 +3,13 @@ fr:
error:
config:
write: "Impossible d'écrire le fichier de configuration!"
- check: "Le fichier de configuration est invalide!"
+ load: "Le fichier de configuration est invalide!"
key_bad_format: "La clé GPG est invalide!"
no_key_public: "Vous ne possédez pas la clé publique de %{key}!"
genkey_gpg:
exception: "La création de la clé GPG n'a pas pu aboutir!"
name: "Vous devez définir un nom pour votre clé GPG!"
password: "Vous devez définir un mot de passe pour votre clé GPG!"
- delete:
- id_no_exist: "Impossible de supprimer l'élément %{id}, car il n'existe pas!"
export: "Impossible d'exporter les données dans le fichier %{file}!"
gpg_file:
decrypt: "Impossible de déchiffrer le fichier GPG!"
@@ -41,41 +39,51 @@ fr:
config: "Spécifie le fichier de configuration à utiliser"
clipboard: "Désactive la fonction presse papier"
export: "Exporte un portefeuille dans un fichier yaml"
- file: "Spécifie un fichier, à utiliser avec les options [--import | --export | --add]"
+ file_export: "Spécifie le fichier où exporter les données"
+ file_import: "Spécifie le fichier à importer"
force: "Ne demande pas de confirmation pour la suppression d'un élément"
generate_password: "Génére un mot de passe aléatoire (défaut 8 caractères)"
+ gpg_exe: "Spécifie le chemin du binaire gpg à utiliser"
+ gpg_key: "Spécifie une clé GPG (ex: user@example.com)"
group: "Recherche les éléments appartenant au groupe spécifié"
help: "Affiche ce message d'aide"
- id: "Spécifie un identifiant, à utiliser avec les options [--delete | --update]"
+ host: "Spécifie le serveur pour la synchronisation"
import: "Importe des éléments depuis un fichier yaml"
- key: "Spécifie le nom d'une clé, à utiliser avec les options [--add | --delete | --update]"
+ init: "Initialise mpw"
+ key: "Spécifie le nom d'une clé"
+ lang: "Spécifie la langue du logiciel (ex: fr)"
+ list: "Liste les portefeuilles"
no_sync: "Désactive la synchronisation avec le serveur"
numeric: "Utilise des chiffre dans la génération d'un mot de passe"
+ password: "Changer le mot de passe de connexion"
+ path: "Spécifie le chemin distant"
+ pattern: "Motif de donnée à chercher"
+ port: "Spécifie le port de connexion"
+ protocol: "Spécifie le protocol utilisé pour la connexion"
setup: "Création d'un nouveau fichier de configuration"
setup_wallet: "Création d'un nouveau fichier de configuration pour un portefeuille"
special_chars: "Utilise des charactères speciaux dans la génération d'un mot de passe"
show: "Recherche et affiche les éléments"
show_all: "Liste tous les éléments"
- remove: "Supprime un élément"
- update: "Met à jour un élément"
usage: "Utilisation"
+ user: "Spécifie l'identifiant de connection"
wallet: "Spécifie le portefeuille à utiliser"
+ wallet_dir: "Spécifie le répertoire des portefeuilles"
form:
select: "Sélectionner l'élément: "
add_key:
valid: "La clé a bien été ajoutée!"
add_item:
- title: "Ajout d'un nouvel élément"
- name: "Entrez le nom: "
- group: "Entrez le groupe (optionnel): "
- server: "Entrez le nom de domaine ou l'ip: "
- protocol: "Entrez le protocole de connexion (ssh, http, other): "
- login: "Entrez l'identifiant de connexion: "
- password: "Entrez le mot de passe: "
- port: "Entrez le port de connexion (optionnel): "
- comment: "Entrez un commentaire (optionnel): "
- otp_key: "Entrez le secret OTP: "
+ name: "Le nom de l'élément (obligatoire)"
+ group: "Le nom du groupe"
+ host: "Le nom de domaine ou l'ip"
+ protocol: "Le protocole de connexion (ssh, http, ...)"
+ login: "L'identifiant de connexion"
+ password: "Le mot de passe"
+ port: "Le port de connexion"
+ comment: "Un commentaire"
+ otp_key: "Le secret OTP"
valid: "L'élément a bien été ajouté!"
clipboard:
choice: "Que voulez-vous copier ? : "
@@ -88,12 +96,12 @@ fr:
login: "Pressez
pour quitter"
delete_key:
valid: "La clé a bien été supprimée!"
delete_item:
- ask: "Êtes vous sûre de vouloir supprimer l'élément %{id} ?"
- valid: "L'élément %{id} a bien été supprimé!"
- not_valid: "L'élément %{id} n'a pu être supprimé, car il n'existe pas!"
+ ask: "Êtes vous sûre de vouloir supprimer l'élément ?"
+ valid: "L'élément a bien été supprimé!"
import:
ask: "Êtes vous sûre de vouloir importer le fichier %{file} ?"
valid: "L'import est un succès!"
@@ -127,16 +135,15 @@ fr:
wait: "Veuillez patienter durant la génération de votre clé GPG, ce processus peut prendre quelques minutes."
valid: "Votre clé GPG a bien été créée ;-)"
update_item:
- title: "Mis à jour d'un élément"
- name: "Entrez le nom [%{name}]: "
- group: "Entrez le groupe [%{group}]: "
- server: "Entrez le nom de domaine ou l'ip du serveur [%{server}]: "
- protocol: "Entrez le protocole de connexion [%{protocol}]: "
- login: "Entrez votre identifiant de connexion [%{login}]: "
- password: "Entrez le mot de passe: "
- port: "Entrez un port de connexion [%{port}]: "
- comment: "Entrez un commentaire [%{comment}]: "
- otp_key: "Entrez le secret OTP: "
+ name: "Le nom de l'élément (obligatoire)"
+ group: "Le nom du groupe"
+ host: "Le nom de domaine ou l'ip"
+ protocol: "Le protocole de connexion (ssh, http, ...)"
+ login: "L'identifiant de connexion"
+ password: "Le mot de passe (laissez vide si vous ne voulez pas le changer)"
+ port: "Le port de connexion"
+ comment: "Un commentaire"
+ otp_key: "Le secret OTP (laissez vide si vous ne voulez pas le changer)"
valid: "L'élément a bien été mis à jour!"
export:
valid: "L'export dans %{file} est un succès!"
diff --git a/lib/mpw/cli.rb b/lib/mpw/cli.rb
index 392e7bd..722b16d 100644
--- a/lib/mpw/cli.rb
+++ b/lib/mpw/cli.rb
@@ -17,10 +17,12 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'readline'
+require 'locale'
require 'i18n'
require 'colorize'
require 'highline/import'
require 'clipboard'
+require 'tmpdir'
require 'mpw/item'
require 'mpw/mpw'
@@ -30,33 +32,35 @@ class Cli
# Constructor
# @args: config -> the config
# sync -> boolean for sync or not
- # clipboard -> enable clopboard
- # otp -> enable otp
- def initialize(config, clipboard=true, sync=true, otp=false)
+ def initialize(config, sync=true)
@config = config
- @clipboard = clipboard
@sync = sync
- @otp = otp
+ end
+
+ # Change a parameter int the config after init
+ # @args: options -> param to change
+ def set_config(options)
+ gpg_key = options[:gpg_key] || @config.key
+ lang = options[:lang] || @config.lang
+ wallet_dir = options[:wallet_dir] || @config.wallet_dir
+ gpg_exe = options[:gpg_exe] || @config.gpg_exe
+
+ @config.setup(gpg_key, lang, wallet_dir, gpg_exe)
+ rescue Exception => e
+ puts "#{I18n.t('display.error')} #15: #{e}".red
+ exit 2
end
# Create a new config file
- # @args: lang -> the software language
- def setup(lang)
- puts I18n.t('form.setup_config.title')
- puts '--------------------'
- language = ask(I18n.t('form.setup_config.lang', lang: lang)).to_s
- key = ask(I18n.t('form.setup_config.gpg_key')).to_s
- wallet_dir = ask(I18n.t('form.setup_config.wallet_dir', home: "#{@config.config_dir}")).to_s
- gpg_exe = ask(I18n.t('form.setup_config.gpg_exe')).to_s
+ # @args: options -> set param
+ def setup(options)
+ lang = options[:lang] || Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1]
- if language.nil? or language.empty?
- language = lang
- end
- I18n.locale = language.to_sym
+ I18n.locale = lang.to_sym
- @config.setup(key, lang, wallet_dir, gpg_exe)
+ @config.setup(options[:gpg_key], lang, options[:wallet_dir], options[:gpg_exe])
- raise I18n.t('error.config.check') if not @config.is_valid?
+ load_config
puts "#{I18n.t('form.setup_config.valid')}".green
rescue Exception => e
@@ -65,16 +69,10 @@ class Cli
end
# Setup a new GPG key
- def setup_gpg_key
- puts I18n.t('form.setup_gpg_key.title')
- puts '--------------------'
- ask = ask(I18n.t('form.setup_gpg_key.ask')).to_s
-
- if not ['Y', 'y', 'O', 'o'].include?(ask)
- raise I18n.t('form.setup_gpg_key.no_create')
- end
+ # @args: gpg_key -> the key name
+ def setup_gpg_key(gpg_key)
+ return if @config.check_gpg_key?
- name = ask(I18n.t('form.setup_gpg_key.name')).to_s
password = ask(I18n.t('form.setup_gpg_key.password')) {|q| q.echo = false}
confirm = ask(I18n.t('form.setup_gpg_key.confirm_password')) {|q| q.echo = false}
@@ -82,16 +80,11 @@ class Cli
raise I18n.t('form.setup_gpg_key.error_password')
end
- length = ask(I18n.t('form.setup_gpg_key.length')).to_s
- expire = ask(I18n.t('form.setup_gpg_key.expire')).to_s
- password = password.to_s
-
- length = length.nil? or length.empty? ? 2048 : length.to_i
- expire = expire.nil? or expire.empty? ? 0 : expire.to_i
+ @password = password.to_s
puts I18n.t('form.setup_gpg_key.wait')
- @config.setup_gpg_key(password, name, length, expire)
+ @config.setup_gpg_key(@password, gpg_key)
puts "#{I18n.t('form.setup_gpg_key.valid')}".green
rescue Exception => e
@@ -100,23 +93,17 @@ class Cli
end
# Setup wallet config for sync
- def setup_wallet_config
- config = {}
- config['sync'] = {}
-
- puts I18n.t('form.setup_wallet.title')
- puts '--------------------'
- config['sync']['type'] = ask(I18n.t('form.setup_wallet.sync_type')).to_s
-
- if ['ftp', 'ssh'].include?(config['sync']['type'].downcase)
- config['sync']['host'] = ask(I18n.t('form.setup_wallet.sync_host')).to_s
- config['sync']['port'] = ask(I18n.t('form.setup_wallet.sync_port')).to_s
- config['sync']['user'] = ask(I18n.t('form.setup_wallet.sync_user')).to_s
- config['sync']['password'] = ask(I18n.t('form.setup_wallet.sync_pwd')).to_s
- config['sync']['path'] = ask(I18n.t('form.setup_wallet.sync_path')).to_s
+ # @args: options -> value to change
+ def setup_wallet_config(options={})
+ if not options[:password].nil?
+ options[:password] = ask(I18n.t('form.setup_wallet.password')) {|q| q.echo = false}
end
- @mpw.set_config(config)
+ #wallet_file = wallet.nil? ? "#{@config.wallet_dir}/default.mpw" : "#{@config.wallet_dir}/#{wallet}.mpw"
+
+ @mpw = MPW.new(@config.key, @wallet_file, @password, @config.gpg_exe)
+ @mpw.read_data
+ @mpw.set_config(options)
@mpw.write_data
puts "#{I18n.t('form.setup_wallet.valid')}".green
@@ -125,6 +112,15 @@ class Cli
exit 2
end
+ # Load config
+ def load_config
+ @config.load_config
+
+ rescue Exception => e
+ puts "#{I18n.t('display.error')} #10: #{e}".red
+ exit 2
+ end
+
# Request the GPG password and decrypt the file
def decrypt
if not defined?(@mpw)
@@ -139,97 +135,119 @@ class Cli
exit 2
end
- # Display the query's result
- # @args: search -> the string to search
- # protocol -> search from a particular protocol
- def display(options={})
- result = @mpw.list(options)
+ # Format items on a table
+ def table(items=[])
+ group = '.'
+ i = 1
+ length_total = 10
+ data = { id: { length: 3, color: 'cyan' },
+ host: { length: 9, color: 'yellow' },
+ user: { length: 7, color: 'green' },
+ protocol: { length: 9, color: 'white' },
+ port: { length: 5, color: 'white' },
+ otp: { length: 4, color: 'white' },
+ comment: { length: 14, color: 'magenta' },
+ }
- case result.length
- when 0
- puts I18n.t('display.nothing')
+ items.each do |item|
+ data.each do |k, v|
+ next if k == :id or k == :otp
- when 1
- display_item(result.first)
+ v[:length] = item.send(k.to_s).length + 3 if item.send(k.to_s).to_s.length >= v[:length]
+ end
+ end
+ data[:id][:length] = items.length.to_s.length + 2 if items.length.to_s.length > data[:id][:length]
+
+ data.each_value { |v| length_total += v[:length] }
+ items.sort! { |a, b| a.group.to_s.downcase <=> b.group.to_s.downcase }
- else
- group = nil
- i = 1
+ items.each do |item|
+ if group != item.group
+ group = item.group
- result.sort! { |a,b| a.group.downcase <=> b.group.downcase }
-
- result.each do |item|
- if group != item.group
- group = item.group
-
- if group.empty?
- puts I18n.t('display.no_group').yellow
- else
- puts "\n#{group}".yellow
- end
+ if group.to_s.empty?
+ puts "\n#{I18n.t('display.no_group')}".red
+ else
+ puts "\n#{group}".red
end
- print " |_ ".yellow
- print "#{i}: ".cyan
- print item.name
- print " -> #{item.comment}".magenta if not item.comment.to_s.empty?
+ print ' '
+ length_total.times { print '=' }
+ print "\n "
+ data.each do |k, v|
+ case k
+ when :id
+ print ' ID'
+ when :otp
+ print '| OTP'
+ else
+ print "| #{k.to_s.capitalize}"
+ end
+
+ (v[:length] - k.to_s.length).times { print ' ' }
+ end
+ print "\n "
+ length_total.times { print '=' }
print "\n"
-
- i += 1
end
+ print " #{i}".send(data[:id][:color])
+ (data[:id][:length] - i.to_s.length).times { print ' ' }
+ data.each do |k, v|
+ next if k == :id
+
+ if k == :otp
+ print '| '
+ if item.otp; print ' X ' else 4.times { print ' ' } end
+
+ next
+ end
+
+ print '| '
+ print "#{item.send(k.to_s)}".send(v[:color])
+ (v[:length] - item.send(k.to_s).to_s.length).times { print ' ' }
+ end
print "\n"
- choice = ask(I18n.t('form.select')).to_i
- if choice >= 1 and choice < i
- display_item(result[choice-1])
- else
- puts "#{I18n.t('display.warning')}: #{I18n.t('warning.select')}".yellow
- end
+ i += 1
+ end
+
+ print "\n"
+ end
+
+ # Display the query's result
+ # @args: options -> the option to search
+ def list(options={})
+ result = @mpw.list(options)
+
+ if result.length == 0
+ puts I18n.t('display.nothing')
+
+ else
+ table(result)
end
end
- # Display an item in the default format
- # @args: item -> an array with the item information
- def display_item(item)
- puts '--------------------'.cyan
- print 'Id: '.cyan
- puts item.id
- print "#{I18n.t('display.name')}: ".cyan
- puts item.name
- print "#{I18n.t('display.group')}: ".cyan
- puts item.group
- print "#{I18n.t('display.server')}: ".cyan
- puts item.host
- print "#{I18n.t('display.protocol')}: ".cyan
- puts item.protocol
- print "#{I18n.t('display.login')}: ".cyan
- puts item.user
+ # Get an item when multiple choice
+ # @args: items -> array of items
+ # @rtrn: item
+ def get_item(items)
+ return items[0] if items.length == 1
- if @clipboard
- print "#{I18n.t('display.password')}: ".cyan
- puts '***********'
+ items.sort! { |a,b| a.group.to_s.downcase <=> b.group.to_s.downcase }
+ choice = ask(I18n.t('form.select')).to_i
+
+ if choice >= 1 and choice <= items.length
+ return items[choice-1]
else
- print "#{I18n.t('display.password')}: ".cyan
- puts @mpw.get_password(item.id)
-
- if @mpw.get_otp_code(item.id) > 0
- print "#{I18n.t('display.otp_code')}: ".cyan
- puts "#{@mpw.get_otp_code(item.id)} (#{@mpw.get_otp_remaining_time}s)"
- end
+ return nil
end
-
- print "#{I18n.t('display.port')}: ".cyan
- puts item.port
- print "#{I18n.t('display.comment')}: ".cyan
- puts item.comment
-
- clipboard(item) if @clipboard
end
# Copy in clipboard the login and password
# @args: item -> the item
- def clipboard(item)
+ # clipboard -> enable clipboard
+ def clipboard(item, clipboard=true)
pid = nil
# Security: force quit after 90s
@@ -246,21 +264,33 @@ class Cli
break
when 'l', 'login'
- Clipboard.copy(item.user)
- puts I18n.t('form.clipboard.login').green
+ if clipboard
+ Clipboard.copy(item.user)
+ puts I18n.t('form.clipboard.login').green
+ else
+ puts item.user
+ end
when 'p', 'password'
- Clipboard.copy(@mpw.get_password(item.id))
- puts I18n.t('form.clipboard.password').yellow
+ if clipboard
+ Clipboard.copy(@mpw.get_password(item.id))
+ puts I18n.t('form.clipboard.password').yellow
- Thread.new do
- sleep 30
+ Thread.new do
+ sleep 30
- Clipboard.clear
+ Clipboard.clear
+ end
+ else
+ puts @mpw.get_password(item.id)
end
when 'o', 'otp'
- Clipboard.copy(@mpw.get_otp_code(item.id))
+ if clipboard
+ Clipboard.copy(@mpw.get_otp_code(item.id))
+ else
+ puts @mpw.get_otp_code(item.id)
+ end
puts I18n.t('form.clipboard.otp', time: @mpw.get_otp_remaining_time).yellow
else
@@ -268,6 +298,7 @@ class Cli
puts I18n.t('form.clipboard.help.login')
puts I18n.t('form.clipboard.help.password')
puts I18n.t('form.clipboard.help.otp_code')
+ puts I18n.t('form.clipboard.help.quit')
next
end
end
@@ -277,34 +308,25 @@ class Cli
Clipboard.clear
end
+ # List all wallets
+ def list_wallet
+ wallets = Dir.glob("#{@config.wallet_dir}/*.mpw")
+
+ wallets.each do |wallet|
+ puts File.basename(wallet, '.mpw')
+ end
+ end
+
# Display the wallet
# @args: wallet -> the wallet name
def get_wallet(wallet=nil)
if wallet.to_s.empty?
wallets = Dir.glob("#{@config.wallet_dir}/*.mpw")
- case wallets.length
- when 0
- puts I18n.t('display.nothing')
- when 1
+ if wallets.length == 1
@wallet_file = wallets[0]
else
- i = 1
- wallets.each do |wallet|
- print "#{i}: ".cyan
- puts File.basename(wallet, '.mpw')
-
- i += 1
- end
-
- choice = ask(I18n.t('form.select')).to_i
-
- if choice >= 1 and choice < i
- @wallet_file = wallets[choice-1]
- else
- puts "#{I18n.t('display.warning')}: #{I18n.t('warning.select')}".yellow
- exit 2
- end
+ @wallet_file = "#{@config.wallet_dir}/default.mpw"
end
else
@wallet_file = "#{@config.wallet_dir}/#{wallet}.mpw"
@@ -336,111 +358,158 @@ class Cli
puts "#{I18n.t('display.error')} #15: #{e}".red
end
- # Form to add a new item
- def add
- options = {}
+ # Text editor interface
+ # @args: template -> template name
+ # item -> the item to edit
+ # password -> disable field password
+ def text_editor(template_name, item=nil, password=false)
+ editor = ENV['EDITOR'] || 'nano'
+ options = {}
+ opts = {}
+ template_file = "#{File.expand_path('../../../templates', __FILE__)}/#{template_name}.erb"
+ template = ERB.new(IO.read(template_file))
- puts I18n.t('form.add_item.title')
- puts '--------------------'
- options[:name] = ask(I18n.t('form.add_item.name')).to_s
- options[:group] = ask(I18n.t('form.add_item.group')).to_s
- options[:host] = ask(I18n.t('form.add_item.server')).to_s
- options[:protocol] = ask(I18n.t('form.add_item.protocol')).to_s
- options[:user] = ask(I18n.t('form.add_item.login')).to_s
- password = ask(I18n.t('form.add_item.password')).to_s
- options[:port] = ask(I18n.t('form.add_item.port')).to_s
- options[:comment] = ask(I18n.t('form.add_item.comment')).to_s
+ Dir.mktmpdir do |dir|
+ tmp_file = "#{dir}/#{template_name}.yml"
- if @otp
- otp_key = ask(I18n.t('form.add_item.otp_key')).to_s
+ File.open(tmp_file, 'w') do |f|
+ f << template.result(binding)
+ end
+
+ system("#{editor} #{tmp_file}")
+
+ opts = YAML::load_file(tmp_file)
end
- item = Item.new(options)
+ opts.delete_if { |k,v| v.to_s.empty? }
+ opts.each do |k,v|
+ options[k.to_sym] = v
+ end
+
+ return options
+ end
+
+ # Form to add a new item
+ # @args: password -> generate a random password
+ def add(password=false)
+ options = text_editor('add_form', nil, password)
+ item = Item.new(options)
+
+ if password
+ options[:password] = MPW::password(length: 24)
+ end
+
@mpw.add(item)
- @mpw.set_password(item.id, password)
- @mpw.set_otp_key(item.id, otp_key)
+ @mpw.set_password(item.id, options[:password]) if options.has_key?(:password)
+ @mpw.set_otp_key(item.id, options[:otp_key]) if options.has_key?(:otp_key)
@mpw.write_data
@mpw.sync(true) if @sync
puts "#{I18n.t('form.add_item.valid')}".green
+ rescue Exception => e
+ puts "#{I18n.t('display.error')} #13: #{e}".red
end
# Update an item
- # @args: id -> the item's id
- def update(id)
- item = @mpw.search_by_id(id)
+ # @args: options -> the option to search
+ def update(options={})
+ items = @mpw.list(options)
+
+ if items.length == 0
+ puts "#{I18n.t('display.warning')}: #{I18n.t('warning.select')}".yellow
+ else
+ table(items) if items.length > 1
- if not item.nil?
- options = {}
-
- puts I18n.t('form.update_item.title')
- puts '--------------------'
- options[:name] = ask(I18n.t('form.update_item.name' , name: item.name)).to_s
- options[:group] = ask(I18n.t('form.update_item.group' , group: item.group)).to_s
- options[:host] = ask(I18n.t('form.update_item.server' , server: item.host)).to_s
- options[:protocol] = ask(I18n.t('form.update_item.protocol', protocol: item.protocol)).to_s
- options[:user] = ask(I18n.t('form.update_item.login' , login: item.user)).to_s
- password = ask(I18n.t('form.update_item.password')).to_s
- options[:port] = ask(I18n.t('form.update_item.port' , port: item.port)).to_s
- options[:comment] = ask(I18n.t('form.update_item.comment' , comment: item.comment)).to_s
-
- if @otp
- otp_key = ask(I18n.t('form.update_item.otp_key')).to_s
- end
-
- options.delete_if { |k,v| v.empty? }
+ item = get_item(items)
+ options = text_editor('update_form', item)
item.update(options)
- @mpw.set_password(item.id, password) if not password.empty?
- @mpw.set_otp_key(item.id, otp_key) if not otp_key.to_s.empty?
+ @mpw.set_password(item.id, options[:password]) if options.has_key?(:password)
+ @mpw.set_otp_key(item.id, options[:otp_key]) if options.has_key?(:otp_key)
@mpw.write_data
@mpw.sync(true) if @sync
puts "#{I18n.t('form.update_item.valid')}".green
- else
- puts I18n.t('display.nothing')
end
rescue Exception => e
puts "#{I18n.t('display.error')} #14: #{e}".red
end
# Remove an item
- # @args: id -> the item's id
- # force -> no resquest a validation
- def delete(id, force=false)
- @clipboard = false
- item = @mpw.search_by_id(id)
+ # @args: options -> the option to search
+ def delete(options={})
+ items = @mpw.list(options)
+
+ if items.length == 0
+ puts "#{I18n.t('display.warning')}: #{I18n.t('warning.select')}".yellow
+ else
+ table(items)
- if item.nil?
- puts I18n.t('form.delete_item.not_valid', id: id)
- return
- end
-
- if not force
- display_item(item)
-
- confirm = ask("#{I18n.t('form.delete_item.ask', id: id)} (y/N) ").to_s
+ item = get_item(items)
+ confirm = ask("#{I18n.t('form.delete_item.ask')} (y/N) ").to_s
+
if not confirm =~ /^(y|yes|YES|Yes|Y)$/
- return
+ return false
end
+
+ item.delete
+ @mpw.write_data
+ @mpw.sync(true) if @sync
+
+ puts "#{I18n.t('form.delete_item.valid')}".green
end
-
- item.delete
- @mpw.write_data
- @mpw.sync(true) if @sync
-
- puts "#{I18n.t('form.delete_item.valid', id: id)}".green
rescue Exception => e
puts "#{I18n.t('display.error')} #16: #{e}".red
end
+ # Copy a password, otp, login
+ # @args: clipboard -> enable clipboard
+ # options -> the option to search
+ def copy(clipboard=true, options={})
+ items = @mpw.list(options)
+
+ if items.length == 0
+ puts "#{I18n.t('display.warning')}: #{I18n.t('warning.select')}".yellow
+ else
+ table(items)
+
+ item = get_item(items)
+ clipboard(item, clipboard)
+ end
+ rescue Exception => e
+ puts "#{I18n.t('display.error')} #14: #{e}".red
+ end
+
# Export the items in a CSV file
# @args: file -> the destination file
- def export(file)
- @mpw.export(file)
+ # options -> option to search
+ def export(file, options)
+ file = 'export-mpw.yml' if file.to_s.empty?
+ items = @mpw.list(options)
+ data = {}
+ i = 1
- puts "#{I18n.t('export.export.valid', file)}".green
+ items.each do |item|
+ data.merge!(i => { 'host' => item.host,
+ 'user' => item.user,
+ 'group' => item.group,
+ 'password' => @mpw.get_password(item.id),
+ 'protocol' => item.protocol,
+ 'port' => item.port,
+ 'otp_key' => @mpw.get_otp_key(item.id),
+ 'comment' => item.comment,
+ 'last_edit' => item.last_edit,
+ 'created' => item.created,
+ }
+ )
+
+ i += 1
+ end
+
+ File.open(file, 'w') {|f| f << data.to_yaml}
+
+ puts "#{I18n.t('export.valid', file)}".green
rescue Exception => e
puts "#{I18n.t('display.error')} #17: #{e}".red
end
@@ -448,7 +517,26 @@ class Cli
# Import items from a YAML file
# @args: file -> the import file
def import(file)
- @mpw.import(file)
+ raise I18n.t('import.file_empty') if file.to_s.empty?
+ raise I18n.t('import.file_not_exist') if not File.exist?(file)
+
+ YAML::load_file(file).each_value do |row|
+
+ item = Item.new(group: row['group'],
+ host: row['host'],
+ protocol: row['protocol'],
+ user: row['user'],
+ port: row['port'],
+ comment: row['comment'],
+ )
+
+ next if item.empty?
+
+ @mpw.add(item)
+ @mpw.set_password(item.id, row['password']) if not row['password'].to_s.empty?
+ @mpw.set_otp_key(item.id, row['otp_key']) if not row['otp_key'].to_s.empty?
+ end
+
@mpw.write_data
puts "#{I18n.t('form.import.valid')}".green
diff --git a/lib/mpw/config.rb b/lib/mpw/config.rb
index 90df409..2c1c496 100644
--- a/lib/mpw/config.rb
+++ b/lib/mpw/config.rb
@@ -57,20 +57,18 @@ class Config
# gpg_exe -> the path of gpg executable
# @rtrn: true if le config file is create
def setup(key, lang, wallet_dir, gpg_exe)
-
if not key =~ /[a-zA-Z0-9.-_]+\@[a-zA-Z0-9]+\.[a-zA-Z]+/
raise I18n.t('error.config.key_bad_format')
end
- if wallet_dir.empty?
+ if wallet_dir.to_s.empty?
wallet_dir = "#{@config_dir}/wallets"
end
- config = {'config' => {'key' => key,
- 'lang' => lang,
- 'wallet_dir' => wallet_dir,
- 'gpg_exe' => gpg_exe,
- }
+ config = { 'key' => key,
+ 'lang' => lang,
+ 'wallet_dir' => wallet_dir,
+ 'gpg_exe' => gpg_exe,
}
FileUtils.mkdir_p(wallet_dir, mode: 0700)
@@ -89,9 +87,9 @@ class Config
# 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?
+ if name.to_s.empty?
raise "#{I18n.t('error.config.genkey_gpg.name')}"
- elsif password.nil? or password.empty?
+ elsif password.to_s.empty?
raise "#{I18n.t('error.config.genkey_gpg.password')}"
end
@@ -114,22 +112,20 @@ class Config
raise "#{I18n.t('error.config.genkey_gpg.exception')}\n#{e}"
end
- # Check the config file
- # @rtrn: true if the config file is correct
- def is_valid?
+ # Load the config file
+ def load_config
config = YAML::load_file(@config_file)
- @key = config['config']['key']
- @lang = config['config']['lang']
- @wallet_dir = config['config']['wallet_dir']
- @gpg_exe = config['config']['gpg_exe']
+ @key = config['key']
+ @lang = config['lang']
+ @wallet_dir = config['wallet_dir']
+ @gpg_exe = config['gpg_exe']
raise if @key.empty? or @wallet_dir.empty?
I18n.locale = @lang.to_sym
- return true
- rescue
- return false
+ rescue Exception => e
+ raise "#{I18n.t('error.config.load')}\n#{e}"
end
# Check if private key exist
diff --git a/lib/mpw/item.rb b/lib/mpw/item.rb
index ef1309e..4beb49f 100644
--- a/lib/mpw/item.rb
+++ b/lib/mpw/item.rb
@@ -22,12 +22,12 @@ module MPW
class Item
attr_accessor :id
- attr_accessor :name
attr_accessor :group
attr_accessor :host
attr_accessor :protocol
attr_accessor :user
attr_accessor :port
+ attr_accessor :otp
attr_accessor :comment
attr_accessor :last_edit
attr_accessor :last_sync
@@ -38,15 +38,15 @@ class 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?
+ if not options.has_key?(:host) or options[:host].to_s.empty?
raise I18n.t('error.update.name_empty')
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
+ 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]
+ @id = options[:id]
@created = options[:created]
@last_edit = options[:last_edit]
options[:no_update_last_edit] = true
@@ -58,16 +58,16 @@ class Item
# Update the item
# @args: options -> a hash of parameter
def update(options={})
- if options.has_key?(:name) and options[:name].to_s.empty?
+ if options.has_key?(:host) and options[:host].to_s.empty?
raise I18n.t('error.update.name_empty')
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?
+ @otp = options[:otp] if options.has_key?(:otp)
@comment = options[:comment] if options.has_key?(:comment)
@last_edit = Time.now.to_i if not options.has_key?(:no_update_last_edit)
end
@@ -80,12 +80,12 @@ class Item
# Delete all data
def delete
@id = nil
- @name = nil
@group = nil
@host = nil
@protocol = nil
@user = nil
@port = nil
+ @otp = nil
@comment = nil
@created = nil
@last_edit = nil
@@ -93,7 +93,7 @@ class Item
end
def empty?
- return @name.to_s.empty?
+ return @id.to_s.empty?
end
def nil?
@@ -104,6 +104,6 @@ class Item
private
def generate_id
return ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(16).join
- end
+ end
+end
end
-end
diff --git a/lib/mpw/mpw.rb b/lib/mpw/mpw.rb
index baa83f5..533077e 100644
--- a/lib/mpw/mpw.rb
+++ b/lib/mpw/mpw.rb
@@ -33,7 +33,7 @@ class MPW
@gpg_exe = gpg_exe
@wallet_file = wallet_file
- if @gpg_exe
+ if not @gpg_exe.to_s.empty?
GPGME::Engine.set_info(GPGME::PROTOCOL_OpenPGP, @gpg_exe, "#{Dir.home}/.gnupg")
end
end
@@ -83,12 +83,12 @@ class MPW
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'],
+ otp: @otp_keys.has_key?(d['id']),
comment: d['comment'],
last_edit: d['last_edit'],
created: d['created'],
@@ -110,16 +110,15 @@ class MPW
@data.each do |item|
next if item.empty?
- 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,
+ data.merge!(item.id => { 'id' => item.id,
+ '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
@@ -158,7 +157,7 @@ class MPW
File.rename(tmp_file, @wallet_file)
rescue Exception => e
- File.unlink(tmp_file)
+ File.unlink(tmp_file) if File.exist?(tmp_file)
raise "#{I18n.t('error.mpw_file.write_data')}\n#{e}"
end
@@ -211,16 +210,16 @@ class MPW
# Set config
# args: config -> a hash with config options
- def set_config(config)
- @config['sync'] = {} if @config['sync'].nil?
+ def set_config(options={})
+ @config = {} if @config.nil?
- @config['sync']['type'] = config['sync']['type']
- @config['sync']['host'] = config['sync']['host']
- @config['sync']['port'] = config['sync']['port']
- @config['sync']['user'] = config['sync']['user']
- @config['sync']['password'] = config['sync']['password']
- @config['sync']['path'] = config['sync']['path']
- @config['sync']['last_sync'] = @config['sync']['last_sync'].nil? ? 0 : @config['sync']['last_sync']
+ @config['protocol'] = options[:protocol] if options.has_key?(:protocol)
+ @config['host'] = options[:host] if options.has_key?(:host)
+ @config['port'] = options[:port] if options.has_key?(:port)
+ @config['user'] = options[:user] if options.has_key?(:user)
+ @config['password'] = options[:password] if options.has_key?(:password)
+ @config['path'] = options[:path] if options.has_key?(:path)
+ @config['last_sync'] = @config['last_sync'].nil? ? 0 : @config['last_sync']
end
# Add a new item
@@ -241,18 +240,17 @@ class MPW
def list(options={})
result = []
- search = options[:search].to_s.downcase
- group = options[:group].to_s.downcase
+ search = options[:pattern].to_s.downcase
+ group = options[:group].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 group.empty? and not group.eql?(item.group.to_s.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}.*$/
+ if not host =~ /^.*#{search}.*$/ and not comment =~ /^.*#{search}.*$/
next
end
@@ -273,56 +271,9 @@ class MPW
return nil
end
- # Export to yaml
- # @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', file: file)}\n#{e}"
- end
-
- # Import to yaml
- # @args: file -> path to file import
- 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', file: file)}\n#{e}"
- end
-
# Get last sync
def get_last_sync
- return @config['sync']['last_sync'].to_i
+ return @config['last_sync'].to_i
rescue
return 0
end
@@ -330,18 +281,18 @@ class MPW
# Sync data with remote file
# @args: force -> force the sync
def sync(force=false)
- return if @config.empty? or @config['sync']['type'].to_s.empty?
+ return if @config.empty? or @config['protocol'].to_s.empty?
return if get_last_sync + 300 > Time.now.to_i and not force
tmp_file = "#{@wallet_file}.sync"
- case @config['sync']['type']
+ case @config['protocol']
when 'sftp', 'scp', 'ssh'
require "mpw/sync/ssh"
- sync = SyncSSH.new(@config['sync'])
+ sync = SyncSSH.new(@config)
when 'ftp'
require 'mpw/sync/ftp'
- sync = SyncFTP.new(@config['sync'])
+ sync = SyncFTP.new(@config)
else
raise I18n.t('error.sync.unknown_type')
end
@@ -365,8 +316,7 @@ class MPW
# Update item
if item.last_edit < r.last_edit
- item.update(name: r.name,
- group: r.group,
+ item.update(group: r.group,
host: r.host,
protocol: r.protocol,
user: r.user,
@@ -394,7 +344,6 @@ class MPW
next if r.last_edit <= get_last_sync
item = Item.new(id: r.id,
- name: r.name,
group: r.group,
host: r.host,
protocol: r.protocol,
@@ -415,7 +364,7 @@ class MPW
item.set_last_sync
end
- @config['sync']['last_sync'] = Time.now.to_i
+ @config['last_sync'] = Time.now.to_i
write_data
sync.update(@wallet_file)
@@ -429,13 +378,27 @@ class MPW
# args: id -> the item id
# key -> the new key
def set_otp_key(id, key)
- @otp_keys[id] = encrypt(key)
+ if not key.to_s.empty?
+ @otp_keys[id] = encrypt(key.to_s)
+ end
end
+ # Get an opt key
+ # args: id -> the item id
+ # key -> the new key
+ def get_otp_key(id)
+ if @otp_keys.has_key?(id)
+ return decrypt(@otp_keys[id])
+ else
+ return nil
+ end
+ end
+
+
# Get an otp code
# @args: id -> the item id
# @rtrn: an otp code
- def get_otp_code(id)
+ def get_otp_code(id)
if not @otp_keys.has_key?(id)
return 0
else
@@ -481,6 +444,8 @@ class MPW
# @args: data -> string to decrypt
private
def decrypt(data)
+ return nil if data.to_s.empty?
+
crypto = GPGME::Crypto.new(armor: true)
return crypto.decrypt(data, password: @gpg_pass).read.force_encoding('utf-8')
@@ -495,12 +460,12 @@ class MPW
recipients = []
crypto = GPGME::Crypto.new(armor: true, always_trust: true)
+ recipients.push(@key)
@keys.each_key do |key|
+ next if key == @key
recipients.push(key)
end
- recipients.push(@key) if not recipients.index(@key).nil?
-
return crypto.encrypt(data, recipients: recipients).read
rescue Exception => e
raise "#{I18n.t('error.gpg_file.encrypt')}\n#{e}"
diff --git a/mpw.gemspec b/mpw.gemspec
index fa4afc7..09fe6d9 100644
--- a/mpw.gemspec
+++ b/mpw.gemspec
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "locale"
spec.add_dependency "colorize"
spec.add_dependency "net-ssh"
- spec.add_dependency "net-scp"
+ spec.add_dependency "net-sftp"
spec.add_dependency "clipboard"
spec.add_dependency "rotp"
end
diff --git a/templates/add_form.erb b/templates/add_form.erb
new file mode 100644
index 0000000..c2d6d73
--- /dev/null
+++ b/templates/add_form.erb
@@ -0,0 +1,9 @@
+---
+host: # <%= I18n.t('form.add_item.host') %>
+user: # <%= I18n.t('form.add_item.login') %>
+group: # <%= I18n.t('form.add_item.group') %>
+protocol: # <%= I18n.t('form.add_item.protocol') %><% if not password %>
+password: # <%= I18n.t('form.add_item.password') %><% end %>
+port: # <%= I18n.t('form.add_item.port') %>
+comment: # <%= I18n.t('form.add_item.comment') %>
+otp_key: # <%= I18n.t('form.add_item.otp_key') %>
diff --git a/templates/setup_form.erb b/templates/setup_form.erb
new file mode 100644
index 0000000..f7e6d7b
--- /dev/null
+++ b/templates/setup_form.erb
@@ -0,0 +1,9 @@
+---
+# <%= I18n.t('form.setup_config.lang') %>
+language: <%= @config.lang %>
+# <%= I18n.t('form.setup_config.gpg_key') %>
+gpg_key: <%= @config.key %>
+# <%= I18n.t('form.setup_config.wallet_dir') %>
+wallet_dir: <%= @config.config_dir %>
+# <%= I18n.t('form.setup_config.gpg_exe') %>
+gpg_exe: <%= @config.gpg_exe %>
diff --git a/templates/update_form.erb b/templates/update_form.erb
new file mode 100644
index 0000000..f4a380b
--- /dev/null
+++ b/templates/update_form.erb
@@ -0,0 +1,17 @@
+---
+# <%= I18n.t('form.update_item.host') %>
+host: <%= item.host %>
+# <%= I18n.t('form.update_item.login') %>
+user: <%= item.user %>
+# <%= I18n.t('form.update_item.password') %>
+password:
+# <%= I18n.t('form.update_item.group') %>
+group: <%= item.group %>
+# <%= I18n.t('form.update_item.protocol') %>
+protocol: <%= item.protocol %>
+# <%= I18n.t('form.update_item.port') %>
+port: <%= item.port %>
+# <%= I18n.t('form.update_item.otp_key') %>
+opt_key:
+# <%= I18n.t('form.update_item.comment') %>
+comment: <%= item.comment %>