ruby-keychain 0.1.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.
- data/LICENSE +8 -0
- data/README +90 -0
- data/lib/keychain.rb +72 -0
- data/lib/keychain/error.rb +34 -0
- data/lib/keychain/item.rb +188 -0
- data/lib/keychain/keychain.rb +188 -0
- data/lib/keychain/protocols.rb +43 -0
- data/lib/keychain/scope.rb +131 -0
- data/lib/keychain/sec.rb +165 -0
- data/lib/keychain/version.rb +4 -0
- data/spec/keychain_item_spec.rb +67 -0
- data/spec/keychain_spec.rb +220 -0
- data/spec/spec_helper.rb +11 -0
- metadata +155 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
# Procotol constants for use when creating/finding a Keychain::Item of type internet password
|
3
|
+
#
|
4
|
+
#
|
5
|
+
module Keychain::Protocols
|
6
|
+
# @private
|
7
|
+
def self.attach_protocol name
|
8
|
+
Sec.send :attach_variable, name, :pointer
|
9
|
+
constant_name = name.to_s.gsub('kSecAttrProtocol', '').upcase
|
10
|
+
const_set constant_name, CF::Base.typecast(Sec.send(name))
|
11
|
+
end
|
12
|
+
attach_protocol :kSecAttrProtocolFTP
|
13
|
+
attach_protocol :kSecAttrProtocolFTPAccount
|
14
|
+
attach_protocol :kSecAttrProtocolHTTP
|
15
|
+
attach_protocol :kSecAttrProtocolIRC
|
16
|
+
attach_protocol :kSecAttrProtocolNNTP
|
17
|
+
attach_protocol :kSecAttrProtocolPOP3
|
18
|
+
attach_protocol :kSecAttrProtocolSMTP
|
19
|
+
attach_protocol :kSecAttrProtocolSOCKS
|
20
|
+
attach_protocol :kSecAttrProtocolIMAP
|
21
|
+
attach_protocol :kSecAttrProtocolLDAP
|
22
|
+
attach_protocol :kSecAttrProtocolAppleTalk
|
23
|
+
attach_protocol :kSecAttrProtocolAFP
|
24
|
+
attach_protocol :kSecAttrProtocolTelnet
|
25
|
+
attach_protocol :kSecAttrProtocolSSH
|
26
|
+
attach_protocol :kSecAttrProtocolFTPS
|
27
|
+
attach_protocol :kSecAttrProtocolHTTPS
|
28
|
+
attach_protocol :kSecAttrProtocolHTTPProxy
|
29
|
+
attach_protocol :kSecAttrProtocolHTTPSProxy
|
30
|
+
attach_protocol :kSecAttrProtocolFTPProxy
|
31
|
+
attach_protocol :kSecAttrProtocolSMB
|
32
|
+
attach_protocol :kSecAttrProtocolRTSP
|
33
|
+
attach_protocol :kSecAttrProtocolRTSPProxy
|
34
|
+
attach_protocol :kSecAttrProtocolDAAP
|
35
|
+
attach_protocol :kSecAttrProtocolEPPC
|
36
|
+
attach_protocol :kSecAttrProtocolIPP
|
37
|
+
attach_protocol :kSecAttrProtocolNNTPS
|
38
|
+
attach_protocol :kSecAttrProtocolLDAPS
|
39
|
+
attach_protocol :kSecAttrProtocolTelnetS
|
40
|
+
attach_protocol :kSecAttrProtocolIMAPS
|
41
|
+
attach_protocol :kSecAttrProtocolIRCS
|
42
|
+
attach_protocol :kSecAttrProtocolPOP3S
|
43
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# A scope that represents the search for a keychain item
|
2
|
+
#
|
3
|
+
#
|
4
|
+
class Keychain::Scope
|
5
|
+
def initialize(kind, keychain=nil)
|
6
|
+
@kind = kind
|
7
|
+
@limit = nil
|
8
|
+
@keychains = [keychain]
|
9
|
+
@conditions = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# Adds conditions to the scope. Conditions are merged with any previously defined conditions.
|
13
|
+
#
|
14
|
+
# The set of possible keys for conditions is given by Sec::ATTR_MAP.values. The legal values for the :protocol key are the constants in
|
15
|
+
# Keychain::Protocols
|
16
|
+
#
|
17
|
+
# @param [Hash] conditions options to create the item with
|
18
|
+
# @option conditions [String] :account The account (user name)
|
19
|
+
# @option conditions [String] :comment A free text comment about the item
|
20
|
+
# @option conditions [Integer] :creator the item's creator, as the unsigned integer representation of a 4 char code
|
21
|
+
# @option conditions [String] :generic generic passwords can have a generic data attribute
|
22
|
+
# @option conditions [String] :invisible whether the item should be invisible
|
23
|
+
# @option conditions [String] :negative A negative item records that the user decided to never save a password
|
24
|
+
# @option conditions [String] :label A label for the item (Shown in keychain access)
|
25
|
+
# @option conditions [String] :path The path the password is associated with (internet passwords only)
|
26
|
+
# @option conditions [String] :port The path the password is associated with (internet passwords only)
|
27
|
+
# @option conditions [String] :port The protocol the password is associated with (internet passwords only)
|
28
|
+
# Should be one of the constants at Keychain::Protocols
|
29
|
+
# @option conditions [String] :domain the domain the password is associated with (internet passwords only)
|
30
|
+
# @option conditions [String] :server the host name the password is associated with (internet passwords only)
|
31
|
+
# @option conditions [String] :service the service the password is associated with (generic passwords only)
|
32
|
+
# @option conditions [Integer] :type the item's type, as the unsigned integer representation of a 4 char code
|
33
|
+
#
|
34
|
+
# @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place
|
35
|
+
def where(conditions)
|
36
|
+
@conditions.merge! conditions
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sets the number of items returned by the scope
|
41
|
+
#
|
42
|
+
# @param [Integer] value The maximum number of items to return
|
43
|
+
# @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place
|
44
|
+
def limit value
|
45
|
+
@limit = value
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set the list of keychains to search
|
50
|
+
#
|
51
|
+
# @param [Array<Keychain::Keychain>] keychains The maximum number of items to return
|
52
|
+
# @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place
|
53
|
+
def in *keychains
|
54
|
+
@keychains = keychains.flatten
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the first matching item in the scope
|
59
|
+
#
|
60
|
+
# @return [Keychain::Item, nil]
|
61
|
+
def first
|
62
|
+
query = to_query
|
63
|
+
execute(query).first
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns an array containing all of the matching items
|
67
|
+
#
|
68
|
+
# @return [Array] The matching items. May be empty
|
69
|
+
def all
|
70
|
+
query = to_query
|
71
|
+
query[Sec::Search::LIMIT] = @limit ? @limit.to_cf : Sec::Search::ALL
|
72
|
+
execute query
|
73
|
+
end
|
74
|
+
|
75
|
+
# Creates a new keychain item
|
76
|
+
#
|
77
|
+
# @param [Hash] attributes options to create the item with
|
78
|
+
# @option attributes [String] :account The account (user name)
|
79
|
+
# @option attributes [String] :comment A free text comment about the item
|
80
|
+
# @option attributes [Integer] :creator the item's creator, as the unsigned integer representation of a 4 char code
|
81
|
+
# @option attributes [String] :generic generic passwords can have a generic data attribute
|
82
|
+
# @option attributes [String] :invisible whether the item should be invisible
|
83
|
+
# @option attributes [String] :negative A negative item records that the user decided to never save a password
|
84
|
+
# @option attributes [String] :label A label for the item (Shown in keychain access)
|
85
|
+
# @option attributes [String] :path The path the password is associated with (internet passwords only)
|
86
|
+
# @option attributes [String] :port The path the password is associated with (internet passwords only)
|
87
|
+
# @option attributes [String] :port The protocol the password is associated with (internet passwords only)
|
88
|
+
# Should be one of the constants at Keychain::Protocols
|
89
|
+
# @option attributes [String] :domain the domain the password is associated with (internet passwords only)
|
90
|
+
# @option attributes [String] :server the host name the password is associated with (internet passwords only)
|
91
|
+
# @option attributes [String] :service the service the password is associated with (generic passwords only)
|
92
|
+
# @option attributes [Integer] :type the item's type, as the unsigned integer representation of a 4 char code
|
93
|
+
#
|
94
|
+
# @return [Keychain::Item]
|
95
|
+
def create(attributes)
|
96
|
+
raise "You must specify a password" unless attributes[:password]
|
97
|
+
|
98
|
+
Keychain::Item.new(attributes.merge(:klass => @kind)).save!(:keychain => @keychains.first)
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def execute query
|
104
|
+
result = FFI::MemoryPointer.new :pointer
|
105
|
+
status = Sec.SecItemCopyMatching(query, result)
|
106
|
+
if status == Sec.enum_value( :errSecItemNotFound)
|
107
|
+
return []
|
108
|
+
end
|
109
|
+
Sec.check_osstatus(status)
|
110
|
+
result = CF::Base.typecast(result.read_pointer).release_on_gc
|
111
|
+
unless result.is_a?(CF::Array)
|
112
|
+
result = CF::Array.immutable([result])
|
113
|
+
end
|
114
|
+
result.collect {|dictionary_of_attributes| Keychain::Item.from_dictionary_of_attributes(dictionary_of_attributes)}
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def to_query
|
119
|
+
query = CF::Dictionary.mutable
|
120
|
+
@conditions.each do |k,v|
|
121
|
+
k = Sec::INVERSE_ATTR_MAP[k]
|
122
|
+
query[k] = v.to_cf
|
123
|
+
end
|
124
|
+
|
125
|
+
query[Sec::Query::CLASS] = @kind
|
126
|
+
query[Sec::Query::SEARCH_LIST] = CF::Array.immutable(@keychains) if @keychains && @keychains.any?
|
127
|
+
query[Sec::Query::RETURN_ATTRIBUTES] = CF::Boolean::TRUE
|
128
|
+
query[Sec::Query::RETURN_REF] = CF::Boolean::TRUE
|
129
|
+
query
|
130
|
+
end
|
131
|
+
end
|
data/lib/keychain/sec.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# The module to which FFI attaches constants
|
2
|
+
module Sec
|
3
|
+
extend FFI::Library
|
4
|
+
ffi_lib '/System/Library/Frameworks/Security.framework/Security'
|
5
|
+
|
6
|
+
typedef :int32, :osstatus
|
7
|
+
typedef :pointer, :keychainref
|
8
|
+
|
9
|
+
enum [
|
10
|
+
:errSecItemNotFound, -25300,
|
11
|
+
:errSecDuplicateItem, -25299,
|
12
|
+
:errSecAuthFailed, -25293,
|
13
|
+
:errSecNoSuchKeychain, -25294,
|
14
|
+
:errCancelled, -128
|
15
|
+
]
|
16
|
+
|
17
|
+
attach_variable 'kSecClassInternetPassword', :pointer
|
18
|
+
attach_variable 'kSecClassGenericPassword', :pointer
|
19
|
+
|
20
|
+
attach_variable 'kSecClass', :pointer
|
21
|
+
|
22
|
+
attach_variable 'kSecAttrAccess', :pointer
|
23
|
+
attach_variable 'kSecAttrAccount', :pointer
|
24
|
+
attach_variable 'kSecAttrAuthenticationType', :pointer
|
25
|
+
attach_variable 'kSecAttrComment', :pointer
|
26
|
+
attach_variable 'kSecAttrCreationDate', :pointer
|
27
|
+
attach_variable 'kSecAttrCreator', :pointer
|
28
|
+
attach_variable 'kSecAttrDescription', :pointer
|
29
|
+
attach_variable 'kSecAttrGeneric', :pointer
|
30
|
+
attach_variable 'kSecAttrIsInvisible', :pointer
|
31
|
+
attach_variable 'kSecAttrIsNegative', :pointer
|
32
|
+
attach_variable 'kSecAttrLabel', :pointer
|
33
|
+
attach_variable 'kSecAttrModificationDate', :pointer
|
34
|
+
attach_variable 'kSecAttrPath', :pointer
|
35
|
+
attach_variable 'kSecAttrPort', :pointer
|
36
|
+
attach_variable 'kSecAttrProtocol', :pointer
|
37
|
+
attach_variable 'kSecAttrSecurityDomain', :pointer
|
38
|
+
attach_variable 'kSecAttrServer', :pointer
|
39
|
+
attach_variable 'kSecAttrService', :pointer
|
40
|
+
attach_variable 'kSecAttrType', :pointer
|
41
|
+
|
42
|
+
attach_variable 'kSecMatchSearchList', :pointer
|
43
|
+
|
44
|
+
attach_variable 'kSecMatchLimit', :pointer
|
45
|
+
attach_variable 'kSecMatchLimitOne', :pointer
|
46
|
+
attach_variable 'kSecMatchLimitAll', :pointer
|
47
|
+
attach_variable 'kSecMatchItemList', :pointer
|
48
|
+
attach_variable 'kSecReturnAttributes', :pointer
|
49
|
+
attach_variable 'kSecReturnRef', :pointer
|
50
|
+
attach_variable 'kSecReturnData', :pointer
|
51
|
+
|
52
|
+
attach_variable 'kSecValueRef', :pointer
|
53
|
+
attach_variable 'kSecValueData', :pointer
|
54
|
+
attach_variable 'kSecUseKeychain', :pointer
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
# map of kSecAttr* constants to the corresponding ruby name for the attribute
|
59
|
+
# Used in {Keychain::Item#attributes}}
|
60
|
+
ATTR_MAP = {
|
61
|
+
CF::Base.typecast(kSecAttrAccess) => :access,
|
62
|
+
CF::Base.typecast(kSecAttrAccount) => :account,
|
63
|
+
CF::Base.typecast(kSecAttrAuthenticationType) => :authentication_type,
|
64
|
+
CF::Base.typecast(kSecAttrComment) => :comment,
|
65
|
+
CF::Base.typecast(kSecAttrCreationDate) => :created_at,
|
66
|
+
CF::Base.typecast(kSecAttrCreator) => :creator,
|
67
|
+
CF::Base.typecast(kSecAttrDescription) => :description,
|
68
|
+
CF::Base.typecast(kSecAttrGeneric) => :generic,
|
69
|
+
CF::Base.typecast(kSecAttrIsInvisible) => :invisible,
|
70
|
+
CF::Base.typecast(kSecAttrIsNegative) => :negative,
|
71
|
+
CF::Base.typecast(kSecAttrLabel) => :label,
|
72
|
+
CF::Base.typecast(kSecAttrModificationDate) => :updated_at,
|
73
|
+
CF::Base.typecast(kSecAttrPath) => :path,
|
74
|
+
CF::Base.typecast(kSecAttrPort) => :port,
|
75
|
+
CF::Base.typecast(kSecAttrProtocol) => :protocol,
|
76
|
+
CF::Base.typecast(kSecAttrSecurityDomain) => :security_domain,
|
77
|
+
CF::Base.typecast(kSecAttrServer) => :server,
|
78
|
+
CF::Base.typecast(kSecAttrService) => :service,
|
79
|
+
CF::Base.typecast(kSecAttrType) => :type,
|
80
|
+
CF::Base.typecast(kSecClass) => :klass
|
81
|
+
}
|
82
|
+
|
83
|
+
# Inverse of {ATTR_MAP}
|
84
|
+
INVERSE_ATTR_MAP = ATTR_MAP.invert
|
85
|
+
|
86
|
+
# Query options for use with SecCopyMatching, SecItemUpdate
|
87
|
+
#
|
88
|
+
module Query
|
89
|
+
#key identifying the class of an item (kSecClass)
|
90
|
+
CLASS = CF::Base.typecast(Sec.kSecClass)
|
91
|
+
#key speciying the list of keychains to search (kSecMatchSearchList)
|
92
|
+
SEARCH_LIST = CF::Base.typecast(Sec.kSecMatchSearchList)
|
93
|
+
#key indicating the list of specific keychain items to the scope the search to
|
94
|
+
ITEM_LIST = CF::Base.typecast(Sec.kSecMatchItemList)
|
95
|
+
#key indicating whether to return attributes (kSecReturnAttributes)
|
96
|
+
RETURN_ATTRIBUTES = CF::Base.typecast(Sec.kSecReturnAttributes)
|
97
|
+
#key indicating whether to return the SecKeychainItemRef (kSecReturnRef)
|
98
|
+
RETURN_REF = CF::Base.typecast(Sec.kSecReturnRef)
|
99
|
+
#key indicating whether to return the password data (kSecReturnData)
|
100
|
+
RETURN_DATA = CF::Base.typecast(Sec.kSecReturnData)
|
101
|
+
#key indicating which keychain to use for the operation (kSecUseKeychain)
|
102
|
+
KEYCHAIN = CF::Base.typecast(Sec.kSecUseKeychain)
|
103
|
+
end
|
104
|
+
|
105
|
+
# defines constants for use as the class of an item
|
106
|
+
module Classes
|
107
|
+
# constant identifiying generic passwords (kSecClassGenericPassword)
|
108
|
+
GENERIC = CF::Base.typecast(Sec.kSecClassGenericPassword)
|
109
|
+
# constant identifying internet passwords (kSecClassInternetPassword)
|
110
|
+
INTERNET = CF::Base.typecast(Sec.kSecClassInternetPassword)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Search match types for use with SecCopyMatching
|
114
|
+
module Search
|
115
|
+
#meta value for {Sec::Search::LIMIT} indicating that all items be returned (kSecMatchLimitAll)
|
116
|
+
ALL = CF::Base.typecast(Sec.kSecMatchLimitAll)
|
117
|
+
# hash key indicating the maximum number of items (kSecMatchLimit)
|
118
|
+
LIMIT = CF::Base.typecast(Sec.kSecMatchLimit)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Constants for use with SecItemAdd/SecItemUpdate
|
122
|
+
module Value
|
123
|
+
# The hash key for the SecKeychainItemRef (in the dictionary returned by SecCopyMatching) (kSecValueRef)
|
124
|
+
REF = CF::Base.typecast(Sec.kSecValueRef)
|
125
|
+
# The hash key for the password data (in the dictionary returned by SecCopyMatching) (kSecValueData)
|
126
|
+
DATA = CF::Base.typecast(Sec.kSecValueData)
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
# The base class of all CF types from the security framework
|
131
|
+
#
|
132
|
+
# @abstract
|
133
|
+
class Base < CF::Base
|
134
|
+
#@private
|
135
|
+
def self.register_type(type_name)
|
136
|
+
Sec.attach_function "#{type_name}GetTypeID", [], CF.find_type(:cftypeid)
|
137
|
+
@@type_map[Sec.send("#{type_name}GetTypeID")] = self
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# If the result is non-zero raises an exception.
|
142
|
+
#
|
143
|
+
# The exception will have the result code as well as a human readable description
|
144
|
+
#
|
145
|
+
# @param [Integer] result the status code to check
|
146
|
+
# @raise [Keychain::Error] is the result is non zero
|
147
|
+
def self.check_osstatus result
|
148
|
+
if result != 0
|
149
|
+
case result
|
150
|
+
when Sec.enum_value(:errSecDuplicateItem)
|
151
|
+
raise Keychain::DuplicateItemError.new(result)
|
152
|
+
when Sec.enum_value(:errCancelled)
|
153
|
+
raise Keychain::UserCancelledError.new(result)
|
154
|
+
when Sec.enum_value(:errSecAuthFailed)
|
155
|
+
raise Keychain::AuthFailedError.new(result)
|
156
|
+
when Sec.enum_value(:errSecNoSuchKeychain)
|
157
|
+
raise Keychain::NoSuchKeychainError.new(result)
|
158
|
+
else
|
159
|
+
raise Keychain::Error.new(result)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Keychain::Item do
|
4
|
+
before(:each) do
|
5
|
+
@keychain = Keychain.create(File.join(Dir.tmpdir, "keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
|
6
|
+
@keychain.generic_passwords.create :service => 'some-service', :account => 'some-account', :password => 'some-password'
|
7
|
+
end
|
8
|
+
|
9
|
+
after(:each) do
|
10
|
+
@keychain.delete
|
11
|
+
end
|
12
|
+
|
13
|
+
def find_item
|
14
|
+
@keychain.generic_passwords.where(:service => 'some-service').first
|
15
|
+
end
|
16
|
+
|
17
|
+
subject {find_item}
|
18
|
+
|
19
|
+
describe 'keychain' do
|
20
|
+
it 'should return the keychain containing the item' do
|
21
|
+
subject.keychain.should == @keychain
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'password' do
|
26
|
+
it 'should retrieve the password' do
|
27
|
+
subject.password.should == 'some-password'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'service' do
|
32
|
+
it 'should retrieve the service' do
|
33
|
+
subject.service.should == 'some-service'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'account' do
|
38
|
+
it 'should retrieve the account' do
|
39
|
+
subject.account.should == 'some-account'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'created_at' do
|
44
|
+
it 'should retrieve the item creation date' do
|
45
|
+
subject.created_at.should be_within(2).of(Time.now)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'save' do
|
50
|
+
it 'should update attributes and password' do
|
51
|
+
subject.password = 'new-password'
|
52
|
+
subject.account = 'new-account'
|
53
|
+
subject.save!
|
54
|
+
|
55
|
+
fresh = find_item
|
56
|
+
fresh.password.should == 'new-password'
|
57
|
+
fresh.account.should == 'new-account'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe 'delete' do
|
62
|
+
it 'should remove the item from the keychain' do
|
63
|
+
subject.delete
|
64
|
+
find_item.should be_nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Keychain do
|
4
|
+
describe 'default' do
|
5
|
+
it "should return the login keychain" do
|
6
|
+
Keychain.default.path.should == File.expand_path(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain'))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'open' do
|
11
|
+
it 'should create a keychain reference to a path' do
|
12
|
+
keychain = Keychain.open(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain'))
|
13
|
+
keychain.path.should == Keychain.default.path
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'new' do
|
18
|
+
it 'should create the keychain' do
|
19
|
+
begin
|
20
|
+
keychain = Keychain.create(File.join(Dir.tmpdir, "other_keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"),
|
21
|
+
'password');
|
22
|
+
File.exists?(keychain.path).should be_true
|
23
|
+
ensure
|
24
|
+
keychain.delete
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'exists?' do
|
30
|
+
context 'the keychain exists' do
|
31
|
+
it 'should return true' do
|
32
|
+
Keychain.default.exists?.should be_true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'the keychain does not exist' do
|
37
|
+
it 'should return false' do
|
38
|
+
k = Keychain.open('/some/path/that/does/not/exist')
|
39
|
+
k.exists?.should be_false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'settings' do
|
45
|
+
before(:all) do
|
46
|
+
@keychain = Keychain.create(File.join(Dir.tmpdir, "keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should read/write lock_on_sleep' do
|
50
|
+
@keychain.lock_on_sleep = true
|
51
|
+
@keychain.lock_on_sleep?.should == true
|
52
|
+
@keychain.lock_on_sleep = false
|
53
|
+
@keychain.lock_on_sleep?.should == false
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should read/write lock_interval' do
|
57
|
+
@keychain.lock_interval = 12345
|
58
|
+
@keychain.lock_interval.should == 12345
|
59
|
+
end
|
60
|
+
|
61
|
+
after(:all) do
|
62
|
+
@keychain.delete
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'locking' do
|
67
|
+
context 'with a locked keychain' do
|
68
|
+
before :each do
|
69
|
+
@keychain = Keychain.create(File.join(Dir.tmpdir, "keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
|
70
|
+
@keychain.lock!
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should raise on invalid password' do
|
74
|
+
expect {@keychain.unlock! 'badpassword'}.to raise_error(Keychain::AuthFailedError)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should unlock on valid password' do
|
78
|
+
@keychain.unlock! 'pass'
|
79
|
+
@keychain.should_not be_locked
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
shared_examples_for 'item collection' do
|
85
|
+
|
86
|
+
before(:each) do
|
87
|
+
@keychain_1 = Keychain.create(File.join(Dir.tmpdir, "other_keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
|
88
|
+
@keychain_2 = Keychain.create(File.join(Dir.tmpdir, "keychain_2_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
|
89
|
+
@keychain_3 = Keychain.create(File.join(Dir.tmpdir, "keychain_3_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
|
90
|
+
|
91
|
+
add_fixtures
|
92
|
+
end
|
93
|
+
|
94
|
+
after(:each) do
|
95
|
+
@keychain_1.delete
|
96
|
+
@keychain_2.delete
|
97
|
+
@keychain_3.delete
|
98
|
+
end
|
99
|
+
|
100
|
+
describe('create') do
|
101
|
+
it 'should add a password' do
|
102
|
+
item = @keychain_1.send(subject).create(create_arguments)
|
103
|
+
item.should be_a(Keychain::Item)
|
104
|
+
item.klass.should == expected_kind
|
105
|
+
item.password.should == 'some-password'
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should be findable' do
|
109
|
+
@keychain_1.send(subject).create(create_arguments)
|
110
|
+
item = @keychain_1.send(subject).where(search_for_created_arguments).first
|
111
|
+
item.password.should == 'some-password'
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'when a duplicate item exists' do
|
115
|
+
before(:each) do
|
116
|
+
@keychain_1.send(subject).create(create_arguments)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'should raise Keychain::DuplicateItemError' do
|
120
|
+
expect {@keychain_1.send(subject).create(create_arguments)}.to raise_error(Keychain::DuplicateItemError)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe('all') do
|
126
|
+
|
127
|
+
context 'when the keychain does not contains a matching item' do
|
128
|
+
it 'should return []' do
|
129
|
+
@keychain_1.send(subject).where(search_arguments_with_no_results).all.should == []
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should return an array of results' do
|
134
|
+
item = @keychain_1.send(subject).where(search_arguments).all.first
|
135
|
+
item.should be_a(Keychain::Item)
|
136
|
+
item.password.should == 'some-password-1'
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'searching all keychains' do
|
140
|
+
context 'when the keychain does contains matching items' do
|
141
|
+
it 'should return all of them' do
|
142
|
+
Keychain.send(subject).where(search_arguments_with_multiple_results).all.length.should == 3
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'when the limit is option is set' do
|
147
|
+
it 'should limit the return set' do
|
148
|
+
Keychain.send(subject).where(search_arguments_with_multiple_results).limit(1).all.length.should == 1
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'when a subset of keychains is specified' do
|
153
|
+
it 'should return items from those keychains' do
|
154
|
+
Keychain.send(subject).where(search_arguments_with_multiple_results).in(@keychain_1, @keychain_2).all.length.should == 2
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
describe 'first' do
|
160
|
+
context 'when the keychain does not contain a matching item' do
|
161
|
+
it 'should return nil' do
|
162
|
+
item = @keychain_1.send(subject).where(search_arguments_with_no_results).first.should be_nil
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
context 'when the keychain does contain a matching item' do
|
167
|
+
it 'should find it' do
|
168
|
+
item = @keychain_1.send(subject).where(search_arguments).first
|
169
|
+
item.should be_a(Keychain::Item)
|
170
|
+
item.password.should == 'some-password-1'
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
context 'when a different keychain contains a matching item' do
|
175
|
+
before(:each) do
|
176
|
+
item = @keychain_1.send(subject).create(create_arguments)
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'should not find it' do
|
180
|
+
@keychain_2.send(subject).where(search_arguments).first.should be_nil
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe 'generic_passwords' do
|
187
|
+
subject { :generic_passwords }
|
188
|
+
let(:create_arguments){{:service => 'aservice', :account => 'anaccount-foo', :password =>'some-password'}}
|
189
|
+
let(:search_for_created_arguments){{:service => 'aservice'}}
|
190
|
+
|
191
|
+
let(:search_arguments){{:service => 'aservice-1'}}
|
192
|
+
let(:search_arguments_with_no_results){{:service => 'doesntexist'}}
|
193
|
+
let(:search_arguments_with_multiple_results){{:account => 'anaccount'}}
|
194
|
+
let(:expected_kind) {'genp'}
|
195
|
+
|
196
|
+
def add_fixtures
|
197
|
+
@keychain_1.generic_passwords.create(:service => 'aservice-1', :account => 'anaccount', :password => 'some-password-1')
|
198
|
+
@keychain_2.generic_passwords.create(:service => 'aservice-2', :account => 'anaccount', :password => 'some-password-2')
|
199
|
+
@keychain_3.generic_passwords.create(:service => 'aservice-2', :account => 'anaccount', :password => 'some-password-3')
|
200
|
+
end
|
201
|
+
it_behaves_like 'item collection'
|
202
|
+
end
|
203
|
+
|
204
|
+
describe 'internet_passwords' do
|
205
|
+
subject { :internet_passwords }
|
206
|
+
let(:create_arguments){{:server => 'dressipi.example.com', :account => 'anaccount-foo', :password =>'some-password', :protocol => Keychain::Protocols::HTTP}}
|
207
|
+
let(:search_for_created_arguments){{:server => 'dressipi.example.com', :protocol => Keychain::Protocols::HTTP}}
|
208
|
+
let(:search_arguments){{:server => 'dressipi-1.example.com', :protocol => Keychain::Protocols::HTTP}}
|
209
|
+
let(:search_arguments_with_no_results){{:server => 'dressipi.example.com'}}
|
210
|
+
let(:search_arguments_with_multiple_results){{:account => 'anaccount'}}
|
211
|
+
let(:expected_kind) {'inet'}
|
212
|
+
|
213
|
+
def add_fixtures
|
214
|
+
@keychain_1.internet_passwords.create(:server => 'dressipi-1.example.com', :account => 'anaccount', :password => 'some-password-1', :protocol => Keychain::Protocols::HTTP)
|
215
|
+
@keychain_2.internet_passwords.create(:server => 'dressipi-2.example.com', :account => 'anaccount', :password => 'some-password-2', :protocol => Keychain::Protocols::HTTP)
|
216
|
+
@keychain_3.internet_passwords.create(:server => 'dressipi-3.example.com', :account => 'anaccount', :password => 'some-password-3', :protocol => Keychain::Protocols::HTTP)
|
217
|
+
end
|
218
|
+
it_behaves_like 'item collection'
|
219
|
+
end
|
220
|
+
end
|