security 0.2.0 → 0.3.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 +4 -4
- data/Gemfile +4 -3
- data/Gemfile.lock +15 -9
- data/README.md +47 -7
- data/UPGRADE.md +7 -0
- data/lib/security/command.rb +52 -0
- data/lib/security/error.rb +16 -0
- data/lib/security/keychain.rb +17 -10
- data/lib/security/password.rb +31 -10
- data/lib/security/version.rb +1 -1
- data/lib/security.rb +2 -0
- data/security.gemspec +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c157bfbd48ec870177f4a269569efc87edad32606bc9a7332b4d26644a13edf9
|
|
4
|
+
data.tar.gz: 275fbb21545cde55939d4512cec08d3c52bcff6a96d5300cb746941813685bed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f8a12a0decc2ab2e277816af45860be94894bf2b01dc307e5f4d3b88251d8a6676dbf5c0469af862c19a73679a8d444eb1b8502345756ac3595c35d7e38e9fb
|
|
7
|
+
data.tar.gz: e2b77b3d17ec02afb5c99d1a69d638633ab83026777fbb7c883fe4c8ae4b6896f269f38df28208508e15af937ea549eb7a9f95ecd82a32472956b3334605b13c
|
data/Gemfile
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
source 'https://rubygems.org'
|
|
4
4
|
|
|
5
5
|
gem 'rake', '~> 13.4'
|
|
6
|
-
gem 'rspec'
|
|
7
|
-
gem 'rubocop'
|
|
8
|
-
|
|
6
|
+
gem 'rspec', '~> 3.13'
|
|
7
|
+
gem 'rubocop', '~> 1.90'
|
|
8
|
+
# simplecov 1.x requires Ruby >= 3.2; hold at 0.x while Ruby 3.1 is supported
|
|
9
|
+
gem 'simplecov', '~> 0.22'
|
|
9
10
|
|
|
10
11
|
gemspec
|
data/Gemfile.lock
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
security (0.
|
|
4
|
+
security (0.3.0)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
8
8
|
specs:
|
|
9
9
|
ast (2.4.3)
|
|
10
10
|
diff-lcs (1.6.2)
|
|
11
|
-
|
|
11
|
+
docile (1.4.1)
|
|
12
|
+
json (3.0.2)
|
|
12
13
|
language_server-protocol (3.17.0.6)
|
|
13
14
|
lint_roller (1.1.0)
|
|
14
|
-
parallel (2.
|
|
15
|
+
parallel (2.2.0)
|
|
15
16
|
parser (3.3.12.0)
|
|
16
17
|
ast (~> 2.4.1)
|
|
17
18
|
racc
|
|
@@ -33,8 +34,8 @@ GEM
|
|
|
33
34
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
34
35
|
rspec-support (~> 3.13.0)
|
|
35
36
|
rspec-support (3.13.7)
|
|
36
|
-
rubocop (1.
|
|
37
|
-
json (
|
|
37
|
+
rubocop (1.91.0)
|
|
38
|
+
json (>= 2.3)
|
|
38
39
|
language_server-protocol (~> 3.17.0.2)
|
|
39
40
|
lint_roller (~> 1.1.0)
|
|
40
41
|
parallel (>= 1.10)
|
|
@@ -48,7 +49,12 @@ GEM
|
|
|
48
49
|
parser (>= 3.3.7.2)
|
|
49
50
|
prism (~> 1.7)
|
|
50
51
|
ruby-progressbar (1.13.0)
|
|
51
|
-
simplecov (
|
|
52
|
+
simplecov (0.22.0)
|
|
53
|
+
docile (~> 1.1)
|
|
54
|
+
simplecov-html (~> 0.11)
|
|
55
|
+
simplecov_json_formatter (~> 0.1)
|
|
56
|
+
simplecov-html (0.13.2)
|
|
57
|
+
simplecov_json_formatter (0.1.4)
|
|
52
58
|
unicode-display_width (3.2.0)
|
|
53
59
|
unicode-emoji (~> 4.1)
|
|
54
60
|
unicode-emoji (4.2.0)
|
|
@@ -59,10 +65,10 @@ PLATFORMS
|
|
|
59
65
|
|
|
60
66
|
DEPENDENCIES
|
|
61
67
|
rake (~> 13.4)
|
|
62
|
-
rspec
|
|
63
|
-
rubocop
|
|
68
|
+
rspec (~> 3.13)
|
|
69
|
+
rubocop (~> 1.90)
|
|
64
70
|
security!
|
|
65
|
-
simplecov
|
|
71
|
+
simplecov (~> 0.22)
|
|
66
72
|
|
|
67
73
|
BUNDLED WITH
|
|
68
74
|
2.6.9
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Security
|
|
2
2
|
|
|
3
|
-
[](https://github.com/fastlane-community/security/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/security)
|
|
5
5
|
[](https://github.com/fastlane-community/security/blob/main/LICENSE.md)
|
|
6
6
|
|
|
@@ -14,14 +14,54 @@
|
|
|
14
14
|
```ruby
|
|
15
15
|
require 'security'
|
|
16
16
|
|
|
17
|
-
Security::Keychain
|
|
17
|
+
Security::Keychain.default_keychain.filename #=> "/Users/jappleseed/Library/Keychains/login.keychain-db"
|
|
18
18
|
|
|
19
|
-
Security::InternetPassword.find(server: "itunesconnect.apple.com")
|
|
19
|
+
item = Security::InternetPassword.find(server: "itunesconnect.apple.com")
|
|
20
|
+
item&.password #=> "p4ssw0rd"
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
##
|
|
23
|
+
## Keychains
|
|
24
|
+
|
|
25
|
+
`find`, `add` and `delete` all take an optional `keychain:`, naming the keychain
|
|
26
|
+
to act on. It accepts a `Security::Keychain` or a path. Without one, `security`
|
|
27
|
+
adds to the default keychain and searches the default search list.
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
keychain = Security::Keychain.new("/path/to/build.keychain-db")
|
|
31
|
+
|
|
32
|
+
Security::InternetPassword.add("example.com", "jappleseed", "p4ssw0rd", keychain: keychain)
|
|
33
|
+
Security::InternetPassword.find(server: "example.com", keychain: keychain)
|
|
34
|
+
Security::InternetPassword.delete(server: "example.com", keychain: keychain)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Errors
|
|
38
|
+
|
|
39
|
+
The `security` command line tool reports failures through its exit status, and
|
|
40
|
+
this library distinguishes the two cases a caller needs to tell apart:
|
|
23
41
|
|
|
24
|
-
|
|
42
|
+
- **Nothing matched.** `find` returns `nil`. The keychain answered, and it holds
|
|
43
|
+
no such item.
|
|
44
|
+
- **The question could not be answered.** `find` raises `Security::Error`,
|
|
45
|
+
carrying the tool's exit `status` and its `output`. A locked keychain, a
|
|
46
|
+
keychain this process is not allowed to read, or a malformed request all
|
|
47
|
+
land here.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
begin
|
|
51
|
+
item = Security::InternetPassword.find(server: "itunesconnect.apple.com")
|
|
52
|
+
rescue Security::Error => e
|
|
53
|
+
warn "could not read the keychain: #{e.message}"
|
|
54
|
+
item = nil
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`Keychain.list`, `Keychain.default_keychain` and `Keychain.login_keychain`
|
|
59
|
+
raise `Security::Error` on failure in the same way.
|
|
60
|
+
|
|
61
|
+
The methods that change the keychain — `add`, `delete`, and the `Keychain`
|
|
62
|
+
instance methods — return `true` or `false` and print what the tool reported,
|
|
63
|
+
the way `Kernel#system` does.
|
|
64
|
+
|
|
65
|
+
## License
|
|
25
66
|
|
|
26
|
-
[
|
|
27
|
-
[build status badge]: https://github.com/mattt/Security/workflows/CI/badge.svg
|
|
67
|
+
[MIT](https://github.com/fastlane-community/security/blob/main/LICENSE.md)
|
data/UPGRADE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Upgrade Guide
|
|
2
|
+
|
|
3
|
+
## Upgrading from 0.2 to 0.3
|
|
4
|
+
|
|
5
|
+
In previous versions, `find` attempted to detect failures by searching for a `security: ` prefix in the output. This approach was unreliable: certain errors (like ACL issues) produced no output, while others (like malformed requests) returned a usage banner. These cases would incorrectly result in a `Password` object with `nil` attributes, making it impossible for callers to distinguish a failed request from a successful one. Additionally, failures that *did* have the prefix returned `nil`, which was indistinguishable from a missing item.
|
|
6
|
+
|
|
7
|
+
Starting with version 0.3, both cases now raise a `Security::Error`. If your code relies on `nil` to handle missing keychain items, you should now rescue `Security::Error` to prevent a locked or misconfigured keychain from being treated as empty.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
module Security
|
|
7
|
+
# :nodoc:
|
|
8
|
+
module Command
|
|
9
|
+
# TODO: replace with Data.define once Ruby 3.1 support is dropped.
|
|
10
|
+
# :nodoc:
|
|
11
|
+
Result = Struct.new(:stdout, :stderr, :status) do
|
|
12
|
+
def success?
|
|
13
|
+
!status.nil? && status.success?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def exitstatus
|
|
17
|
+
status&.exitstatus
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# `security` splits what it has to say across both streams: find-*-password
|
|
21
|
+
# prints the attributes on stdout and the password on stderr.
|
|
22
|
+
def output
|
|
23
|
+
stdout + stderr
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# Runs a `security` subcommand and captures what it produced. A missing
|
|
30
|
+
# `security` is a failed result rather than an exception: the tool is only
|
|
31
|
+
# present on macOS, and callers elsewhere should see the same failure they
|
|
32
|
+
# would get from a keychain that could not answer.
|
|
33
|
+
def run(command)
|
|
34
|
+
Result.new(*Open3.capture3(command))
|
|
35
|
+
rescue Errno::ENOENT => e
|
|
36
|
+
# Nothing ran, but the child Ruby forked exited 127 before exec, which is
|
|
37
|
+
# what a shell reports for a missing command, and what this library
|
|
38
|
+
# produced while it still went through one.
|
|
39
|
+
Result.new('', "#{e.message}\n", $CHILD_STATUS)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Runs a `security` subcommand and lets the caller see what it said. The
|
|
43
|
+
# tool reports on stderr whether or not it succeeded: show-keychain-info
|
|
44
|
+
# prints its result there on exit 0.
|
|
45
|
+
def relay(command)
|
|
46
|
+
result = run(command)
|
|
47
|
+
warn result.stderr.chomp unless result.stderr.empty?
|
|
48
|
+
|
|
49
|
+
result
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Security
|
|
4
|
+
# :nodoc:
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
attr_reader :status, :output
|
|
7
|
+
|
|
8
|
+
def initialize(status, output)
|
|
9
|
+
@status = status
|
|
10
|
+
@output = output
|
|
11
|
+
|
|
12
|
+
details = output.strip
|
|
13
|
+
super(details.empty? ? "`security` exited with status #{status}" : "#{details} (status #{status})")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/security/keychain.rb
CHANGED
|
@@ -14,19 +14,19 @@ module Security
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def info
|
|
17
|
-
|
|
17
|
+
Command.relay(%(security show-keychain-info #{@filename.shellescape})).success?
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def lock
|
|
21
|
-
|
|
21
|
+
Command.relay(%(security lock-keychain #{@filename.shellescape})).success?
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def unlock(password)
|
|
25
|
-
|
|
25
|
+
Command.relay(%(security unlock-keychain -p #{password.shellescape} #{@filename.shellescape})).success?
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def delete
|
|
29
|
-
|
|
29
|
+
Command.relay(%(security delete-keychain #{@filename.shellescape})).success?
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
class << self
|
|
@@ -35,29 +35,36 @@ module Security
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def list(domain = :user)
|
|
38
|
-
raise ArgumentError "Invalid domain #{domain}, expected one of: #{DOMAINS}" unless DOMAINS.include?(domain)
|
|
38
|
+
raise ArgumentError, "Invalid domain #{domain}, expected one of: #{DOMAINS}" unless DOMAINS.include?(domain)
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
keychains_from_command("security list-keychains -d #{domain}")
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def lock
|
|
44
|
-
|
|
44
|
+
Command.relay(%(security lock-keychain -a)).success?
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def unlock(password)
|
|
48
|
-
|
|
48
|
+
Command.relay(%(security unlock-keychain -p #{password.shellescape})).success?
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def default_keychain
|
|
52
|
-
|
|
52
|
+
keychains_from_command('security default-keychain').first
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def login_keychain
|
|
56
|
-
|
|
56
|
+
keychains_from_command('security login-keychain').first
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
private
|
|
60
60
|
|
|
61
|
+
def keychains_from_command(command)
|
|
62
|
+
result = Command.run(command)
|
|
63
|
+
raise Error.new(result.exitstatus, result.stderr) unless result.success?
|
|
64
|
+
|
|
65
|
+
keychains_from_output(result.stdout)
|
|
66
|
+
end
|
|
67
|
+
|
|
61
68
|
def keychains_from_output(output)
|
|
62
69
|
output.split("\n").collect { |line| new(line.strip.gsub(/^"|"$/, '')) }
|
|
63
70
|
end
|
data/lib/security/password.rb
CHANGED
|
@@ -5,6 +5,10 @@ require 'shellwords'
|
|
|
5
5
|
module Security
|
|
6
6
|
# :nodoc:
|
|
7
7
|
class Password
|
|
8
|
+
# `security` reports a missing item with this exit status. Every other
|
|
9
|
+
# non-zero status is a failure the caller needs to know about.
|
|
10
|
+
ITEM_NOT_FOUND = 44
|
|
11
|
+
|
|
8
12
|
attr_reader :keychain, :attributes, :password
|
|
9
13
|
|
|
10
14
|
private_class_method :new
|
|
@@ -18,9 +22,15 @@ module Security
|
|
|
18
22
|
class << self
|
|
19
23
|
private
|
|
20
24
|
|
|
21
|
-
def
|
|
22
|
-
|
|
25
|
+
def password_from_command(command)
|
|
26
|
+
result = Command.run(command)
|
|
27
|
+
return nil if result.exitstatus == ITEM_NOT_FOUND
|
|
28
|
+
raise Error.new(result.exitstatus, result.stderr) unless result.success?
|
|
23
29
|
|
|
30
|
+
password_from_output(result.output)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def password_from_output(output)
|
|
24
34
|
keychain = nil
|
|
25
35
|
attributes = {}
|
|
26
36
|
password = nil
|
|
@@ -44,6 +54,7 @@ module Security
|
|
|
44
54
|
|
|
45
55
|
def flags_for_options(options = {})
|
|
46
56
|
flags = options.dup
|
|
57
|
+
keychain = flags.delete(:keychain)
|
|
47
58
|
flags[:a] ||= flags.delete(:account)
|
|
48
59
|
flags[:c] ||= flags.delete(:creator)
|
|
49
60
|
flags[:C] ||= flags.delete(:type)
|
|
@@ -51,7 +62,18 @@ module Security
|
|
|
51
62
|
flags[:G] ||= flags.delete(:value)
|
|
52
63
|
flags[:j] ||= flags.delete(:comment)
|
|
53
64
|
|
|
54
|
-
|
|
65
|
+
arguments = flags.compact.collect { |k, v| "-#{k} #{v.shellescape}".strip }
|
|
66
|
+
arguments << filename_for_keychain(keychain) if keychain
|
|
67
|
+
|
|
68
|
+
arguments.join(' ')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# `security` takes the keychain to act on as a trailing argument. Without
|
|
72
|
+
# one it uses the default keychain, or the default search list.
|
|
73
|
+
def filename_for_keychain(keychain)
|
|
74
|
+
filename = keychain.is_a?(Keychain) ? keychain.filename : keychain
|
|
75
|
+
|
|
76
|
+
filename.shellescape
|
|
55
77
|
end
|
|
56
78
|
|
|
57
79
|
def decode_hex_blob(string)
|
|
@@ -68,15 +90,15 @@ module Security
|
|
|
68
90
|
options[:s] = service
|
|
69
91
|
options[:w] = password
|
|
70
92
|
|
|
71
|
-
|
|
93
|
+
Command.relay("security add-generic-password #{flags_for_options(options)}").success?
|
|
72
94
|
end
|
|
73
95
|
|
|
74
96
|
def find(options)
|
|
75
|
-
|
|
97
|
+
password_from_command("security find-generic-password -g #{flags_for_options(options)}")
|
|
76
98
|
end
|
|
77
99
|
|
|
78
100
|
def delete(options)
|
|
79
|
-
|
|
101
|
+
Command.run("security delete-generic-password #{flags_for_options(options)}").success?
|
|
80
102
|
end
|
|
81
103
|
|
|
82
104
|
private
|
|
@@ -95,16 +117,15 @@ module Security
|
|
|
95
117
|
options[:a] = account
|
|
96
118
|
options[:s] = server
|
|
97
119
|
options[:w] = password
|
|
98
|
-
|
|
99
|
-
system "security add-internet-password #{flags_for_options(options)}"
|
|
120
|
+
Command.relay("security add-internet-password #{flags_for_options(options)}").success?
|
|
100
121
|
end
|
|
101
122
|
|
|
102
123
|
def find(options)
|
|
103
|
-
|
|
124
|
+
password_from_command("security find-internet-password -g #{flags_for_options(options)}")
|
|
104
125
|
end
|
|
105
126
|
|
|
106
127
|
def delete(options)
|
|
107
|
-
|
|
128
|
+
Command.run("security delete-internet-password #{flags_for_options(options)}").success?
|
|
108
129
|
end
|
|
109
130
|
|
|
110
131
|
private
|
data/lib/security/version.rb
CHANGED
data/lib/security.rb
CHANGED
data/security.gemspec
CHANGED
|
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
|
|
|
15
15
|
s.files = Dir['./**/*'].grep_v(%r{\./(bin|log|pkg|script|spec|test|vendor)})
|
|
16
16
|
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
|
17
17
|
s.require_paths = ['lib']
|
|
18
|
-
s.required_ruby_version = '>= 3.
|
|
18
|
+
s.required_ruby_version = '>= 3.1.0'
|
|
19
19
|
|
|
20
20
|
s.metadata['rubygems_mfa_required'] = 'true'
|
|
21
21
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: security
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Josh Holtz
|
|
@@ -21,8 +21,11 @@ files:
|
|
|
21
21
|
- "./LICENSE.md"
|
|
22
22
|
- "./README.md"
|
|
23
23
|
- "./Rakefile"
|
|
24
|
+
- "./UPGRADE.md"
|
|
24
25
|
- "./lib/security.rb"
|
|
25
26
|
- "./lib/security/certificate.rb"
|
|
27
|
+
- "./lib/security/command.rb"
|
|
28
|
+
- "./lib/security/error.rb"
|
|
26
29
|
- "./lib/security/keychain.rb"
|
|
27
30
|
- "./lib/security/password.rb"
|
|
28
31
|
- "./lib/security/version.rb"
|
|
@@ -39,7 +42,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
39
42
|
requirements:
|
|
40
43
|
- - ">="
|
|
41
44
|
- !ruby/object:Gem::Version
|
|
42
|
-
version: 3.
|
|
45
|
+
version: 3.1.0
|
|
43
46
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
47
|
requirements:
|
|
45
48
|
- - ">="
|