adamantite 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []