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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c157bfbd48ec870177f4a269569efc87edad32606bc9a7332b4d26644a13edf9
4
- data.tar.gz: 275fbb21545cde55939d4512cec08d3c52bcff6a96d5300cb746941813685bed
3
+ metadata.gz: 9c1d9c484b22dfdf437dbd6af09ccb5b1c197c94c1d396496cd93b35c93ae9fc
4
+ data.tar.gz: 72f35ca3c02f14e83e75aed6b10048bf85f78b4f95176302a34af671a3807567
5
5
  SHA512:
6
- metadata.gz: 0f8a12a0decc2ab2e277816af45860be94894bf2b01dc307e5f4d3b88251d8a6676dbf5c0469af862c19a73679a8d444eb1b8502345756ac3595c35d7e38e9fb
7
- data.tar.gz: e2b77b3d17ec02afb5c99d1a69d638633ab83026777fbb7c883fe4c8ae4b6896f269f38df28208508e15af937ea549eb7a9f95ecd82a32472956b3334605b13c
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.90'
8
- # simplecov 1.x requires Ruby >= 3.2; hold at 0.x while Ruby 3.1 is supported
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.3.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
- docile (1.4.1)
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 (2.2.0)
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.12.0)
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.91.0)
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.9.3, < 3.0)
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 (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)
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.90)
62
+ rubocop (~> 1.92)
70
63
  security!
71
- simplecov (~> 0.22)
64
+ simplecov (~> 1.2)
72
65
 
73
66
  BUNDLED WITH
74
- 2.6.9
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` and `Keychain.login_keychain`
59
- raise `Security::Error` on failure in the same way.
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
- 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.
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.
@@ -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
- def find
18
- raise NotImplementedError
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
@@ -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
- def run(command)
34
- Result.new(*Open3.capture3(command))
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
- result = run(command)
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
@@ -13,4 +13,7 @@ module Security
13
13
  super(details.empty? ? "`security` exited with status #{status}" : "#{details} (status #{status})")
14
14
  end
15
15
  end
16
+
17
+ # Raised when the keychain already holds the item being added.
18
+ class DuplicateItemError < Error; end
16
19
  end
@@ -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
@@ -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(%(security show-keychain-info #{@filename.shellescape})).success?
19
+ Command.relay('security', 'show-keychain-info', @filename).success?
18
20
  end
19
21
 
20
22
  def lock
21
- Command.relay(%(security lock-keychain #{@filename.shellescape})).success?
23
+ Command.relay('security', 'lock-keychain', @filename).success?
22
24
  end
23
25
 
24
26
  def unlock(password)
25
- Command.relay(%(security unlock-keychain -p #{password.shellescape} #{@filename.shellescape})).success?
27
+ Command.relay_interactive('unlock-keychain', '-p', password.to_s, @filename).success?
26
28
  end
27
29
 
28
30
  def delete
29
- Command.relay(%(security delete-keychain #{@filename.shellescape})).success?
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
- raise NotImplementedError
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
- raise ArgumentError, "Invalid domain #{domain}, expected one of: #{DOMAINS}" unless DOMAINS.include?(domain)
64
+ keychains_from_command('security', 'list-keychains', '-d', domain_name(domain))
65
+ end
39
66
 
40
- keychains_from_command("security list-keychains -d #{domain}")
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(%(security lock-keychain -a)).success?
73
+ Command.relay('security', 'lock-keychain', '-a').success?
45
74
  end
46
75
 
47
76
  def unlock(password)
48
- Command.relay(%(security unlock-keychain -p #{password.shellescape})).success?
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 keychains_from_command(command)
62
- result = Command.run(command)
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)
@@ -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.collect { |k, v| "-#{k} #{v.shellescape}".strip }
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.join(' ')
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
- filename = keychain.is_a?(Keychain) ? keychain.filename : keychain
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.relay("security add-generic-password #{flags_for_options(options)}").success?
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("security find-generic-password -g #{flags_for_options(options)}")
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("security delete-generic-password #{flags_for_options(options)}").success?
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.relay("security add-internet-password #{flags_for_options(options)}").success?
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("security find-internet-password -g #{flags_for_options(options)}")
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("security delete-internet-password #{flags_for_options(options)}").success?
124
+ Command.run('security', 'delete-internet-password', *flags_for_options(options)).success?
129
125
  end
130
126
 
131
127
  private
@@ -2,5 +2,5 @@
2
2
 
3
3
  # :nodoc:
4
4
  module Security
5
- VERSION = '0.3.0'
5
+ VERSION = '0.4.0'
6
6
  end
data/lib/security.rb CHANGED
@@ -5,4 +5,5 @@ require 'security/error'
5
5
  require 'security/command'
6
6
  require 'security/keychain'
7
7
  require 'security/certificate'
8
+ require 'security/identity'
8
9
  require 'security/password'
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.1.0'
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.3.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.1.0
46
+ version: 3.2.0
46
47
  required_rubygems_version: !ruby/object:Gem::Requirement
47
48
  requirements:
48
49
  - - ">="