keeper_secrets_manager 17.2.1
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 +7 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +139 -0
- data/Gemfile +16 -0
- data/LICENSE +21 -0
- data/README.md +113 -0
- data/Rakefile +30 -0
- data/bin/console +47 -0
- data/keeper_secrets_manager.gemspec +36 -0
- data/lib/keeper_secrets_manager/cache.rb +139 -0
- data/lib/keeper_secrets_manager/config_keys.rb +29 -0
- data/lib/keeper_secrets_manager/core.rb +1781 -0
- data/lib/keeper_secrets_manager/crypto.rb +333 -0
- data/lib/keeper_secrets_manager/dto/payload.rb +153 -0
- data/lib/keeper_secrets_manager/dto.rb +557 -0
- data/lib/keeper_secrets_manager/errors.rb +90 -0
- data/lib/keeper_secrets_manager/field_types.rb +152 -0
- data/lib/keeper_secrets_manager/folder_manager.rb +110 -0
- data/lib/keeper_secrets_manager/keeper_globals.rb +53 -0
- data/lib/keeper_secrets_manager/notation.rb +463 -0
- data/lib/keeper_secrets_manager/notation_enhancements.rb +67 -0
- data/lib/keeper_secrets_manager/storage.rb +254 -0
- data/lib/keeper_secrets_manager/totp.rb +140 -0
- data/lib/keeper_secrets_manager/utils.rb +263 -0
- data/lib/keeper_secrets_manager/version.rb +3 -0
- data/lib/keeper_secrets_manager.rb +46 -0
- metadata +102 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
module KeeperSecretsManager
|
|
2
|
+
module FieldTypes
|
|
3
|
+
# Base field helper
|
|
4
|
+
class Field
|
|
5
|
+
attr_accessor :type, :label, :value, :required, :privacy_screen
|
|
6
|
+
|
|
7
|
+
def initialize(type:, value:, label: nil, required: false, privacy_screen: false)
|
|
8
|
+
@type = type
|
|
9
|
+
@label = label
|
|
10
|
+
@required = required
|
|
11
|
+
@privacy_screen = privacy_screen
|
|
12
|
+
|
|
13
|
+
# Ensure value is always an array
|
|
14
|
+
@value = value.is_a?(Array) ? value : [value]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
h = { 'type' => type, 'value' => value }
|
|
19
|
+
h['label'] = label if label
|
|
20
|
+
h['required'] = required if required
|
|
21
|
+
h['privacyScreen'] = privacy_screen if privacy_screen
|
|
22
|
+
h
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Helper methods for creating common fields
|
|
27
|
+
module Helpers
|
|
28
|
+
def self.login(value, label: nil)
|
|
29
|
+
Field.new(type: 'login', value: value, label: label)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.password(value, label: nil)
|
|
33
|
+
Field.new(type: 'password', value: value, label: label)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.url(value, label: nil)
|
|
37
|
+
Field.new(type: 'url', value: value, label: label)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.file_ref(value, label: nil)
|
|
41
|
+
Field.new(type: 'fileRef', value: value, label: label)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.one_time_code(value, label: nil)
|
|
45
|
+
Field.new(type: 'oneTimeCode', value: value, label: label)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.name(first:, last:, middle: nil, label: nil)
|
|
49
|
+
value = { 'first' => first, 'last' => last }
|
|
50
|
+
value['middle'] = middle if middle
|
|
51
|
+
Field.new(type: 'name', value: value, label: label)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.phone(number:, region: 'US', type: nil, ext: nil, label: nil)
|
|
55
|
+
value = { 'region' => region, 'number' => number }
|
|
56
|
+
value['type'] = type if type
|
|
57
|
+
value['ext'] = ext if ext
|
|
58
|
+
Field.new(type: 'phone', value: value, label: label)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.email(email, label: nil)
|
|
62
|
+
Field.new(type: 'email', value: email, label: label)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.address(street1:, city:, state:, zip:, country: 'US', street2: nil, label: nil)
|
|
66
|
+
value = {
|
|
67
|
+
'street1' => street1,
|
|
68
|
+
'city' => city,
|
|
69
|
+
'state' => state,
|
|
70
|
+
'zip' => zip,
|
|
71
|
+
'country' => country
|
|
72
|
+
}
|
|
73
|
+
value['street2'] = street2 if street2
|
|
74
|
+
Field.new(type: 'address', value: value, label: label)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.payment_card(number:, expiration_date:, security_code:, cardholder_name: nil, label: nil)
|
|
78
|
+
value = {
|
|
79
|
+
'cardNumber' => number,
|
|
80
|
+
'cardExpirationDate' => expiration_date,
|
|
81
|
+
'cardSecurityCode' => security_code
|
|
82
|
+
}
|
|
83
|
+
value['cardholderName'] = cardholder_name if cardholder_name
|
|
84
|
+
Field.new(type: 'paymentCard', value: value, label: label)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.bank_account(account_type:, routing_number:, account_number:, label: nil)
|
|
88
|
+
value = {
|
|
89
|
+
'accountType' => account_type,
|
|
90
|
+
'routingNumber' => routing_number,
|
|
91
|
+
'accountNumber' => account_number
|
|
92
|
+
}
|
|
93
|
+
Field.new(type: 'bankAccount', value: value, label: label)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.birth_date(date, label: nil)
|
|
97
|
+
# Date should be in unix timestamp (milliseconds)
|
|
98
|
+
timestamp = case date
|
|
99
|
+
when Date, Time, DateTime
|
|
100
|
+
(date.to_time.to_f * 1000).to_i
|
|
101
|
+
when Integer
|
|
102
|
+
date
|
|
103
|
+
when String
|
|
104
|
+
(Date.parse(date).to_time.to_f * 1000).to_i
|
|
105
|
+
else
|
|
106
|
+
raise ArgumentError, 'Invalid date format'
|
|
107
|
+
end
|
|
108
|
+
Field.new(type: 'birthDate', value: timestamp, label: label)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.secure_note(note, label: nil)
|
|
112
|
+
Field.new(type: 'secureNote', value: note, label: label)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.ssh_key(private_key:, public_key: nil, label: nil)
|
|
116
|
+
value = { 'privateKey' => private_key }
|
|
117
|
+
value['publicKey'] = public_key if public_key
|
|
118
|
+
Field.new(type: 'sshKey', value: value, label: label)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.host(hostname:, port: nil, label: nil)
|
|
122
|
+
value = { 'hostName' => hostname }
|
|
123
|
+
value['port'] = port.to_s if port
|
|
124
|
+
Field.new(type: 'host', value: value, label: label)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.database_type(type, label: nil)
|
|
128
|
+
Field.new(type: 'databaseType', value: type, label: label)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.script(script, label: nil)
|
|
132
|
+
Field.new(type: 'script', value: script, label: label)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.passkey(private_key:, credential_id:, rp_id:, user_id:, username:, label: nil)
|
|
136
|
+
value = {
|
|
137
|
+
'privateKey' => private_key,
|
|
138
|
+
'credentialId' => credential_id,
|
|
139
|
+
'relyingParty' => rp_id,
|
|
140
|
+
'userId' => user_id,
|
|
141
|
+
'username' => username
|
|
142
|
+
}
|
|
143
|
+
Field.new(type: 'passkey', value: value, label: label)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Generic field for any type
|
|
147
|
+
def self.custom(type:, value:, label: nil, required: false)
|
|
148
|
+
Field.new(type: type, value: value, label: label, required: required)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module KeeperSecretsManager
|
|
2
|
+
class FolderManager
|
|
3
|
+
def initialize(folders)
|
|
4
|
+
@folders = folders
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# Build a hierarchical tree structure from flat folder list
|
|
8
|
+
def build_folder_tree
|
|
9
|
+
# Create a hash for quick lookup
|
|
10
|
+
folder_map = {}
|
|
11
|
+
@folders.each { |f| folder_map[f.uid] = f }
|
|
12
|
+
|
|
13
|
+
# Find root folders (no parent) and build tree
|
|
14
|
+
root_folders = []
|
|
15
|
+
@folders.each do |folder|
|
|
16
|
+
root_folders << build_node(folder, folder_map) if folder.parent_uid.nil? || folder.parent_uid.empty?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
root_folders
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get folder path from root to given folder
|
|
23
|
+
def get_folder_path(folder_uid)
|
|
24
|
+
folder = @folders.find { |f| f.uid == folder_uid }
|
|
25
|
+
return nil unless folder
|
|
26
|
+
|
|
27
|
+
path = []
|
|
28
|
+
current = folder
|
|
29
|
+
|
|
30
|
+
# Walk up the tree
|
|
31
|
+
while current
|
|
32
|
+
path.unshift(current.name)
|
|
33
|
+
current = @folders.find { |f| f.uid == current.parent_uid }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
path.join('/')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get all ancestors of a folder (parent, grandparent, etc.)
|
|
40
|
+
def get_ancestors(folder_uid)
|
|
41
|
+
ancestors = []
|
|
42
|
+
folder = @folders.find { |f| f.uid == folder_uid }
|
|
43
|
+
return ancestors unless folder
|
|
44
|
+
|
|
45
|
+
current_parent_uid = folder.parent_uid
|
|
46
|
+
while current_parent_uid && !current_parent_uid.empty?
|
|
47
|
+
parent = @folders.find { |f| f.uid == current_parent_uid }
|
|
48
|
+
break unless parent
|
|
49
|
+
|
|
50
|
+
ancestors << parent
|
|
51
|
+
current_parent_uid = parent.parent_uid
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
ancestors
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get all descendants of a folder (children, grandchildren, etc.)
|
|
58
|
+
def get_descendants(folder_uid)
|
|
59
|
+
descendants = []
|
|
60
|
+
children = @folders.select { |f| f.parent_uid == folder_uid }
|
|
61
|
+
|
|
62
|
+
children.each do |child|
|
|
63
|
+
descendants << child
|
|
64
|
+
descendants.concat(get_descendants(child.uid))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
descendants
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Find folder by name (optionally within a parent)
|
|
71
|
+
def find_folder_by_name(name, parent_uid: nil)
|
|
72
|
+
if parent_uid
|
|
73
|
+
@folders.find { |f| f.name == name && f.parent_uid == parent_uid }
|
|
74
|
+
else
|
|
75
|
+
@folders.find { |f| f.name == name }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Print folder tree to console
|
|
80
|
+
def print_tree(folders = nil, indent = 0)
|
|
81
|
+
folders ||= build_folder_tree
|
|
82
|
+
|
|
83
|
+
folders.each do |node|
|
|
84
|
+
puts "#{' ' * indent}├── #{node[:folder].name} (#{node[:folder].uid})"
|
|
85
|
+
if node[:folder].records && !node[:folder].records.empty?
|
|
86
|
+
node[:folder].records.each do |record|
|
|
87
|
+
puts "#{' ' * (indent + 4)}└─ #{record.title} (#{record.uid})"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
print_tree(node[:children], indent + 4) if node[:children]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def build_node(folder, folder_map)
|
|
97
|
+
node = {
|
|
98
|
+
folder: folder,
|
|
99
|
+
children: []
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Find children
|
|
103
|
+
@folders.each do |f|
|
|
104
|
+
node[:children] << build_node(f, folder_map) if f.parent_uid == folder.uid
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
node
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require_relative 'version'
|
|
2
|
+
|
|
3
|
+
module KeeperSecretsManager
|
|
4
|
+
module KeeperGlobals
|
|
5
|
+
CLIENT_VERSION_PREFIX = 'mb'.freeze
|
|
6
|
+
|
|
7
|
+
def self.client_version
|
|
8
|
+
"#{CLIENT_VERSION_PREFIX}#{KeeperSecretsManager::VERSION}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Keeper public keys by ID
|
|
12
|
+
KEEPER_PUBLIC_KEYS = {
|
|
13
|
+
'1' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
|
14
|
+
'2' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
|
15
|
+
'3' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
|
16
|
+
'4' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
|
17
|
+
'5' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
|
18
|
+
'6' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
|
19
|
+
'7' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
|
20
|
+
'8' => 'BKnhy0obglZJK-igwthNLdknoSXRrGB-mvFRzyb_L-DKKefWjYdFD2888qN1ROczz4n3keYSfKz9Koj90Z6w_tQ',
|
|
21
|
+
'9' => 'BAsPQdCpLIGXdWNLdAwx-3J5lNqUtKbaOMV56hUj8VzxE2USLHuHHuKDeno0ymJt-acxWV1xPlBfNUShhRTR77g',
|
|
22
|
+
'10' => 'BNYIh_Sv03nRZUUJveE8d2mxKLIDXv654UbshaItHrCJhd6cT7pdZ_XwbdyxAOCWMkBb9AZ4t1XRCsM8-wkEBRg',
|
|
23
|
+
'11' => 'BA6uNfeYSvqagwu4TOY6wFK4JyU5C200vJna0lH4PJ-SzGVXej8l9dElyQ58_ljfPs5Rq6zVVXpdDe8A7Y3WRhk',
|
|
24
|
+
'12' => 'BMjTIlXfohI8TDymsHxo0DqYysCy7yZGJ80WhgOBR4QUd6LBDA6-_318a-jCGW96zxXKMm8clDTKpE8w75KG-FY',
|
|
25
|
+
'13' => 'BJBDU1P1H21IwIdT2brKkPqbQR0Zl0TIHf7Bz_OO9jaNgIwydMkxt4GpBmkYoprZ_DHUGOrno2faB7pmTR7HhuI',
|
|
26
|
+
'14' => 'BJFF8j-dH7pDEw_U347w2CBM6xYM8Dk5fPPAktjib-opOqzvvbsER-WDHM4ONCSBf9O_obAHzCyygxmtpktDuiE',
|
|
27
|
+
'15' => 'BDKyWBvLbyZ-jMueORl3JwJnnEpCiZdN7yUvT0vOyjwpPBCDf6zfL4RWzvSkhAAFnwOni_1tQSl8dfXHbXqXsQ8',
|
|
28
|
+
'16' => 'BDXyZZnrl0tc2jdC5I61JjwkjK2kr7uet9tZjt8StTiJTAQQmnVOYBgbtP08PWDbecxnHghx3kJ8QXq1XE68y8c',
|
|
29
|
+
'17' => 'BFX68cb97m9_sweGdOVavFM3j5ot6gveg6xT4BtGahfGhKib-zdZyO9pwvv1cBda9ahkSzo1BQ4NVXp9qRyqVGU',
|
|
30
|
+
'18' => 'BNhngQqTT1bPKxGuB6FhbPTAeNVFl8PKGGSGo5W06xWIReutm6ix6JPivqnbvkydY-1uDQTr-5e6t70G01Bb5JA'
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# Keeper servers by region
|
|
34
|
+
KEEPER_SERVERS = {
|
|
35
|
+
'US' => 'keepersecurity.com',
|
|
36
|
+
'EU' => 'keepersecurity.eu',
|
|
37
|
+
'AU' => 'keepersecurity.com.au',
|
|
38
|
+
'GOV' => 'govcloud.keepersecurity.us',
|
|
39
|
+
'JP' => 'keepersecurity.jp',
|
|
40
|
+
'CA' => 'keepersecurity.ca',
|
|
41
|
+
'IL5' => 'il5.keepersecurity.us'
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
# Default server (US)
|
|
45
|
+
DEFAULT_SERVER = KEEPER_SERVERS['US'].freeze
|
|
46
|
+
|
|
47
|
+
# Default public key ID
|
|
48
|
+
DEFAULT_KEY_ID = '7'.freeze
|
|
49
|
+
|
|
50
|
+
# Logger name
|
|
51
|
+
LOGGER_NAME = 'keeper_secrets_manager'.freeze
|
|
52
|
+
end
|
|
53
|
+
end
|