Class: MPW::MPW

Inherits:
Object
  • Object
show all
Defined in:
lib/mpw/mpw.rb

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key, wallet_file, gpg_pass = nil, gpg_exe = nil, pinmode = false) ⇒ MPW

Returns a new instance of MPW.

Parameters:

  • key (String)

    gpg key name

  • wallet_file (String)

    path of the wallet file

  • gpg_pass (String) (defaults to: nil)

    password of the gpg key

  • gpg_exe (String) (defaults to: nil)

    path of the gpg executable

  • pinmode (Boolean) (defaults to: false)

    enable the gpg pinmode



33
34
35
36
37
38
39
40
41
# File 'lib/mpw/mpw.rb', line 33

def initialize(key, wallet_file, gpg_pass = nil, gpg_exe = nil, pinmode = false)
  @key         = key
  @gpg_pass    = gpg_pass
  @gpg_exe     = gpg_exe
  @wallet_file = wallet_file
  @pinmode     = pinmode

  GPGME::Engine.set_info(GPGME::PROTOCOL_OpenPGP, @gpg_exe, "#{Dir.home}/.gnupg") unless @gpg_exe.to_s.empty?
end

Class Method Details

.password(**options) ⇒ String

Generate a random password

Parameters:

  • options (Hash)

    :length, :special, :alpha, :numeric

Returns:

  • (String)

    a random string



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/mpw/mpw.rb', line 287

def self.password(**options)
  length =
    if !options.include?(:length) || options[:length].to_i <= 0
      8
    elsif options[:length].to_i >= 32_768
      32_768
    else
      options[:length].to_i
    end

  chars = []
  chars += [*('!'..'?')] - [*('0'..'9')]          if options[:special]
  chars += [*('A'..'Z'), *('a'..'z')]             if options[:alpha]
  chars += [*('0'..'9')]                          if options[:numeric]
  chars = [*('A'..'Z'), *('a'..'z'), *('0'..'9')] if chars.empty?

  result = ''
  length.times do
    result << chars.sample
  end

  result
end

Instance Method Details

#add(item) ⇒ Object

Add a new item

Parameters:



216
217
218
219
220
221
# File 'lib/mpw/mpw.rb', line 216

def add(item)
  raise I18n.t('error.bad_class') unless item.instance_of?(Item)
  raise I18n.t('error.empty')     if item.empty?

  @data.push(item)
end

#add_key(key) ⇒ Object

Add a public key

Parameters:

  • key (String)

    new public key file or name



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/mpw/mpw.rb', line 190

def add_key(key)
  if File.exist?(key)
    data       = File.open(key).read
    key_import = GPGME::Key.import(data, armor: true)
    key        = GPGME::Key.get(key_import.imports[0].fpr).uids[0].email
  else
    data = GPGME::Key.export(key, armor: true).read
  end

  raise I18n.t('error.export_key') if data.to_s.empty?

  @keys[key] = data
  @passwords.each_key { |id| set_password(id, get_password(id)) }
  @otp_keys.each_key { |id| set_otp_key(id, get_otp_key(id)) }
end

#delete_key(key) ⇒ Object

Delete a public key

Parameters:

  • key (String)

    public key to delete



208
209
210
211
212
# File 'lib/mpw/mpw.rb', line 208

def delete_key(key)
  @keys.delete(key)
  @passwords.each_key { |id| set_password(id, get_password(id)) }
  @otp_keys.each_key { |id| set_otp_key(id, get_otp_key(id)) }
end

#get_otp_code(id) ⇒ String

Get an otp code

Parameters:

  • id (String)

    the item id

Returns:

  • (String)

    an otp code



274
275
276
# File 'lib/mpw/mpw.rb', line 274

def get_otp_code(id)
  @otp_keys.key?(id) ? ROTP::TOTP.new(decrypt(@otp_keys[id])).now : 0
end

#get_otp_key(id) ⇒ Object

Get an opt key

Parameters:

  • id (String)

    the item id



267
268
269
# File 'lib/mpw/mpw.rb', line 267

def get_otp_key(id)
  @otp_keys.key?(id) ? decrypt(@otp_keys[id]) : nil
end

#get_otp_remaining_timeInteger

Get remaining time before expire otp code

Returns:

  • (Integer)

    time in seconde



280
281
282
# File 'lib/mpw/mpw.rb', line 280

def get_otp_remaining_time
  (Time.now.utc.to_i / 30 + 1) * 30 - Time.now.utc.to_i
end

#get_password(id) ⇒ Object

Get a password

Parameters:

  • id (String)

    the item id



162
163
164
165
166
167
168
169
170
# File 'lib/mpw/mpw.rb', line 162

def get_password(id)
  password = decrypt(@passwords[id])

  if /^\$[a-zA-Z0-9]{4,9}::(?<password>.+)$/ =~ password
    Regexp.last_match('password')
  else
    password
  end
end

