ruby-keychain 0.1.2 → 0.2.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/lib/keychain.rb +3 -0
- data/lib/keychain/certificate.rb +57 -0
- data/lib/keychain/identity.rb +53 -0
- data/lib/keychain/item.rb +35 -51
- data/lib/keychain/key.rb +105 -0
- data/lib/keychain/scope.rb +20 -3
- data/lib/keychain/sec.rb +63 -34
- data/lib/keychain/version.rb +1 -1
- data/spec/certificate_spec.rb +30 -0
- data/spec/identity_spec.rb +42 -0
- data/spec/key_spec.rb +20 -0
- data/spec/keychain_item_spec.rb +8 -8
- data/spec/keychain_spec.rb +32 -27
- data/spec/spec.keychain +0 -0
- data/spec/spec_helper.rb +6 -1
- metadata +28 -22
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bb0999fd346873c8e15ccf0c8bc3c5628038b624
|
|
4
|
+
data.tar.gz: fc4c28aadbd999c1f7f209846cc3d108c96ac7a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca9482d75b40a474dd0c006132b317b40439a70bbb85a74970ad35dda8fb239bf18c57f548942d87fb57513955e418ea976ba9e594daefbdb6c2b4e27e038731
|
|
7
|
+
data.tar.gz: befc1c3a074dc4be549b2705135fbfcda18c958cbc3ef685eac1a88ead2f5140814c2019e53a1b9e48c10fda53c928fde08bb27c5bbbc1e4ae5b9f3efc09c34b
|
data/lib/keychain.rb
CHANGED
|
@@ -4,6 +4,9 @@ require 'keychain/sec'
|
|
|
4
4
|
require 'keychain/keychain'
|
|
5
5
|
require 'keychain/error'
|
|
6
6
|
require 'keychain/item'
|
|
7
|
+
require 'keychain/key'
|
|
8
|
+
require 'keychain/certificate'
|
|
9
|
+
require 'keychain/identity'
|
|
7
10
|
require 'keychain/scope'
|
|
8
11
|
require 'keychain/protocols'
|
|
9
12
|
# top level constant for this library
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'openssl'
|
|
2
|
+
|
|
3
|
+
module Sec
|
|
4
|
+
SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION = 0
|
|
5
|
+
|
|
6
|
+
attach_function 'SecCertificateCopyPublicKey', [:pointer, :pointer], :osstatus
|
|
7
|
+
attach_function 'SecCertificateCopyData', [:pointer], :pointer
|
|
8
|
+
|
|
9
|
+
attach_variable 'kSecAttrCertificateType', :pointer
|
|
10
|
+
attach_variable 'kSecAttrCertificateEncoding', :pointer
|
|
11
|
+
attach_variable 'kSecAttrSubject', :pointer
|
|
12
|
+
attach_variable 'kSecAttrIssuer', :pointer
|
|
13
|
+
attach_variable 'kSecAttrSerialNumber', :pointer
|
|
14
|
+
attach_variable 'kSecAttrSubjectKeyID', :pointer
|
|
15
|
+
attach_variable 'kSecAttrPublicKeyHash', :pointer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Keychain::Certificate < Sec::Base
|
|
19
|
+
register_type 'SecCertificate'
|
|
20
|
+
|
|
21
|
+
ATTR_MAP = {CF::Base.typecast(Sec::kSecAttrAccessible) => :accessible,
|
|
22
|
+
CF::Base.typecast(Sec::kSecAttrAccessGroup) => :access_group,
|
|
23
|
+
CF::Base.typecast(Sec::kSecAttrCertificateType) => :certificate_type,
|
|
24
|
+
CF::Base.typecast(Sec::kSecAttrCertificateEncoding) => :certificate_encoding,
|
|
25
|
+
CF::Base.typecast(Sec::kSecAttrLabel) => :label,
|
|
26
|
+
CF::Base.typecast(Sec::kSecAttrSubject) => :subject,
|
|
27
|
+
CF::Base.typecast(Sec::kSecAttrIssuer) => :issuer,
|
|
28
|
+
CF::Base.typecast(Sec::kSecAttrSerialNumber) => :serial_number,
|
|
29
|
+
CF::Base.typecast(Sec::kSecAttrSubjectKeyID) => :subject_key_id,
|
|
30
|
+
CF::Base.typecast(Sec::kSecAttrPublicKeyHash) => :public_key_hash}
|
|
31
|
+
|
|
32
|
+
ATTR_MAP[CF::Base.typecast(Sec::kSecAttrAccessControl)] = :access_control if defined?(Sec::kSecAttrAccessControl)
|
|
33
|
+
|
|
34
|
+
INVERSE_ATTR_MAP = ATTR_MAP.invert
|
|
35
|
+
define_attributes(ATTR_MAP)
|
|
36
|
+
|
|
37
|
+
def klass
|
|
38
|
+
Sec::Classes::CERTIFICATE.to_ruby
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def public_key
|
|
42
|
+
key_ref = FFI::MemoryPointer.new(:pointer)
|
|
43
|
+
status = Sec.SecCertificateCopyPublicKey(self, key_ref)
|
|
44
|
+
Sec.check_osstatus(status)
|
|
45
|
+
|
|
46
|
+
Keychain::Key.new(key_ref.read_pointer)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def x509
|
|
50
|
+
data_ptr = Sec.SecCertificateCopyData(self)
|
|
51
|
+
data = CF::Data.new(data_ptr)
|
|
52
|
+
|
|
53
|
+
result = OpenSSL::X509::Certificate.new(data.to_s)
|
|
54
|
+
data.release
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require 'openssl'
|
|
2
|
+
|
|
3
|
+
module Sec
|
|
4
|
+
attach_function 'SecIdentityCopyPrivateKey', [:pointer, :pointer], :osstatus
|
|
5
|
+
attach_function 'SecIdentityCopyCertificate', [:pointer, :pointer], :osstatus
|
|
6
|
+
|
|
7
|
+
attach_variable 'kSecAttrKeyClass', :pointer
|
|
8
|
+
attach_variable 'kSecAttrLabel', :pointer
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class Keychain::Identity < Sec::Base
|
|
12
|
+
register_type 'SecIdentity'
|
|
13
|
+
|
|
14
|
+
ATTR_MAP = Keychain::Certificate::ATTR_MAP.merge(Keychain::Key::ATTR_MAP)
|
|
15
|
+
|
|
16
|
+
INVERSE_ATTR_MAP = ATTR_MAP.invert
|
|
17
|
+
define_attributes(ATTR_MAP)
|
|
18
|
+
|
|
19
|
+
def klass
|
|
20
|
+
Sec::Classes::IDENTITY.to_ruby
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def certificate
|
|
24
|
+
certificate_ref = FFI::MemoryPointer.new(:pointer)
|
|
25
|
+
status = Sec.SecIdentityCopyCertificate(self, certificate_ref)
|
|
26
|
+
Sec.check_osstatus(status)
|
|
27
|
+
|
|
28
|
+
Keychain::Certificate.new(certificate_ref.read_pointer)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def private_key
|
|
32
|
+
key_ref = FFI::MemoryPointer.new(:pointer)
|
|
33
|
+
status = Sec.SecIdentityCopyPrivateKey(self, key_ref)
|
|
34
|
+
Sec.check_osstatus(status)
|
|
35
|
+
|
|
36
|
+
Keychain::Key.new(key_ref.read_pointer)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def pkcs12(passphrase='')
|
|
40
|
+
flags = Sec::SecItemImportExportKeyParameters.new
|
|
41
|
+
flags[:version] = Sec::SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION
|
|
42
|
+
flags[:passphrase] = CF::String.from_string(passphrase).to_ptr
|
|
43
|
+
|
|
44
|
+
data_ptr = FFI::MemoryPointer.new(:pointer)
|
|
45
|
+
status = Sec.SecItemExport(self, :kSecFormatPKCS12, 0, flags, data_ptr)
|
|
46
|
+
Sec.check_osstatus(status)
|
|
47
|
+
|
|
48
|
+
data = CF::Data.new(data_ptr.read_pointer)
|
|
49
|
+
result = OpenSSL::PKCS12.new(data.to_s)
|
|
50
|
+
data.release
|
|
51
|
+
result
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/keychain/item.rb
CHANGED
|
@@ -4,33 +4,44 @@ module Sec
|
|
|
4
4
|
attach_function 'SecItemAdd', [:pointer, :pointer], :osstatus
|
|
5
5
|
attach_function 'SecItemUpdate', [:pointer, :pointer], :osstatus
|
|
6
6
|
attach_function 'SecKeychainItemCopyKeychain', [:pointer, :pointer], :osstatus
|
|
7
|
-
|
|
8
7
|
end
|
|
9
8
|
|
|
10
9
|
# An individual item from the keychain. Individual accessors are generated for the items attributes
|
|
11
10
|
#
|
|
12
11
|
#
|
|
13
12
|
class Keychain::Item < Sec::Base
|
|
14
|
-
attr_accessor :attributes
|
|
15
13
|
register_type 'SecKeychainItem'
|
|
16
14
|
|
|
15
|
+
ATTR_MAP = {CF::Base.typecast(Sec::kSecAttrAccess) => :access,
|
|
16
|
+
CF::Base.typecast(Sec::kSecAttrAccount) => :account,
|
|
17
|
+
CF::Base.typecast(Sec::kSecAttrAuthenticationType) => :authentication_type,
|
|
18
|
+
CF::Base.typecast(Sec::kSecAttrComment) => :comment,
|
|
19
|
+
CF::Base.typecast(Sec::kSecAttrCreationDate) => :created_at,
|
|
20
|
+
CF::Base.typecast(Sec::kSecAttrCreator) => :creator,
|
|
21
|
+
CF::Base.typecast(Sec::kSecAttrDescription) => :description,
|
|
22
|
+
CF::Base.typecast(Sec::kSecAttrGeneric) => :generic,
|
|
23
|
+
CF::Base.typecast(Sec::kSecAttrIsInvisible) => :invisible,
|
|
24
|
+
CF::Base.typecast(Sec::kSecAttrIsNegative) => :negative,
|
|
25
|
+
CF::Base.typecast(Sec::kSecAttrLabel) => :label,
|
|
26
|
+
CF::Base.typecast(Sec::kSecAttrModificationDate) => :updated_at,
|
|
27
|
+
CF::Base.typecast(Sec::kSecAttrPath) => :path,
|
|
28
|
+
CF::Base.typecast(Sec::kSecAttrPort) => :port,
|
|
29
|
+
CF::Base.typecast(Sec::kSecAttrProtocol) => :protocol,
|
|
30
|
+
CF::Base.typecast(Sec::kSecAttrSecurityDomain) => :security_domain,
|
|
31
|
+
CF::Base.typecast(Sec::kSecAttrServer) => :server,
|
|
32
|
+
CF::Base.typecast(Sec::kSecAttrService) => :service,
|
|
33
|
+
CF::Base.typecast(Sec::kSecAttrType) => :type,
|
|
34
|
+
CF::Base.typecast(Sec::kSecClass) => :klass}
|
|
35
|
+
|
|
36
|
+
INVERSE_ATTR_MAP = ATTR_MAP.invert
|
|
37
|
+
define_attributes(ATTR_MAP)
|
|
38
|
+
|
|
17
39
|
# returns a programmer friendly description of the item
|
|
18
40
|
# @return [String]
|
|
19
41
|
def inspect
|
|
20
42
|
"<SecKeychainItem 0x#{@ptr.address.to_s(16)} #{service ? "service: #{service}" : "server: #{server}"} account: #{account}>"
|
|
21
43
|
end
|
|
22
44
|
|
|
23
|
-
Sec::ATTR_MAP.values.each do |ruby_name|
|
|
24
|
-
unless method_defined?(ruby_name)
|
|
25
|
-
define_method ruby_name do
|
|
26
|
-
@attributes[ruby_name]
|
|
27
|
-
end
|
|
28
|
-
define_method ruby_name.to_s+'=' do |value|
|
|
29
|
-
@attributes[ruby_name] = value
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
45
|
# Creates a new keychain item either from an FFI::Pointer or a hash of attributes
|
|
35
46
|
#
|
|
36
47
|
# @param [FFI::Pointer, Hash] attrs_or_pointer Either an FFI::Pointer to an existing
|
|
@@ -47,12 +58,6 @@ class Keychain::Item < Sec::Base
|
|
|
47
58
|
end
|
|
48
59
|
end
|
|
49
60
|
|
|
50
|
-
# @private
|
|
51
|
-
def initialize(*args)
|
|
52
|
-
super
|
|
53
|
-
@attributes = {}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
61
|
# Removes the item from the associated keychain
|
|
57
62
|
#
|
|
58
63
|
def delete
|
|
@@ -69,25 +74,15 @@ class Keychain::Item < Sec::Base
|
|
|
69
74
|
@unsaved_password = value
|
|
70
75
|
end
|
|
71
76
|
|
|
72
|
-
# Returns the keychain the item is in
|
|
73
|
-
#
|
|
74
|
-
# @return [Keychain::Keychain]
|
|
75
|
-
def keychain
|
|
76
|
-
out = FFI::MemoryPointer.new :pointer
|
|
77
|
-
status = Sec.SecKeychainItemCopyKeychain(self,out)
|
|
78
|
-
Sec.check_osstatus(status)
|
|
79
|
-
CF::Base.new(out.read_pointer).release_on_gc
|
|
80
|
-
end
|
|
81
|
-
|
|
82
77
|
# Fetches the password data associated with the item. This may cause the user to be asked for access
|
|
83
78
|
# @return [String] The password data, an ASCII_8BIT encoded string
|
|
84
79
|
def password
|
|
85
80
|
return @unsaved_password if @unsaved_password
|
|
86
81
|
out_buffer = FFI::MemoryPointer.new(:pointer)
|
|
87
82
|
status = Sec.SecItemCopyMatching({Sec::Query::ITEM_LIST => CF::Array.immutable([self]),
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
Sec::Query::SEARCH_LIST => [self.keychain],
|
|
84
|
+
Sec::Query::CLASS => self.klass,
|
|
85
|
+
Sec::Query::RETURN_DATA => true}.to_cf, out_buffer)
|
|
91
86
|
Sec.check_osstatus(status)
|
|
92
87
|
CF::Base.typecast(out_buffer.read_pointer).to_s
|
|
93
88
|
end
|
|
@@ -102,7 +97,9 @@ class Keychain::Item < Sec::Base
|
|
|
102
97
|
cf_dict = update
|
|
103
98
|
else
|
|
104
99
|
cf_dict = create(options)
|
|
105
|
-
|
|
100
|
+
self.ptr = cf_dict[Sec::Value::REF].to_ptr
|
|
101
|
+
self.retain.release_on_gc
|
|
102
|
+
end
|
|
106
103
|
@unsaved_password = nil
|
|
107
104
|
update_self_from_dictionary(cf_dict)
|
|
108
105
|
cf_dict.release
|
|
@@ -132,7 +129,9 @@ class Keychain::Item < Sec::Base
|
|
|
132
129
|
end
|
|
133
130
|
|
|
134
131
|
def update
|
|
135
|
-
status = Sec.SecItemUpdate({Sec::Query::SEARCH_LIST => [self.keychain],
|
|
132
|
+
status = Sec.SecItemUpdate({Sec::Query::SEARCH_LIST => [self.keychain],
|
|
133
|
+
Sec::Query::ITEM_LIST => [self],
|
|
134
|
+
Sec::Query::CLASS => klass}.to_cf, build_new_attributes);
|
|
136
135
|
Sec.check_osstatus(status)
|
|
137
136
|
|
|
138
137
|
result = FFI::MemoryPointer.new :pointer
|
|
@@ -142,21 +141,6 @@ class Keychain::Item < Sec::Base
|
|
|
142
141
|
cf_dict = CF::Base.typecast(result.read_pointer)
|
|
143
142
|
end
|
|
144
143
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def update_self_from_dictionary(cf_dict)
|
|
148
|
-
if !persisted?
|
|
149
|
-
self.ptr = cf_dict[Sec::Value::REF].to_ptr
|
|
150
|
-
self.retain.release_on_gc
|
|
151
|
-
end
|
|
152
|
-
@attributes = cf_dict.inject({}) do |memo, (k,v)|
|
|
153
|
-
if ruby_name = Sec::ATTR_MAP[k]
|
|
154
|
-
memo[ruby_name] = v.to_ruby
|
|
155
|
-
end
|
|
156
|
-
memo
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
144
|
def build_create_query options
|
|
161
145
|
query = CF::Dictionary.mutable
|
|
162
146
|
query[Sec::Value::DATA] = CF::Data.from_string(@unsaved_password) if @unsaved_password
|
|
@@ -172,7 +156,7 @@ class Keychain::Item < Sec::Base
|
|
|
172
156
|
query[Sec::Query::ITEM_LIST] = CF::Array.immutable([self])
|
|
173
157
|
query[Sec::Query::RETURN_ATTRIBUTES] = CF::Boolean::TRUE
|
|
174
158
|
query[Sec::Query::RETURN_REF] = CF::Boolean::TRUE
|
|
175
|
-
query[Sec::
|
|
159
|
+
query[Sec::Query::CLASS] = klass.to_cf
|
|
176
160
|
query
|
|
177
161
|
end
|
|
178
162
|
|
|
@@ -181,7 +165,7 @@ class Keychain::Item < Sec::Base
|
|
|
181
165
|
@attributes.each do |k,v|
|
|
182
166
|
next if k == :created_at || k == :updated_at
|
|
183
167
|
next if k == :klass && persisted?
|
|
184
|
-
k =
|
|
168
|
+
k = self.class::INVERSE_ATTR_MAP[k]
|
|
185
169
|
new_attributes[k] = v.to_cf
|
|
186
170
|
end
|
|
187
171
|
new_attributes[Sec::Value::DATA] = CF::Data.from_string(@unsaved_password) if @unsaved_password
|
data/lib/keychain/key.rb
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module Sec
|
|
2
|
+
attach_variable 'kSecAttrAccessible', :pointer
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
attach_variable 'kSecAttrAccessControl', :pointer
|
|
6
|
+
rescue FFI::NotFoundError #Only available in 10.10
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attach_variable 'kSecAttrAccessGroup', :pointer
|
|
10
|
+
attach_variable 'kSecAttrKeyClass', :pointer
|
|
11
|
+
attach_variable 'kSecAttrApplicationLabel', :pointer
|
|
12
|
+
attach_variable 'kSecAttrIsPermanent', :pointer
|
|
13
|
+
attach_variable 'kSecAttrApplicationTag', :pointer
|
|
14
|
+
attach_variable 'kSecAttrKeyType', :pointer
|
|
15
|
+
attach_variable 'kSecAttrKeySizeInBits', :pointer
|
|
16
|
+
attach_variable 'kSecAttrEffectiveKeySize', :pointer
|
|
17
|
+
attach_variable 'kSecAttrCanEncrypt', :pointer
|
|
18
|
+
attach_variable 'kSecAttrCanDecrypt', :pointer
|
|
19
|
+
attach_variable 'kSecAttrCanDerive', :pointer
|
|
20
|
+
attach_variable 'kSecAttrCanSign', :pointer
|
|
21
|
+
attach_variable 'kSecAttrCanVerify', :pointer
|
|
22
|
+
attach_variable 'kSecAttrCanWrap', :pointer
|
|
23
|
+
attach_variable 'kSecAttrCanUnwrap', :pointer
|
|
24
|
+
|
|
25
|
+
enum :SecItemImportExportFlags, [:kSecItemPemArmour, 1]
|
|
26
|
+
|
|
27
|
+
enum :SecExternalFormat, [:kSecFormatUnknown, 0,
|
|
28
|
+
:kSecFormatOpenSSL,
|
|
29
|
+
:kSecFormatSSH,
|
|
30
|
+
:kSecFormatBSAFE,
|
|
31
|
+
:kSecFormatRawKey,
|
|
32
|
+
:kSecFormatWrappedPKCS8,
|
|
33
|
+
:kSecFormatWrappedOpenSSL,
|
|
34
|
+
:kSecFormatWrappedSSH,
|
|
35
|
+
:kSecFormatWrappedLSH,
|
|
36
|
+
:kSecFormatX509Cert,
|
|
37
|
+
:kSecFormatPEMSequence,
|
|
38
|
+
:kSecFormatPKCS7,
|
|
39
|
+
:kSecFormatPKCS12,
|
|
40
|
+
:kSecFormatNetscapeCertSequence,
|
|
41
|
+
:kSecFormatSSHv2]
|
|
42
|
+
|
|
43
|
+
enum :SecKeyImportExportParameters, [:kSecKeyImportOnlyOne, 1,
|
|
44
|
+
:kSecKeySecurePassphrase, 2,
|
|
45
|
+
:kSecKeyNoAccessControl, 4]
|
|
46
|
+
|
|
47
|
+
class SecItemImportExportKeyParameters < FFI::Struct
|
|
48
|
+
layout :version, :uint32,
|
|
49
|
+
:flags, :SecKeyImportExportParameters,
|
|
50
|
+
:passphrase, :pointer,
|
|
51
|
+
:alertTitle, :pointer,
|
|
52
|
+
:alertPrompt, :pointer,
|
|
53
|
+
:accessRef, :pointer,
|
|
54
|
+
:keyUsage, :pointer,
|
|
55
|
+
:keyAttributes, :pointer
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
attach_function 'SecItemExport', [:pointer, :SecExternalFormat, :SecItemImportExportFlags, :pointer, :pointer], :osstatus
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Keychain::Key < Sec::Base
|
|
62
|
+
register_type 'SecKey'
|
|
63
|
+
|
|
64
|
+
ATTR_MAP = {CF::Base.typecast(Sec::kSecAttrAccessible) => :accessible,
|
|
65
|
+
CF::Base.typecast(Sec::kSecAttrAccessGroup) => :access_group,
|
|
66
|
+
CF::Base.typecast(Sec::kSecAttrKeyClass) => :key_class,
|
|
67
|
+
CF::Base.typecast(Sec::kSecAttrLabel) => :label,
|
|
68
|
+
CF::Base.typecast(Sec::kSecAttrApplicationLabel) => :application_label,
|
|
69
|
+
CF::Base.typecast(Sec::kSecAttrIsPermanent) => :is_permanent,
|
|
70
|
+
CF::Base.typecast(Sec::kSecAttrApplicationTag) => :application_tag,
|
|
71
|
+
CF::Base.typecast(Sec::kSecAttrKeyType) => :key_type,
|
|
72
|
+
CF::Base.typecast(Sec::kSecAttrKeySizeInBits) => :size_in_bites,
|
|
73
|
+
CF::Base.typecast(Sec::kSecAttrEffectiveKeySize) => :effective_key_size,
|
|
74
|
+
CF::Base.typecast(Sec::kSecAttrCanEncrypt) => :can_encrypt,
|
|
75
|
+
CF::Base.typecast(Sec::kSecAttrCanDecrypt) => :can_decrypt,
|
|
76
|
+
CF::Base.typecast(Sec::kSecAttrCanDerive) => :can_derive,
|
|
77
|
+
CF::Base.typecast(Sec::kSecAttrCanSign) => :can_sign,
|
|
78
|
+
CF::Base.typecast(Sec::kSecAttrCanVerify) => :can_verify,
|
|
79
|
+
CF::Base.typecast(Sec::kSecAttrCanWrap) => :can_wrap,
|
|
80
|
+
CF::Base.typecast(Sec::kSecAttrCanUnwrap) => :can_unwrap}
|
|
81
|
+
|
|
82
|
+
ATTR_MAP[CF::Base.typecast(Sec::kSecAttrAccessControl)] = :access_control if defined?(Sec::kSecAttrAccessControl)
|
|
83
|
+
|
|
84
|
+
INVERSE_ATTR_MAP = ATTR_MAP.invert
|
|
85
|
+
define_attributes(ATTR_MAP)
|
|
86
|
+
|
|
87
|
+
def klass
|
|
88
|
+
Sec::Classes::KEY.to_ruby
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def export(passphrase = nil, format = :kSecFormatUnknown)
|
|
92
|
+
flags = Sec::SecItemImportExportKeyParameters.new
|
|
93
|
+
flags[:version] = Sec::SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION
|
|
94
|
+
flags[:passphrase] = CF::String.from_string(passphrase).to_ptr if passphrase
|
|
95
|
+
|
|
96
|
+
data_ptr = FFI::MemoryPointer.new(:pointer)
|
|
97
|
+
status = Sec.SecItemExport(self, format, :kSecItemPemArmour, flags, data_ptr)
|
|
98
|
+
Sec.check_osstatus(status)
|
|
99
|
+
|
|
100
|
+
data = CF::Data.new(data_ptr.read_pointer)
|
|
101
|
+
result = data.to_s
|
|
102
|
+
data.release
|
|
103
|
+
result
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/keychain/scope.rb
CHANGED
|
@@ -111,14 +111,31 @@ class Keychain::Scope
|
|
|
111
111
|
unless result.is_a?(CF::Array)
|
|
112
112
|
result = CF::Array.immutable([result])
|
|
113
113
|
end
|
|
114
|
-
result.collect
|
|
114
|
+
result.collect do |dictionary_of_attributes|
|
|
115
|
+
item = dictionary_of_attributes[Sec::Value::REF]
|
|
116
|
+
item.update_self_from_dictionary(dictionary_of_attributes)
|
|
117
|
+
item
|
|
118
|
+
end
|
|
115
119
|
end
|
|
116
120
|
|
|
117
|
-
|
|
118
121
|
def to_query
|
|
119
122
|
query = CF::Dictionary.mutable
|
|
123
|
+
# This is terrible but we need to know the result class to get the list of attributes
|
|
124
|
+
inverse_attributes = case @kind
|
|
125
|
+
when Sec::Classes::CERTIFICATE
|
|
126
|
+
Keychain::Certificate::INVERSE_ATTR_MAP
|
|
127
|
+
when Sec::Classes::GENERIC
|
|
128
|
+
Keychain::Item::INVERSE_ATTR_MAP
|
|
129
|
+
when Sec::Classes::IDENTITY
|
|
130
|
+
Keychain::Identity::INVERSE_ATTR_MAP
|
|
131
|
+
when Sec::Classes::INTERNET
|
|
132
|
+
Keychain::Item::INVERSE_ATTR_MAP
|
|
133
|
+
when Sec::Classes::KEY
|
|
134
|
+
Keychain::Key::INVERSE_ATTR_MAP
|
|
135
|
+
end
|
|
136
|
+
|
|
120
137
|
@conditions.each do |k,v|
|
|
121
|
-
k =
|
|
138
|
+
k = inverse_attributes[k]
|
|
122
139
|
query[k] = v.to_cf
|
|
123
140
|
end
|
|
124
141
|
|
data/lib/keychain/sec.rb
CHANGED
|
@@ -15,10 +15,12 @@ module Sec
|
|
|
15
15
|
:errSecInteractionNotAllowed, -25308
|
|
16
16
|
]
|
|
17
17
|
|
|
18
|
+
attach_variable 'kSecClass', :pointer
|
|
18
19
|
attach_variable 'kSecClassInternetPassword', :pointer
|
|
19
20
|
attach_variable 'kSecClassGenericPassword', :pointer
|
|
20
|
-
|
|
21
|
-
attach_variable '
|
|
21
|
+
attach_variable 'kSecClassCertificate', :pointer
|
|
22
|
+
attach_variable 'kSecClassIdentity', :pointer
|
|
23
|
+
attach_variable 'kSecClassKey', :pointer
|
|
22
24
|
|
|
23
25
|
attach_variable 'kSecAttrAccess', :pointer
|
|
24
26
|
attach_variable 'kSecAttrAccount', :pointer
|
|
@@ -54,36 +56,6 @@ module Sec
|
|
|
54
56
|
attach_variable 'kSecValueData', :pointer
|
|
55
57
|
attach_variable 'kSecUseKeychain', :pointer
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# map of kSecAttr* constants to the corresponding ruby name for the attribute
|
|
60
|
-
# Used in {Keychain::Item#attributes}}
|
|
61
|
-
ATTR_MAP = {
|
|
62
|
-
CF::Base.typecast(kSecAttrAccess) => :access,
|
|
63
|
-
CF::Base.typecast(kSecAttrAccount) => :account,
|
|
64
|
-
CF::Base.typecast(kSecAttrAuthenticationType) => :authentication_type,
|
|
65
|
-
CF::Base.typecast(kSecAttrComment) => :comment,
|
|
66
|
-
CF::Base.typecast(kSecAttrCreationDate) => :created_at,
|
|
67
|
-
CF::Base.typecast(kSecAttrCreator) => :creator,
|
|
68
|
-
CF::Base.typecast(kSecAttrDescription) => :description,
|
|
69
|
-
CF::Base.typecast(kSecAttrGeneric) => :generic,
|
|
70
|
-
CF::Base.typecast(kSecAttrIsInvisible) => :invisible,
|
|
71
|
-
CF::Base.typecast(kSecAttrIsNegative) => :negative,
|
|
72
|
-
CF::Base.typecast(kSecAttrLabel) => :label,
|
|
73
|
-
CF::Base.typecast(kSecAttrModificationDate) => :updated_at,
|
|
74
|
-
CF::Base.typecast(kSecAttrPath) => :path,
|
|
75
|
-
CF::Base.typecast(kSecAttrPort) => :port,
|
|
76
|
-
CF::Base.typecast(kSecAttrProtocol) => :protocol,
|
|
77
|
-
CF::Base.typecast(kSecAttrSecurityDomain) => :security_domain,
|
|
78
|
-
CF::Base.typecast(kSecAttrServer) => :server,
|
|
79
|
-
CF::Base.typecast(kSecAttrService) => :service,
|
|
80
|
-
CF::Base.typecast(kSecAttrType) => :type,
|
|
81
|
-
CF::Base.typecast(kSecClass) => :klass
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# Inverse of {ATTR_MAP}
|
|
85
|
-
INVERSE_ATTR_MAP = ATTR_MAP.invert
|
|
86
|
-
|
|
87
59
|
# Query options for use with SecCopyMatching, SecItemUpdate
|
|
88
60
|
#
|
|
89
61
|
module Query
|
|
@@ -105,10 +77,16 @@ module Sec
|
|
|
105
77
|
|
|
106
78
|
# defines constants for use as the class of an item
|
|
107
79
|
module Classes
|
|
108
|
-
# constant
|
|
80
|
+
# constant identifying certificates (kSecClassCertificate)
|
|
81
|
+
CERTIFICATE = CF::Base.typecast(Sec.kSecClassCertificate)
|
|
82
|
+
# constant identifying generic passwords (kSecClassGenericPassword)
|
|
109
83
|
GENERIC = CF::Base.typecast(Sec.kSecClassGenericPassword)
|
|
84
|
+
# constant identifying generic passwords (kSecClassIdentity)
|
|
85
|
+
IDENTITY = CF::Base.typecast(Sec.kSecClassIdentity)
|
|
110
86
|
# constant identifying internet passwords (kSecClassInternetPassword)
|
|
111
87
|
INTERNET = CF::Base.typecast(Sec.kSecClassInternetPassword)
|
|
88
|
+
# constant identifying public/private key items (kSecClassKey)
|
|
89
|
+
KEY = CF::Base.typecast(Sec.kSecClassKey)
|
|
112
90
|
end
|
|
113
91
|
|
|
114
92
|
# Search match types for use with SecCopyMatching
|
|
@@ -132,11 +110,62 @@ module Sec
|
|
|
132
110
|
#
|
|
133
111
|
# @abstract
|
|
134
112
|
class Base < CF::Base
|
|
135
|
-
|
|
113
|
+
attr_reader :attributes
|
|
114
|
+
|
|
136
115
|
def self.register_type(type_name)
|
|
137
116
|
Sec.attach_function "#{type_name}GetTypeID", [], CF.find_type(:cftypeid)
|
|
138
117
|
@@type_map[Sec.send("#{type_name}GetTypeID")] = self
|
|
139
118
|
end
|
|
119
|
+
|
|
120
|
+
def self.define_attributes(attr_map)
|
|
121
|
+
attr_map.values.each do |ruby_name|
|
|
122
|
+
unless method_defined?(ruby_name)
|
|
123
|
+
define_method ruby_name do
|
|
124
|
+
self.attributes[ruby_name]
|
|
125
|
+
end
|
|
126
|
+
define_method ruby_name.to_s+'=' do |value|
|
|
127
|
+
self.attributes[ruby_name] = value
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def initialize(ptr)
|
|
134
|
+
super
|
|
135
|
+
@attributes = {}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def update_self_from_dictionary(cf_dict)
|
|
139
|
+
@attributes = cf_dict.inject({}) do |memo, (k,v)|
|
|
140
|
+
if ruby_name = self.class::ATTR_MAP[k]
|
|
141
|
+
memo[ruby_name] = v.to_ruby
|
|
142
|
+
end
|
|
143
|
+
memo
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Returns the keychain the item is in
|
|
148
|
+
#
|
|
149
|
+
# @return [Keychain::Keychain]
|
|
150
|
+
def keychain
|
|
151
|
+
out = FFI::MemoryPointer.new :pointer
|
|
152
|
+
status = Sec.SecKeychainItemCopyKeychain(self,out)
|
|
153
|
+
Sec.check_osstatus(status)
|
|
154
|
+
CF::Base.new(out.read_pointer).release_on_gc
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def load_attributes
|
|
158
|
+
result = FFI::MemoryPointer.new :pointer
|
|
159
|
+
status = Sec.SecItemCopyMatching({Sec::Query::SEARCH_LIST => [self.keychain],
|
|
160
|
+
Sec::Query::ITEM_LIST => [self],
|
|
161
|
+
Sec::Query::CLASS => self.klass,
|
|
162
|
+
Sec::Query::RETURN_ATTRIBUTES => true,
|
|
163
|
+
Sec::Query::RETURN_REF => false}.to_cf, result)
|
|
164
|
+
Sec.check_osstatus(status)
|
|
165
|
+
|
|
166
|
+
cf_dict = CF::Base.typecast(result.read_pointer)
|
|
167
|
+
update_self_from_dictionary(cf_dict)
|
|
168
|
+
end
|
|
140
169
|
end
|
|
141
170
|
|
|
142
171
|
# If the result is non-zero raises an exception.
|
data/lib/keychain/version.rb
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Keychain::Certificate do
|
|
4
|
+
# Test using the com.apple.systemdefault self-signed certificate that
|
|
5
|
+
# all OSX machines should have installed.
|
|
6
|
+
let(:query){{:label => 'com.apple.systemdefault'}}
|
|
7
|
+
|
|
8
|
+
describe 'query' do
|
|
9
|
+
it 'should return a certificate' do
|
|
10
|
+
scope = Keychain::Scope.new(Sec::Classes::CERTIFICATE)
|
|
11
|
+
certs = scope.where(query).all
|
|
12
|
+
expect(certs.length).to be > 0
|
|
13
|
+
expect(certs.first).to be_kind_of(Keychain::Certificate)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe 'certificate' do
|
|
18
|
+
it 'should have a public key' do
|
|
19
|
+
scope = Keychain::Scope.new(Sec::Classes::CERTIFICATE)
|
|
20
|
+
cert = scope.where(query).first
|
|
21
|
+
expect(cert.public_key).to be_kind_of(Keychain::Key)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'should be exportable to x509' do
|
|
25
|
+
scope = Keychain::Scope.new(Sec::Classes::CERTIFICATE)
|
|
26
|
+
cert = scope.where(query).first
|
|
27
|
+
expect(cert.x509).to be_kind_of(OpenSSL::X509::Certificate)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Keychain::Identity do
|
|
4
|
+
before(:context) do
|
|
5
|
+
Keychain.user_interaction_allowed = false
|
|
6
|
+
@keychain = Keychain.open(File.join(File.dirname(__FILE__), 'spec.keychain'))
|
|
7
|
+
@keychain.unlock! 'DummyPassword'
|
|
8
|
+
|
|
9
|
+
end
|
|
10
|
+
after(:context) do
|
|
11
|
+
Keychain.user_interaction_allowed = true
|
|
12
|
+
end
|
|
13
|
+
describe 'query' do
|
|
14
|
+
it 'should return a identity' do
|
|
15
|
+
scope = Keychain::Scope.new(Sec::Classes::IDENTITY, @keychain)
|
|
16
|
+
identities = scope.all
|
|
17
|
+
expect(identities.length).to be > 0
|
|
18
|
+
expect(identities.first).to be_kind_of(Keychain::Identity)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe 'identify' do
|
|
23
|
+
it 'should have a certificate' do
|
|
24
|
+
scope = Keychain::Scope.new(Sec::Classes::IDENTITY, @keychain)
|
|
25
|
+
identity = scope.all.first
|
|
26
|
+
expect(identity.certificate).to be_kind_of(Keychain::Certificate)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'should have a private key' do
|
|
30
|
+
scope = Keychain::Scope.new(Sec::Classes::IDENTITY, @keychain)
|
|
31
|
+
identity = scope.all.first
|
|
32
|
+
expect(identity.private_key).to be_kind_of(Keychain::Key)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#this fails on travis - not sure 100% why yet
|
|
36
|
+
skip 'should be exportable to pkcs12' do
|
|
37
|
+
scope = Keychain::Scope.new(Sec::Classes::IDENTITY, @keychain)
|
|
38
|
+
identity = scope.all.first
|
|
39
|
+
expect(identity.pkcs12).to be_kind_of(OpenSSL::PKCS12)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/spec/key_spec.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Keychain::Key do
|
|
4
|
+
describe 'query' do
|
|
5
|
+
it 'should return a certificate' do
|
|
6
|
+
scope = Keychain::Scope.new(Sec::Classes::KEY)
|
|
7
|
+
keys = scope.all
|
|
8
|
+
expect(keys.length).to be > 0
|
|
9
|
+
expect(keys.first).to be_kind_of(Keychain::Key)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe 'identify' do
|
|
14
|
+
it 'should be exportable to a string' do
|
|
15
|
+
scope = Keychain::Scope.new(Sec::Classes::KEY)
|
|
16
|
+
key = scope.first
|
|
17
|
+
expect(key.export).to be_kind_of(String)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/spec/keychain_item_spec.rb
CHANGED
|
@@ -18,31 +18,31 @@ describe Keychain::Item do
|
|
|
18
18
|
|
|
19
19
|
describe 'keychain' do
|
|
20
20
|
it 'should return the keychain containing the item' do
|
|
21
|
-
subject.keychain.
|
|
21
|
+
expect(subject.keychain).to eq(@keychain)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
describe 'password' do
|
|
26
26
|
it 'should retrieve the password' do
|
|
27
|
-
subject.password.
|
|
27
|
+
expect(subject.password).to eq('some-password')
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
describe 'service' do
|
|
32
32
|
it 'should retrieve the service' do
|
|
33
|
-
subject.service.
|
|
33
|
+
expect(subject.service).to eq('some-service')
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
describe 'account' do
|
|
38
38
|
it 'should retrieve the account' do
|
|
39
|
-
subject.account.
|
|
39
|
+
expect(subject.account).to eq('some-account')
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
describe 'created_at' do
|
|
44
44
|
it 'should retrieve the item creation date' do
|
|
45
|
-
subject.created_at.
|
|
45
|
+
expect(subject.created_at).to be_within(2).of(Time.now)
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -53,15 +53,15 @@ describe Keychain::Item do
|
|
|
53
53
|
subject.save!
|
|
54
54
|
|
|
55
55
|
fresh = find_item
|
|
56
|
-
fresh.password.
|
|
57
|
-
fresh.account.
|
|
56
|
+
expect(fresh.password).to eq('new-password')
|
|
57
|
+
expect(fresh.account).to eq('new-account')
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
describe 'delete' do
|
|
62
62
|
it 'should remove the item from the keychain' do
|
|
63
63
|
subject.delete
|
|
64
|
-
find_item.
|
|
64
|
+
expect(find_item).to be_nil
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
end
|
data/spec/keychain_spec.rb
CHANGED
|
@@ -4,27 +4,27 @@ describe Keychain do
|
|
|
4
4
|
|
|
5
5
|
describe 'user interaction' do
|
|
6
6
|
it 'should be true by default' do
|
|
7
|
-
Keychain.user_interaction_allowed
|
|
7
|
+
expect(Keychain.user_interaction_allowed?).to be_truthy
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
it 'should be changeable' do
|
|
11
11
|
Keychain.user_interaction_allowed = false
|
|
12
|
-
Keychain.user_interaction_allowed
|
|
12
|
+
expect(Keychain.user_interaction_allowed?).to be_falsey
|
|
13
13
|
Keychain.user_interaction_allowed = true
|
|
14
|
-
Keychain.user_interaction_allowed
|
|
14
|
+
expect(Keychain.user_interaction_allowed?).to be_truthy
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
describe 'default' do
|
|
19
19
|
it "should return the login keychain" do
|
|
20
|
-
Keychain.default.path.
|
|
20
|
+
expect(Keychain.default.path).to eq(File.expand_path(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain')))
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
describe 'open' do
|
|
25
25
|
it 'should create a keychain reference to a path' do
|
|
26
26
|
keychain = Keychain.open(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain'))
|
|
27
|
-
keychain.path.
|
|
27
|
+
expect(keychain.path).to eq(Keychain.default.path)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
it 'should raise when passed a nil path' do
|
|
@@ -37,7 +37,7 @@ describe Keychain do
|
|
|
37
37
|
begin
|
|
38
38
|
keychain = Keychain.create(File.join(Dir.tmpdir, "other_keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"),
|
|
39
39
|
'password');
|
|
40
|
-
File.exists?(keychain.path).
|
|
40
|
+
expect(File.exists?(keychain.path)).to be_truthy
|
|
41
41
|
ensure
|
|
42
42
|
keychain.delete
|
|
43
43
|
end
|
|
@@ -46,7 +46,12 @@ describe Keychain do
|
|
|
46
46
|
context 'no password supplied' do
|
|
47
47
|
#we have to stub this out as it would trigger a dialog box prompting for a password
|
|
48
48
|
it 'should create a keychain by prompting the user' do
|
|
49
|
-
|
|
49
|
+
#we can't just use a kind_of matcher becaue FFI::Pointer#== raises an exception
|
|
50
|
+
#when compared to non pointer values
|
|
51
|
+
mock_pointer = double(FFI::MemoryPointer, :read_pointer => 0)
|
|
52
|
+
allow(FFI::MemoryPointer).to receive(:new).with(:pointer).and_return(mock_pointer)
|
|
53
|
+
|
|
54
|
+
expect(Sec).to receive('SecKeychainCreate').with('akeychain', 0, nil, 1, nil,mock_pointer).and_return(0)
|
|
50
55
|
Keychain.create('akeychain')
|
|
51
56
|
end
|
|
52
57
|
end
|
|
@@ -55,14 +60,14 @@ describe Keychain do
|
|
|
55
60
|
describe 'exists?' do
|
|
56
61
|
context 'the keychain exists' do
|
|
57
62
|
it 'should return true' do
|
|
58
|
-
Keychain.default.exists
|
|
63
|
+
expect(Keychain.default.exists?).to be_truthy
|
|
59
64
|
end
|
|
60
65
|
end
|
|
61
66
|
|
|
62
67
|
context 'the keychain does not exist' do
|
|
63
68
|
it 'should return false' do
|
|
64
69
|
k = Keychain.open('/some/path/that/does/not/exist')
|
|
65
|
-
k.exists
|
|
70
|
+
expect(k.exists?).to be_falsey
|
|
66
71
|
end
|
|
67
72
|
end
|
|
68
73
|
end
|
|
@@ -74,14 +79,14 @@ describe Keychain do
|
|
|
74
79
|
|
|
75
80
|
it 'should read/write lock_on_sleep' do
|
|
76
81
|
@keychain.lock_on_sleep = true
|
|
77
|
-
@keychain.lock_on_sleep
|
|
82
|
+
expect(@keychain.lock_on_sleep?).to eq(true)
|
|
78
83
|
@keychain.lock_on_sleep = false
|
|
79
|
-
@keychain.lock_on_sleep
|
|
84
|
+
expect(@keychain.lock_on_sleep?).to eq(false)
|
|
80
85
|
end
|
|
81
86
|
|
|
82
87
|
it 'should read/write lock_interval' do
|
|
83
88
|
@keychain.lock_interval = 12345
|
|
84
|
-
@keychain.lock_interval.
|
|
89
|
+
expect(@keychain.lock_interval).to eq(12345)
|
|
85
90
|
end
|
|
86
91
|
|
|
87
92
|
after(:all) do
|
|
@@ -102,7 +107,7 @@ describe Keychain do
|
|
|
102
107
|
|
|
103
108
|
it 'should unlock on valid password' do
|
|
104
109
|
@keychain.unlock! 'pass'
|
|
105
|
-
@keychain.
|
|
110
|
+
expect(@keychain).not_to be_locked
|
|
106
111
|
end
|
|
107
112
|
end
|
|
108
113
|
end
|
|
@@ -125,15 +130,15 @@ describe Keychain do
|
|
|
125
130
|
describe('create') do
|
|
126
131
|
it 'should add a password' do
|
|
127
132
|
item = @keychain_1.send(subject).create(create_arguments)
|
|
128
|
-
item.
|
|
129
|
-
item.klass.
|
|
130
|
-
item.password.
|
|
133
|
+
expect(item).to be_a(Keychain::Item)
|
|
134
|
+
expect(item.klass).to eq(expected_kind)
|
|
135
|
+
expect(item.password).to eq('some-password')
|
|
131
136
|
end
|
|
132
137
|
|
|
133
138
|
it 'should be findable' do
|
|
134
139
|
@keychain_1.send(subject).create(create_arguments)
|
|
135
140
|
item = @keychain_1.send(subject).where(search_for_created_arguments).first
|
|
136
|
-
item.password.
|
|
141
|
+
expect(item.password).to eq('some-password')
|
|
137
142
|
end
|
|
138
143
|
|
|
139
144
|
context 'when a duplicate item exists' do
|
|
@@ -151,32 +156,32 @@ describe Keychain do
|
|
|
151
156
|
|
|
152
157
|
context 'when the keychain does not contains a matching item' do
|
|
153
158
|
it 'should return []' do
|
|
154
|
-
@keychain_1.send(subject).where(search_arguments_with_no_results).all.
|
|
159
|
+
expect(@keychain_1.send(subject).where(search_arguments_with_no_results).all).to eq([])
|
|
155
160
|
end
|
|
156
161
|
end
|
|
157
162
|
|
|
158
163
|
it 'should return an array of results' do
|
|
159
164
|
item = @keychain_1.send(subject).where(search_arguments).all.first
|
|
160
|
-
item.
|
|
161
|
-
item.password.
|
|
165
|
+
expect(item).to be_a(Keychain::Item)
|
|
166
|
+
expect(item.password).to eq('some-password-1')
|
|
162
167
|
end
|
|
163
168
|
|
|
164
169
|
context 'searching all keychains' do
|
|
165
170
|
context 'when the keychain does contains matching items' do
|
|
166
171
|
it 'should return all of them' do
|
|
167
|
-
Keychain.send(subject).where(search_arguments_with_multiple_results).all.length.
|
|
172
|
+
expect(Keychain.send(subject).where(search_arguments_with_multiple_results).all.length).to eq(3)
|
|
168
173
|
end
|
|
169
174
|
end
|
|
170
175
|
|
|
171
176
|
context 'when the limit is option is set' do
|
|
172
177
|
it 'should limit the return set' do
|
|
173
|
-
Keychain.send(subject).where(search_arguments_with_multiple_results).limit(1).all.length.
|
|
178
|
+
expect(Keychain.send(subject).where(search_arguments_with_multiple_results).limit(1).all.length).to eq(1)
|
|
174
179
|
end
|
|
175
180
|
end
|
|
176
181
|
|
|
177
182
|
context 'when a subset of keychains is specified' do
|
|
178
183
|
it 'should return items from those keychains' do
|
|
179
|
-
Keychain.send(subject).where(search_arguments_with_multiple_results).in(@keychain_1, @keychain_2).all.length.
|
|
184
|
+
expect(Keychain.send(subject).where(search_arguments_with_multiple_results).in(@keychain_1, @keychain_2).all.length).to eq(2)
|
|
180
185
|
end
|
|
181
186
|
end
|
|
182
187
|
end
|
|
@@ -184,15 +189,15 @@ describe Keychain do
|
|
|
184
189
|
describe 'first' do
|
|
185
190
|
context 'when the keychain does not contain a matching item' do
|
|
186
191
|
it 'should return nil' do
|
|
187
|
-
item = @keychain_1.send(subject).where(search_arguments_with_no_results).first.
|
|
192
|
+
item = expect(@keychain_1.send(subject).where(search_arguments_with_no_results).first).to be_nil
|
|
188
193
|
end
|
|
189
194
|
end
|
|
190
195
|
|
|
191
196
|
context 'when the keychain does contain a matching item' do
|
|
192
197
|
it 'should find it' do
|
|
193
198
|
item = @keychain_1.send(subject).where(search_arguments).first
|
|
194
|
-
item.
|
|
195
|
-
item.password.
|
|
199
|
+
expect(item).to be_a(Keychain::Item)
|
|
200
|
+
expect(item.password).to eq('some-password-1')
|
|
196
201
|
end
|
|
197
202
|
end
|
|
198
203
|
|
|
@@ -202,7 +207,7 @@ describe Keychain do
|
|
|
202
207
|
end
|
|
203
208
|
|
|
204
209
|
it 'should not find it' do
|
|
205
|
-
@keychain_2.send(subject).where(search_arguments).first.
|
|
210
|
+
expect(@keychain_2.send(subject).where(search_arguments).first).to be_nil
|
|
206
211
|
end
|
|
207
212
|
end
|
|
208
213
|
end
|
data/spec/spec.keychain
ADDED
|
Binary file
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby-keychain
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Frederick Cheung
|
|
@@ -14,84 +14,84 @@ dependencies:
|
|
|
14
14
|
name: ffi
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- -
|
|
17
|
+
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: '0'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
|
-
- -
|
|
24
|
+
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: corefoundation
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- - ~>
|
|
31
|
+
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
33
|
version: 0.1.3
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
|
-
- - ~>
|
|
38
|
+
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: 0.1.3
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: rspec
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
|
-
- - ~>
|
|
45
|
+
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version:
|
|
47
|
+
version: 3.1.0
|
|
48
48
|
type: :development
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
|
-
- - ~>
|
|
52
|
+
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version:
|
|
54
|
+
version: 3.1.0
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: rake
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
|
-
- -
|
|
59
|
+
- - ">="
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
61
|
version: '0'
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
|
-
- -
|
|
66
|
+
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '0'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: yard
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
|
-
- -
|
|
73
|
+
- - ">="
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
75
|
version: '0'
|
|
76
76
|
type: :development
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
|
-
- -
|
|
80
|
+
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
82
|
version: '0'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
84
|
name: redcarpet
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
86
86
|
requirements:
|
|
87
|
-
- -
|
|
87
|
+
- - ">="
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
89
|
version: '0'
|
|
90
90
|
type: :development
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
|
-
- -
|
|
94
|
+
- - ">="
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
96
|
version: '0'
|
|
97
97
|
description: 'Ruby wrapper for OS X''s keychain '
|
|
@@ -100,19 +100,26 @@ executables: []
|
|
|
100
100
|
extensions: []
|
|
101
101
|
extra_rdoc_files: []
|
|
102
102
|
files:
|
|
103
|
+
- LICENSE
|
|
104
|
+
- README.markdown
|
|
105
|
+
- lib/keychain.rb
|
|
106
|
+
- lib/keychain/certificate.rb
|
|
103
107
|
- lib/keychain/error.rb
|
|
108
|
+
- lib/keychain/identity.rb
|
|
104
109
|
- lib/keychain/item.rb
|
|
110
|
+
- lib/keychain/key.rb
|
|
105
111
|
- lib/keychain/keychain.rb
|
|
106
112
|
- lib/keychain/protocols.rb
|
|
107
113
|
- lib/keychain/scope.rb
|
|
108
114
|
- lib/keychain/sec.rb
|
|
109
115
|
- lib/keychain/version.rb
|
|
110
|
-
-
|
|
116
|
+
- spec/certificate_spec.rb
|
|
117
|
+
- spec/identity_spec.rb
|
|
118
|
+
- spec/key_spec.rb
|
|
111
119
|
- spec/keychain_item_spec.rb
|
|
112
120
|
- spec/keychain_spec.rb
|
|
121
|
+
- spec/spec.keychain
|
|
113
122
|
- spec/spec_helper.rb
|
|
114
|
-
- README.markdown
|
|
115
|
-
- LICENSE
|
|
116
123
|
homepage: http://github.com/fcheung/keychain
|
|
117
124
|
licenses:
|
|
118
125
|
- MIT
|
|
@@ -123,19 +130,18 @@ require_paths:
|
|
|
123
130
|
- lib
|
|
124
131
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
125
132
|
requirements:
|
|
126
|
-
- -
|
|
133
|
+
- - ">="
|
|
127
134
|
- !ruby/object:Gem::Version
|
|
128
135
|
version: 1.9.2
|
|
129
136
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
137
|
requirements:
|
|
131
|
-
- -
|
|
138
|
+
- - ">="
|
|
132
139
|
- !ruby/object:Gem::Version
|
|
133
140
|
version: '0'
|
|
134
141
|
requirements: []
|
|
135
142
|
rubyforge_project:
|
|
136
|
-
rubygems_version: 2.
|
|
143
|
+
rubygems_version: 2.4.5
|
|
137
144
|
signing_key:
|
|
138
145
|
specification_version: 4
|
|
139
146
|
summary: Ruby wrapper for OS X's keychain
|
|
140
147
|
test_files: []
|
|
141
|
-
has_rdoc:
|