Class: Gtk2PasswordApp

Inherits:
Object
  • Object
show all
Extended by:
Rafini::Empty
Defined in:
lib/gtk2passwordapp.rb,
lib/gtk2passwordapp/cli.rb,
lib/gtk2passwordapp/gui.rb,
lib/gtk2passwordapp/config.rb,
lib/gtk2passwordapp/account.rb,
lib/gtk2passwordapp/accounts.rb

Defined Under Namespace

Classes: Account, Accounts

Constant Summary collapse

VERSION =
'6.2.210911'
HELP =
<<~HELP
  Usage:
    gtk2passwordapp [:gui+]
    gtk2passwordapp :cli [<pattern> [<file>]]
    gtk2passwordapp :info
  Gui:
    --minime      \t Real minime
    --notoggle    \t Minime wont toggle decorated and keep above
    --notdecorated\t Dont decorate window
  Cli:
    --nogui
  Info:
    -v --version   \tShow version and exit
    -h --help      \tShow help and exit
  # Notes #
  With the --nogui cli-option,
  one can give a pattern to filter by account names.
  Default passwords data file is:
    ~/.cache/gtk3app/gtk2passwordapp/dump.yzb
HELP
H2Q =
BaseConvert::FromTo.new(base: 16, digits: '0123456789ABCDEF',
to_base:91, to_digits: :qgraph)
RND =
SuperRandom.new
TOTPx =
/^[A-Z2-7]{16,}$/
CONFIG =
{

  # Hashing

  Salt: s0,
  LongPwd: 14,

  # Miscellaneous Strings

  Logo: "#{UserSpace::XDG['data']}/gtk3app/gtk2passwordapp/logo.png",
  HiddenPwd: ' * * * ',

  # Overriding Gtk3App's window, main:

  main: { set_title: 'Password Manager' },

  # Overriding Gtk3App's toolbar
  toolbar: {
    set_expanded: true,
    into: [:pack_start, expand:true, fill:true, padding:4],
  },

  # Overriding Gtk3App's app_menu:

  app_menu: {
    add_menu_item: [:minime!,:about!,:quit!],
  },

  # Colors

  Red: '#900',
  Green: '#090',
  Blue: '#009',
  White: '#FFF',
  TooOld: 60*60*24*365, # Year

  # Buttons

  tool_button: {
    set_width_request: 1,
    into: [:pack_start, expand:true, fill:true, padding:1],
  },

  # Spin Buttons

  PWDLEN: [3,40,1],
  pwdlen: {set_value: 13},
  pwdlen!: [:PWDLEN,:pwdlen],

  # Tools Labels

  ADD:      [label: 'Add'],
  EDIT:     [label: 'Edit'],
  GO:       [label: 'Go'],
  CANCEL:   [label: 'Cancel'],
  DELETE:   [label: 'Delete'],
  SAVE:     [label: 'Save'],
  CURRENT:  [label: 'Current'],
  PREVIOUS: [label: 'Previous'],
  RAND:     [label: 'Random'],

  # Initialize
  # Stage and Toolbar

  ClipboardTimeout: 15,
  PwdFile: "#{UserSpace::XDG['cache']}/gtk3app/gtk2passwordapp/dump.yzb",

  # Logo's Main Menu

  MAIN_MENU: a0,
  main_menu: h0,
  main_menu!: [:MAIN_MENU,:main_menu],

  main_menu_item: h0,

  # Toolbar's Toolbox

  TOOLBOX: [:horizontal],
  toolbox: h0,
  toolbox!: [:TOOLBOX,:toolbox],

  # Stage's Pages

  PAGES: [:vertical],
  pages: h0,
  pages!: [:PAGES,:pages],

  # Page

  PAGE: [:vertical],
  page: h0,
  page!: [:PAGE,:page],

  # Page Label

  page_label: h0,

  # Field Row

  FIELD_ROW: [:horizontal],
  field_row: h0,
  field_row!: [:FIELD_ROW,:field_row],

  # Field Label

  FIELD_LABEL: a0,
  field_label: {
    set_selectable: false,
    set_width_request: 80,
    set_alignment: [1.0,0.5],
    set_padding: [4,4],
  },
  field_label!: [:FIELD_LABEL, :field_label],

  # Field View

  FIELD_VIEW: a0,
  field_view: {
    set_selectable: true,
    set_width_request: 250,
    set_alignment: [0.0,0.5],
    set_padding: [4,4],
  },
  field_view!: [:FIELD_VIEW, :field_view],

  # Field Entry

  FIELD_ENTRY: a0,
  field_entry: {
    set_width_request: 250,
    into: [:pack_start, expand:true, fill:true, padding:4],
  },
  field_entry!: [:FIELD_ENTRY,:field_entry],

  # Password Entry

  PASSWORD_ENTRY: a0,
  password_entry: {
    set_visibility: false,
    set_width_request: 250,
    into: [:pack_start, expand:true, fill:true, padding:4],
  },
  password_entry!: [:PASSWORD_ENTRY,:password_entry],

  # Error Label

  ERROR_LABEL: a0,
  error_label: h0,
  error_label!: [:ERROR_LABEL,:error_label],

  # Password Page

  MinPwdLen: 7,
  Confirm: "Confirm password!",

  # Page Labels

  PASSWORD_PAGE_LABEL: ['Enter Master Password'],
  ADD_PAGE_LABEL: ['Add Account'],
  EDIT_PAGE_LABEL: ['Edit Account'],
  MAIN_PAGE_LABEL: ['View Account'],

  # Fields

  NAME: ['Name:'],
  URL: ['URL:'],
  NOTE: ['Note:'],
  USERNAME: ['Username:'],
  PASSWORD: ['Password:'],

  # About Dialog

  about_dialog: {
    set_program_name: 'Password Manager',
    set_version: VERSION.semantic(0..1),
    set_copyright: '(c) 2021 CarlosJHR64',
    set_comments: 'A Gtk3App Password Manager',
    set_website: 'https://github.com/carlosjhr64/gtk2passwordapp',
    set_website_label: 'See it at GitHub!',
  },

  # Delete Dialog

  DELETE_URSURE: a0,
  delete_ursure: {add_label: 'Delete?'},
  delete_ursure!: [:DELETE_URSURE,:delete_ursure],

  # Reset Dialog

  RESET_URSURE: a0,
  reset_ursure: {add_label: 'Reset Master Password?'},
  reset_ursure!: [:RESET_URSURE,:reset_ursure],

  # Errors

  BadUrl: 'URL must be like http://site.',
  BadUsername: 'Username must be all graph.',
  BadPassword: 'Password must be all graph.',
  BadName: 'Account name must be a non-empty String.',
  CipherError: 'Decryption error.',
  AccountHit: 'Account exists.',
  AccountMiss: 'Account does NOT exist.',
  TooShort: "Password too short!",

}

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(stage, toolbar, options) ⇒ Gtk2PasswordApp