#list(**options) ⇒ Array

Search in some csv data

Parameters:

  • options (Hash)

Returns:

  • (Array)

    a list with the resultat of the search



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/mpw/mpw.rb', line 226

def list(**options)
  result = []

  search = options[:pattern].to_s.downcase
  group  = options[:group].to_s.downcase

  @data.each do |item|
    next if item.empty?
    next unless group.empty? || group.eql?(item.group.to_s.downcase)

    host    = item.host.to_s.downcase
    comment = item.comment.to_s.downcase

    next unless host =~ /^.*#{search}.*$/ || comment =~ /^.*#{search}.*$/

    result.push(item)
  end

  result
end

#list_keysArray

Return the list of all gpg keys

Returns:

  • (Array)

    the gpg keys name



184
185
186
# File 'lib/mpw/mpw.rb', line 184

def list_keys
  @keys.keys
end

#read_dataObject

Read mpw file



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/mpw/mpw.rb', line 44

def read_data
  @data      = []
  @keys      = {}
  @passwords = {}
  @otp_keys  = {}

  data       = nil

  return unless File.exist?(@wallet_file)

  Gem::Package::TarReader.new(File.open(@wallet_file)) do |tar|
    tar.each do |f|
      case f.full_name
      when 'wallet/meta.gpg'
        data = decrypt(f.read)

      when %r{^wallet/keys/(?<key>.+)\.pub$}
        key = Regexp.last_match('key')

        if GPGME::Key.find(:public, key).empty?
          GPGME::Key.import(f.read, armor: true)
        end

        @keys[key] = f.read

      when %r{^wallet/passwords/(?<id>[a-zA-Z0-9]+)\.gpg$}
        @passwords[Regexp.last_match('id')] = f.read

      when %r{^wallet/otp_keys/(?<id>[a-zA-Z0-9]+)\.gpg$}
        @otp_keys[Regexp.last_match('id')] = f.read

      else
        next
      end
    end
  end

  unless data.to_s.empty?
    YAML.safe_load(data).each_value do |d|
      @data.push(
        Item.new(
          id:        d['id'],
          group:     d['group'],
          host:      d['host'],
          protocol:  d['protocol'],
          user:      d['user'],
          port:      d['port'],
          otp:       @otp_keys.key?(d['id']),
          comment:   d['comment'],
          last_edit: d['last_edit'],
          created:   d['created'],
        )
      )
    end
  end

  add_key(@key) unless @keys.key?(@key)
rescue => e
  raise "#{I18n.t('error.mpw_file.read_data')}\n#{e}"
end

#search_by_id(id) ⇒ Item

Search an item with an id

Parameters:

  • id (String)

    the id item

Returns:

  • (Item)

    an item or nil



250
251
252
253
254
255
256
# File 'lib/mpw/mpw.rb', line 250

def search_by_id(id)
  @data.each do |item|
    return item if item.id == id
  end

  nil
end

#set_otp_key(id, key) ⇒ Object

Set a new opt key

Parameters:

  • id (String)

    the item id

  • key (String)

    the new key



261
262
263
# File 'lib/mpw/mpw.rb', line 261

def set_otp_key(id, key)
  @otp_keys[id] = encrypt(key.to_s) unless key.to_s.empty?
end

#set_password(id, password) ⇒ Object

Set a new password for an item

Parameters:

  • id (String)

    the item id

  • password (String)

    the new password



175
176
177
178
179
180
# File 'lib/mpw/mpw.rb', line 175

def set_password(id, password)
  salt     = MPW.password(length: Random.rand(4..9))
  password = "$#{salt}::#{password}"

  @passwords[id] = encrypt(password)
end

#write_dataObject

Encrypt all data in tarball



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/mpw/mpw.rb', line 106

def write_data
  data     = {}
  tmp_file = "#{@wallet_file}.tmp"

  @data.each do |item|
    next if item.empty?

    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

  Gem::Package::TarWriter.new(File.open(tmp_file, 'w+')) do |tar|
    data_encrypt = encrypt(data.to_yaml)
    tar.add_file_simple('wallet/meta.gpg', 0400, data_encrypt.length) do |io|
      io.write(data_encrypt)
    end

    @passwords.each do |id, password|
      tar.add_file_simple("wallet/passwords/#{id}.gpg", 0400, password.length) do |io|
        io.write(password)
      end
    end

    @otp_keys.each do |id, key|
      tar.add_file_simple("wallet/otp_keys/#{id}.gpg", 0400, key.length) do |io|
        io.write(key)
      end
    end

    @keys.each do |id, key|
      tar.add_file_simple("wallet/keys/#{id}.pub", 0400, key.length) do |io|
        io.write(key)
      end
    end
  end

  File.rename(tmp_file, @wallet_file)
rescue => e
  File.unlink(tmp_file) if File.exist?(tmp_file)

  raise "#{I18n.t('error.mpw_file.write_data')}\n#{e}"
end