adamantite 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 93fd9127b45b4717d382be6dae9f956d6f19b8fa9e9afe2cae036f3b7acfd082
4
+ data.tar.gz: ef419d87bc500e2f0120b7b924bcf63eb8e75a86a35330b51cc1b2282cec46d9
5
+ SHA512:
6
+ metadata.gz: 4a05165d913cbb1c2b19b286805643057107c8ff1847291003f5585916d6d6c81c789bdb9bb4216a0192ebc57951d7abbd214b76cbb895ea06aa2a5ffa60c701
7
+ data.tar.gz: 5818cc7e919e296f6bf8f0653bdaae0313e131a0bdeee2cb1445605c7e18c8ef0b36ba1da68fa3dfc4db8eb26ea0a147f1f5afc927555b9588aaf394761c214b
data/bin/adamantite ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require "adamantite"
3
+
4
+ AdamantiteApp.launch
data/lib/adamantite.rb ADDED
@@ -0,0 +1,134 @@
1
+ require "glimmer-dsl-libui"
2
+ require "bcrypt"
3
+ require "openssl"
4
+ require "base64"
5
+ require "json"
6
+ require "io/console"
7
+
8
+ require "file_utils/file_utils"
9
+ require "pw_utils/pw_utils"
10
+ require "base/adamantite"
11
+ require "gui/screen/login_screen"
12
+ require "gui/screen/copy_screen"
13
+ require "gui/screen/show_screen"
14
+ require "gui/screen/set_master_password_screen"
15
+ require "gui/screen/update_master_password_screen"
16
+ require "gui/request/login_request"
17
+ require "gui/request/add_password_request"
18
+ require "gui/request/update_master_password_request"
19
+ require "gui/request/set_master_password_request"
20
+
21
+ include Adamantite::FileUtils
22
+ include Adamantite::PWUtils
23
+
24
+ class AdamantiteApp
25
+ include Glimmer::LibUI::Application
26
+
27
+ attr_accessor :add_password_request, :stored_passwords
28
+
29
+ before_body do
30
+ if !pw_file_exists?('master')
31
+ set_master_password_request = Adamantite::GUI::Request::SetMasterPasswordRequest.new
32
+ set_master_password_screen(set_master_password_request: set_master_password_request).show
33
+ end
34
+
35
+ login_request = Adamantite::GUI::Request::LoginRequest.new
36
+ login_screen(login_request: login_request).show
37
+
38
+ if !login_request.authenticated
39
+ exit(0)
40
+ end
41
+
42
+ @stored_passwords = get_stored_pws.map do |title|
43
+ pw_info = get_pw_file(title)
44
+ [title, pw_info["username"], 'Copy', 'Show', 'Delete']
45
+ end
46
+ @master_password = login_request.master_password
47
+ @master_password_salt = login_request.master_password_salt
48
+ @adamantite_object = Adamantite::Base::Adamantite.new(@master_password)
49
+ @adamantite_object.authenticate!
50
+ @add_password_request = Adamantite::GUI::Request::AddPasswordRequest.new(@master_password, @master_password_salt)
51
+ end
52
+
53
+ body {
54
+ window('Adamantite', 600, 400) {
55
+ margined true
56
+
57
+ vertical_box {
58
+ table {
59
+ text_column('Title')
60
+ text_column('Username')
61
+ button_column('Copy') {
62
+ on_clicked do |row|
63
+ password_title = @stored_passwords[row].first
64
+ pw_info = get_pw_file(password_title)
65
+ stored_pw_selection = decrypt_pw(pw_info["iv"], pw_info["password"], @master_password, @master_password_salt)
66
+ IO.popen('pbcopy', 'w') { |f| f << stored_pw_selection }
67
+ copy_screen(password_title: password_title).show
68
+ end
69
+ }
70
+ button_column('Show') {
71
+ on_clicked do |row|
72
+ pw_info = get_pw_file(@stored_passwords[row].first)
73
+ stored_pw_selection = decrypt_pw(pw_info["iv"], pw_info["password"], @master_password, @master_password_salt)
74
+ show_screen(password: stored_pw_selection).show
75
+ end
76
+ }
77
+ button_column('Delete') {
78
+ on_clicked do |row|
79
+ delete_pw_file(@stored_passwords[row].first)
80
+ @stored_passwords.delete_at(row)
81
+ end
82
+ }
83
+
84
+ cell_rows <=> [self, :stored_passwords]
85
+
86
+ }
87
+ vertical_box {
88
+ form {
89
+ entry {
90
+ label 'Website Title'
91
+ text <=> [@add_password_request, :website_title]
92
+ }
93
+ entry {
94
+ label 'Username'
95
+ text <=> [@add_password_request, :username]
96
+ }
97
+ password_entry {
98
+ label 'Password'
99
+ text <=> [@add_password_request, :password]
100
+ }
101
+ password_entry {
102
+ label 'Confirm Password'
103
+ text <=> [@add_password_request, :password_confirmation]
104
+ }
105
+ }
106
+ horizontal_box {
107
+ button('Add Password') {
108
+ on_clicked do
109
+ @add_password_request.confirm_and_add_password!
110
+ if @add_password_request.password_saved
111
+ new_stored_password = [@add_password_request.website_title, @add_password_request.username]
112
+ new_stored_password << 'Copy'
113
+ new_stored_password << 'Show'
114
+ new_stored_password << 'Delete'
115
+ @stored_passwords << new_stored_password
116
+ @add_password_request.website_title = ''
117
+ @add_password_request.username = ''
118
+ @add_password_request.password = ''
119
+ @add_password_request.password_confirmation = ''
120
+ end
121
+ end
122
+ }
123
+ button('Update Master Password') {
124
+ on_clicked do
125
+ update_master_password_request = Adamantite::GUI::Request::UpdateMasterPasswordRequest.new(@adamantite_object)
126
+ update_master_password_screen(update_master_password_request: update_master_password_request).show
127
+ end
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ end
@@ -0,0 +1,95 @@
1
+ require "bcrypt"
2
+ require "openssl"
3
+ require "base64"
4
+ require "json"
5
+ require "io/console"
6
+
7
+ require "file_utils/file_utils"
8
+ require "pw_utils/pw_utils"
9
+
10
+ include Adamantite::FileUtils
11
+ include Adamantite::PWUtils
12
+
13
+ puts "Welcome to Adamantite."
14
+
15
+ if pw_file_exists?('master')
16
+ user_master_pw_info = get_master_pw_info
17
+ master_pw_hash = user_master_pw_info['password']
18
+ master_pw_salt = user_master_pw_info['salt']
19
+
20
+ master_pw = IO::console.getpass("Please enter your master password:")
21
+ master_pw_comparator = BCrypt::Password.new(master_pw_hash)
22
+
23
+ while master_pw_comparator != master_pw + master_pw_salt
24
+ puts "Entered password didn't match."
25
+ master_pw = IO::console.getpass("Please enter your master password:")
26
+ master_pw_hash = BCrypt::Password.create(master_pw + user_master_pw_info['salt'])
27
+ end
28
+
29
+ puts "Master password successfully entered."
30
+ puts "Here are your stored passwords:"
31
+ get_stored_pws.each_with_index do |pw, index|
32
+ puts "#{index + 1}. #{pw}"
33
+ end
34
+
35
+ puts "Would you like to enter another password? (Y/N)"
36
+ response = gets.chomp
37
+ while !["Y", "N"].include?(response)
38
+ puts "Please enter Y or N"
39
+ end
40
+
41
+ if response == "Y"
42
+ puts "What do you want to call this password?"
43
+ title = gets.chomp
44
+ puts "What is the username for #{title}?"
45
+ username = gets.chomp
46
+ pw = IO::console.getpass("Enter the password for this site.")
47
+ pw_confirmation = IO::console.getpass("Confirm the password for this site.")
48
+
49
+ while pw != pw_confirmation
50
+ puts "Those didn't match, please enter them again."
51
+ pw = IO::console.getpass("Enter the password for this site.")
52
+ pw_confirmation = IO::console.getpass("Confirm the password for this site.")
53
+ end
54
+
55
+ pw_info_for_file = make_pw_info(username, pw, master_pw, master_pw_salt)
56
+ write_pw_to_file(title, **pw_info_for_file)
57
+ puts "Successfully stored password for #{title}."
58
+
59
+ elsif response == "N"
60
+ puts "Exiting"
61
+ end
62
+
63
+ puts "Here are your stored passwords:"
64
+ stored_pws = get_stored_pws
65
+ stored_pws.each_with_index do |pw, index|
66
+ puts "#{index + 1}. #{pw}"
67
+ end
68
+
69
+ puts "Enter the number of the password that you would like to retrieve."
70
+ pw_entry = gets.chomp.to_i
71
+
72
+ pw_info = get_pw_file(stored_pws[pw_entry - 1])
73
+ stored_pw_selection = decrypt_pw(pw_info["iv"], pw_info['password'], master_pw, master_pw_salt)
74
+
75
+ IO.popen('pbcopy', 'w') { |f| f << stored_pw_selection }
76
+
77
+ puts "Your password has been copied to your clipboard."
78
+
79
+ else
80
+ puts "You don't have a master password. Please enter one now."
81
+ master_pw = IO::console.getpass("Enter your master password:")
82
+ master_pw_confirmation = IO::console.getpass("Confirm your master password:")
83
+
84
+ while master_pw != master_pw_confirmation
85
+ puts "Those didn't match, please enter them again."
86
+ master_pw = IO::console.getpass("Enter your master password:")
87
+ master_pw_confirmation = IO::console.getpass("Confirm your master password:")
88
+ end
89
+
90
+ master_pw_info = generate_master_pw_hash(master_pw)
91
+
92
+ write_pw_to_file('master', password: master_pw_info[:master_pw_hash], salt: master_pw_info[:salt])
93
+
94
+ puts "Wrote master pw to file."
95
+ end
@@ -0,0 +1,59 @@
1
+ require "file_utils/file_utils"
2
+ require "pw_utils/pw_utils"
3
+
4
+ include Adamantite::FileUtils
5
+ include Adamantite::PWUtils
6
+
7
+ module Adamantite
8
+ module Base
9
+ class Adamantite
10
+
11
+ attr_accessor :authenticated
12
+
13
+ def initialize(master_pw)
14
+ @master_pw = master_pw
15
+ @authenticated = false
16
+ @master_pw_exists = pw_file_exists?('master')
17
+ end
18
+
19
+ def authenticate!
20
+ return false unless @master_pw_exists
21
+ master_pw_info = get_master_pw_info
22
+ master_pw_hash = master_pw_info['password']
23
+ master_pw_salt = master_pw_info['salt']
24
+ master_pw_comparator = generate_master_pw_comparator(master_pw_hash)
25
+
26
+ if master_pw_comparator == @master_pw + master_pw_salt
27
+ @authenticated = true
28
+ @master_pw_hash = master_pw_hash
29
+ @master_pw_salt = master_pw_salt
30
+ @stored_passwords = get_stored_pws
31
+ true
32
+ else
33
+ false
34
+ end
35
+ end
36
+
37
+ def update_master_password!(new_master_pw, new_master_pw_confirmation)
38
+ return false unless new_master_pw == new_master_pw_confirmation && @authenticated
39
+
40
+ new_master_pw_info = generate_master_pw_hash(new_master_pw)
41
+ new_master_pw_hash = new_master_pw_info[:master_pw_hash]
42
+ new_master_pw_salt = new_master_pw_info[:salt]
43
+
44
+ @stored_passwords.each do |stored_password|
45
+ pw_info = get_pw_file(stored_password)
46
+ pw = decrypt_pw(pw_info['iv'], pw_info['password'], @master_pw, @master_pw_salt)
47
+ pw_info_for_file = make_pw_info(pw_info['username'], pw, new_master_pw, new_master_pw_salt)
48
+ write_pw_to_file(stored_password, **pw_info_for_file)
49
+ end
50
+
51
+ write_pw_to_file('master', password: new_master_pw_hash, salt: new_master_pw_salt)
52
+ @master_pw_hash = get_master_pw_info
53
+ @master_pw = new_master_pw
54
+ @master_pw_salt = new_master_pw_salt
55
+ true
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,55 @@
1
+ require "json"
2
+
3
+ module Adamantite
4
+ module FileUtils
5
+ def home_dir
6
+ ENV['HOME']
7
+ end
8
+
9
+ def pwmanager_dir
10
+ File.join(home_dir, '.pwmanager')
11
+ end
12
+
13
+ def pwmanager_dir_exists?
14
+ Dir.exists?(pwmanager_dir)
15
+ end
16
+
17
+ def make_pwmanager_dir
18
+ Dir.mkdir(pwmanager_dir)
19
+ end
20
+
21
+ def pw_file(title)
22
+ File.join(pwmanager_dir, title)
23
+ end
24
+
25
+ def pw_file_exists?(title)
26
+ File.exists?(pw_file(title))
27
+ end
28
+
29
+ def write_pw_to_file(title, **kwargs)
30
+ if !pwmanager_dir_exists?
31
+ make_pwmanager_dir
32
+ end
33
+
34
+ File.open(pw_file(title), "w") do |f|
35
+ JSON.dump(kwargs, f)
36
+ end
37
+ end
38
+
39
+ def delete_pw_file(title)
40
+ File.delete(pw_file(title))
41
+ end
42
+
43
+ def get_pw_file(title)
44
+ JSON.load_file(pw_file(title))
45
+ end
46
+
47
+ def get_master_pw_info
48
+ get_pw_file('master')
49
+ end
50
+
51
+ def get_stored_pws
52
+ Dir.entries(pwmanager_dir).filter { |f| ![".", "..", "master"].include?(f) }
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,24 @@
1
+ module Adamantite
2
+ module GUI
3
+ module Request
4
+ class AddPasswordRequest
5
+
6
+ attr_accessor :website_title, :username, :password, :password_confirmation, :password_saved
7
+
8
+ def initialize(master_password, master_password_salt)
9
+ @master_password = master_password
10
+ @master_password_salt = master_password_salt
11
+ @password_saved = false
12
+ end
13
+
14
+ def confirm_and_add_password!
15
+ if @password == @password_confirmation
16
+ @password_saved = true
17
+ pw_info_for_file = make_pw_info(@username, @password, @master_password, @master_password_salt)
18
+ write_pw_to_file(@website_title, **pw_info_for_file)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ module Adamantite
2
+ module GUI
3
+ module Request
4
+ class LoginRequest
5
+
6
+ attr_accessor :master_password, :master_password_salt, :authenticated
7
+
8
+ def authenticate!
9
+ user_master_pw_info = get_master_pw_info
10
+ master_pw_hash = user_master_pw_info['password']
11
+ master_pw_salt = user_master_pw_info['salt']
12
+ master_pw_comparator = generate_master_pw_comparator(master_pw_hash)
13
+
14
+ if master_pw_comparator == master_password + master_pw_salt
15
+ @authenticated = true
16
+ @master_password = master_password
17
+ @master_password_salt = master_pw_salt
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module Adamantite
2
+ module GUI
3
+ module Request
4
+ class SetMasterPasswordRequest
5
+
6
+ attr_accessor :new_master_pw, :new_master_pw_confirmation, :success
7
+
8
+ def set_master_password!
9
+ @success = false
10
+ if @new_master_pw == @new_master_pw_confirmation
11
+ master_pw_info = generate_master_pw_hash(@new_master_pw)
12
+ write_pw_to_file('master', password: master_pw_info[:master_pw_hash], salt: master_pw_info[:salt])
13
+ @success = true
14
+ end
15
+ @success
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ module Adamantite
2
+ module GUI
3
+ module Request
4
+ class UpdateMasterPasswordRequest
5
+
6
+ attr_accessor :new_master_pw, :new_master_pw_confirmation, :adamantite_object
7
+
8
+ def initialize(adamantite_object)
9
+ @adamantite_object = adamantite_object
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ module Adamantite
2
+ module GUI
3
+ module Screen
4
+ class CopyScreen
5
+ include Glimmer::LibUI::CustomWindow
6
+
7
+ option :password_title
8
+
9
+ body {
10
+ window('Copy', 400, 100) {
11
+ margined true
12
+ label("Copied password for #{password_title} to your clipboard.")
13
+ }
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,38 @@
1
+
2
+ module PWManager
3
+ module GUI
4
+ module Screen
5
+ class LoginScreen
6
+ include Glimmer::LibUI::CustomWindow
7
+
8
+ option :login_request
9
+
10
+ body {
11
+ window('Adamantite', 400, 100) {
12
+ margined true
13
+
14
+ vertical_box {
15
+ form {
16
+ password_entry {
17
+ label 'Master Password'
18
+ text <=> [login_request, :master_password]
19
+ }
20
+ }
21
+
22
+ button('Login') {
23
+ on_clicked do
24
+ login_request.authenticate!
25
+ # Destroy window if password is correct.
26
+ if login_request.authenticated
27
+ body_root.destroy
28
+ ::LibUI.quit
29
+ end
30
+ end
31
+ }
32
+ }
33
+ }
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ module Adamantite
2
+ module GUI
3
+ module Screen
4
+ class SetMasterPasswordScreen
5
+ include Glimmer::LibUI::CustomWindow
6
+
7
+ option :set_master_password_request
8
+
9
+ body {
10
+ window('Adamantite - Create Master Password', 450, 150) {
11
+ margined true
12
+ vertical_box {
13
+ form {
14
+ password_entry {
15
+ label 'Master Password'
16
+ text <=> [set_master_password_request, :new_master_pw]
17
+ }
18
+ password_entry {
19
+ label 'Master Password Confirmation'
20
+ text <=> [set_master_password_request, :new_master_pw_confirmation]
21
+ }
22
+ }
23
+ button('Set Master Password') {
24
+ on_clicked do
25
+ set_master_password_request.set_master_password!
26
+ if set_master_password_request.success
27
+ body_root.destroy
28
+ ::LibUI.quit
29
+ else
30
+ set_master_password_request.new_master_pw = ''
31
+ set_master_password_request.new_master_pw_confirmation = ''
32
+ end
33
+ end
34
+ }
35
+ }
36
+ }
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,19 @@
1
+ module Adamantite
2
+ module GUI
3
+ module Screen
4
+ class ShowScreen
5
+ include Glimmer::LibUI::CustomWindow
6
+
7
+ option :password
8
+
9
+ body {
10
+ window('Show', 400, 100) {
11
+ margined true
12
+
13
+ label("#{password}")
14
+ }
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ module Adamantite
2
+ module GUI
3
+ module Screen
4
+ class UpdateMasterPasswordScreen
5
+ include Glimmer::LibUI::CustomWindow
6
+
7
+ option :update_master_password_request
8
+
9
+ body {
10
+ window('Adamantite - Update Master Password', 450, 150) {
11
+ margined true
12
+ vertical_box {
13
+ form {
14
+ password_entry {
15
+ label 'New Master Password'
16
+ text <=> [update_master_password_request, :new_master_pw]
17
+ }
18
+ password_entry {
19
+ label 'New Master Password Confirmation'
20
+ text <=> [update_master_password_request, :new_master_pw_confirmation]
21
+ }
22
+ }
23
+ button('Update') {
24
+ on_clicked do
25
+ new_master_pw = update_master_password_request.new_master_pw
26
+ new_master_pw_confirmation = update_master_password_request.new_master_pw_confirmation
27
+ success = update_master_password_request.adamantite_object.update_master_password!(new_master_pw, new_master_pw_confirmation)
28
+ if success
29
+ body_root.destroy
30
+ ::LibUI.quit
31
+ else
32
+ update_master_password_request.new_master_pw = ''
33
+ update_master_password_request.new_master_pw_confirmation = ''
34
+ end
35
+ end
36
+ }
37
+ }
38
+ }
39
+ }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ require "bcrypt"
2
+ require "openssl"
3
+ require "base64"
4
+
5
+ module Adamantite
6
+ module PWUtils
7
+
8
+ def make_pw_info(username, pw, master_pw, master_pw_salt)
9
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
10
+ cipher.encrypt
11
+ iv = cipher.random_iv
12
+ cipher.key = Digest::MD5.hexdigest(master_pw + master_pw_salt)
13
+ cipher_text = cipher.update(pw) + cipher.final
14
+ utf8_cipher_text = Base64.encode64(cipher_text).encode('utf-8')
15
+ utf8_iv = Base64.encode64(iv).encode('utf-8')
16
+
17
+ {username: username, password: utf8_cipher_text, iv: utf8_iv}
18
+ end
19
+
20
+ def decrypt_pw(iv, pw_hash, master_pw, master_pw_salt)
21
+ decrypt_cipher = OpenSSL::Cipher::AES256.new(:CBC)
22
+ decrypt_cipher.decrypt
23
+ iv = Base64.decode64(iv.encode('ascii-8bit'))
24
+ decrypt_cipher.iv = iv
25
+ decrypt_cipher.key = Digest::MD5.hexdigest(master_pw + master_pw_salt)
26
+ decrypt_text = Base64.decode64(pw_hash.encode('ascii-8bit'))
27
+ decrypt_cipher.update(decrypt_text) + decrypt_cipher.final
28
+ end
29
+
30
+ def generate_master_pw_hash(master_pw)
31
+ salt = BCrypt::Engine.generate_salt
32
+ master_pw_hash = BCrypt::Password.create(master_pw + salt)
33
+ {'salt': salt, 'master_pw_hash': master_pw_hash}
34
+ end
35
+
36
+ def generate_master_pw_comparator(master_pw_hash)
37
+ BCrypt::Password.new(master_pw_hash)
38
+ end
39
+ end
40
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adamantite
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jake Bruemmer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-11-08 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A local password manager written in Ruby.
14
+ email: jakebruemmer@gmail.com
15
+ executables:
16
+ - adamantite
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - bin/adamantite
21
+ - lib/adamantite.rb
22
+ - lib/adamantite_command_line.rb
23
+ - lib/base/adamantite.rb
24
+ - lib/file_utils/file_utils.rb
25
+ - lib/gui/request/add_password_request.rb
26
+ - lib/gui/request/login_request.rb
27
+ - lib/gui/request/set_master_password_request.rb
28
+ - lib/gui/request/update_master_password_request.rb
29
+ - lib/gui/screen/copy_screen.rb
30
+ - lib/gui/screen/login_screen.rb
31
+ - lib/gui/screen/set_master_password_screen.rb
32
+ - lib/gui/screen/show_screen.rb
33
+ - lib/gui/screen/update_master_password_screen.rb
34
+ - lib/pw_utils/pw_utils.rb
35
+ homepage: https://x.com/jakebruemmer
36
+ licenses:
37
+ - MIT
38
+ metadata: {}
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.3.26
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: Yet another password manager.
58
+ test_files: []