Returns a new instance of Gtk2PasswordApp.



2
3
4
5
6
7
8
9
10
11
12
13
14
15
# File 'lib/gtk2passwordapp/gui.rb', line 2

def initialize(stage, toolbar, options)
  @primary   = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)  
  @clipboard = Gtk::Clipboard.get(Gdk::Selection::CLIPBOARD)
  @accounts  = Accounts.new(CONFIG[:PwdFile])
  @toolbox   = Such::Box.new toolbar, :toolbox!
  @pages     = Such::Box.new stage, :pages!
  @recent = []
  @reset = @on_main_page = false
  @red,@green,@blue,@white =
    [:Red,:Green,:Blue,:White].map{Gdk::RGBA.parse(CONFIG[_1])}
  @tools = @password_page = @add_page = @edit_page = @main_page = @menu = nil
  build_password_page
  build_logo_menu
end

Class Method Details

.cliObject



24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/gtk2passwordapp.rb', line 24

def self.cli
  # User using cli may be experiencing system problems.
  begin
    require 'gtk2passwordapp/cli'
    require 'yaml_zlib_blowfish'
    require 'base_convert'
  rescue LoadError
    $stderr.puts 'Missing Gem:'
    $stderr.puts $!.message
    exit 72
  end
end

.guiObject



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/gtk2passwordapp.rb', line 37

def self.gui
  # This is a Gtk3App.
  require 'gtk3app'

  # Helper Gems.
  require 'base32'
  require 'totp'
  require 'yaml_zlib_blowfish'
  require 'super_random'
  require 'base_convert'

  # This Gem.
  require_relative 'gtk2passwordapp/config.rb'
  require_relative 'gtk2passwordapp/account.rb'
  require_relative 'gtk2passwordapp/accounts.rb'
  require_relative 'gtk2passwordapp/gui.rb'
