2014-01-29 19:49:39 +00:00
|
|
|
#!/usr/bin/ruby
|
|
|
|
# author: nishiki
|
|
|
|
# mail: nishiki@yaegashi.fr
|
|
|
|
# info: a simple script who manage your passwords
|
|
|
|
|
2014-12-06 17:20:43 +00:00
|
|
|
require 'rubygems'
|
|
|
|
require 'gpgme'
|
|
|
|
require 'csv'
|
|
|
|
require 'i18n'
|
|
|
|
require 'fileutils'
|
2014-01-29 19:49:39 +00:00
|
|
|
|
2014-12-06 17:20:43 +00:00
|
|
|
module MPW
|
2014-01-29 19:49:39 +00:00
|
|
|
class MPW
|
|
|
|
|
|
|
|
attr_accessor :error_msg
|
|
|
|
|
|
|
|
# Constructor
|
2014-02-02 16:34:05 +00:00
|
|
|
def initialize(file_gpg, key=nil, share_keys='')
|
|
|
|
@error_msg = nil
|
|
|
|
@file_gpg = file_gpg
|
|
|
|
@key = key
|
|
|
|
@share_keys = share_keys
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Decrypt a gpg file
|
|
|
|
# @args: password -> the GPG key password
|
|
|
|
# @rtrn: true if data has been decrypted
|
|
|
|
def decrypt(passwd=nil)
|
2014-12-06 20:37:18 +00:00
|
|
|
@data = {}
|
2014-01-29 19:49:39 +00:00
|
|
|
|
2014-01-30 22:08:38 +00:00
|
|
|
if File.exist?(@file_gpg)
|
2014-08-31 10:28:20 +00:00
|
|
|
crypto = GPGME::Crypto.new(armor: true)
|
2014-12-06 15:24:40 +00:00
|
|
|
data_decrypt = crypto.decrypt(IO.read(@file_gpg), password: passwd).read.force_encoding('utf-8')
|
2014-12-06 20:37:18 +00:00
|
|
|
@data = YAML.load(data_decrypt) if not data_decrypt.to_s.empty?
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
2014-01-30 22:08:38 +00:00
|
|
|
|
|
|
|
return true
|
|
|
|
rescue Exception => e
|
|
|
|
@error_msg = "#{I18n.t('error.gpg_file.decrypt')}\n#{e}"
|
|
|
|
return false
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Encrypt a file
|
|
|
|
# @rtrn: true if the file has been encrypted
|
2014-01-30 22:08:38 +00:00
|
|
|
def encrypt
|
2014-12-06 15:24:40 +00:00
|
|
|
FileUtils.cp(@file_gpg, "#{@file_gpg}.bk") if File.exist?(@file_gpg)
|
2014-01-29 19:49:39 +00:00
|
|
|
|
2014-12-06 15:24:40 +00:00
|
|
|
data_to_encrypt = @data.to_yaml
|
2014-01-29 19:49:39 +00:00
|
|
|
|
2014-08-31 16:31:14 +00:00
|
|
|
recipients = []
|
2014-02-02 16:34:05 +00:00
|
|
|
recipients.push(@key)
|
2014-11-16 18:39:38 +00:00
|
|
|
if not @share_keys.nil?
|
2014-02-02 16:34:05 +00:00
|
|
|
@share_keys.split.each { |k| recipients.push(k) }
|
|
|
|
end
|
|
|
|
|
2014-12-06 15:24:40 +00:00
|
|
|
crypto = GPGME::Crypto.new(armor: true)
|
|
|
|
file_gpg = File.open(@file_gpg, 'w+')
|
2014-08-31 10:28:20 +00:00
|
|
|
crypto.encrypt(data_to_encrypt, recipients: recipients, output: file_gpg)
|
2014-01-30 22:08:38 +00:00
|
|
|
file_gpg.close
|
2014-01-29 19:49:39 +00:00
|
|
|
|
2014-12-06 15:24:40 +00:00
|
|
|
FileUtils.rm("#{@file_gpg}.bk") if File.exist?("#{@file_gpg}.bk")
|
2014-01-30 22:08:38 +00:00
|
|
|
return true
|
|
|
|
rescue Exception => e
|
|
|
|
@error_msg = "#{I18n.t('error.gpg_file.encrypt')}\n#{e}"
|
2014-12-06 15:24:40 +00:00
|
|
|
FileUtils.mv("#{@file_gpg}.bk", @file_gpg) if File.exist?("#{@file_gpg}.bk")
|
2014-01-30 22:08:38 +00:00
|
|
|
return false
|
2014-01-29 19:49:39 +00:00
|
|
|
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)
|
2014-01-31 23:09:22 +00:00
|
|
|
result = []
|
2014-01-29 19:49:39 +00:00
|
|
|
|
2014-11-16 18:39:38 +00:00
|
|
|
if not search.nil?
|
2014-01-29 19:49:39 +00:00
|
|
|
search = search.downcase
|
|
|
|
end
|
|
|
|
|
2014-12-06 15:24:40 +00:00
|
|
|
@data.each do |id, row|
|
|
|
|
name = row['name'].nil? ? nil : row['name'].downcase
|
|
|
|
server = row['host'].nil? ? nil : row['host'].downcase
|
|
|
|
comment = row['comment'].nil? ? nil : row['comment'].downcase
|
2014-01-29 19:49:39 +00:00
|
|
|
|
2014-11-16 18:39:38 +00:00
|
|
|
if name =~ /^.*#{search}.*$/ or server =~ /^.*#{search}.*$/ or comment =~ /^.*#{search}.*$/
|
|
|
|
if (protocol.nil? or protocol.eql?(row[:protocol])) and (group.nil? or group.eql?(row[:group]))
|
2014-01-29 19:49:39 +00:00
|
|
|
result.push(row)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return result
|
|
|
|
end
|
|
|
|
|
|
|
|
# Search in some csv data
|
2014-12-06 15:24:40 +00:00
|
|
|
# @args: id_search -> the id item
|
2014-01-29 19:49:39 +00:00
|
|
|
# @rtrn: a row with the resultat of the search
|
2014-12-06 15:24:40 +00:00
|
|
|
def search_by_id(id_search)
|
|
|
|
@data.each do |id, row|
|
|
|
|
return row if id == id_search
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
|
|
|
|
2014-01-31 23:09:22 +00:00
|
|
|
return []
|
2014-01-29 19:49:39 +00:00
|
|
|
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)
|
2014-08-31 10:28:20 +00:00
|
|
|
row = {}
|
2014-01-29 19:49:39 +00:00
|
|
|
update = false
|
|
|
|
|
|
|
|
i = 0
|
2014-12-06 20:37:18 +00:00
|
|
|
if @data.instance_of?(Hash) and @data.has_key?(id)
|
2014-12-06 15:24:40 +00:00
|
|
|
row = @data[id]
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
if port.to_i <= 0
|
|
|
|
port = nil
|
|
|
|
end
|
|
|
|
|
2014-12-06 15:24:40 +00:00
|
|
|
row_update = {}
|
|
|
|
row_update['id'] = id.to_s.empty? ? MPW.password(16) : id
|
|
|
|
row_update['name'] = name.to_s.empty? ? row['name'] : name
|
|
|
|
row_update['group'] = group.to_s.empty? ? row['group'] : group
|
|
|
|
row_update['host'] = server.to_s.empty? ? row['host'] : server
|
|
|
|
row_update['protocol'] = protocol.to_s.empty? ? row['protocol'] : protocol
|
|
|
|
row_update['login'] = login.to_s.empty? ? row['login'] : login
|
|
|
|
row_update['password'] = passwd.to_s.empty? ? row['password'] : passwd
|
|
|
|
row_update['port'] = port.to_s.empty? ? row['port'] : port
|
|
|
|
row_update['comment'] = comment.to_s.empty? ? row['comment'] : comment
|
|
|
|
row_update['date'] = Time.now.to_i
|
2014-08-31 10:28:20 +00:00
|
|
|
|
2014-12-06 15:24:40 +00:00
|
|
|
if row_update['name'].to_s.empty?
|
2014-01-29 19:49:39 +00:00
|
|
|
@error_msg = I18n.t('error.update.name_empty')
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
if update
|
2014-12-06 15:24:40 +00:00
|
|
|
@data[id] = row_update
|
2014-01-29 19:49:39 +00:00
|
|
|
else
|
2014-12-06 15:24:40 +00:00
|
|
|
@data[row_update['id']] = row_update
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
# Remove an item
|
|
|
|
# @args: id -> the item's identifiant
|
|
|
|
# @rtrn: true if the item has been deleted
|
|
|
|
def remove(id)
|
2014-12-06 15:24:40 +00:00
|
|
|
@data.each do |k, row|
|
|
|
|
if k == id
|
|
|
|
@data.delete(id)
|
2014-01-29 19:49:39 +00:00
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-08-31 10:28:20 +00:00
|
|
|
@error_msg = I18n.t('error.delete.id_no_exist', id: id)
|
2014-01-29 19:49:39 +00:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
# Export to csv
|
2014-11-15 18:14:42 +00:00
|
|
|
# @args: file -> file where you export the data
|
2014-12-06 15:24:40 +00:00
|
|
|
# type -> udata type
|
2014-01-29 19:49:39 +00:00
|
|
|
# @rtrn: true if export work
|
2014-12-06 22:32:09 +00:00
|
|
|
def export(file, type=:yaml)
|
2014-11-15 18:14:42 +00:00
|
|
|
case type
|
|
|
|
when :csv
|
|
|
|
CSV.open(file, 'w', write_headers: true,
|
|
|
|
headers: ['name', 'group', 'protocol', 'host', 'login', 'password', 'port', 'comment']) do |csv|
|
2014-12-06 15:24:40 +00:00
|
|
|
@data.each do |id, r|
|
|
|
|
csv << [r['name'], r['group'], r['protocol'], r['host'], r['login'], r['password'], r['port'], r['comment']]
|
2014-11-15 18:14:42 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-12-06 17:20:43 +00:00
|
|
|
when :yaml
|
|
|
|
File.open(file, 'w') {|f| f << @data.to_yaml}
|
2014-11-15 18:14:42 +00:00
|
|
|
|
|
|
|
else
|
|
|
|
@error_msg = "#{I18n.t('error.export.unknown_type', type: type)}"
|
|
|
|
return false
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
2014-08-31 15:40:37 +00:00
|
|
|
|
2014-01-30 22:08:38 +00:00
|
|
|
return true
|
|
|
|
rescue Exception => e
|
2014-08-31 10:28:20 +00:00
|
|
|
@error_msg = "#{I18n.t('error.export.write', file: file)}\n#{e}"
|
2014-01-30 22:08:38 +00:00
|
|
|
return false
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Import to csv
|
|
|
|
# @args: file -> path to file import
|
2014-12-06 15:24:40 +00:00
|
|
|
# type -> udata type
|
2014-01-29 19:49:39 +00:00
|
|
|
# @rtrn: true if the import work
|
2014-12-06 22:32:09 +00:00
|
|
|
def import(file, type=:yaml)
|
2014-12-06 15:24:40 +00:00
|
|
|
case type
|
|
|
|
when :csv
|
2014-12-06 17:20:43 +00:00
|
|
|
CSV.foreach(file, {headers: true}) do |row|
|
|
|
|
if not update(row['name'], row['group'], row['host'], row['protocol'], row['login'], row['password'], row['port'], row['comment'])
|
2014-12-06 15:24:40 +00:00
|
|
|
return false
|
|
|
|
end
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
2014-12-06 17:20:43 +00:00
|
|
|
|
2014-12-06 15:24:40 +00:00
|
|
|
when :yaml
|
|
|
|
YAML::load_file(file).each do |k, row|
|
|
|
|
if not update(row['name'], row['group'], row['host'], row['protocol'], row['login'], row['password'], row['port'], row['comment'])
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
end
|
2014-12-06 17:20:43 +00:00
|
|
|
|
2014-12-06 15:24:40 +00:00
|
|
|
else
|
|
|
|
@error_msg = "#{I18n.t('error.export.unknown_type', type: type)}"
|
|
|
|
return false
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
2014-01-30 22:08:38 +00:00
|
|
|
|
|
|
|
return true
|
|
|
|
rescue Exception => e
|
2014-08-31 10:28:20 +00:00
|
|
|
@error_msg = "#{I18n.t('error.import.read', file: file)}\n#{e}"
|
2014-01-30 22:08:38 +00:00
|
|
|
return false
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Return a preview import
|
|
|
|
# @args: file -> path to file import
|
2014-12-06 20:37:18 +00:00
|
|
|
# @rtrn: a hash with the items to import, if there is an error return false
|
2014-12-06 15:24:40 +00:00
|
|
|
def import_preview(file, type=:csv)
|
2014-01-31 23:09:22 +00:00
|
|
|
result = []
|
2014-12-06 15:24:40 +00:00
|
|
|
case type
|
|
|
|
when :csv
|
|
|
|
CSV.foreach(file, {headers: true}) do |row|
|
|
|
|
result << row
|
|
|
|
end
|
|
|
|
when :yaml
|
|
|
|
YAML::load_file(file).each do |k, row|
|
|
|
|
result << row
|
|
|
|
end
|
|
|
|
else
|
|
|
|
@error_msg = "#{I18n.t('error.export.unknown_type', type: type)}"
|
|
|
|
return false
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
2014-01-30 22:08:38 +00:00
|
|
|
|
|
|
|
return result
|
|
|
|
rescue Exception => e
|
2014-08-31 10:28:20 +00:00
|
|
|
@error_msg = "#{I18n.t('error.import.read', file: file)}\n#{e}"
|
2014-01-30 22:08:38 +00:00
|
|
|
return false
|
2014-01-29 19:49:39 +00:00
|
|
|
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)
|
2014-12-06 20:37:18 +00:00
|
|
|
if not data_remote.instance_of?(Array)
|
|
|
|
@error_msg = I18n.t('error.sync.array')
|
2014-01-29 19:49:39 +00:00
|
|
|
return false
|
2014-12-06 17:20:43 +00:00
|
|
|
else not data_remote.to_s.empty?
|
|
|
|
@data.each do |lk, l|
|
2014-04-27 16:10:21 +00:00
|
|
|
j = 0
|
|
|
|
update = false
|
|
|
|
|
|
|
|
# Update item
|
2014-12-06 20:37:18 +00:00
|
|
|
data_remote.each do |r|
|
2014-12-06 17:20:43 +00:00
|
|
|
if l['id'] == r['id']
|
|
|
|
if l['date'].to_i < r['date'].to_i
|
|
|
|
update(r['name'], r['group'], r['host'], r['protocol'], r['login'], r['password'], r['port'], r['comment'], l['id'])
|
2014-04-27 16:10:21 +00:00
|
|
|
end
|
|
|
|
update = true
|
2014-12-06 20:37:18 +00:00
|
|
|
data_remote.delete(r['id'])
|
2014-04-27 16:10:21 +00:00
|
|
|
break
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
2014-04-27 16:10:21 +00:00
|
|
|
j += 1
|
|
|
|
end
|
|
|
|
|
|
|
|
# Delete an old item
|
2014-12-06 17:20:43 +00:00
|
|
|
if not update and l['date'].to_i < last_update
|
|
|
|
remove(l['id'])
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Add item
|
2014-12-06 20:37:18 +00:00
|
|
|
data_remote.each do |r|
|
2014-12-06 17:20:43 +00:00
|
|
|
if r['date'].to_i > last_update
|
|
|
|
update(r['name'], r['group'], r['host'], r['protocol'], r['login'], r['password'], r['port'], r['comment'], r['id'])
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-01-30 22:08:38 +00:00
|
|
|
return encrypt
|
2014-01-29 19:49:39 +00:00
|
|
|
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
|
|
|
|
|
2014-12-06 17:20:43 +00:00
|
|
|
end
|
2014-01-29 19:49:39 +00:00
|
|
|
end
|