security 0.3.0 → 0.4.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 +2 -3
- data/Gemfile.lock +13 -20
- data/README.md +32 -5
- data/UPGRADE.md +18 -0
- data/lib/security/certificate.rb +70 -2
- data/lib/security/command.rb +59 -4
- data/lib/security/error.rb +3 -0
- data/lib/security/identity.rb +55 -0
- data/lib/security/keychain.rb +62 -15
- data/lib/security/password.rb +11 -15
- data/lib/security/version.rb +1 -1
- data/lib/security.rb +1 -0
- data/security.gemspec +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c1d9c484b22dfdf437dbd6af09ccb5b1c197c94c1d396496cd93b35c93ae9fc
|
|
4
|
+
data.tar.gz: 72f35ca3c02f14e83e75aed6b10048bf85f78b4f95176302a34af671a3807567
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 40675d74911733f6316286730b6fe68dd4e28905f3dc00f7d260645de0707b905c0be386e42062601d8226b1e1240179c6607660dc349c92d08d982c83007f7a
|
|
7
|
+
data.tar.gz: 98ef0545f2155fc4c5e55d4969c62b2ed1ddb63634b633f7fec30cadb1726159be3cd7823a0ee0fa11cdc0b75210c7deb2df78bd24fa9a3a4d2b8f95ac1fcbbd
|
data/Gemfile
CHANGED
|
@@ -4,8 +4,7 @@ source 'https://rubygems.org'
|
|
|
4
4
|
|
|
5
5
|
gem 'rake', '~> 13.4'
|
|
6
6
|
gem 'rspec', '~> 3.13'
|
|
7
|
-
gem 'rubocop', '~> 1.
|
|
8
|
-
|
|
9
|
-
gem 'simplecov', '~> 0.22'
|
|
7
|
+
gem 'rubocop', '~> 1.92'
|
|
8
|
+
gem 'simplecov', '~> 1.2'
|
|
10
9
|
|
|
11
10
|
gemspec
|
data/Gemfile.lock
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
security (0.
|
|
4
|
+
security (0.4.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
|
-
|
|
12
|
-
json (3.0.2)
|
|
11
|
+
json (3.1.0)
|
|
13
12
|
language_server-protocol (3.17.0.6)
|
|
14
13
|
lint_roller (1.1.0)
|
|
15
|
-
parallel (
|
|
14
|
+
parallel (1.28.0)
|
|
16
15
|
parser (3.3.12.0)
|
|
17
16
|
ast (~> 2.4.1)
|
|
18
17
|
racc
|
|
@@ -20,7 +19,7 @@ GEM
|
|
|
20
19
|
racc (1.8.1)
|
|
21
20
|
rainbow (3.1.1)
|
|
22
21
|
rake (13.4.2)
|
|
23
|
-
regexp_parser (2.
|
|
22
|
+
regexp_parser (2.13.1)
|
|
24
23
|
rspec (3.13.2)
|
|
25
24
|
rspec-core (~> 3.13.0)
|
|
26
25
|
rspec-expectations (~> 3.13.0)
|
|
@@ -34,14 +33,14 @@ GEM
|
|
|
34
33
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
35
34
|
rspec-support (~> 3.13.0)
|
|
36
35
|
rspec-support (3.13.7)
|
|
37
|
-
rubocop (1.
|
|
36
|
+
rubocop (1.92.0)
|
|
38
37
|
json (>= 2.3)
|
|
39
38
|
language_server-protocol (~> 3.17.0.2)
|
|
40
39
|
lint_roller (~> 1.1.0)
|
|
41
40
|
parallel (>= 1.10)
|
|
42
41
|
parser (>= 3.3.0.2)
|
|
43
42
|
rainbow (>= 2.2.2, < 4.0)
|
|
44
|
-
regexp_parser (>= 2.
|
|
43
|
+
regexp_parser (>= 2.11.3, < 3.0)
|
|
45
44
|
rubocop-ast (>= 1.49.0, < 2.0)
|
|
46
45
|
ruby-progressbar (~> 1.7)
|
|
47
46
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
@@ -49,26 +48,20 @@ GEM
|
|
|
49
48
|
parser (>= 3.3.7.2)
|
|
50
49
|
prism (~> 1.7)
|
|
51
50
|
ruby-progressbar (1.13.0)
|
|
52
|
-
simplecov (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
simplecov-html (0.13.2)
|
|
57
|
-
simplecov_json_formatter (0.1.4)
|
|
58
|
-
unicode-display_width (3.2.0)
|
|
59
|
-
unicode-emoji (~> 4.1)
|
|
60
|
-
unicode-emoji (4.2.0)
|
|
51
|
+
simplecov (1.2.0)
|
|
52
|
+
unicode-display_width (3.3.0)
|
|
53
|
+
unicode-emoji (~> 4.3)
|
|
54
|
+
unicode-emoji (4.3.0)
|
|
61
55
|
|
|
62
56
|
PLATFORMS
|
|
63
57
|
ruby
|
|
64
|
-
x86_64-linux
|
|
65
58
|
|
|
66
59
|
DEPENDENCIES
|
|
67
60
|
rake (~> 13.4)
|
|
68
61
|
rspec (~> 3.13)
|
|
69
|
-
rubocop (~> 1.
|
|
62
|
+
rubocop (~> 1.92)
|
|
70
63
|
security!
|
|
71
|
-
simplecov (~>
|
|
64
|
+
simplecov (~> 1.2)
|
|
72
65
|
|
|
73
66
|
BUNDLED WITH
|
|
74
|
-
2.
|
|
67
|
+
2.7.2
|
data/README.md
CHANGED
|
@@ -34,6 +34,25 @@ Security::InternetPassword.find(server: "example.com", keychain: keychain)
|
|
|
34
34
|
Security::InternetPassword.delete(server: "example.com", keychain: keychain)
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
Keychains themselves:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
keychain = Security::Keychain.create("/path/to/build.keychain-db", "p4ssw0rd")
|
|
41
|
+
keychain.update_settings(timeout: 3600, lock_when_sleeping: true)
|
|
42
|
+
keychain.set_key_partition_list("p4ssw0rd")
|
|
43
|
+
|
|
44
|
+
Security::Keychain.set_search_list(Security::Keychain.list + [keychain])
|
|
45
|
+
Security::Keychain.set_default_keychain(keychain)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Certificates and identities
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
Security::Certificate.import("/path/to/identity.p12", keychain: keychain, password: "p12 password")
|
|
52
|
+
Security::Certificate.find(name: "Developer ID Installer", keychain: keychain).map(&:sha1)
|
|
53
|
+
Security::Identity.find(keychain: keychain).map(&:name) #=> ["Apple Development: Jane Appleseed (TEAMID)"]
|
|
54
|
+
```
|
|
55
|
+
|
|
37
56
|
## Errors
|
|
38
57
|
|
|
39
58
|
The `security` command line tool reports failures through its exit status, and
|
|
@@ -55,12 +74,20 @@ rescue Security::Error => e
|
|
|
55
74
|
end
|
|
56
75
|
```
|
|
57
76
|
|
|
58
|
-
`Keychain.list`, `Keychain.default_keychain
|
|
59
|
-
raise `Security::Error`
|
|
77
|
+
`Keychain.list`, `Keychain.default_keychain`, `Keychain.login_keychain`,
|
|
78
|
+
`Keychain.create`, `Certificate.find` and `Identity.find` raise `Security::Error`
|
|
79
|
+
on failure in the same way. `Certificate.find` and `Identity.find` return an
|
|
80
|
+
empty array when nothing matched.
|
|
81
|
+
|
|
82
|
+
The methods that change the keychain — `add`, `delete` and the `Keychain`
|
|
83
|
+
setters — return `true` or `false` and print what the tool reported, the way
|
|
84
|
+
`Kernel#system` does. Two raise instead, because the caller needs to know why
|
|
85
|
+
they failed:
|
|
60
86
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
- `Certificate.import` raises `Security::DuplicateItemError` when the keychain
|
|
88
|
+
already holds the item, and `Security::Error` for any other failure.
|
|
89
|
+
- `Keychain#set_key_partition_list` raises `Security::Error`, most often for a
|
|
90
|
+
wrong keychain password.
|
|
64
91
|
|
|
65
92
|
## License
|
|
66
93
|
|
data/UPGRADE.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Upgrade Guide
|
|
2
2
|
|
|
3
|
+
## Upgrading from 0.3 to 0.4
|
|
4
|
+
|
|
5
|
+
Ruby 3.2 is now the minimum.
|
|
6
|
+
|
|
7
|
+
`Security::Command.run` and `Security::Command.relay` no longer go through a shell. They take the program and its arguments separately, so nothing needs escaping:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# 0.3
|
|
11
|
+
Security::Command.run("security show-keychain-info #{path.shellescape}")
|
|
12
|
+
|
|
13
|
+
# 0.4
|
|
14
|
+
Security::Command.run("security", "show-keychain-info", path)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
A single string is now the name of the program to run, so a 0.3 command line fails as a missing command, with exit status 127.
|
|
18
|
+
|
|
19
|
+
Passwords no longer appear in the arguments of the `security` process, where any local user could read them with `ps`: the commands that take one (adding a password, creating, unlocking or setting the partition list of a keychain, importing with a passphrase) are sent to `security -i` on stdin. A password or other argument containing a line break now raises `ArgumentError`, and so does a command longer than `security -i` reads (4096 bytes, 32 arguments).
|
|
20
|
+
|
|
3
21
|
## Upgrading from 0.2 to 0.3
|
|
4
22
|
|
|
5
23
|
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.
|
data/lib/security/certificate.rb
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
3
5
|
module Security
|
|
4
6
|
# :nodoc:
|
|
5
7
|
class Certificate
|
|
8
|
+
# The tools allowed to use an imported item without asking the user. A
|
|
9
|
+
# keychain prompt in the middle of a build is a hang rather than a failure,
|
|
10
|
+
# which is why these are named rather than left to the default.
|
|
11
|
+
DEFAULT_TRUSTED_APPLICATIONS = [
|
|
12
|
+
'/usr/bin/codesign',
|
|
13
|
+
'/usr/bin/security',
|
|
14
|
+
'/usr/bin/productbuild',
|
|
15
|
+
'/usr/bin/productsign'
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
# What `security import` reports for an item the keychain already holds.
|
|
19
|
+
ALREADY_EXISTS = 'The specified item already exists in the keychain'
|
|
20
|
+
|
|
21
|
+
PEM = /-----BEGIN CERTIFICATE-----\n.*?-----END CERTIFICATE-----\n/m
|
|
22
|
+
|
|
23
|
+
attr_reader :sha1, :sha256, :pem
|
|
24
|
+
|
|
6
25
|
private_class_method :new
|
|
7
26
|
|
|
27
|
+
def initialize(sha1:, sha256:, pem:)
|
|
28
|
+
@sha1 = sha1
|
|
29
|
+
@sha256 = sha256
|
|
30
|
+
@pem = pem
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The subject's common name, which is what the keychain shows as its name.
|
|
34
|
+
def name
|
|
35
|
+
OpenSSL::X509::Certificate.new(pem).subject.to_a.find { |key, _value, _type| key == 'CN' }&.at(1)
|
|
36
|
+
end
|
|
37
|
+
|
|
8
38
|
def delete!
|
|
9
39
|
raise NotImplementedError
|
|
10
40
|
end
|
|
@@ -14,8 +44,46 @@ module Security
|
|
|
14
44
|
end
|
|
15
45
|
|
|
16
46
|
class << self
|
|
17
|
-
|
|
18
|
-
|
|
47
|
+
# Every certificate whose name contains `name`. Finding none is not a
|
|
48
|
+
# failure: `security` prints nothing and exits 0.
|
|
49
|
+
def find(name:, keychain: nil)
|
|
50
|
+
command = ['security', 'find-certificate', '-a', '-c', name.to_s, '-Z', '-p']
|
|
51
|
+
command << filename_for(keychain) if keychain
|
|
52
|
+
|
|
53
|
+
result = Command.run(*command)
|
|
54
|
+
raise Error.new(result.exitstatus, result.stderr) unless result.success?
|
|
55
|
+
|
|
56
|
+
certificates_from_output(result.stdout)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Imports a certificate or identity file into `keychain`. `password` is
|
|
60
|
+
# the one protecting the file, not the keychain's. Raises
|
|
61
|
+
# DuplicateItemError when the keychain already holds it.
|
|
62
|
+
def import(path, keychain:, password: nil, format: nil,
|
|
63
|
+
trusted_applications: DEFAULT_TRUSTED_APPLICATIONS)
|
|
64
|
+
command = ['security', 'import', path.to_s, '-k', filename_for(keychain)]
|
|
65
|
+
command += ['-P', password.to_s] if password
|
|
66
|
+
command += ['-f', format.to_s] if format
|
|
67
|
+
trusted_applications.each { |application| command += ['-T', application] }
|
|
68
|
+
|
|
69
|
+
result = password ? Command.run_interactive(*command.drop(1)) : Command.run(*command)
|
|
70
|
+
return true if result.success?
|
|
71
|
+
|
|
72
|
+
# Every failure exits 1, so only the message tells a duplicate apart.
|
|
73
|
+
error = result.stderr.include?(ALREADY_EXISTS) ? DuplicateItemError : Error
|
|
74
|
+
raise error.new(result.exitstatus, result.stderr)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def certificates_from_output(output)
|
|
80
|
+
output.scan(/^SHA-256 hash: (\h+)\nSHA-1 hash: (\h+)\n(#{PEM})/m).map do |sha256, sha1, pem|
|
|
81
|
+
new(sha1: sha1, sha256: sha256, pem: pem)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def filename_for(keychain)
|
|
86
|
+
keychain.respond_to?(:filename) ? keychain.filename : keychain.to_s
|
|
19
87
|
end
|
|
20
88
|
end
|
|
21
89
|
end
|
data/lib/security/command.rb
CHANGED
|
@@ -24,14 +24,27 @@ module Security
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# What `security -i` reads at most: bytes per line, and arguments per command.
|
|
28
|
+
INTERACTIVE_LINE_LENGTH = 4096
|
|
29
|
+
INTERACTIVE_ARGUMENTS = 32
|
|
30
|
+
|
|
27
31
|
module_function
|
|
28
32
|
|
|
29
33
|
# Runs a `security` subcommand and captures what it produced. A missing
|
|
30
34
|
# `security` is a failed result rather than an exception: the tool is only
|
|
31
35
|
# present on macOS, and callers elsewhere should see the same failure they
|
|
32
36
|
# would get from a keychain that could not answer.
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
#
|
|
38
|
+
# Never through a shell, so a path containing a space or a quote needs no
|
|
39
|
+
# escaping:
|
|
40
|
+
#
|
|
41
|
+
# run("security", "show-keychain-info", path)
|
|
42
|
+
def run(program, *, stdin: nil)
|
|
43
|
+
# [program, program] rather than program: Ruby hands a lone string with
|
|
44
|
+
# shell metacharacters to a shell.
|
|
45
|
+
# stdin_data only when there is some, so a command without a password is
|
|
46
|
+
# run, and can be stubbed, exactly as before.
|
|
47
|
+
Result.new(*Open3.capture3([program, program], *, **(stdin ? { stdin_data: stdin } : {})))
|
|
35
48
|
rescue Errno::ENOENT => e
|
|
36
49
|
# Nothing ran, but the child Ruby forked exited 127 before exec, which is
|
|
37
50
|
# what a shell reports for a missing command, and what this library
|
|
@@ -42,8 +55,50 @@ module Security
|
|
|
42
55
|
# Runs a `security` subcommand and lets the caller see what it said. The
|
|
43
56
|
# tool reports on stderr whether or not it succeeded: show-keychain-info
|
|
44
57
|
# prints its result there on exit 0.
|
|
45
|
-
def relay(command)
|
|
46
|
-
|
|
58
|
+
def relay(*command)
|
|
59
|
+
relayed(run(*command))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Runs a `security` subcommand that takes a password: through `security -i`,
|
|
63
|
+
# which reads the command from stdin, so the password never shows in the
|
|
64
|
+
# arguments, which any local user can read with `ps`. Without -p (or -P),
|
|
65
|
+
# the tool would instead read it from the terminal when there is one, or
|
|
66
|
+
# import would ask through a dialog.
|
|
67
|
+
#
|
|
68
|
+
# run_interactive("unlock-keychain", "-p", password, path)
|
|
69
|
+
def run_interactive(subcommand, *)
|
|
70
|
+
result = run('security', '-i', stdin: interactive_input(subcommand, *))
|
|
71
|
+
# -i adds a line naming the command and its status after any error.
|
|
72
|
+
result.stderr = result.stderr.sub(/^#{Regexp.escape(subcommand)}: returned -?\d+\n\z/, '')
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def relay_interactive(*command)
|
|
77
|
+
relayed(run_interactive(*command))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# One command line for `security -i`: every argument double quoted, with
|
|
81
|
+
# backslashes and double quotes escaped. A line break would end the command,
|
|
82
|
+
# and a longer line or more arguments than the tool reads would be cut.
|
|
83
|
+
def interactive_input(*command)
|
|
84
|
+
if command.size > INTERACTIVE_ARGUMENTS
|
|
85
|
+
raise ArgumentError, "security -i takes at most #{INTERACTIVE_ARGUMENTS} arguments"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
line = command.map do |argument|
|
|
89
|
+
argument = argument.to_s
|
|
90
|
+
raise ArgumentError, 'An argument for security -i cannot contain a line break' if argument.match?(/[\r\n]/)
|
|
91
|
+
|
|
92
|
+
%("#{argument.gsub(/[\\"]/) { |character| "\\#{character}" }}")
|
|
93
|
+
end.join(' ')
|
|
94
|
+
if line.bytesize >= INTERACTIVE_LINE_LENGTH - 1
|
|
95
|
+
raise ArgumentError, "A command for security -i must be shorter than #{INTERACTIVE_LINE_LENGTH} bytes"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
"#{line}\n"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def relayed(result)
|
|
47
102
|
warn result.stderr.chomp unless result.stderr.empty?
|
|
48
103
|
|
|
49
104
|
result
|
data/lib/security/error.rb
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Security
|
|
4
|
+
# :nodoc:
|
|
5
|
+
class Identity
|
|
6
|
+
attr_reader :sha1, :name, :status
|
|
7
|
+
|
|
8
|
+
private_class_method :new
|
|
9
|
+
|
|
10
|
+
# `status` is what `security` prints after the name for an identity it
|
|
11
|
+
# considers invalid, such as CSSMERR_TP_CERT_REVOKED, or nil.
|
|
12
|
+
def initialize(sha1, name, status = nil)
|
|
13
|
+
@sha1 = sha1
|
|
14
|
+
@name = name
|
|
15
|
+
@status = status
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# The identities `security` can see, newest tool output shaped like:
|
|
20
|
+
#
|
|
21
|
+
# 1) A1B2... "Apple Development: someone (TEAM)"
|
|
22
|
+
# 1 valid identities found
|
|
23
|
+
#
|
|
24
|
+
# `valid_only` drops the ones the tool marks unusable. `keychain` limits
|
|
25
|
+
# the search to one; without it `security` searches the user's default,
|
|
26
|
+
# which is what `find-identity` does and why callers that do not name a
|
|
27
|
+
# keychain get whatever happens to be in the login one.
|
|
28
|
+
def find(policy: 'codesigning', valid_only: true, keychain: nil)
|
|
29
|
+
command = %w[security find-identity]
|
|
30
|
+
command << '-v' if valid_only
|
|
31
|
+
command += ['-p', policy.to_s] if policy
|
|
32
|
+
command << filename_for(keychain) if keychain
|
|
33
|
+
|
|
34
|
+
result = Command.run(*command)
|
|
35
|
+
raise Error.new(result.exitstatus, result.stderr) unless result.success?
|
|
36
|
+
|
|
37
|
+
identities_from_output(result.stdout)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Finding none is not a failure: `security` prints "0 valid identities
|
|
43
|
+
# found" and exits 0.
|
|
44
|
+
def identities_from_output(output)
|
|
45
|
+
output.scan(/^\s*\d+\)\s+(\h+)\s+"(.*)"(?:\s+\(([^()]*)\))?\s*$/).map do |sha1, name, status|
|
|
46
|
+
new(sha1, name, status)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def filename_for(keychain)
|
|
51
|
+
keychain.respond_to?(:filename) ? keychain.filename : keychain.to_s
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/security/keychain.rb
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'shellwords'
|
|
4
|
-
|
|
5
3
|
module Security
|
|
6
4
|
# :nodoc:
|
|
7
5
|
class Keychain
|
|
8
6
|
DOMAINS = %i[user system common dynamic].freeze
|
|
9
7
|
|
|
8
|
+
# The partition IDs that let Apple's signing tools use an imported key
|
|
9
|
+
# without a keychain prompt.
|
|
10
|
+
DEFAULT_PARTITION_IDS = %w[apple-tool: apple: codesign:].freeze
|
|
11
|
+
|
|
10
12
|
attr_reader :filename
|
|
11
13
|
|
|
12
14
|
def initialize(filename)
|
|
@@ -14,52 +16,97 @@ module Security
|
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def info
|
|
17
|
-
Command.relay(
|
|
19
|
+
Command.relay('security', 'show-keychain-info', @filename).success?
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def lock
|
|
21
|
-
Command.relay(
|
|
23
|
+
Command.relay('security', 'lock-keychain', @filename).success?
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
def unlock(password)
|
|
25
|
-
Command.
|
|
27
|
+
Command.relay_interactive('unlock-keychain', '-p', password.to_s, @filename).success?
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def delete
|
|
29
|
-
Command.relay(
|
|
31
|
+
Command.relay('security', 'delete-keychain', @filename).success?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# With no options the keychain never locks on its own.
|
|
35
|
+
def update_settings(timeout: nil, lock_when_sleeping: false, lock_after_timeout: false)
|
|
36
|
+
command = %w[security set-keychain-settings]
|
|
37
|
+
command += ['-t', timeout.to_s] if timeout
|
|
38
|
+
command << '-l' if lock_when_sleeping
|
|
39
|
+
command << '-u' if lock_after_timeout
|
|
40
|
+
|
|
41
|
+
Command.relay(*command, @filename).success?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Raises rather than returning false: a wrong keychain password is the usual
|
|
45
|
+
# failure, and the caller needs the output to tell it from the others.
|
|
46
|
+
def set_key_partition_list(password, partition_ids: DEFAULT_PARTITION_IDS)
|
|
47
|
+
result = Command.run_interactive('set-key-partition-list', '-S', partition_ids.join(','), '-s',
|
|
48
|
+
'-k', password.to_s, @filename)
|
|
49
|
+
raise Error.new(result.exitstatus, result.stderr) unless result.success?
|
|
50
|
+
|
|
51
|
+
true
|
|
30
52
|
end
|
|
31
53
|
|
|
32
54
|
class << self
|
|
55
|
+
# Raises on failure, like the other class methods that answer with a keychain.
|
|
33
56
|
def create(filename, password)
|
|
34
|
-
|
|
57
|
+
result = Command.run_interactive('create-keychain', '-p', password.to_s, filename.to_s)
|
|
58
|
+
raise Error.new(result.exitstatus, result.stderr) unless result.success?
|
|
59
|
+
|
|
60
|
+
new(filename.to_s)
|
|
35
61
|
end
|
|
36
62
|
|
|
37
63
|
def list(domain = :user)
|
|
38
|
-
|
|
64
|
+
keychains_from_command('security', 'list-keychains', '-d', domain_name(domain))
|
|
65
|
+
end
|
|
39
66
|
|
|
40
|
-
|
|
67
|
+
def set_search_list(keychains, domain: :user)
|
|
68
|
+
Command.relay('security', 'list-keychains', '-d', domain_name(domain), '-s',
|
|
69
|
+
*keychains.map { |keychain| filename_for(keychain) }).success?
|
|
41
70
|
end
|
|
42
71
|
|
|
43
72
|
def lock
|
|
44
|
-
Command.relay(
|
|
73
|
+
Command.relay('security', 'lock-keychain', '-a').success?
|
|
45
74
|
end
|
|
46
75
|
|
|
47
76
|
def unlock(password)
|
|
48
|
-
Command.
|
|
77
|
+
Command.relay_interactive('unlock-keychain', '-p', password.to_s).success?
|
|
49
78
|
end
|
|
50
79
|
|
|
51
80
|
def default_keychain
|
|
52
|
-
keychains_from_command('security default-keychain').first
|
|
81
|
+
keychains_from_command('security', 'default-keychain').first
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def set_default_keychain(keychain, domain: :user)
|
|
85
|
+
Command.relay('security', 'default-keychain', '-d', domain_name(domain), '-s', filename_for(keychain)).success?
|
|
53
86
|
end
|
|
54
87
|
|
|
55
88
|
def login_keychain
|
|
56
|
-
keychains_from_command('security login-keychain').first
|
|
89
|
+
keychains_from_command('security', 'login-keychain').first
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def supports_key_partition_list?
|
|
93
|
+
Command.run('security', '-h').stdout.include?('set-key-partition-list')
|
|
57
94
|
end
|
|
58
95
|
|
|
59
96
|
private
|
|
60
97
|
|
|
61
|
-
def
|
|
62
|
-
|
|
98
|
+
def domain_name(domain)
|
|
99
|
+
raise ArgumentError, "Invalid domain #{domain}, expected one of: #{DOMAINS}" unless DOMAINS.include?(domain)
|
|
100
|
+
|
|
101
|
+
domain.to_s
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def filename_for(keychain)
|
|
105
|
+
keychain.respond_to?(:filename) ? keychain.filename : keychain.to_s
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def keychains_from_command(*command)
|
|
109
|
+
result = Command.run(*command)
|
|
63
110
|
raise Error.new(result.exitstatus, result.stderr) unless result.success?
|
|
64
111
|
|
|
65
112
|
keychains_from_output(result.stdout)
|
data/lib/security/password.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'shellwords'
|
|
4
|
-
|
|
5
3
|
module Security
|
|
6
4
|
# :nodoc:
|
|
7
5
|
class Password
|
|
@@ -22,8 +20,8 @@ module Security
|
|
|
22
20
|
class << self
|
|
23
21
|
private
|
|
24
22
|
|
|
25
|
-
def password_from_command(command)
|
|
26
|
-
result = Command.run(command)
|
|
23
|
+
def password_from_command(*command)
|
|
24
|
+
result = Command.run(*command)
|
|
27
25
|
return nil if result.exitstatus == ITEM_NOT_FOUND
|
|
28
26
|
raise Error.new(result.exitstatus, result.stderr) unless result.success?
|
|
29
27
|
|
|
@@ -62,18 +60,16 @@ module Security
|
|
|
62
60
|
flags[:G] ||= flags.delete(:value)
|
|
63
61
|
flags[:j] ||= flags.delete(:comment)
|
|
64
62
|
|
|
65
|
-
arguments = flags.compact.
|
|
63
|
+
arguments = flags.compact.flat_map { |k, v| ["-#{k}", v.to_s] }
|
|
66
64
|
arguments << filename_for_keychain(keychain) if keychain
|
|
67
65
|
|
|
68
|
-
arguments
|
|
66
|
+
arguments
|
|
69
67
|
end
|
|
70
68
|
|
|
71
69
|
# `security` takes the keychain to act on as a trailing argument. Without
|
|
72
70
|
# one it uses the default keychain, or the default search list.
|
|
73
71
|
def filename_for_keychain(keychain)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
filename.shellescape
|
|
72
|
+
keychain.is_a?(Keychain) ? keychain.filename : keychain.to_s
|
|
77
73
|
end
|
|
78
74
|
|
|
79
75
|
def decode_hex_blob(string)
|
|
@@ -90,15 +86,15 @@ module Security
|
|
|
90
86
|
options[:s] = service
|
|
91
87
|
options[:w] = password
|
|
92
88
|
|
|
93
|
-
Command.
|
|
89
|
+
Command.relay_interactive('add-generic-password', *flags_for_options(options)).success?
|
|
94
90
|
end
|
|
95
91
|
|
|
96
92
|
def find(options)
|
|
97
|
-
password_from_command(
|
|
93
|
+
password_from_command('security', 'find-generic-password', '-g', *flags_for_options(options))
|
|
98
94
|
end
|
|
99
95
|
|
|
100
96
|
def delete(options)
|
|
101
|
-
Command.run(
|
|
97
|
+
Command.run('security', 'delete-generic-password', *flags_for_options(options)).success?
|
|
102
98
|
end
|
|
103
99
|
|
|
104
100
|
private
|
|
@@ -117,15 +113,15 @@ module Security
|
|
|
117
113
|
options[:a] = account
|
|
118
114
|
options[:s] = server
|
|
119
115
|
options[:w] = password
|
|
120
|
-
Command.
|
|
116
|
+
Command.relay_interactive('add-internet-password', *flags_for_options(options)).success?
|
|
121
117
|
end
|
|
122
118
|
|
|
123
119
|
def find(options)
|
|
124
|
-
password_from_command(
|
|
120
|
+
password_from_command('security', 'find-internet-password', '-g', *flags_for_options(options))
|
|
125
121
|
end
|
|
126
122
|
|
|
127
123
|
def delete(options)
|
|
128
|
-
Command.run(
|
|
124
|
+
Command.run('security', 'delete-internet-password', *flags_for_options(options)).success?
|
|
129
125
|
end
|
|
130
126
|
|
|
131
127
|
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.2.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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Josh Holtz
|
|
@@ -26,6 +26,7 @@ files:
|
|
|
26
26
|
- "./lib/security/certificate.rb"
|
|
27
27
|
- "./lib/security/command.rb"
|
|
28
28
|
- "./lib/security/error.rb"
|
|
29
|
+
- "./lib/security/identity.rb"
|
|
29
30
|
- "./lib/security/keychain.rb"
|
|
30
31
|
- "./lib/security/password.rb"
|
|
31
32
|
- "./lib/security/version.rb"
|
|
@@ -42,7 +43,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
42
43
|
requirements:
|
|
43
44
|
- - ">="
|
|
44
45
|
- !ruby/object:Gem::Version
|
|
45
|
-
version: 3.
|
|
46
|
+
version: 3.2.0
|
|
46
47
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
48
|
requirements:
|
|
48
49
|
- - ">="
|