end

.runObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/gtk2passwordapp/cli.rb', line 2

def self.run(pattern='.',
             dump=File.expand_path('~/.cache/gtk3app/gtk2passwordapp/dump.yzb'),
             *trash)
  unless trash.empty?
    $stderr.puts HELP
    $stderr.puts "Please match usage."
    exit 64
  end
  begin
    pattern = Regexp.new pattern, Regexp::IGNORECASE
  rescue RegexpError
    $stderr.puts $!.message
    exit 65
  end
  unless File.exist? dump
    $stderr.puts "Passwords data file missing: #{dump}"
    exit 66
  end
  system('clear; clear')
  print "Enter password: "
  pwd = $stdin.gets.strip
  system('clear; clear')
  print "Enter salt: "
  pwd << $stdin.gets.strip
  system('clear; clear')
  h2q = BaseConvert::FromTo.new base: 16, digits: '0123456789ABCDEF',
                                to_base: 91, to_digits: :qgraph
  pwd = h2q.convert Digest::SHA256.hexdigest(pwd).upcase
  begin
    lst = YamlZlibBlowfish.new(pwd).load(dump)
  rescue OpenSSL::Cipher::CipherError
    $stderr.puts "Bad password+salt"
    exit 65
  end
  pp lst.select{|k,v|pattern.match? k}
end

Instance Method Details

#add_menu_item(name, color = nil) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
# File 'lib/gtk2passwordapp/gui.rb', line 52

def add_menu_item(name, color=nil)
  menu_item = Such::MenuItem.new [label:name], :main_menu_item, 'activate' do
    (name)
  end
   = @accounts.get name
  unless color
    color = (Time.now.to_i - .updated > CONFIG[:TooOld])? @red : @blue
  end
  menu_item.override_color :normal, color
  @menu.append menu_item
end

#bootstrap_setupsObject



105
106
107
108
109
110
# File 'lib/gtk2passwordapp/gui.rb', line 105

def bootstrap_setups
  names = @accounts.data.keys
   = (names.empty?)? nil : @accounts.get(names[rand(names.length)])
  setup_edit_page() if 
  setup_main_page()
end

#build_add_pageObject



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/gtk2passwordapp/gui.rb', line 147

def build_add_page
  @add_page = Such::Box.new @pages, :page!
  Such::Label.new @add_page, :ADD_PAGE_LABEL, :page_label
  error_label = nil # updates below
   = field_row(@add_page, :NAME) do
    name = .text.strip
    @accounts.add name
    @accounts.save
     = @accounts.get name
    setup_edit_page()
    setup_main_page()
    @add_page.hide
    .text = ''
    @edit_page.show_all
  rescue
    error_label.text = $!.message
  end
  Such::Button.new @add_page, :CANCEL, :tool_button do
    @add_page.hide
    .text = ''
    show_main_page
  end
  error_label = Such::Label.new @add_page, :error_label!
end

#build_edit_pageObject



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/gtk2passwordapp/gui.rb', line 202

def build_edit_page
  @edit_page = Such::Box.new @pages, :page!
  Such::Label.new @edit_page, :EDIT_PAGE_LABEL, :page_label

  @edit_name     = view_row  @edit_page, :NAME
  @edit_url      = field_row @edit_page, :URL
  @edit_note     = field_row @edit_page, :NOTE
  @edit_username = field_row @edit_page, :USERNAME
  @edit_password = field_row @edit_page, :PASSWORD, :password_entry!

  visibility_toggleling @edit_password

  generator = Such::Box.new @edit_page, :toolbox!
  pwdlen=rndpwd=nil
  Such::Button.new generator, :RAND, :tool_button do
    rndpwd = H2Q.convert RND.hexadecimal
    @edit_password.text = rndpwd[0...pwdlen.value]
  end
  pwdlen = Such::SpinButton.new generator, :pwdlen!, 'value-changed' do
    @edit_password.text = rndpwd[0...pwdlen.value] if rndpwd
  end

  toolbox     = Such::Box.new @edit_page,   :toolbox!
  error_label = Such::Label.new @edit_page, :error_label!

  Such::Button.new toolbox, :SAVE, :tool_button do
              = @accounts.get @edit_name.text.strip
    .url      = @edit_url.text.strip
    .note     = @edit_note.text.strip
    .username = @edit_username.text.strip
    .password = @edit_password.text.strip
    @accounts.save
    rndpwd = nil
    @edit_page.hide
    setup_main_page()
    show_main_page
  rescue
    error_label.text = $!.message
  end
  Such::Button.new toolbox, :CANCEL, :tool_button do
     = @accounts.get @edit_name.text
    rndpwd = nil
    @edit_page.hide
    setup_edit_page() # restore values
    show_main_page
  end
  Such::Button.new toolbox, :DELETE, :tool_button do
    ursure = Gtk3App::YesNoDialog.new :delete_ursure!
    Gtk3App.transient ursure
    if ursure.ok?
      @recent.delete @edit_name.text
      @accounts.delete @edit_name.text
      @accounts.save
      rndpwd = nil
      @edit_page.hide
      bootstrap_setups
      show_main_page
    end
  end
