passvault 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Laurent_Vansteenberghe.zip +0 -0
- data/README.md +65 -0
- data/bin/passvault +3 -0
- data/bin/passvault-gui +3 -0
- data/lib/bytes_manip.rb +47 -0
- data/lib/conn_cmds.rb +337 -0
- data/lib/conn_constants.rb +116 -0
- data/lib/conn_init.rb +92 -0
- data/lib/conn_transmit.rb +96 -0
- data/lib/connection.rb +28 -0
- data/lib/console_ui.rb +243 -0
- data/lib/crypto.rb +146 -0
- data/lib/file_access.rb +103 -0
- data/lib/gui.rb +270 -0
- data/lib/key_settings.rb +124 -0
- data/lib/rights.rb +6 -0
- data/lib/vault.rb +357 -0
- data/passvault.gemspec +30 -0
- data/pres/secu.pdf +0 -0
- data/pres/secu.pptx +0 -0
- metadata +136 -0
data/lib/crypto.rb
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
require_relative 'bytes_manip'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
# This class contains the implementation of cryptographics utilities and functions.
|
5
|
+
#
|
6
|
+
# All keys used to communicate with the tag (Triple DES keys) are always
|
7
|
+
# represented as 16 bytes long byte-strings. Byte-strings is what the OpenSSL
|
8
|
+
# library use, so it makes sense to use that. But often, it is useful to
|
9
|
+
# manipulate those as array of bytes. The functions String.unpack() and
|
10
|
+
# Array.pack() ((un)pack into(from) a byte string) are very useful for this,
|
11
|
+
# especially with 'C*' as argument .
|
12
|
+
#
|
13
|
+
# The card is only able to perform TDES encryption (not decryption) with 2 keys
|
14
|
+
# (keying option 2, aka 2TDEA). When the two keys are identical, the result is
|
15
|
+
# the same as plain DES. We elected to use only TDES on the user end (which is
|
16
|
+
# presumably also the case on the tag's end). Therefore all TDES and DES keys
|
17
|
+
# are 16-byte key. A 16 byte DES key is formed by concatening the 8 byte DES key
|
18
|
+
# to itself.
|
19
|
+
#
|
20
|
+
# AES encryption is used to encrypt the credentials stored on the tag.
|
21
|
+
#
|
22
|
+
# OpenSSL modes:
|
23
|
+
#
|
24
|
+
# <tt>DES-EDE-CBC</tt> = Triple DES (Encrypt Decrypt Encrypt) in the regular CBC
|
25
|
+
# mode (send for encryption, receive for decryption). The IV is zeroed by
|
26
|
+
# default.
|
27
|
+
#
|
28
|
+
# <tt>AES-256-CBC</tt> = AES with 256 bit keys in the regular CBC mode. The IV
|
29
|
+
# is zeroed by default.
|
30
|
+
|
31
|
+
module Crypto
|
32
|
+
include BytesManipulation
|
33
|
+
|
34
|
+
# Block length for DES.
|
35
|
+
DES_BLEN = 8
|
36
|
+
|
37
|
+
# An IV of +DES_BLEN+ zeroes.
|
38
|
+
ZERO_IV = Array.new(DES_BLEN, 0).pack("C*")
|
39
|
+
|
40
|
+
# Size of the HMAC (SHA-256) digest.
|
41
|
+
HMAC_LEN = 32
|
42
|
+
|
43
|
+
# Deciphers a byte string using 3DES in CBC receive mode. Returns a byte
|
44
|
+
# string.
|
45
|
+
def decipher_receive(bytes, key)
|
46
|
+
des = OpenSSL::Cipher.new('DES-EDE-CBC')
|
47
|
+
des.decrypt
|
48
|
+
des.key = key
|
49
|
+
des.padding = 0 # no padding
|
50
|
+
return des.update(bytes) + des.final
|
51
|
+
end
|
52
|
+
|
53
|
+
# Deciphers a byte string using 3DES in CBC send mode.
|
54
|
+
def decipher_send(bytes, key)
|
55
|
+
mix = ZERO_IV
|
56
|
+
partial_block_count = (bytes.bytesize % DES_BLEN > 0) ? 1 : 0
|
57
|
+
nblocks = bytes.length / DES_BLEN + partial_block_count
|
58
|
+
out = Array.new(nblocks)
|
59
|
+
|
60
|
+
out.each_index do |i|
|
61
|
+
block = bytes.byteslice(i*DES_BLEN, DES_BLEN)
|
62
|
+
block = bs_xor(block, mix)
|
63
|
+
block = decipher_receive(block, key)
|
64
|
+
out[i] = block
|
65
|
+
mix = block
|
66
|
+
end
|
67
|
+
return out.reduce("", :+)
|
68
|
+
end
|
69
|
+
|
70
|
+
# The following enciphering functions are not needed by the passworld vault,
|
71
|
+
# but are always nice to have to perform some verifications.
|
72
|
+
|
73
|
+
# Enciphers a byte string using 3DES in CBC send mode. Returns a byte
|
74
|
+
# string. Unused.
|
75
|
+
def encipher_send(bytes, key)
|
76
|
+
des = OpenSSL::Cipher.new('DES-EDE-CBC')
|
77
|
+
des.encrypt
|
78
|
+
des.key = key
|
79
|
+
des.padding = 0 # no padding
|
80
|
+
return des.update(bytes) + des.final
|
81
|
+
end
|
82
|
+
|
83
|
+
# Enciphers a byte string using 3DES in CBC receive mode. Returns a byte
|
84
|
+
# string. Unused.
|
85
|
+
def encipher_receive(bytes, key)
|
86
|
+
mix = ZERO_IV
|
87
|
+
partial_block_count = (bytes.bytesize % DES_BLEN > 0) ? 1 : 0
|
88
|
+
nblocks = bytes.length / DES_BLEN + partial_block_count
|
89
|
+
out = Array.new(nblocks)
|
90
|
+
|
91
|
+
out.each_index do |i|
|
92
|
+
block1 = bytes.byteslice(i*DES_BLEN, DES_BLEN)
|
93
|
+
block = encipher_send(block1, key)
|
94
|
+
block = bs_xor(block, mix)
|
95
|
+
out[i] = block
|
96
|
+
mix = block1
|
97
|
+
end
|
98
|
+
return out.reduce("", :+)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Derive a key of the given size using the PBKDF2 derivation function, from
|
102
|
+
# given pass and salt. 50k iterations of SHA-256 are used.
|
103
|
+
def derive_key(pass, salt, key_size)
|
104
|
+
sha256_d = OpenSSL::Digest::SHA256.new
|
105
|
+
# (pass, salt, iter, keylength, digest)
|
106
|
+
return OpenSSL::PKCS5::pbkdf2_hmac(pass, salt, 50000, key_size, sha256_d)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Derive a DESFire Triple DES key. This is similar to <tt>derive_key()</tt>
|
110
|
+
# with +key_size+ = 16, excepted that we overwrite the parity bits of the
|
111
|
+
# key. Anyone can ask to get those bits from the tag ("key versionning"
|
112
|
+
# feature). As such, keeping the orignal parity bits makes the key less
|
113
|
+
# secure.
|
114
|
+
def derive_desfire_key(pass, salt)
|
115
|
+
key = derive_key(pass, salt, 16)
|
116
|
+
array = key.unpack('C*')
|
117
|
+
array.map! { |e| e & 0xFE }
|
118
|
+
return array.pack('C*')
|
119
|
+
end
|
120
|
+
|
121
|
+
# Enciphers using AES with 256 bits key in CBC (send) mode.
|
122
|
+
def aes_encipher(bytes, key)
|
123
|
+
aes = OpenSSL::Cipher.new('AES-256-CBC')
|
124
|
+
aes.encrypt
|
125
|
+
aes.key = key
|
126
|
+
aes.padding = 0 # no padding
|
127
|
+
return aes.update(bytes) + aes.final
|
128
|
+
end
|
129
|
+
|
130
|
+
# Deciphers using AES with 256 bits key in CBC (receive) mode.
|
131
|
+
def aes_decipher(bytes, key)
|
132
|
+
aes = OpenSSL::Cipher.new('AES-256-CBC')
|
133
|
+
aes.decrypt
|
134
|
+
aes.key = key
|
135
|
+
aes.padding = 0 # no padding
|
136
|
+
return aes.update(bytes) + aes.final
|
137
|
+
end
|
138
|
+
|
139
|
+
# Computes a HMAC digest on given data using given key. Uses SHA-1 as
|
140
|
+
# digest.
|
141
|
+
def hmac(bytes, key)
|
142
|
+
sha256_d = sha256_d = OpenSSL::Digest::SHA256.new
|
143
|
+
return OpenSSL::HMAC.digest(sha256_d, key, bytes)
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
data/lib/file_access.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
module Rights
|
2
|
+
|
3
|
+
# Communication setting: plain (unencrypted, unauthenticated) communication.
|
4
|
+
CS_PLAIN = 0x0
|
5
|
+
|
6
|
+
# Communication setting: unencrypted, authenticated communication.
|
7
|
+
CS_PLAIN_MAC = 0x1
|
8
|
+
|
9
|
+
# Communication setting: encrypted, authenticated communication.
|
10
|
+
CS_CIPHERED = 0x3
|
11
|
+
|
12
|
+
# Used in file access rights in place of a key number, to allow unauthenticated
|
13
|
+
# access.
|
14
|
+
FREE_ACCESS = 0xE
|
15
|
+
|
16
|
+
# Used in file access rights in place of a key number, to indicate that no key
|
17
|
+
# is authorized.
|
18
|
+
DENY_ACCESS = 0xF
|
19
|
+
|
20
|
+
# The file access settings determine the keys needed to authenticate in order to
|
21
|
+
# perform some operations on files (inside an application), and how the file
|
22
|
+
# transfer are performed. Those two separate concepts are called in the DESFire
|
23
|
+
# documentation "file access rights" and "communication settings".
|
24
|
+
#
|
25
|
+
# The keys can be a key number from the application (from 0x0 to 0xD) or the
|
26
|
+
# constants +FREE_ACCESS+ (no authentication needed) or +DENY_ACCESS+ (the operation
|
27
|
+
# cannot be performed) (constants defined in the +Rights+ module).
|
28
|
+
class FileAccess
|
29
|
+
|
30
|
+
# Key to be able to read.
|
31
|
+
attr_reader :read
|
32
|
+
|
33
|
+
# Key to be able to write.
|
34
|
+
attr_reader :write
|
35
|
+
|
36
|
+
# Key to be able to read and write.
|
37
|
+
attr_reader :rw
|
38
|
+
|
39
|
+
# Key to change the file access rights.
|
40
|
+
attr_reader :change
|
41
|
+
|
42
|
+
# Do the data transfer to/from the file need to authenticated or ciphered? The
|
43
|
+
# valid values are the <tt>CS_...</tt> constants defined in the +Rights+
|
44
|
+
# module. +com_set+ stands for "communication settings".
|
45
|
+
attr_reader :com_set
|
46
|
+
|
47
|
+
# The constructor takes a hash of parameters, which correspond to the class
|
48
|
+
# attributes. See the class documentation for an explanation of valid key
|
49
|
+
# values.
|
50
|
+
def initialize(hash={})
|
51
|
+
@read = hash[:read] || DENY_ACCESS
|
52
|
+
@write = hash[:write] || DENY_ACCESS
|
53
|
+
@rw = hash[:rw] || DENY_ACCESS
|
54
|
+
@change = hash[:change] || DENY_ACCESS
|
55
|
+
@com_set = hash[:com_set] || CS_CIPHERED
|
56
|
+
end
|
57
|
+
|
58
|
+
# Builds and returns a +FileAccess+ object from the byte array returned by the
|
59
|
+
# +GET_FILE_SETTINGS+ command.
|
60
|
+
def self.from_a(ba)
|
61
|
+
FileAccess.new(
|
62
|
+
:read => ba[3] >> 4,
|
63
|
+
:write => ba[3] % 16,
|
64
|
+
:rw => ba[2] >> 4,
|
65
|
+
:change => ba[2] % 16,
|
66
|
+
:com_set => ba[1])
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a two-byte little-endian view of the access rights (not the
|
70
|
+
# communication settings), fit to be used as a command parameter.
|
71
|
+
def to_a
|
72
|
+
[rw*16 + change, read*16 + write]
|
73
|
+
end
|
74
|
+
|
75
|
+
# Can the file be read when authenticated with the given key, and how? Returns
|
76
|
+
# either +false+ or a <tt>CS_...</tt> constant (see +Rights+ module
|
77
|
+
# documentation).
|
78
|
+
def read_type(key_no)
|
79
|
+
if @read == key_no or @rw == key_no
|
80
|
+
return @com_set
|
81
|
+
elsif @read == FREE_ACCESS or @rw == FREE_ACCESS
|
82
|
+
return CS_PLAIN
|
83
|
+
else
|
84
|
+
return false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Can the file be written when authenticated with the given key, and how?
|
89
|
+
# Returns either +false+ or a <tt>CS_...</tt> constant (see +Rights+ module
|
90
|
+
# documentation).
|
91
|
+
def write_type(key_no)
|
92
|
+
if @write == key_no or @rw == key_no
|
93
|
+
return @com_set
|
94
|
+
elsif @write == FREE_ACCESS or @rw == FREE_ACCESS
|
95
|
+
return CS_PLAIN
|
96
|
+
else
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
data/lib/gui.rb
ADDED
@@ -0,0 +1,270 @@
|
|
1
|
+
require "wx"
|
2
|
+
require_relative 'vault'
|
3
|
+
include Wx
|
4
|
+
|
5
|
+
module VaultGUI
|
6
|
+
TIMEOUT = 300
|
7
|
+
|
8
|
+
# Initialize the vault
|
9
|
+
@@vault = Vault.new
|
10
|
+
|
11
|
+
# Return the vault associated to the GUI.
|
12
|
+
def self.vault
|
13
|
+
return @@vault
|
14
|
+
end
|
15
|
+
|
16
|
+
class GUILauncher < App
|
17
|
+
def on_init
|
18
|
+
GUI.new.show
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class GUI < Frame
|
23
|
+
def initialize
|
24
|
+
super(nil, 0, "Password Vault GUI",
|
25
|
+
:style => DEFAULT_FRAME_STYLE ^ RESIZE_BORDER ^ MAXIMIZE_BOX)
|
26
|
+
|
27
|
+
ask_password()
|
28
|
+
|
29
|
+
vbox = BoxSizer.new(VERTICAL)
|
30
|
+
pl = PasswordList.new(self)
|
31
|
+
evt_list_item_activated(pl) { |event|
|
32
|
+
item_activated(pl.get_item(event.get_index, 0).get_text())
|
33
|
+
}
|
34
|
+
panel = Panel.new(self)
|
35
|
+
|
36
|
+
hbox = BoxSizer.new(HORIZONTAL)
|
37
|
+
add_button = Button.new(panel, :label=>"ADD", :size => [80, 20])
|
38
|
+
evt_button(add_button) {
|
39
|
+
check_and_reset_timer()
|
40
|
+
add_pushed(pl)
|
41
|
+
}
|
42
|
+
remove_button = Button.new(panel, :label=>"REMOVE", :size => [80, 20])
|
43
|
+
evt_button(remove_button) {
|
44
|
+
check_and_reset_timer()
|
45
|
+
remove_pushed(pl)
|
46
|
+
}
|
47
|
+
edit_button = Button.new(panel, :label=>"EDIT", :size => [80, 20])
|
48
|
+
evt_button(edit_button) {
|
49
|
+
check_and_reset_timer()
|
50
|
+
edit_pushed(pl)
|
51
|
+
}
|
52
|
+
hbox.add(add_button)
|
53
|
+
hbox.add(remove_button)
|
54
|
+
hbox.add(edit_button)
|
55
|
+
panel.set_sizer(hbox)
|
56
|
+
|
57
|
+
vbox.add(pl)
|
58
|
+
vbox.add(panel)
|
59
|
+
self.set_sizer(vbox)
|
60
|
+
vbox.set_size_hints( self )
|
61
|
+
self.centre
|
62
|
+
|
63
|
+
evt_close() { |event|
|
64
|
+
VaultGUI.vault.destroy
|
65
|
+
self.destroy
|
66
|
+
}
|
67
|
+
|
68
|
+
@timer = Time.now.to_i
|
69
|
+
end
|
70
|
+
|
71
|
+
def ask_password()
|
72
|
+
password_popup = PasswordPopup.new(self)
|
73
|
+
if password_popup.show_modal == ID_OK
|
74
|
+
pass_candidate = password_popup.get_value()
|
75
|
+
begin
|
76
|
+
VaultGUI.vault.authenticate(pass_candidate)
|
77
|
+
rescue => e
|
78
|
+
Kernel.raise e unless e.message == Vault::AUTHENTICATION_ERROR
|
79
|
+
ErrorPopup.new("Error: wrong password!", "Error: wrong password!", true)
|
80
|
+
end
|
81
|
+
else
|
82
|
+
Kernel.exit
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def check_and_reset_timer()
|
87
|
+
now = Time.now.to_i
|
88
|
+
if now - @timer > TIMEOUT
|
89
|
+
VaultGUI.vault.deauthenticate()
|
90
|
+
ask_password()
|
91
|
+
end
|
92
|
+
@timer = now
|
93
|
+
end
|
94
|
+
|
95
|
+
def item_activated(name)
|
96
|
+
info_popup = ViewPopup.new(self)
|
97
|
+
loginPassword = VaultGUI.vault.credential(name)
|
98
|
+
info_popup.set_name_value(name)
|
99
|
+
info_popup.set_login_value(loginPassword[0])
|
100
|
+
info_popup.set_password_value(loginPassword[1])
|
101
|
+
info_popup.show
|
102
|
+
evt_button(info_popup) {
|
103
|
+
info_popup.destroy
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
def add_pushed(pl)
|
108
|
+
add_popup = AddPopup.new(self)
|
109
|
+
add_popup.show
|
110
|
+
evt_button(add_popup) {
|
111
|
+
new_name = add_popup.get_name_value
|
112
|
+
new_login = add_popup.get_login_value
|
113
|
+
new_password = add_popup.get_password_value
|
114
|
+
begin
|
115
|
+
VaultGUI.vault.add(new_name, new_login, new_password)
|
116
|
+
pl.insert_item(ListItem.new)
|
117
|
+
add_popup.destroy
|
118
|
+
rescue => e
|
119
|
+
display_error_if_needed(e)
|
120
|
+
add_popup.destroy
|
121
|
+
end
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
def remove_pushed(pl)
|
126
|
+
selection = pl.get_selections
|
127
|
+
if selection.length > 0
|
128
|
+
index_deleted = selection[0]
|
129
|
+
deleted = pl.get_item(index_deleted, 0).get_text
|
130
|
+
pl.delete_item(index_deleted)
|
131
|
+
VaultGUI.vault.remove(deleted)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def edit_pushed(pl)
|
136
|
+
selection = pl.get_selections
|
137
|
+
if selection.length > 0
|
138
|
+
index_edited = selection[0]
|
139
|
+
edit_popup = EditPopup.new(self)
|
140
|
+
edit_popup.show
|
141
|
+
evt_button(edit_popup) {
|
142
|
+
name = pl.get_item(index_edited, 0).get_text
|
143
|
+
new_login = edit_popup.get_login_value
|
144
|
+
new_password = edit_popup.get_password_value
|
145
|
+
begin
|
146
|
+
VaultGUI.vault.edit(name, new_login, new_password)
|
147
|
+
edit_popup.destroy
|
148
|
+
rescue => e
|
149
|
+
display_error_if_needed(e)
|
150
|
+
end
|
151
|
+
}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def display_error_if_needed(e)
|
156
|
+
if e.class == Vault::CredentialError
|
157
|
+
ErrorPopup.new(e.message, "Credential error", false)
|
158
|
+
else
|
159
|
+
Kernel.raise e
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class PasswordList < ListCtrl
|
165
|
+
def initialize(parent)
|
166
|
+
super(parent,
|
167
|
+
:style=>LC_REPORT | LC_VIRTUAL | LC_SINGLE_SEL,
|
168
|
+
:size=>[240, 240])
|
169
|
+
insert_column(0, "Name")
|
170
|
+
set_column_width(0, 240)
|
171
|
+
set_item_count(VaultGUI.vault.credentials_names.count)
|
172
|
+
end
|
173
|
+
|
174
|
+
def on_get_item_text(item, column)
|
175
|
+
return VaultGUI.vault.credentials_names.to_a[item]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
class ErrorPopup < MessageDialog
|
180
|
+
def initialize (message, caption, kill)
|
181
|
+
super(nil,
|
182
|
+
:message => message,
|
183
|
+
:caption => caption,
|
184
|
+
:style => OK | ICON_ERROR)
|
185
|
+
if self.show_modal == ID_OK && kill
|
186
|
+
VaultGUI.vault.destroy
|
187
|
+
Kernel.exit
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
class PasswordPopup < TextEntryDialog
|
193
|
+
def initialize(parent)
|
194
|
+
super(parent,
|
195
|
+
:message => "Enter your password:",
|
196
|
+
:caption => "Enter your password",
|
197
|
+
:style => OK | TE_PASSWORD)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
class InfoPopup < Frame
|
202
|
+
def initialize(parent, title, edit_name_tf, edit_login_tf, edit_password_tf)
|
203
|
+
super(parent, 0, title,
|
204
|
+
:style => DEFAULT_FRAME_STYLE ^ RESIZE_BORDER ^ MAXIMIZE_BOX)
|
205
|
+
panel = Panel.new(self)
|
206
|
+
namel = StaticText.new(panel, 0, "Name", :style => ALIGN_CENTER)
|
207
|
+
@name_tf = TextCtrl.new(panel, 0)
|
208
|
+
@name_tf.set_editable(edit_name_tf)
|
209
|
+
loginl = StaticText.new(panel, 0, "Login", :style => ALIGN_CENTER)
|
210
|
+
@login_tf = TextCtrl.new(panel, 0)
|
211
|
+
@login_tf.set_editable(edit_login_tf)
|
212
|
+
password_l = StaticText.new(panel, 0, "Password", :style => ALIGN_CENTER)
|
213
|
+
@password_tf = TextCtrl.new(panel, 0, :style => TE_PASSWORD)
|
214
|
+
@password_tf.set_editable(edit_password_tf)
|
215
|
+
okb = Button.new(panel, 0, "OK")
|
216
|
+
vbox = BoxSizer.new(VERTICAL)
|
217
|
+
vbox.add(namel)
|
218
|
+
vbox.add(@name_tf)
|
219
|
+
vbox.add(loginl)
|
220
|
+
vbox.add(@login_tf)
|
221
|
+
vbox.add(password_l)
|
222
|
+
vbox.add(@password_tf)
|
223
|
+
vbox.add(okb)
|
224
|
+
panel.set_sizer(vbox)
|
225
|
+
self.centre
|
226
|
+
end
|
227
|
+
|
228
|
+
def get_name_value()
|
229
|
+
return @name_tf.get_value
|
230
|
+
end
|
231
|
+
|
232
|
+
def get_login_value()
|
233
|
+
return @login_tf.get_value
|
234
|
+
end
|
235
|
+
|
236
|
+
def get_password_value()
|
237
|
+
return @password_tf.get_value
|
238
|
+
end
|
239
|
+
|
240
|
+
def set_name_value(name)
|
241
|
+
@name_tf.set_value(name)
|
242
|
+
end
|
243
|
+
|
244
|
+
def set_login_value(login)
|
245
|
+
@login_tf.set_value(login)
|
246
|
+
end
|
247
|
+
|
248
|
+
def set_password_value(password)
|
249
|
+
@password_tf.set_value(password)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
class EditPopup < InfoPopup
|
254
|
+
def initialize(parent)
|
255
|
+
super(parent, "Edit entry", false, true, true)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
class AddPopup < InfoPopup
|
260
|
+
def initialize(parent)
|
261
|
+
super(parent, "Add new entry", true, true, true)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
class ViewPopup < InfoPopup
|
266
|
+
def initialize(parent)
|
267
|
+
super(parent, "View entry", false, false, false)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|