end

#build_logo_menuObject



17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/gtk2passwordapp/gui.rb', line 17

def build_logo_menu
  Gtk3App.logo_press_event do |button|
    next unless @on_main_page
    case button
    when 1
      popup_accounts_menu unless @accounts.data.empty?
    when 2
      reset_password
    when 3
      # Gets captured by Gtk3App's main menu
    end
  end
end

#build_main_pageObject



271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/gtk2passwordapp/gui.rb', line 271

def build_main_page
  @main_page = Such::Box.new @pages, :page!
  Such::Label.new @main_page, :MAIN_PAGE_LABEL, :page_label

  @name     = view_row @main_page, :NAME
  @url      = view_row @main_page, :URL
  @note     = view_row @main_page, :NOTE
  @username = view_row @main_page, :USERNAME
  @password = view_row @main_page, :PASSWORD

  show_toggleling @password
end

#build_password_pageObject



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
# File 'lib/gtk2passwordapp/gui.rb', line 118

def build_password_page
  @password_page = Such::Box.new @pages, :page!
  Such::Label.new @password_page, :PASSWORD_PAGE_LABEL, :page_label
  error_label,previous = nil,'' # updates below
  password_entry = field_row(@password_page, :PASSWORD, :password_entry!) do
    pwd = password_entry.text.strip
    password_entry.text = ''
    raise CONFIG[:TooShort] if pwd.length < CONFIG[:MinPwdLen]
    if not @reset and @accounts.exist?
      @accounts.load rehash pwd
    else
      raise CONFIG[:Confirm] unless pwd==previous
      @accounts.save rehash pwd
      @reset = false
    end
    @password_page.hide
    build_add_page  unless @add_page
    build_edit_page unless @edit_page
    build_main_page unless @main_page
    build_tools     unless @tools
    bootstrap_setups
    show_main_page
  rescue
    error_label.text = $!.message
    previous = pwd
  end
  error_label = Such::Label.new @password_page, :error_label!
end

#build_toolsObject



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/gtk2passwordapp/gui.rb', line 292

def build_tools
  @tools = true
  Such::Button.new @toolbox, :ADD, :tool_button do
    hide_main_page
    @add_page.show_all
  end
  Such::Button.new @toolbox, :EDIT, :tool_button do
    unless (name=@name.text).empty?
      hide_main_page
      @edit_page.show_all
    end
  end
  Such::Button.new @toolbox, :GO, :tool_button do
    unless (name=@name.text).empty?
      url = @accounts.get(name).url
      system(Gtk3App::CONFIG[:Open], url) unless url.empty?
    end
  end
  Such::Button.new @toolbox, :CURRENT, :tool_button do
    unless (name=@name.text).empty?
       = @accounts.get name
      copy2clipboard(.password, .username)
    end
  end
  Such::Button.new @toolbox, :PREVIOUS, :tool_button do
    unless (name=@name.text).empty?
       = @accounts.get name
      copy2clipboard(.previous, .password)
    end
  end
end

#copy2clipboard(pwd, user) ⇒ Object



72
73
74
75
76
77
78
79
# File 'lib/gtk2passwordapp/gui.rb', line 72

def copy2clipboard(pwd, user)
  @primary.text, @clipboard.text = pwd, user
  GLib::Timeout.add_seconds(CONFIG[:ClipboardTimeout]) do
    @primary.text = '' if @primary.wait_for_text == pwd
    @clipboard.text = '' if @clipboard.wait_for_text == user
    false
  end
end

#field_row(page, label, entry = :field_entry!, &block) ⇒ Object



87
88
89
90
91
# File 'lib/gtk2passwordapp/gui.rb', line 87

def field_row(page, label, entry=:field_entry!, &block)
  row = Such::Box.new page, :field_row!
  Such::Label.new(row, label, :field_label)
  Such::Entry.new(row, entry, ((block)? 'activate' : ''), &block)
end

#hide_main_pageObject



99
100
101
102
103
# File 'lib/gtk2passwordapp/gui.rb', line 99

def hide_main_page
  @on_main_page = false
  @main_page.hide
  @toolbox.hide
end


41
42
43
44
45
46
47
48
49
50
# File 'lib/gtk2passwordapp/gui.rb', line 41

def popup_accounts_menu
  @menu = Such::Menu.new :main_menu!
  @menu.override_background_color(:normal, @white)
  @recent.each do |name| add_menu_item(name, @green) end
  @accounts.data.keys.sort{|a,b|a.upcase<=>b.upcase}.each do |name|
    add_menu_item(name)
  end
  @menu.show_all
  @menu.popup_at_pointer
end

#rehash(pwd) ⇒ Object



112
113
114
115
116
# File 'lib/gtk2passwordapp/gui.rb', line 112

def rehash(pwd)
  pwd += CONFIG[:Salt] if pwd.length < CONFIG[:LongPwd]
  pwd = Digest::SHA256.hexdigest(pwd).upcase
  H2Q.convert pwd
end

#reset_passwordObject



31
32
33
34
35
36
37
38
39
# File 'lib/gtk2passwordapp/gui.rb', line 31

def reset_password
  ursure = Gtk3App::YesNoDialog.new :reset_ursure!
  Gtk3App.transient ursure
  if ursure.ok?
    @reset = true
    hide_main_page
    @password_page.show
  end
end

#selected_account(name) ⇒ Object



64
65
66
67
68
69
70
# File 'lib/gtk2passwordapp/gui.rb', line 64

def (name)
  @recent.unshift name; @recent.uniq!; @recent.pop if @recent.length>3
   = @accounts.get name
  setup_main_page 
  setup_edit_page 
  copy2clipboard(.password, .username)
end

#setup_edit_page(account) ⇒ Object



263
264
265
266
267
268
269
# File 'lib/gtk2passwordapp/gui.rb', line 263

def setup_edit_page()
  @edit_name.text     = .name
  @edit_url.text      = .url
  @edit_note.text     = .note
  @edit_username.text = .username
  @edit_password.text = .password
end

#setup_main_page(account) ⇒ Object



284
285
286
287
288
289
290
# File 'lib/gtk2passwordapp/gui.rb', line 284

def setup_main_page()
  @name.text     = &.name     || ''
  @url.text      = &.url      || ''
  @note.text     = &.note     || ''
  @username.text = &.username || ''
  @password.text =(&.password&.>'')? CONFIG[:HiddenPwd] : ''
end

#show_main_pageObject



93
94
95
96
97
# File 'lib/gtk2passwordapp/gui.rb', line 93

def show_main_page
  @on_main_page = true
  @main_page.show_all
  @toolbox.show_all
end

#show_toggleling(label) ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/gtk2passwordapp/gui.rb', line 187

def show_toggleling(label)
  label.signal_connect('button-press-event') do |_,e|
    if e.button==1 and not (pwd=label.text).empty?
      case pwd
      when CONFIG[:HiddenPwd]
        label.text = @accounts.get(@name.text).password
      when TOTPx
        label.text = TOTP.passwords(label.text)[1].to_s
      else
        label.text = CONFIG[:HiddenPwd]
      end
    end
  end
end

#view_row(page, label) ⇒ Object



81
82
83
84
85
# File 'lib/gtk2passwordapp/gui.rb', line 81

def view_row(page, label)
  row = Such::Box.new page, :field_row!
  Such::Label.new(row, label, :field_label)
  Such::Label.new(row, :field_view!)
end

#visibility_toggleling(entry) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/gtk2passwordapp/gui.rb', line 172

def visibility_toggleling(entry)
  entry.signal_connect('enter-notify-event') do
    entry.set_visibility true unless entry.has_focus?
  end
  entry.signal_connect('leave-notify-event') do
    entry.set_visibility false unless entry.has_focus?
  end
  entry.signal_connect('focus-in-event') do
    entry.set_visibility true
  end
  entry.signal_connect('focus-out-event') do
    entry.set_visibility false
  end
end