From 81f5861e87eabe9284398a7c5600c358fe36a71e Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Thu, 16 Feb 2017 20:13:54 -0700 Subject: [PATCH 01/22] =?UTF-8?q?The=20goal=20of=20this=20patch=20is=20to?= =?UTF-8?q?=20enable=20updating=20keys=20and=20certificates=20stored=20in?= =?UTF-8?q?=20a=20key=20chain.=20It=20does=20this=20by=20moving=20access?= =?UTF-8?q?=20to=20SecItemUpdate=20to=20the=20Sec::Base=20class.=20=20This?= =?UTF-8?q?=20was=20inspired=20by=20the=20section=20=E2=80=9CAdding,=20Rem?= =?UTF-8?q?oving,=20and=20Working=20With=20Keys=20and=20Certificates?= =?UTF-8?q?=E2=80=9D=20in=20the=20Key=20Services=20Programming=20Guide=20w?= =?UTF-8?q?hich=20mentions=20SecItemAdd,=20SecItemUpdate=20and=20SecItemCo?= =?UTF-8?q?pyMatching=20as=20base=20functions=20that=20should=20be=20used?= =?UTF-8?q?=20on=20keys=20and=20certificates=20stored=20in=20the=20KeyChai?= =?UTF-8?q?n.=20=20This=20patch=20does=20make=20it=20possible=20for=20exam?= =?UTF-8?q?ple=20to=20change=20a=20key=E2=80=99s=20name=20as=20stored=20in?= =?UTF-8?q?=20the=20keychain=20(my=20particular=20use=20case).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/keychain/item.rb | 33 +-------------------------------- lib/keychain/keychain.rb | 5 ++++- lib/keychain/sec.rb | 36 ++++++++++++++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/lib/keychain/item.rb b/lib/keychain/item.rb index 161287d..42276b9 100644 --- a/lib/keychain/item.rb +++ b/lib/keychain/item.rb @@ -1,8 +1,6 @@ module Sec attach_function 'SecKeychainItemDelete', [:pointer], :osstatus - attach_function 'SecItemAdd', [:pointer, :pointer], :osstatus - attach_function 'SecItemUpdate', [:pointer, :pointer], :osstatus attach_function 'SecKeychainItemCopyKeychain', [:pointer, :pointer], :osstatus end @@ -129,19 +127,6 @@ def create(options) cf_dict = CF::Base.typecast(result.read_pointer) end - def update - status = Sec.SecItemUpdate({Sec::Query::SEARCH_LIST => [self.keychain], - Sec::Query::ITEM_LIST => [self], - Sec::Query::CLASS => klass}.to_cf, build_new_attributes); - Sec.check_osstatus(status) - - result = FFI::MemoryPointer.new :pointer - query = build_refresh_query - status = Sec.SecItemCopyMatching(query, result); - Sec.check_osstatus(status) - cf_dict = CF::Base.typecast(result.read_pointer) - end - def build_create_query options query = CF::Dictionary.mutable query[Sec::Value::DATA] = CF::Data.from_string(@unsaved_password) if @unsaved_password @@ -151,24 +136,8 @@ def build_create_query options query end - def build_refresh_query - query = CF::Dictionary.mutable - query[Sec::Query::SEARCH_LIST] = CF::Array.immutable([self.keychain]) - query[Sec::Query::ITEM_LIST] = CF::Array.immutable([self]) - query[Sec::Query::RETURN_ATTRIBUTES] = CF::Boolean::TRUE - query[Sec::Query::RETURN_REF] = CF::Boolean::TRUE - query[Sec::Query::CLASS] = klass.to_cf - query - end - def build_new_attributes - new_attributes = CF::Dictionary.mutable - @attributes.each do |k,v| - next if k == :created_at || k == :updated_at - next if k == :klass && persisted? - k = self.class::INVERSE_ATTR_MAP[k] - new_attributes[k] = v.to_cf - end + new_attributes = super new_attributes[Sec::Value::DATA] = CF::Data.from_string(@unsaved_password) if @unsaved_password new_attributes end diff --git a/lib/keychain/keychain.rb b/lib/keychain/keychain.rb index 13b7cbf..209a125 100644 --- a/lib/keychain/keychain.rb +++ b/lib/keychain/keychain.rb @@ -16,8 +16,11 @@ module Sec attach_function 'SecKeychainGetPath', [:keychainref, :pointer, :pointer], :osstatus attach_function 'SecKeychainCreate', [:string, :uint32, :pointer, :char, :pointer, :pointer], :osstatus + + attach_function 'SecItemAdd', [:pointer, :pointer], :osstatus attach_function 'SecItemCopyMatching', [:pointer, :pointer], :osstatus - + attach_function 'SecItemUpdate', [:pointer, :pointer], :osstatus + attach_function 'SecKeychainSetSearchList', [:pointer], :osstatus attach_function 'SecKeychainCopySearchList', [:pointer], :osstatus diff --git a/lib/keychain/sec.rb b/lib/keychain/sec.rb index 279ffad..0852bd1 100644 --- a/lib/keychain/sec.rb +++ b/lib/keychain/sec.rb @@ -166,6 +166,40 @@ def load_attributes cf_dict = CF::Base.typecast(result.read_pointer).release_on_gc update_self_from_dictionary(cf_dict) end + + def build_new_attributes + new_attributes = CF::Dictionary.mutable + @attributes.each do |k,v| + next if k == :created_at || k == :updated_at + next if k == :klass && persisted? + k = self.class::INVERSE_ATTR_MAP[k] + new_attributes[k] = v.to_cf + end + new_attributes + end + + def update + status = Sec.SecItemUpdate({Sec::Query::SEARCH_LIST => [self.keychain], + Sec::Query::ITEM_LIST => [self], + Sec::Query::CLASS => klass}.to_cf, build_new_attributes) + Sec.check_osstatus(status) + + result = FFI::MemoryPointer.new :pointer + query = build_refresh_query + status = Sec.SecItemCopyMatching(query, result) + Sec.check_osstatus(status) + cf_dict = CF::Base.typecast(result.read_pointer) + end + + def build_refresh_query + query = CF::Dictionary.mutable + query[Sec::Query::SEARCH_LIST] = CF::Array.immutable([self.keychain]) + query[Sec::Query::ITEM_LIST] = CF::Array.immutable([self]) + query[Sec::Query::RETURN_ATTRIBUTES] = CF::Boolean::TRUE + query[Sec::Query::RETURN_REF] = CF::Boolean::TRUE + query[Sec::Query::CLASS] = klass.to_cf + query + end end # If the result is non-zero raises an exception. @@ -192,6 +226,4 @@ def self.check_osstatus result end end end - - end From 315934f6fb5f42fd60aa728ba186be79ce123185 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 12:07:52 -0700 Subject: [PATCH 02/22] Add support for querying certificate information. Add helper methods for certificate start and finish times. --- lib/keychain/certificate.rb | 295 ++++++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) diff --git a/lib/keychain/certificate.rb b/lib/keychain/certificate.rb index 2ab864a..1b50738 100644 --- a/lib/keychain/certificate.rb +++ b/lib/keychain/certificate.rb @@ -5,6 +5,7 @@ module Sec attach_function 'SecCertificateCopyPublicKey', [:pointer, :pointer], :osstatus attach_function 'SecCertificateCopyData', [:pointer], :pointer + attach_function 'SecCertificateCopyValues', [:pointer, :pointer, :pointer], :pointer attach_variable 'kSecAttrCertificateType', :pointer attach_variable 'kSecAttrCertificateEncoding', :pointer @@ -13,6 +14,124 @@ module Sec attach_variable 'kSecAttrSerialNumber', :pointer attach_variable 'kSecAttrSubjectKeyID', :pointer attach_variable 'kSecAttrPublicKeyHash', :pointer + + # OIDS + attach_variable 'kSecOIDADC_CERT_POLICY', :pointer + attach_variable 'kSecOIDAPPLE_CERT_POLICY', :pointer + attach_variable 'kSecOIDAPPLE_EKU_CODE_SIGNING', :pointer + attach_variable 'kSecOIDAPPLE_EKU_CODE_SIGNING_DEV', :pointer + attach_variable 'kSecOIDAPPLE_EKU_ICHAT_ENCRYPTION', :pointer + attach_variable 'kSecOIDAPPLE_EKU_ICHAT_SIGNING', :pointer + attach_variable 'kSecOIDAPPLE_EKU_RESOURCE_SIGNING', :pointer + attach_variable 'kSecOIDAPPLE_EKU_SYSTEM_IDENTITY', :pointer + attach_variable 'kSecOIDAPPLE_EXTENSION', :pointer + attach_variable 'kSecOIDAPPLE_EXTENSION_ADC_APPLE_SIGNING', :pointer + attach_variable 'kSecOIDAPPLE_EXTENSION_ADC_DEV_SIGNING', :pointer + attach_variable 'kSecOIDAPPLE_EXTENSION_APPLE_SIGNING', :pointer + attach_variable 'kSecOIDAPPLE_EXTENSION_CODE_SIGNING', :pointer + attach_variable 'kSecOIDAuthorityInfoAccess', :pointer + attach_variable 'kSecOIDAuthorityKeyIdentifier', :pointer + attach_variable 'kSecOIDBasicConstraints', :pointer + attach_variable 'kSecOIDBiometricInfo', :pointer + attach_variable 'kSecOIDCSSMKeyStruct', :pointer + attach_variable 'kSecOIDCertIssuer', :pointer + attach_variable 'kSecOIDCertificatePolicies', :pointer + attach_variable 'kSecOIDClientAuth', :pointer + attach_variable 'kSecOIDCollectiveStateProvinceName', :pointer + attach_variable 'kSecOIDCollectiveStreetAddress', :pointer + attach_variable 'kSecOIDCommonName', :pointer + attach_variable 'kSecOIDCountryName', :pointer + attach_variable 'kSecOIDCrlDistributionPoints', :pointer + attach_variable 'kSecOIDCrlNumber', :pointer + attach_variable 'kSecOIDCrlReason', :pointer + attach_variable 'kSecOIDDOTMAC_CERT_EMAIL_ENCRYPT', :pointer + attach_variable 'kSecOIDDOTMAC_CERT_EMAIL_SIGN', :pointer + attach_variable 'kSecOIDDOTMAC_CERT_EXTENSION', :pointer + attach_variable 'kSecOIDDOTMAC_CERT_IDENTITY', :pointer + attach_variable 'kSecOIDDOTMAC_CERT_POLICY', :pointer + attach_variable 'kSecOIDDeltaCrlIndicator', :pointer + attach_variable 'kSecOIDDescription', :pointer + attach_variable 'kSecOIDEKU_IPSec', :pointer + attach_variable 'kSecOIDEmailAddress', :pointer + attach_variable 'kSecOIDEmailProtection', :pointer + attach_variable 'kSecOIDExtendedKeyUsage', :pointer + attach_variable 'kSecOIDExtendedKeyUsageAny', :pointer + attach_variable 'kSecOIDExtendedUseCodeSigning', :pointer + attach_variable 'kSecOIDGivenName', :pointer + attach_variable 'kSecOIDHoldInstructionCode', :pointer + attach_variable 'kSecOIDInvalidityDate', :pointer + attach_variable 'kSecOIDIssuerAltName', :pointer + attach_variable 'kSecOIDIssuingDistributionPoint', :pointer + attach_variable 'kSecOIDIssuingDistributionPoints', :pointer + attach_variable 'kSecOIDKERBv5_PKINIT_KP_CLIENT_AUTH', :pointer + attach_variable 'kSecOIDKERBv5_PKINIT_KP_KDC', :pointer + attach_variable 'kSecOIDKeyUsage', :pointer + attach_variable 'kSecOIDLocalityName', :pointer + attach_variable 'kSecOIDMS_NTPrincipalName', :pointer + attach_variable 'kSecOIDMicrosoftSGC', :pointer + attach_variable 'kSecOIDNameConstraints', :pointer + attach_variable 'kSecOIDNetscapeCertSequence', :pointer + attach_variable 'kSecOIDNetscapeCertType', :pointer + attach_variable 'kSecOIDNetscapeSGC', :pointer + attach_variable 'kSecOIDOCSPSigning', :pointer + attach_variable 'kSecOIDOrganizationName', :pointer + attach_variable 'kSecOIDOrganizationalUnitName', :pointer + attach_variable 'kSecOIDPolicyConstraints', :pointer + attach_variable 'kSecOIDPolicyMappings', :pointer + attach_variable 'kSecOIDPrivateKeyUsagePeriod', :pointer + attach_variable 'kSecOIDQC_Statements', :pointer + attach_variable 'kSecOIDSerialNumber', :pointer + attach_variable 'kSecOIDServerAuth', :pointer + attach_variable 'kSecOIDStateProvinceName', :pointer + attach_variable 'kSecOIDStreetAddress', :pointer + attach_variable 'kSecOIDSubjectAltName', :pointer + attach_variable 'kSecOIDSubjectDirectoryAttributes', :pointer + attach_variable 'kSecOIDSubjectEmailAddress', :pointer + attach_variable 'kSecOIDSubjectInfoAccess', :pointer + attach_variable 'kSecOIDSubjectKeyIdentifier', :pointer + attach_variable 'kSecOIDSubjectPicture', :pointer + attach_variable 'kSecOIDSubjectSignatureBitmap', :pointer + attach_variable 'kSecOIDSurname', :pointer + attach_variable 'kSecOIDTimeStamping', :pointer + attach_variable 'kSecOIDTitle', :pointer + attach_variable 'kSecOIDUseExemptions', :pointer + attach_variable 'kSecOIDX509V1CertificateIssuerUniqueId', :pointer + attach_variable 'kSecOIDX509V1CertificateSubjectUniqueId', :pointer + attach_variable 'kSecOIDX509V1IssuerName', :pointer + attach_variable 'kSecOIDX509V1IssuerNameCStruct', :pointer + attach_variable 'kSecOIDX509V1IssuerNameLDAP', :pointer + attach_variable 'kSecOIDX509V1IssuerNameStd', :pointer + attach_variable 'kSecOIDX509V1SerialNumber', :pointer + attach_variable 'kSecOIDX509V1Signature', :pointer + attach_variable 'kSecOIDX509V1SignatureAlgorithm', :pointer + attach_variable 'kSecOIDX509V1SignatureAlgorithmParameters', :pointer + attach_variable 'kSecOIDX509V1SignatureAlgorithmTBS', :pointer + attach_variable 'kSecOIDX509V1SignatureCStruct', :pointer + attach_variable 'kSecOIDX509V1SignatureStruct', :pointer + attach_variable 'kSecOIDX509V1SubjectName', :pointer + attach_variable 'kSecOIDX509V1SubjectNameCStruct', :pointer + attach_variable 'kSecOIDX509V1SubjectNameLDAP', :pointer + attach_variable 'kSecOIDX509V1SubjectNameStd', :pointer + attach_variable 'kSecOIDX509V1SubjectPublicKey', :pointer + attach_variable 'kSecOIDX509V1SubjectPublicKeyAlgorithm', :pointer + attach_variable 'kSecOIDX509V1SubjectPublicKeyAlgorithmParameters', :pointer + attach_variable 'kSecOIDX509V1SubjectPublicKeyCStruct', :pointer + attach_variable 'kSecOIDX509V1ValidityNotAfter', :pointer + attach_variable 'kSecOIDX509V1ValidityNotBefore', :pointer + attach_variable 'kSecOIDX509V1Version', :pointer + attach_variable 'kSecOIDX509V3Certificate', :pointer + attach_variable 'kSecOIDX509V3CertificateCStruct', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionCStruct', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionCritical', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionId', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionStruct', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionType', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionValue', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionsCStruct', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionsStruct', :pointer + attach_variable 'kSecOIDX509V3CertificateNumberOfExtensions', :pointer + attach_variable 'kSecOIDX509V3SignedCertificate', :pointer + attach_variable 'kSecOIDX509V3SignedCertificateCStruct', :pointer end module Keychain @@ -29,6 +148,124 @@ class Certificate < Sec::Base CF::Base.typecast(Sec::kSecAttrSubjectKeyID) => :subject_key_id, CF::Base.typecast(Sec::kSecAttrPublicKeyHash) => :public_key_hash} + + OID_MAP = {CF::Base.typecast(Sec::kSecOIDADC_CERT_POLICY) => 'ADC_CERT_POLICY', + CF::Base.typecast(Sec::kSecOIDAPPLE_CERT_POLICY) => 'APPLE_CERT_POLICY', + CF::Base.typecast(Sec::kSecOIDAPPLE_EKU_CODE_SIGNING) => 'APPLE_EKU_CODE_SIGNING', + CF::Base.typecast(Sec::kSecOIDAPPLE_EKU_CODE_SIGNING_DEV) => 'APPLE_EKU_CODE_SIGNING_DEV', + CF::Base.typecast(Sec::kSecOIDAPPLE_EKU_ICHAT_ENCRYPTION) => 'APPLE_EKU_ICHAT_ENCRYPTION', + CF::Base.typecast(Sec::kSecOIDAPPLE_EKU_ICHAT_SIGNING) => 'APPLE_EKU_ICHAT_SIGNING', + CF::Base.typecast(Sec::kSecOIDAPPLE_EKU_RESOURCE_SIGNING) => 'APPLE_EKU_RESOURCE_SIGNING', + CF::Base.typecast(Sec::kSecOIDAPPLE_EKU_SYSTEM_IDENTITY) => 'APPLE_EKU_SYSTEM_IDENTITY', + CF::Base.typecast(Sec::kSecOIDAPPLE_EXTENSION) => 'APPLE_EXTENSION', + CF::Base.typecast(Sec::kSecOIDAPPLE_EXTENSION_ADC_APPLE_SIGNING) => 'APPLE_EXTENSION_ADC_APPLE_SIGNING', + CF::Base.typecast(Sec::kSecOIDAPPLE_EXTENSION_ADC_DEV_SIGNING) => 'APPLE_EXTENSION_ADC_DEV_SIGNING', + CF::Base.typecast(Sec::kSecOIDAPPLE_EXTENSION_APPLE_SIGNING) => 'APPLE_EXTENSION_APPLE_SIGNING', + CF::Base.typecast(Sec::kSecOIDAPPLE_EXTENSION_CODE_SIGNING) => 'APPLE_EXTENSION_CODE_SIGNING', + CF::Base.typecast(Sec::kSecOIDAuthorityInfoAccess) => 'AuthorityInfoAccess', + CF::Base.typecast(Sec::kSecOIDAuthorityKeyIdentifier) => 'AuthorityKeyIdentifier', + CF::Base.typecast(Sec::kSecOIDBasicConstraints) => 'BasicConstraints', + CF::Base.typecast(Sec::kSecOIDBiometricInfo) => 'BiometricInfo', + CF::Base.typecast(Sec::kSecOIDCSSMKeyStruct) => 'CSSMKeyStruct', + CF::Base.typecast(Sec::kSecOIDCertIssuer) => 'CertIssuer', + CF::Base.typecast(Sec::kSecOIDCertificatePolicies) => 'CertificatePolicies', + CF::Base.typecast(Sec::kSecOIDClientAuth) => 'ClientAuth', + CF::Base.typecast(Sec::kSecOIDCollectiveStateProvinceName) => 'CollectiveStateProvinceName', + CF::Base.typecast(Sec::kSecOIDCollectiveStreetAddress) => 'CollectiveStreetAddress', + CF::Base.typecast(Sec::kSecOIDCommonName) => 'CommonName', + CF::Base.typecast(Sec::kSecOIDCountryName) => 'CountryName', + CF::Base.typecast(Sec::kSecOIDCrlDistributionPoints) => 'CrlDistributionPoints', + CF::Base.typecast(Sec::kSecOIDCrlNumber) => 'CrlNumber', + CF::Base.typecast(Sec::kSecOIDCrlReason) => 'CrlReason', + CF::Base.typecast(Sec::kSecOIDDOTMAC_CERT_EMAIL_ENCRYPT) => 'DOTMAC_CERT_EMAIL_ENCRYPT', + CF::Base.typecast(Sec::kSecOIDDOTMAC_CERT_EMAIL_SIGN) => 'DOTMAC_CERT_EMAIL_SIGN', + CF::Base.typecast(Sec::kSecOIDDOTMAC_CERT_EXTENSION) => 'DOTMAC_CERT_EXTENSION', + CF::Base.typecast(Sec::kSecOIDDOTMAC_CERT_IDENTITY) => 'DOTMAC_CERT_IDENTITY', + CF::Base.typecast(Sec::kSecOIDDOTMAC_CERT_POLICY) => 'DOTMAC_CERT_POLICY', + CF::Base.typecast(Sec::kSecOIDDeltaCrlIndicator) => 'DeltaCrlIndicator', + CF::Base.typecast(Sec::kSecOIDDescription) => 'Description', + CF::Base.typecast(Sec::kSecOIDEKU_IPSec) => 'EKU_IPSec', + CF::Base.typecast(Sec::kSecOIDEmailAddress) => 'EmailAddress', + CF::Base.typecast(Sec::kSecOIDEmailProtection) => 'EmailProtection', + CF::Base.typecast(Sec::kSecOIDExtendedKeyUsage) => 'ExtendedKeyUsage', + CF::Base.typecast(Sec::kSecOIDExtendedKeyUsageAny) => 'ExtendedKeyUsageAny', + CF::Base.typecast(Sec::kSecOIDExtendedUseCodeSigning) => 'ExtendedUseCodeSigning', + CF::Base.typecast(Sec::kSecOIDGivenName) => 'GivenName', + CF::Base.typecast(Sec::kSecOIDHoldInstructionCode) => 'HoldInstructionCode', + CF::Base.typecast(Sec::kSecOIDInvalidityDate) => 'InvalidityDate', + CF::Base.typecast(Sec::kSecOIDIssuerAltName) => 'IssuerAltName', + CF::Base.typecast(Sec::kSecOIDIssuingDistributionPoint) => 'IssuingDistributionPoint', + CF::Base.typecast(Sec::kSecOIDIssuingDistributionPoints) => 'IssuingDistributionPoints', + CF::Base.typecast(Sec::kSecOIDKERBv5_PKINIT_KP_CLIENT_AUTH) => 'KERBv5_PKINIT_KP_CLIENT_AUTH', + CF::Base.typecast(Sec::kSecOIDKERBv5_PKINIT_KP_KDC) => 'KERBv5_PKINIT_KP_KDC', + CF::Base.typecast(Sec::kSecOIDKeyUsage) => 'KeyUsage', + CF::Base.typecast(Sec::kSecOIDLocalityName) => 'LocalityName', + CF::Base.typecast(Sec::kSecOIDMS_NTPrincipalName) => 'MS_NTPrincipalName', + CF::Base.typecast(Sec::kSecOIDMicrosoftSGC) => 'MicrosoftSGC', + CF::Base.typecast(Sec::kSecOIDNameConstraints) => 'NameConstraints', + CF::Base.typecast(Sec::kSecOIDNetscapeCertSequence) => 'NetscapeCertSequence', + CF::Base.typecast(Sec::kSecOIDNetscapeCertType) => 'NetscapeCertType', + CF::Base.typecast(Sec::kSecOIDNetscapeSGC) => 'NetscapeSGC', + CF::Base.typecast(Sec::kSecOIDOCSPSigning) => 'OCSPSigning', + CF::Base.typecast(Sec::kSecOIDOrganizationName) => 'OrganizationName', + CF::Base.typecast(Sec::kSecOIDOrganizationalUnitName) => 'OrganizationalUnitName', + CF::Base.typecast(Sec::kSecOIDPolicyConstraints) => 'PolicyConstraints', + CF::Base.typecast(Sec::kSecOIDPolicyMappings) => 'PolicyMappings', + CF::Base.typecast(Sec::kSecOIDPrivateKeyUsagePeriod) => 'PrivateKeyUsagePeriod', + CF::Base.typecast(Sec::kSecOIDQC_Statements) => 'QC_Statements', + CF::Base.typecast(Sec::kSecOIDSerialNumber) => 'SerialNumber', + CF::Base.typecast(Sec::kSecOIDServerAuth) => 'ServerAuth', + CF::Base.typecast(Sec::kSecOIDStateProvinceName) => 'StateProvinceName', + CF::Base.typecast(Sec::kSecOIDStreetAddress) => 'StreetAddress', + CF::Base.typecast(Sec::kSecOIDSubjectAltName) => 'SubjectAltName', + CF::Base.typecast(Sec::kSecOIDSubjectDirectoryAttributes) => 'SubjectDirectoryAttributes', + CF::Base.typecast(Sec::kSecOIDSubjectEmailAddress) => 'SubjectEmailAddress', + CF::Base.typecast(Sec::kSecOIDSubjectInfoAccess) => 'SubjectInfoAccess', + CF::Base.typecast(Sec::kSecOIDSubjectKeyIdentifier) => 'SubjectKeyIdentifier', + CF::Base.typecast(Sec::kSecOIDSubjectPicture) => 'SubjectPicture', + CF::Base.typecast(Sec::kSecOIDSubjectSignatureBitmap) => 'SubjectSignatureBitmap', + CF::Base.typecast(Sec::kSecOIDSurname) => 'Surname', + CF::Base.typecast(Sec::kSecOIDTimeStamping) => 'TimeStamping', + CF::Base.typecast(Sec::kSecOIDTitle) => 'Title', + CF::Base.typecast(Sec::kSecOIDUseExemptions) => 'UseExemptions', + CF::Base.typecast(Sec::kSecOIDX509V1CertificateIssuerUniqueId) => 'X509V1CertificateIssuerUniqueId', + CF::Base.typecast(Sec::kSecOIDX509V1CertificateSubjectUniqueId) => 'X509V1CertificateSubjectUniqueId', + CF::Base.typecast(Sec::kSecOIDX509V1IssuerName) => 'X509V1IssuerName', + CF::Base.typecast(Sec::kSecOIDX509V1IssuerNameCStruct) => 'X509V1IssuerNameCStruct', + CF::Base.typecast(Sec::kSecOIDX509V1IssuerNameLDAP) => 'X509V1IssuerNameLDAP', + CF::Base.typecast(Sec::kSecOIDX509V1IssuerNameStd) => 'X509V1IssuerNameStd', + CF::Base.typecast(Sec::kSecOIDX509V1SerialNumber) => 'X509V1SerialNumber', + CF::Base.typecast(Sec::kSecOIDX509V1Signature) => 'X509V1Signature', + CF::Base.typecast(Sec::kSecOIDX509V1SignatureAlgorithm) => 'X509V1SignatureAlgorithm', + CF::Base.typecast(Sec::kSecOIDX509V1SignatureAlgorithmParameters) => 'X509V1SignatureAlgorithmParameters', + CF::Base.typecast(Sec::kSecOIDX509V1SignatureAlgorithmTBS) => 'X509V1SignatureAlgorithmTBS', + CF::Base.typecast(Sec::kSecOIDX509V1SignatureCStruct) => 'X509V1SignatureCStruct', + CF::Base.typecast(Sec::kSecOIDX509V1SignatureStruct) => 'X509V1SignatureStruct', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectName) => 'X509V1SubjectName', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectNameCStruct) => 'X509V1SubjectNameCStruct', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectNameLDAP) => 'X509V1SubjectNameLDAP', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectNameStd) => 'X509V1SubjectNameStd', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectPublicKey) => 'X509V1SubjectPublicKey', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectPublicKeyAlgorithm) => 'X509V1SubjectPublicKeyAlgorithm', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectPublicKeyAlgorithmParameters) => 'X509V1SubjectPublicKeyAlgorithmParameters', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectPublicKeyCStruct) => 'X509V1SubjectPublicKeyCStruct', + CF::Base.typecast(Sec::kSecOIDX509V1ValidityNotAfter) => 'X509V1ValidityNotAfter', + CF::Base.typecast(Sec::kSecOIDX509V1ValidityNotBefore) => 'X509V1ValidityNotBefore', + CF::Base.typecast(Sec::kSecOIDX509V1Version) => 'X509V1Version', + CF::Base.typecast(Sec::kSecOIDX509V3Certificate) => 'X509V3Certificate', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateCStruct) => 'X509V3CertificateCStruct', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionCStruct) => 'X509V3CertificateExtensionCStruct', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionCritical) => 'X509V3CertificateExtensionCritical', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionId) => 'X509V3CertificateExtensionId', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionStruct) => 'X509V3CertificateExtensionStruct', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionType) => 'X509V3CertificateExtensionType', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionValue) => 'X509V3CertificateExtensionValue', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionsCStruct) => 'X509V3CertificateExtensionsCStruct', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionsStruct) => 'X509V3CertificateExtensionsStruct', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateNumberOfExtensions) => 'X509V3CertificateNumberOfExtensions', + CF::Base.typecast(Sec::kSecOIDX509V3SignedCertificate) => 'X509V3SignedCertificate', + CF::Base.typecast(Sec::kSecOIDX509V3SignedCertificateCStruct) => 'X509V3SignedCertificateCStruct'} + ATTR_MAP[CF::Base.typecast(Sec::kSecAttrAccessible)] = :accessible if defined?(Sec::kSecAttrAccessible) ATTR_MAP[CF::Base.typecast(Sec::kSecAttrAccessControl)] = :access_control if defined?(Sec::kSecAttrAccessControl) @@ -55,5 +292,63 @@ def x509 data.release result end + + def contents + @contents ||= begin + data_ptr = Sec.SecCertificateCopyValues(self, nil, nil) + data = CF::Dictionary.new(data_ptr) + + result = data.inject(Hash.new) do |hash, pair| + # Get human readable key + key = pair.first + human_key = OID_MAP[key] || key.to_ruby + + value = pair.last + + # Map values to ruby hash + human_value = value.to_ruby + + # Store current result + hash[human_key] = human_value + + # Now fix labels + fix_label(human_value, value) + value['value'].each_with_index do |oid_value, index| + fix_label(human_value['value'][index], oid_value) + end + + hash + end + + data.release + result + end + end + + def fix_label(ruby_dictionary, cf_dictionary) + label = cf_dictionary['label'] + return unless label + ruby_dictionary['label'] = OID_MAP[label] || label.to_ruby + end + + def valid? + self.start < Time.now && self.finish > Time.now + end + + def start + hash = self.contents['X509V1ValidityNotBefore'] + to_time(hash['value']) + end + + def finish + hash = self.contents['X509V1ValidityNotAfter'] + to_time(hash['value']) + end + + def to_time(value) + # Returned times are NSDates which use 00:00:00 UTC on 1 January 2001 as the reference time + reference_time = Time.new(2001, 1, 1, 0, 0, 0, 0) + Time.at(value + reference_time.to_i) + end end end \ No newline at end of file From 951527726be6a373abe051c27556ca27253eca47 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 12:08:48 -0700 Subject: [PATCH 03/22] Add support for determining the type of key - public, private or symmetric. --- lib/keychain/key.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/keychain/key.rb b/lib/keychain/key.rb index 719d590..cb3444b 100644 --- a/lib/keychain/key.rb +++ b/lib/keychain/key.rb @@ -1,4 +1,6 @@ module Sec + attach_function 'SecKeyCopyPublicKey', [:pointer], :pointer + begin attach_variable 'kSecAttrAccessible', :pointer rescue FFI::NotFoundError #Only available in 10.9 @@ -25,6 +27,11 @@ module Sec attach_variable 'kSecAttrCanWrap', :pointer attach_variable 'kSecAttrCanUnwrap', :pointer + # kSecAttrKeyClass values + attach_variable 'kSecAttrKeyClassPublic', :pointer + attach_variable 'kSecAttrKeyClassPrivate', :pointer + attach_variable 'kSecAttrKeyClassSymmetric', :pointer + enum :SecItemImportExportFlags, [:kSecItemPemArmour, 1] enum :SecExternalFormat, [:kSecFormatUnknown, 0, @@ -70,6 +77,11 @@ class SecItemImportExportKeyParameters < FFI::Struct module Keychain class Key < Sec::Base register_type 'SecKey' + include AccessMixin + + KEY_CLASS_PUBLIC = CF::Base.typecast(Sec::kSecAttrKeyClassPublic) + KEY_CLASS_PRIVATE = CF::Base.typecast(Sec::kSecAttrKeyClassPrivate) + KEY_CLASS_SYMMETRIC = CF::Base.typecast(Sec::kSecAttrKeyClassSymmetric) ATTR_MAP = {CF::Base.typecast(Sec::kSecAttrAccessGroup) => :access_group, CF::Base.typecast(Sec::kSecAttrKeyClass) => :key_class, @@ -98,6 +110,15 @@ def klass Sec::Classes::KEY.to_ruby end + def public_key + if self.key_class == KEY_CLASS_PUBLIC.to_ruby + self + else + pointer = Sec.SecKeyCopyPublicKey(self) + self.class.new(pointer) + end + end + def export(passphrase = nil, format = :kSecFormatUnknown) flags = Sec::SecItemImportExportKeyParameters.new flags[:version] = Sec::SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION From 21dcbbad91ea811b93f63590de969098e13e5aaf Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 12:09:32 -0700 Subject: [PATCH 04/22] Read attributes on demand and move delete method to the base class. --- lib/keychain/item.rb | 8 -------- lib/keychain/sec.rb | 22 +++++++++++++--------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/lib/keychain/item.rb b/lib/keychain/item.rb index 42276b9..8c13c9c 100644 --- a/lib/keychain/item.rb +++ b/lib/keychain/item.rb @@ -57,14 +57,6 @@ def self.new(attrs_or_pointer) end end - # Removes the item from the associated keychain - # - def delete - status = Sec.SecKeychainItemDelete(self) - Sec.check_osstatus(status) - self - end - # Set a new password for the item # @note The new password is not saved into the keychain until you call {Keychain::Item#save!} # @param [String] value The new value for the password diff --git a/lib/keychain/sec.rb b/lib/keychain/sec.rb index 0852bd1..54a35b3 100644 --- a/lib/keychain/sec.rb +++ b/lib/keychain/sec.rb @@ -81,7 +81,7 @@ module Classes CERTIFICATE = CF::Base.typecast(Sec.kSecClassCertificate) # constant identifying generic passwords (kSecClassGenericPassword) GENERIC = CF::Base.typecast(Sec.kSecClassGenericPassword) - # constant identifying generic passwords (kSecClassIdentity) + # constant identifying certificates and associated private keys (kSecClassIdentity) IDENTITY = CF::Base.typecast(Sec.kSecClassIdentity) # constant identifying internet passwords (kSecClassInternetPassword) INTERNET = CF::Base.typecast(Sec.kSecClassInternetPassword) @@ -110,8 +110,6 @@ module Value # # @abstract class Base < CF::Base - attr_reader :attributes - def self.register_type(type_name) Sec.attach_function "#{type_name}GetTypeID", [], CF.find_type(:cftypeid) @@type_map[Sec.send("#{type_name}GetTypeID")] = self @@ -130,11 +128,6 @@ def self.define_attributes(attr_map) end end - def initialize(ptr) - super - @attributes = {} - end - def update_self_from_dictionary(cf_dict) @attributes = cf_dict.inject({}) do |memo, (k,v)| if ruby_name = self.class::ATTR_MAP[k] @@ -149,11 +142,22 @@ def update_self_from_dictionary(cf_dict) # @return [Keychain::Keychain] def keychain out = FFI::MemoryPointer.new :pointer - status = Sec.SecKeychainItemCopyKeychain(self,out) + status = Sec.SecKeychainItemCopyKeychain(self, out) Sec.check_osstatus(status) CF::Base.new(out.read_pointer).release_on_gc end + # Removes the item from the associated keychain + def delete + status = Sec.SecKeychainItemDelete(self) + Sec.check_osstatus(status) + self + end + + def attributes + @attributes || load_attributes + end + def load_attributes result = FFI::MemoryPointer.new :pointer status = Sec.SecItemCopyMatching({Sec::Query::SEARCH_LIST => [self.keychain], From 54d376245788e062a5d80d0cf6c24bb7cde27a27 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 12:10:14 -0700 Subject: [PATCH 05/22] Add helper methods for certificates and identities. --- lib/keychain/keychain.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/keychain/keychain.rb b/lib/keychain/keychain.rb index 209a125..1fb0611 100644 --- a/lib/keychain/keychain.rb +++ b/lib/keychain/keychain.rb @@ -116,6 +116,20 @@ def generic_passwords Scope.new(Sec::Classes::GENERIC, self) end + # Returns a scope for the identities (certificates and private keys) contained in this keychain + # + # @return [Keychain::Scope] a new scope object + def identities + Scope.new(Sec::Classes::IDENTITY, self) + end + + # Returns a scope for public and private keys contained in this keychain + # + # @return [Keychain::Scope] a new scope object + def keys + Scope.new(Sec::Classes::KEY, self) + end + # Imports item from string or file to this keychain # # @param [IO, String] input IO object or String with raw data to import From aa0c32271a7af004341a7101384196a2a1ab54a2 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 12:10:30 -0700 Subject: [PATCH 06/22] Separate out and expand access apis. --- lib/keychain.rb | 1 + lib/keychain/access.rb | 44 +++++++++++++++++ lib/keychain/acl.rb | 74 +++++++++++++++++++++++++++++ lib/keychain/keychain.rb | 30 ++++-------- lib/keychain/trusted_application.rb | 7 +++ 5 files changed, 134 insertions(+), 22 deletions(-) create mode 100644 lib/keychain/acl.rb diff --git a/lib/keychain.rb b/lib/keychain.rb index cd3ad49..8ce8290 100644 --- a/lib/keychain.rb +++ b/lib/keychain.rb @@ -2,6 +2,7 @@ require 'corefoundation' require 'keychain/sec' require 'keychain/access' +require 'keychain/acl' require 'keychain/trusted_application' require 'keychain/keychain' require 'keychain/error' diff --git a/lib/keychain/access.rb b/lib/keychain/access.rb index a7d4446..88e1e67 100644 --- a/lib/keychain/access.rb +++ b/lib/keychain/access.rb @@ -1,5 +1,49 @@ +module Sec + attach_function 'SecAccessCreate', [:pointer, :pointer, :pointer], :osstatus + attach_function 'SecKeychainItemCopyAccess', [:pointer, :pointer], :osstatus + attach_function 'SecAccessCopyACLList', [:pointer, :pointer], :osstatus + attach_function 'SecAccessCopyMatchingACLList', [:pointer, :pointer], :pointer + attach_function 'SecKeychainItemSetAccess', [:pointer, :pointer], :osstatus +end + module Keychain + module AccessMixin + def access + access_buffer = FFI::MemoryPointer.new(:pointer) + status = Sec.SecKeychainItemCopyAccess(self, access_buffer) + Sec.check_osstatus status + Access.new(access_buffer.read_pointer) + end + + def access=(value) + status = Sec.SecKeychainItemSetAccess(self, value.to_cf) + Sec.check_osstatus status + end + end + class Access < Sec::Base register_type 'SecAccess' + + def self.create(description, trusted_apps = []) + access_buffer = FFI::MemoryPointer.new(:pointer) + status = Sec.SecAccessCreate(description.to_cf, trusted_apps.to_cf, access_buffer) + Sec.check_osstatus status + self.new(access_buffer.read_pointer) + end + + def acls + acl_list_ref = FFI::MemoryPointer.new(:pointer) + status = Sec.SecAccessCopyACLList(self, acl_list_ref) + Sec.check_osstatus status + array_ref = CF::Base.typecast(acl_list_ref.read_pointer).release_on_gc + array_ref.to_ruby + end + + def matching_acls(authorization_tag) + access_buffer = FFI::MemoryPointer.new(:pointer) + authorization_tag_cf = CF::Base.typecast(authorization_tag) + acl_list_ref = Sec.SecAccessCopyMatchingACLList(self, authorization_tag_cf) + acl_list_ref.null? ? Array.new : CF::Base.typecast(acl_list_ref) + end end end \ No newline at end of file diff --git a/lib/keychain/acl.rb b/lib/keychain/acl.rb new file mode 100644 index 0000000..8c76fff --- /dev/null +++ b/lib/keychain/acl.rb @@ -0,0 +1,74 @@ +module Sec + attach_function 'SecACLCopyContents', [:pointer, :pointer, :pointer, :pointer], :osstatus + attach_function 'SecACLRemove', [:pointer], :osstatus + attach_function 'SecACLCopyAuthorizations', [:pointer], :pointer + attach_function 'SecACLSetContents', [:pointer, :pointer, :pointer, :pointer], :osstatus + attach_variable 'kSecACLAuthorizationAny', :pointer + attach_variable 'kSecACLAuthorizationLogin', :pointer + attach_variable 'kSecACLAuthorizationGenKey', :pointer + attach_variable 'kSecACLAuthorizationDelete', :pointer + attach_variable 'kSecACLAuthorizationExportWrapped', :pointer + attach_variable 'kSecACLAuthorizationExportClear', :pointer + attach_variable 'kSecACLAuthorizationImportWrapped', :pointer + attach_variable 'kSecACLAuthorizationImportClear', :pointer + attach_variable 'kSecACLAuthorizationSign', :pointer + attach_variable 'kSecACLAuthorizationEncrypt', :pointer + attach_variable 'kSecACLAuthorizationDecrypt', :pointer + attach_variable 'kSecACLAuthorizationMAC', :pointer + attach_variable 'kSecACLAuthorizationDerive', :pointer +end + +module Keychain + class Acl < Sec::Base + register_type 'SecACL' + + attr_reader :applications, :description, :prompt + + def initialize(ptr) + super(ptr) + + applications_ref = FFI::MemoryPointer.new(:pointer) + description_ref = FFI::MemoryPointer.new(:pointer) + prompt_ref = FFI::MemoryPointer.new(:pointer) + status = Sec.SecACLCopyContents(self, applications_ref, description_ref, prompt_ref) + Sec.check_osstatus(status) + + unless applications_ref.read_pointer.null? + applications_cf = CF::Base.typecast(applications_ref.read_pointer).release_on_gc + @applications = applications_cf.to_ruby + applications_cf.release + end + + unless description_ref.read_pointer.null? + description_cf = CF::Base.typecast(description_ref.read_pointer).release_on_gc + @description = description_cf.to_ruby + end + + unless prompt_ref.read_pointer.null? + prompt_cf = CF::Base.typecast(prompt_ref.read_pointer).release_on_gc + @prompt = prompt_cf.to_ruby + end + end + + def authorizations + authorizations_ref = Sec.SecACLCopyAuthorizations(self) + authorizations_cf = CF::Base.typecast(authorizations_ref) + result = authorizations_cf.to_ruby + authorizations_cf.release + result + end + + def delete + status = Sec.SecACLRemove(self) + Sec.check_osstatus(status) + end + + def applications=(apps) + apps_cf = apps ? apps.to_cf : nil + description_cf = apps ? self.description.to_cf : nil + prompt_cf = apps ? self.prompt.to_cf : nil + status = Sec.SecACLSetContents(self, apps_cf, description_cf, prompt_cf) + Sec.check_osstatus(status) + end + end +end \ No newline at end of file diff --git a/lib/keychain/keychain.rb b/lib/keychain/keychain.rb index 1fb0611..a606849 100644 --- a/lib/keychain/keychain.rb +++ b/lib/keychain/keychain.rb @@ -7,7 +7,6 @@ module Sec :kSecItemTypeCertificate, :kSecItemTypeAggregate] - attach_function 'SecAccessCreate', [:pointer, :pointer, :pointer], :osstatus attach_function 'SecTrustedApplicationCreateFromPath', [:string, :pointer], :osstatus attach_function 'SecKeychainCopyDefault', [:pointer], :osstatus @@ -137,17 +136,14 @@ def keys # permitted to access imported items # @return [Array ] List of imported keychain objects, # each of which may be a SecCertificate, SecKey, or SecIdentity instance - def import(input, app_list=[]) + def import(input, app_paths = []) input = input.read if input.is_a? IO - # Create array of TrustedApplication objects - trusted_apps = get_trusted_apps(app_list) + apps = paths.map do |path| + TrustedApplication.create_from_path(path) + end - # Create an Access object - access_buffer = FFI::MemoryPointer.new(:pointer) - status = Sec.SecAccessCreate(path.to_cf, trusted_apps, access_buffer) - Sec.check_osstatus status - access = CF::Base.typecast(access_buffer.read_pointer) + access = Access.create(path, apps) key_params = Sec::SecItemImportExportKeyParameters.new key_params[:accessRef] = access @@ -156,10 +152,11 @@ def import(input, app_list=[]) cf_data = CF::Data.from_string(input).release_on_gc cf_array = FFI::MemoryPointer.new(:pointer) status = Sec.SecItemImport(cf_data, nil, :kSecFormatUnknown, :kSecItemTypeUnknown, :kSecItemPemArmour, key_params, self, cf_array) - access.release Sec.check_osstatus status item_array = CF::Base.typecast(cf_array.read_pointer).release_on_gc + access.release + item_array.to_ruby end @@ -189,7 +186,7 @@ def path io_size = FFI::MemoryPointer.new(:uint32) io_size.put_uint32(0, out_buffer.size) - status = Sec.SecKeychainGetPath(self,io_size, out_buffer) + status = Sec.SecKeychainGetPath(self, io_size, out_buffer) Sec.check_osstatus(status) out_buffer.read_string(io_size.get_uint32(0)).force_encoding(Encoding::UTF_8) @@ -259,17 +256,6 @@ def get_settings settings end - def get_trusted_apps apps - trusted_app_array = apps.map do |path| - trusted_app_buffer = FFI::MemoryPointer.new(:pointer) - status = Sec.SecTrustedApplicationCreateFromPath( - path.encode(Encoding::UTF_8), trusted_app_buffer) - Sec.check_osstatus(status) - CF::Base.typecast(trusted_app_buffer.read_pointer).release_on_gc - end - trusted_app_array.to_cf - end - def put_settings settings status = Sec.SecKeychainSetSettings(self, settings) Sec.check_osstatus status diff --git a/lib/keychain/trusted_application.rb b/lib/keychain/trusted_application.rb index 41525d4..9c1ae0f 100644 --- a/lib/keychain/trusted_application.rb +++ b/lib/keychain/trusted_application.rb @@ -1,5 +1,12 @@ module Keychain class TrustedApplication < Sec::Base register_type 'SecTrustedApplication' + + def self.create_from_path(path) + trusted_app_buffer = FFI::MemoryPointer.new(:pointer) + status = Sec.SecTrustedApplicationCreateFromPath(path.encode(Encoding::UTF_8), trusted_app_buffer) + Sec.check_osstatus(status) + self.new(trusted_app_buffer.read_pointer).release_on_gc + end end end \ No newline at end of file From 0bba4ff19882d0d76678e9ed73c809e549ae23fb Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 13:54:46 -0700 Subject: [PATCH 07/22] Fix up local variable. --- lib/keychain/keychain.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/keychain/keychain.rb b/lib/keychain/keychain.rb index a606849..c2024a4 100644 --- a/lib/keychain/keychain.rb +++ b/lib/keychain/keychain.rb @@ -139,7 +139,7 @@ def keys def import(input, app_paths = []) input = input.read if input.is_a? IO - apps = paths.map do |path| + apps = app_paths.map do |path| TrustedApplication.create_from_path(path) end From cbc5f2a805efa160bf4ed5c6ec6607da37161c97 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 21:08:35 -0700 Subject: [PATCH 08/22] Fix creating ruby PKCS12 object. --- lib/keychain/identity.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/keychain/identity.rb b/lib/keychain/identity.rb index 5b9d8dc..7c93a44 100644 --- a/lib/keychain/identity.rb +++ b/lib/keychain/identity.rb @@ -47,7 +47,7 @@ def pkcs12(passphrase='') Sec.check_osstatus(status) data = CF::Data.new(data_ptr.read_pointer) - result = OpenSSL::PKCS12.new(data.to_s) + result = OpenSSL::PKCS12.new(data.to_s, passphrase) data.release result end From 33c83f60032e50e9d793bcd01c58098561277525 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 21:47:15 -0700 Subject: [PATCH 09/22] Update to newer gem versions. --- keychain.gemspec | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/keychain.gemspec b/keychain.gemspec index 2df6f38..06a36bd 100644 --- a/keychain.gemspec +++ b/keychain.gemspec @@ -23,10 +23,9 @@ Gem::Specification.new do |s| s.add_runtime_dependency "ffi" s.add_runtime_dependency "corefoundation", "~>0.2.0" - s.add_development_dependency "rspec", '~> 3.3', '>= 3.3.0' - s.add_development_dependency "rake", '~> 10.4', '>= 10.4.2' - s.add_development_dependency "yard", '~> 0.8.7' - s.add_development_dependency "redcarpet", '~>3.2', '>= 3.2.3' - + s.add_development_dependency "rspec", '>= 3.6.0' + s.add_development_dependency "rake", '>= 12.0.0' + s.add_development_dependency "yard", '>= 0.9.9' + s.add_development_dependency "redcarpet", '>= 3.4.0' end From 5a92aef07b31beeca0861ec43c0fbf5aee0e4aa9 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 21:47:54 -0700 Subject: [PATCH 10/22] Allow any app to access certificate - helps in identity tests. --- spec/spec.keychain | Bin 27944 -> 27144 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/spec/spec.keychain b/spec/spec.keychain index 92088191e3e9ffd2a7fdbe9a61da8c0318945e6d..5d92727dd3d0d8848785fa752987c038f64e3712 100644 GIT binary patch delta 1690 zcmXxkdpML?90%}s#$|>TQuLyXa_n0DexKGr2%LI8-dM$31A6W1Gc~+$O6JRf`v2S26zHKfFBSD1Os7T z-(gmNe-Fw5T&#E9P=7j5D}4?bblQZJx|*v66o!P5-wn#lv=9aeN={2%kp*~U#&rwk z`%}@#Nsn!+u>5}?`8QBt-n6oh!9YR?vQ9=0dJy2ZSyhR#>J8RKa0DqNCctOzbhcVv zpHDK|H#FEl5a47`6scLE)^e*VweYxo8G%;sUq*bmEK|O#+sc)uUO{Hz1?SD0f|=iH zS5L^=G7GhBhMzG~X5GV#YX--Ydt2fM=fZs+G3EBG?H`yx@=M+0h;);Xy@oarW~_&t z4foFS?t5zY>3HX0X__2Km2UHRJ6EbYjf(BcJXGvH)Dd+1dYYtCU4Jy)j=CI^ORm>j z+xYzU`C|>f{@AvZs2cLGSL)(E>!MhxDph_EIX<-LFeT`hzzijqvk}F;G+AD~LWO7d z{>8CQJF2sdYso3{rh}Juj6^u(DB(_c!No=F5=(;+d{enE()!ku=5CtQ)9!X7dZg2F z_9~AV-sCdSQ@UA2j1}e$2u?hTnt8=%AiI02)AnZklyfE4F$MoM$4S@d#Yi*7w>a^} zl%iMXXNA>r^vCnb$^`hMSNW{-MM2eb6p_o8u~Q7nZmg+{`a8?Kn)PK>|9Mg|{Ne-t zqt6D53$MgErgKSCwewtfpnB%2L`x{D*Ds zMx%w}$MjPei9dJPm{t!@Ei^yw9%5z$cu;Uwb_A}LNKSO-#x&gRc6VH4W|e`?{iKWy z6zV*R>KY{zmqt2XcROx5wdy#WrPDf+vK*Fb%n$Uk=%-Pc@${GDtbkYoHG0h&VYda} zDS%`2Q5-6&E!$enE4I5MC>*Blxt8WcV#meiRn$vaN9>L|DAP-9@)KSpGL_9`(!{7v z{jn*7$_3M`W_!3Vm7df7t>Q>+0q&M-$-Mvakvhh=(jLC4<1$O$1lFBM;5A*)!r4g( zGGU9InKQOB;Wsxj?)^-7KS z+b=x7LF94n7&g~i79FsT)RyFzB{1pJ+o!U#cj&DLDLhWuMRTmLb@niUZf(fedTDTy z-?~3NP_b>z@$!d?5WERzA+Ngqa_H}G(k;xxZ#K0PD@^T6?339ADq-riwida`m@cWi zd>39ui$dApts!9R`S)EaF3I+9%3A-cSEIdTF(F2sTazgz9-M1>QaMC@`Q=&Cb5>#C z8o6&lRJPcIel%|Xai@Ioh^?B5o3A)#eiA2=jdBtVeD<$f1+TU43Hds;>@jqW%UTeQ zotY{#4o3ZTKl9$ZC60tnw??mje*h)dps@|Rqk_GZ=_rlxjJQ%GxT8T@QwFhNPOxm+g;V=SJm!Kn(S-o{&2a3spo26X=qZnyG?y7B{RPLbQ{fV z8%MO@S4+}CDd9UW8pqQfTrR^(5!kZHB}|$dFZ84xM1(4@&3_R$PLICnKUG9sjC@)nb$0P-dc8dB zWb{|7g1PO(W!;*p4-OG(PB9MnOV7+Pq-QCV^==oEsEs!2ZSyVtQ6D7c<=R9|Rk*N# zel5s8c;s9R*35YN?1biZJF~Zz9HYKH8lMop&r3s0Gw9@?tz&t>YhV$A6hK>70Nqs~ z2Dl1 z&zT-nkqnihiRR!_wMm-(;}8_!4;X?VCIq+xL2TpeF40~h6&ohB65-<(UPy(m`>;R5{ZGfEs55NHc9RLGZ&=yFU z0|uc0As_+}1&9Hh0we&=E(|CTmgA9Q0|z?-eJAr}J6oPfhuqdm3-{XTv1=j^G7o>h zwOR8C*Lt*|*{hqGyAt(rsin8RNC=!kA2UGoH~?hwF+;(Msz096jPb;pv`W^*G`zU^ zX1uLD+61iarFipU9-nfU2Qw)ayZ|^jj1mTJutI|QlnoXeEG#JjtY8XH7yv?F7{Gxo zknzfyia^zUC`=8})E8$4;^l8O^r>uIly<(*1`D4Y3rB=i7(2D5W8{9PsWCsxDM{Qh zWS!y_!56?BiVG`%N9RRQs3APq3+9Ef7%-ngg?R$jVHSfffXUnVd=_6o4;EO^x!g}d zIIIM*3aAL_9f=XbK`hv?#qV~1+6*ip2m|;F0u%ku$o^Ddgtx_~J+UbHn^kAFq-L6< zh*oEhfWUW9wt?)BntW`IE1kt&@he*Hiy1IN0g#d)j#YoF2nQI0JAbHviEG+!-5MBE zHbb~)y0ngOYfl3YDzud%#HYXzGL0GKN~P(d+*AU%MSBG?g^?dDjw^~)Hip44SF9$|4^WpTP0UKsB71)K>CU2GHoPt z?>C6c`xlvv*RAbH@*VA|QkHMIDCv$$aMmz-=bh^yCVc4ev0C*uUgXw3s(0}64~~^; zP4Qvs%;7P#bpmd%CA+YmR&k*`#KWf7Gri@FSYgua7T$Vv-{tIsHL53VZ?ktcJKw!X z>m;_0L+%&1$3-@pZXpGFd6QI%P17B|qgvd(v)& zte0UMPe|PTTdU_~I;x`P!H4<#_>-gA5pHTjIiI~MoA2fZnDDJ;hVaeUKwJvO#_-xk+781u&)NWs>IjQ?w?@%~pC~BfN*pbx2US2# zT&&Lt9QZLqYDG)Mx(|+tcPl2Pmk^J~2-ogU4^qP=^)z(MY1#Yv?Ein?g>U}pSs1CRaOjbDLQs2{VR)kIVem%VL-cQB$mAAGt z&vp9ZBs#u*PofuW%;x%Z73!k&Cr=S0F#MKtCnqG% z)zk3AT?1c|`({TbyPV?JeKk9IU1g-{oBiShouWZZYP+w{{0@B(K+E)L5BU}kZDTiWLMDF?>{8+T_QBo6^&HKJhXwJ1h1x4Xw`h}{)sou8}Fz8yk_*; zSqhqCb4IsBYU!#4uf91ijvEQ@ zo_-~7Byx)nWLTP4)^uo~3Z5Zi!e1nf4@c>y#W0)q# zQ9Z`$sy!DeGFG6-?;Z%5ih7U#kB@9&UCE_H#yOYS21_sfQ<9Ng81BAZQ>CtTg3Koz z#n9)C?@(JA8m}HT#zn}=AVxHNV=eAcX|3UpV=aZ0_sXKN1YyD75cV-(7J_hk0Pr`2178kY8lV9HY*cX4B;QSFELmFNk&MPx zNVj_0qp@$KDSk$1oVv6Y5eMlIB21~`0U$|IOGK)rG(`HPCxNKQNsACMtLi+k8HZB+ E1+h+}H2?qr From 1414f70bdfb90e4ca2359a96cc9c7fbfc5268f3b Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 21:48:05 -0700 Subject: [PATCH 11/22] Fix up tests. --- spec/identity_spec.rb | 10 ++++++---- spec/keychain_spec.rb | 8 ++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/spec/identity_spec.rb b/spec/identity_spec.rb index f31586d..b017248 100644 --- a/spec/identity_spec.rb +++ b/spec/identity_spec.rb @@ -4,12 +4,13 @@ before(:context) do Keychain.user_interaction_allowed = false @keychain = Keychain.open(File.join(File.dirname(__FILE__), 'spec.keychain')) - @keychain.unlock! 'DummyPassword' - + @keychain.unlock!('DummyPassword') end + after(:context) do Keychain.user_interaction_allowed = true end + describe 'query' do it 'should return a identity' do scope = Keychain::Scope.new(Sec::Classes::IDENTITY, @keychain) @@ -33,10 +34,11 @@ end #this fails on travis - not sure 100% why yet - skip 'should be exportable to pkcs12' do + it 'should be exportable to pkcs12' do scope = Keychain::Scope.new(Sec::Classes::IDENTITY, @keychain) identity = scope.all.first - expect(identity.pkcs12).to be_kind_of(OpenSSL::PKCS12) + pkcs12 = identity.pkcs12('passphrase') + expect(pkcs12).to be_kind_of(OpenSSL::PKCS12) end end end \ No newline at end of file diff --git a/spec/keychain_spec.rb b/spec/keychain_spec.rb index 86632e4..db5b812 100644 --- a/spec/keychain_spec.rb +++ b/spec/keychain_spec.rb @@ -17,7 +17,7 @@ describe 'default' do it "should return the login keychain" do - expect(Keychain.default.path).to eq(File.expand_path(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain'))) + expect(Keychain.default.path).to eq(File.expand_path(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain-db'))) end end @@ -84,7 +84,7 @@ it 'should return true' do expect(Keychain.default.exists?).to be_truthy end - end + end context 'the keychain does not exist' do it 'should return false' do @@ -148,7 +148,7 @@ @keychain_2.delete @keychain_3.delete end - + describe('create') do it 'should add a password' do item = @keychain_1.send(subject).create(create_arguments) @@ -157,7 +157,7 @@ expect(item.password).to eq('some-password') end - it 'should be findable' do + it 'should be findable' do @keychain_1.send(subject).create(create_arguments) item = @keychain_1.send(subject).where(search_for_created_arguments).first expect(item.password).to eq('some-password') From 49ca1c97a2cf7cfd94106d9f59a467153372d95a Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 21:48:48 -0700 Subject: [PATCH 12/22] Go back to previous way of handling attributes. --- lib/keychain/sec.rb | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/keychain/sec.rb b/lib/keychain/sec.rb index 54a35b3..e8f8365 100644 --- a/lib/keychain/sec.rb +++ b/lib/keychain/sec.rb @@ -110,6 +110,8 @@ module Value # # @abstract class Base < CF::Base + attr_reader :attributes + def self.register_type(type_name) Sec.attach_function "#{type_name}GetTypeID", [], CF.find_type(:cftypeid) @@type_map[Sec.send("#{type_name}GetTypeID")] = self @@ -128,8 +130,13 @@ def self.define_attributes(attr_map) end end + def initialize(ptr) + super + @attributes = {} + end + def update_self_from_dictionary(cf_dict) - @attributes = cf_dict.inject({}) do |memo, (k,v)| + @attributes = cf_dict.inject(Hash.new) do |memo, (k,v)| if ruby_name = self.class::ATTR_MAP[k] memo[ruby_name] = v.to_ruby end @@ -154,10 +161,6 @@ def delete self end - def attributes - @attributes || load_attributes - end - def load_attributes result = FFI::MemoryPointer.new :pointer status = Sec.SecItemCopyMatching({Sec::Query::SEARCH_LIST => [self.keychain], @@ -173,11 +176,11 @@ def load_attributes def build_new_attributes new_attributes = CF::Dictionary.mutable - @attributes.each do |k,v| - next if k == :created_at || k == :updated_at - next if k == :klass && persisted? - k = self.class::INVERSE_ATTR_MAP[k] - new_attributes[k] = v.to_cf + @attributes.each do |key, value| + next unless self.class::ATTR_UPDATABLE.include?(key) + next if key == :klass && self.persisted? + key_cf = self.class::INVERSE_ATTR_MAP[key] + new_attributes[key_cf] = value.to_cf end new_attributes end From 9821085ab8b1ade16e4a80efc81e195ef6460e82 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 21:49:03 -0700 Subject: [PATCH 13/22] Update item attribute based on latest Apple documentation. --- lib/keychain/sec.rb | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/keychain/sec.rb b/lib/keychain/sec.rb index e8f8365..bd1317e 100644 --- a/lib/keychain/sec.rb +++ b/lib/keychain/sec.rb @@ -22,28 +22,36 @@ module Sec attach_variable 'kSecClassIdentity', :pointer attach_variable 'kSecClassKey', :pointer + # General Item Attribute Keys attach_variable 'kSecAttrAccess', :pointer - attach_variable 'kSecAttrAccount', :pointer - attach_variable 'kSecAttrAuthenticationType', :pointer - attach_variable 'kSecAttrComment', :pointer + attach_variable 'kSecAttrAccessControl', :pointer + attach_variable 'kSecAttrAccessible', :pointer + attach_variable 'kSecAttrAccessGroup', :pointer + attach_variable 'kSecAttrSynchronizable', :pointer attach_variable 'kSecAttrCreationDate', :pointer - attach_variable 'kSecAttrCreator', :pointer + attach_variable 'kSecAttrModificationDate', :pointer + attach_variable 'kSecAttrComment', :pointer attach_variable 'kSecAttrDescription', :pointer - attach_variable 'kSecAttrGeneric', :pointer + attach_variable 'kSecAttrCreator', :pointer + attach_variable 'kSecAttrType', :pointer + attach_variable 'kSecAttrLabel', :pointer attach_variable 'kSecAttrIsInvisible', :pointer attach_variable 'kSecAttrIsNegative', :pointer - attach_variable 'kSecAttrLabel', :pointer - attach_variable 'kSecAttrModificationDate', :pointer - attach_variable 'kSecAttrPath', :pointer - attach_variable 'kSecAttrPort', :pointer - attach_variable 'kSecAttrProtocol', :pointer + attach_variable 'kSecAttrSyncViewHint', :pointer + + # Password Attribute Keys + attach_variable 'kSecAttrAccount', :pointer + attach_variable 'kSecAttrService', :pointer + attach_variable 'kSecAttrGeneric', :pointer attach_variable 'kSecAttrSecurityDomain', :pointer attach_variable 'kSecAttrServer', :pointer - attach_variable 'kSecAttrService', :pointer - attach_variable 'kSecAttrType', :pointer + attach_variable 'kSecAttrProtocol', :pointer + attach_variable 'kSecAttrAuthenticationType', :pointer + attach_variable 'kSecAttrPort', :pointer + attach_variable 'kSecAttrPath', :pointer + # Item Search Matching Keys attach_variable 'kSecMatchSearchList', :pointer - attach_variable 'kSecMatchLimit', :pointer attach_variable 'kSecMatchLimitOne', :pointer attach_variable 'kSecMatchLimitAll', :pointer From 76a547c3fb8db9c882dab777a65e1d7aa3aa97fd Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 25 Jul 2017 21:49:22 -0700 Subject: [PATCH 14/22] Add concept of updateable attributes versus hard coding them in update method. --- lib/keychain/certificate.rb | 1 + lib/keychain/identity.rb | 1 + lib/keychain/item.rb | 2 ++ lib/keychain/key.rb | 41 +++++++++++++++++++++++++++++++------ lib/keychain/keychain.rb | 6 +----- 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/lib/keychain/certificate.rb b/lib/keychain/certificate.rb index 1b50738..883350d 100644 --- a/lib/keychain/certificate.rb +++ b/lib/keychain/certificate.rb @@ -148,6 +148,7 @@ class Certificate < Sec::Base CF::Base.typecast(Sec::kSecAttrSubjectKeyID) => :subject_key_id, CF::Base.typecast(Sec::kSecAttrPublicKeyHash) => :public_key_hash} + ATTR_UPDATABLE = Set.new(ATTR_MAP.values) OID_MAP = {CF::Base.typecast(Sec::kSecOIDADC_CERT_POLICY) => 'ADC_CERT_POLICY', CF::Base.typecast(Sec::kSecOIDAPPLE_CERT_POLICY) => 'APPLE_CERT_POLICY', diff --git a/lib/keychain/identity.rb b/lib/keychain/identity.rb index 7c93a44..198f097 100644 --- a/lib/keychain/identity.rb +++ b/lib/keychain/identity.rb @@ -13,6 +13,7 @@ class Identity < Sec::Base register_type 'SecIdentity' ATTR_MAP = Certificate::ATTR_MAP.merge(Key::ATTR_MAP) + ATTR_UPDATABLE = Certificate::ATTR_UPDATABLE.merge(Key::ATTR_UPDATABLE) INVERSE_ATTR_MAP = ATTR_MAP.invert define_attributes(ATTR_MAP) diff --git a/lib/keychain/item.rb b/lib/keychain/item.rb index 8c13c9c..69b98c5 100644 --- a/lib/keychain/item.rb +++ b/lib/keychain/item.rb @@ -32,6 +32,8 @@ class Item < Sec::Base CF::Base.typecast(Sec::kSecAttrType) => :type, CF::Base.typecast(Sec::kSecClass) => :klass} + ATTR_UPDATABLE = Set.new(ATTR_MAP.values - [:created_at, :updated_at]) + INVERSE_ATTR_MAP = ATTR_MAP.invert define_attributes(ATTR_MAP) diff --git a/lib/keychain/key.rb b/lib/keychain/key.rb index cb3444b..452ca05 100644 --- a/lib/keychain/key.rb +++ b/lib/keychain/key.rb @@ -11,14 +11,22 @@ module Sec rescue FFI::NotFoundError #Only available in 10.10 end - attach_variable 'kSecAttrAccessGroup', :pointer + # Cryptographic Key Attribute Keys attach_variable 'kSecAttrKeyClass', :pointer attach_variable 'kSecAttrApplicationLabel', :pointer - attach_variable 'kSecAttrIsPermanent', :pointer attach_variable 'kSecAttrApplicationTag', :pointer attach_variable 'kSecAttrKeyType', :pointer + attach_variable 'kSecAttrPRF', :pointer + attach_variable 'kSecAttrRounds', :pointer + attach_variable 'kSecAttrSalt', :pointer attach_variable 'kSecAttrKeySizeInBits', :pointer attach_variable 'kSecAttrEffectiveKeySize', :pointer + attach_variable 'kSecAttrTokenID', :pointer + + # Cryptographic Key Usage Attribute Keys + attach_variable 'kSecAttrIsPermanent', :pointer + attach_variable 'kSecAttrIsSensitive', :pointer + attach_variable 'kSecAttrIsExtractable', :pointer attach_variable 'kSecAttrCanEncrypt', :pointer attach_variable 'kSecAttrCanDecrypt', :pointer attach_variable 'kSecAttrCanDerive', :pointer @@ -83,15 +91,21 @@ class Key < Sec::Base KEY_CLASS_PRIVATE = CF::Base.typecast(Sec::kSecAttrKeyClassPrivate) KEY_CLASS_SYMMETRIC = CF::Base.typecast(Sec::kSecAttrKeyClassSymmetric) - ATTR_MAP = {CF::Base.typecast(Sec::kSecAttrAccessGroup) => :access_group, - CF::Base.typecast(Sec::kSecAttrKeyClass) => :key_class, + ATTR_MAP = {CF::Base.typecast(Sec::kSecAttrKeyClass) => :klass, CF::Base.typecast(Sec::kSecAttrLabel) => :label, CF::Base.typecast(Sec::kSecAttrApplicationLabel) => :application_label, - CF::Base.typecast(Sec::kSecAttrIsPermanent) => :is_permanent, CF::Base.typecast(Sec::kSecAttrApplicationTag) => :application_tag, CF::Base.typecast(Sec::kSecAttrKeyType) => :key_type, - CF::Base.typecast(Sec::kSecAttrKeySizeInBits) => :size_in_bites, + CF::Base.typecast(Sec::kSecAttrPRF) => :prf, + CF::Base.typecast(Sec::kSecAttrRounds) => :rounds, + CF::Base.typecast(Sec::kSecAttrSalt) => :salt, + CF::Base.typecast(Sec::kSecAttrKeySizeInBits) => :size_in_bits, CF::Base.typecast(Sec::kSecAttrEffectiveKeySize) => :effective_key_size, + CF::Base.typecast(Sec::kSecAttrTokenID) => :token_id, + + CF::Base.typecast(Sec::kSecAttrIsPermanent) => :is_permanent, + CF::Base.typecast(Sec::kSecAttrIsSensitive) => :is_sensitive, + CF::Base.typecast(Sec::kSecAttrIsExtractable) => :is_extractable, CF::Base.typecast(Sec::kSecAttrCanEncrypt) => :can_encrypt, CF::Base.typecast(Sec::kSecAttrCanDecrypt) => :can_decrypt, CF::Base.typecast(Sec::kSecAttrCanDerive) => :can_derive, @@ -100,12 +114,27 @@ class Key < Sec::Base CF::Base.typecast(Sec::kSecAttrCanWrap) => :can_wrap, CF::Base.typecast(Sec::kSecAttrCanUnwrap) => :can_unwrap} + ATTR_UPDATABLE = Set.new([:klass, + :label, + :application_tag, + :key_type, + :prf, + :rounds, + :salt, + :token_id, + :is_sensitive, + :is_extractable]) + ATTR_MAP[CF::Base.typecast(Sec::kSecAttrAccessible)] = :accessible if defined?(Sec::kSecAttrAccessible) ATTR_MAP[CF::Base.typecast(Sec::kSecAttrAccessControl)] = :access_control if defined?(Sec::kSecAttrAccessControl) INVERSE_ATTR_MAP = ATTR_MAP.invert define_attributes(ATTR_MAP) + def persisted? + true + end + def klass Sec::Classes::KEY.to_ruby end diff --git a/lib/keychain/keychain.rb b/lib/keychain/keychain.rb index c2024a4..15a4c2b 100644 --- a/lib/keychain/keychain.rb +++ b/lib/keychain/keychain.rb @@ -143,10 +143,8 @@ def import(input, app_paths = []) TrustedApplication.create_from_path(path) end - access = Access.create(path, apps) - key_params = Sec::SecItemImportExportKeyParameters.new - key_params[:accessRef] = access + key_params[:accessRef] = Access.create(path, apps) # Import item to the keychain cf_data = CF::Data.from_string(input).release_on_gc @@ -155,8 +153,6 @@ def import(input, app_paths = []) Sec.check_osstatus status item_array = CF::Base.typecast(cf_array.read_pointer).release_on_gc - access.release - item_array.to_ruby end From cd3d6b697edf5f15548f2bef6210bc5cfd47292c Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 1 Aug 2017 15:35:06 -0700 Subject: [PATCH 15/22] Add support for scope/search match keys. Also centralize all search options in Scope class. --- lib/keychain/scope.rb | 310 ++++++++++++++++++++++++------------------ lib/keychain/sec.rb | 19 --- spec/keychain_spec.rb | 2 +- 3 files changed, 181 insertions(+), 150 deletions(-) diff --git a/lib/keychain/scope.rb b/lib/keychain/scope.rb index 820e4d3..1d238cc 100644 --- a/lib/keychain/scope.rb +++ b/lib/keychain/scope.rb @@ -1,148 +1,198 @@ # A scope that represents the search for a keychain item -# -# -class Keychain::Scope - def initialize(kind, keychain=nil) - @kind = kind - @limit = nil - @keychains = [keychain].compact - @conditions = {} - end - # Adds conditions to the scope. Conditions are merged with any previously defined conditions. - # - # 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 - # Keychain::Protocols - # - # @param [Hash] conditions options to create the item with - # @option conditions [String] :account The account (user name) - # @option conditions [String] :comment A free text comment about the item - # @option conditions [Integer] :creator the item's creator, as the unsigned integer representation of a 4 char code - # @option conditions [String] :generic generic passwords can have a generic data attribute - # @option conditions [String] :invisible whether the item should be invisible - # @option conditions [String] :negative A negative item records that the user decided to never save a password - # @option conditions [String] :label A label for the item (Shown in keychain access) - # @option conditions [String] :path The path the password is associated with (internet passwords only) - # @option conditions [String] :port The path the password is associated with (internet passwords only) - # @option conditions [String] :port The protocol the password is associated with (internet passwords only) - # Should be one of the constants at Keychain::Protocols - # @option conditions [String] :domain the domain the password is associated with (internet passwords only) - # @option conditions [String] :server the host name the password is associated with (internet passwords only) - # @option conditions [String] :service the service the password is associated with (generic passwords only) - # @option conditions [Integer] :type the item's type, as the unsigned integer representation of a 4 char code - # - # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place - def where(conditions) - @conditions.merge! conditions - self - end +module Sec + attach_variable 'kSecMatchLimitAll', :pointer + attach_variable 'kSecMatchLimitOne', :pointer - # Sets the number of items returned by the scope - # - # @param [Integer] value The maximum number of items to return - # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place - def limit value - @limit = value - self - end + attach_variable 'kSecMatchPolicy', :pointer + attach_variable 'kSecMatchItemList', :pointer + attach_variable 'kSecMatchSearchList', :pointer + attach_variable 'kSecMatchIssuers', :pointer + attach_variable 'kSecMatchEmailAddressIfPresent', :pointer + attach_variable 'kSecMatchSubjectContains', :pointer + attach_variable 'kSecMatchSubjectStartsWith', :pointer + attach_variable 'kSecMatchSubjectEndsWith', :pointer + attach_variable 'kSecMatchSubjectWholeString', :pointer + attach_variable 'kSecMatchCaseInsensitive', :pointer + attach_variable 'kSecMatchDiacriticInsensitive', :pointer + attach_variable 'kSecMatchWidthInsensitive', :pointer + attach_variable 'kSecMatchTrustedOnly', :pointer + attach_variable 'kSecMatchValidOnDate', :pointer + attach_variable 'kSecMatchLimit', :pointer - # Set the list of keychains to search - # - # @param [Array] keychains The maximum number of items to return - # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place - def in *keychains - @keychains = keychains.flatten - self + # Query options for use with SecCopyMatching, SecItemUpdate + module Query + #key identifying the class of an item (kSecClass) + CLASS = CF::Base.typecast(Sec.kSecClass) + #key speciying the list of keychains to search (kSecMatchSearchList) + SEARCH_LIST = CF::Base.typecast(Sec.kSecMatchSearchList) + #key indicating the list of specific keychain items to the scope the search to + ITEM_LIST = CF::Base.typecast(Sec.kSecMatchItemList) + #key indicating whether to return attributes (kSecReturnAttributes) + RETURN_ATTRIBUTES = CF::Base.typecast(Sec.kSecReturnAttributes) + #key indicating whether to return the SecKeychainItemRef (kSecReturnRef) + RETURN_REF = CF::Base.typecast(Sec.kSecReturnRef) + #key indicating whether to return the password data (kSecReturnData) + RETURN_DATA = CF::Base.typecast(Sec.kSecReturnData) + #key indicating which keychain to use for the operation (kSecUseKeychain) + KEYCHAIN = CF::Base.typecast(Sec.kSecUseKeychain) end +end - # Returns the first matching item in the scope - # - # @return [Keychain::Item, nil] - def first - query = to_query - execute(query).first - end +module Keychain + class Scope + # Match attributes that can be used to search for keychain items, see #where + ATTR_MAP = {CF::Base.typecast(Sec::kSecMatchPolicy) => :policy, + CF::Base.typecast(Sec::kSecMatchItemList) => :item_list, + CF::Base.typecast(Sec::kSecMatchSearchList) => :search_list, + CF::Base.typecast(Sec::kSecMatchIssuers) => :issuers, + CF::Base.typecast(Sec::kSecMatchEmailAddressIfPresent) => :email_address_if_present, + CF::Base.typecast(Sec::kSecMatchSubjectContains) => :subject_contains, + CF::Base.typecast(Sec::kSecMatchSubjectStartsWith) => :subject_starts_with, + CF::Base.typecast(Sec::kSecMatchSubjectEndsWith) => :subject_ends_with, + CF::Base.typecast(Sec::kSecMatchSubjectWholeString) => :subject_whole_string, + CF::Base.typecast(Sec::kSecMatchCaseInsensitive) => :case_insensitive, + CF::Base.typecast(Sec::kSecMatchDiacriticInsensitive) => :diacritic_insensitive, + CF::Base.typecast(Sec::kSecMatchWidthInsensitive) => :width_insensitive, + CF::Base.typecast(Sec::kSecMatchTrustedOnly) => :trusted_only, + CF::Base.typecast(Sec::kSecMatchValidOnDate) => :valid_on_date, + CF::Base.typecast(Sec::kSecMatchLimit) => :limit} - # Returns an array containing all of the matching items - # - # @return [Array] The matching items. May be empty - def all - query = to_query - query[Sec::Search::LIMIT] = @limit ? @limit.to_cf : Sec::Search::ALL - execute query - end + INVERSE_ATTR_MAP = ATTR_MAP.invert - # Creates a new keychain item - # - # @param [Hash] attributes options to create the item with - # @option attributes [String] :account The account (user name) - # @option attributes [String] :comment A free text comment about the item - # @option attributes [Integer] :creator the item's creator, as the unsigned integer representation of a 4 char code - # @option attributes [String] :generic generic passwords can have a generic data attribute - # @option attributes [String] :invisible whether the item should be invisible - # @option attributes [String] :negative A negative item records that the user decided to never save a password - # @option attributes [String] :label A label for the item (Shown in keychain access) - # @option attributes [String] :path The path the password is associated with (internet passwords only) - # @option attributes [String] :port The path the password is associated with (internet passwords only) - # @option attributes [String] :port The protocol the password is associated with (internet passwords only) - # Should be one of the constants at Keychain::Protocols - # @option attributes [String] :domain the domain the password is associated with (internet passwords only) - # @option attributes [String] :server the host name the password is associated with (internet passwords only) - # @option attributes [String] :service the service the password is associated with (generic passwords only) - # @option attributes [Integer] :type the item's type, as the unsigned integer representation of a 4 char code - # - # @return [Keychain::Item] - def create(attributes) - raise "You must specify a password" unless attributes[:password] - - Keychain::Item.new(attributes.merge(:klass => @kind)).save!(:keychain => @keychains.first) - end + def initialize(kind, keychain=nil) + @kind = kind + @keychains = [keychain].compact + @conditions = {} + end - private + # Adds search conditions to the scope. Conditions are merged with any previously defined conditions. + # + # The list of allowed conditions depends on the type of keychain item. See the ATTR_MAP constant + # in the Certificate, Identity, Item and Key classes for allowed values. + # + # In addition, conditions can also use the values in Scope::ATTR_MAP. Those values provide greater control + # over how the search is performed and how many results are returned. + # + # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place + def where(conditions) + @conditions.merge! conditions + self + end - def execute query - result = FFI::MemoryPointer.new :pointer - status = Sec.SecItemCopyMatching(query, result) - if status == Sec.enum_value( :errSecItemNotFound) - return [] + # Set the list of keychains to search + # + # @param [Array] keychains The maximum number of items to return + # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place + def in *keychains + @keychains = keychains.flatten + self end - Sec.check_osstatus(status) - result = CF::Base.typecast(result.read_pointer).release_on_gc - unless result.is_a?(CF::Array) - result = CF::Array.immutable([result]) + + # Returns the first matching item in the scope + # + # @return [Keychain::Item, nil] + def first + where(:limit => CF::Base.typecast(Sec.kSecMatchLimitOne)) unless @conditions.include?(:limit) + execute(to_query).first end - result.collect do |dictionary_of_attributes| - item = dictionary_of_attributes[Sec::Value::REF] - item.update_self_from_dictionary(dictionary_of_attributes) - item + + # Returns an array containing all of the matching items + # + # @return [Array] The matching items. May be empty + def all + where(:limit => CF::Base.typecast(Sec.kSecMatchLimitAll)) unless @conditions.include?(:limit) + execute(to_query) end - end - def to_query - query = CF::Dictionary.mutable - # This is terrible but we need to know the result class to get the list of attributes - inverse_attributes = case @kind - when Sec::Classes::CERTIFICATE - Keychain::Certificate::INVERSE_ATTR_MAP - when Sec::Classes::GENERIC - Keychain::Item::INVERSE_ATTR_MAP - when Sec::Classes::IDENTITY - Keychain::Identity::INVERSE_ATTR_MAP - when Sec::Classes::INTERNET - Keychain::Item::INVERSE_ATTR_MAP - when Sec::Classes::KEY - Keychain::Key::INVERSE_ATTR_MAP - end - - @conditions.each do |k,v| - k = inverse_attributes[k] - query[k] = v.to_cf + # Creates a new keychain item + # + # @param [Hash] attributes options to create the item with + # @option attributes [String] :account The account (user name) + # @option attributes [String] :comment A free text comment about the item + # @option attributes [Integer] :creator the item's creator, as the unsigned integer representation of a 4 char code + # @option attributes [String] :generic generic passwords can have a generic data attribute + # @option attributes [String] :invisible whether the item should be invisible + # @option attributes [String] :negative A negative item records that the user decided to never save a password + # @option attributes [String] :label A label for the item (Shown in keychain access) + # @option attributes [String] :path The path the password is associated with (internet passwords only) + # @option attributes [String] :port The path the password is associated with (internet passwords only) + # @option attributes [String] :port The protocol the password is associated with (internet passwords only) + # Should be one of the constants at Keychain::Protocols + # @option attributes [String] :domain the domain the password is associated with (internet passwords only) + # @option attributes [String] :server the host name the password is associated with (internet passwords only) + # @option attributes [String] :service the service the password is associated with (generic passwords only) + # @option attributes [Integer] :type the item's type, as the unsigned integer representation of a 4 char code + # + # @return [Keychain::Item] + def create(attributes) + raise "You must specify a password" unless attributes[:password] + + Item.new(attributes.merge(:klass => @kind)).save!(:keychain => @keychains.first) end - query[Sec::Query::CLASS] = @kind - query[Sec::Query::SEARCH_LIST] = CF::Array.immutable(@keychains) if @keychains && @keychains.any? - query[Sec::Query::RETURN_ATTRIBUTES] = CF::Boolean::TRUE - query[Sec::Query::RETURN_REF] = CF::Boolean::TRUE - query + private + + def execute query + result = FFI::MemoryPointer.new :pointer + status = Sec.SecItemCopyMatching(query, result) + if status == Sec.enum_value( :errSecItemNotFound) + return [] + end + Sec.check_osstatus(status) + result = CF::Base.typecast(result.read_pointer).release_on_gc + unless result.is_a?(CF::Array) + result = CF::Array.immutable([result]) + end + result.collect do |dictionary_of_attributes| + item = dictionary_of_attributes[Sec::Value::REF] + item.update_self_from_dictionary(dictionary_of_attributes) + item + end + end + + def to_query + query = CF::Dictionary.mutable + + # Specify what type of keychain item we are looking for + query[Sec::Query::CLASS] = @kind + + # Specify the keychains to search + query[Sec::Query::SEARCH_LIST] = CF::Array.immutable(@keychains) if @keychains && @keychains.any? + + # Return attributes for found items + query[Sec::Query::RETURN_ATTRIBUTES] = CF::Boolean::TRUE + + # Return references for found items + query[Sec::Query::RETURN_REF] = CF::Boolean::TRUE + + # Now add user specified values + inverse_attributes = case @kind + when Sec::Classes::CERTIFICATE + Certificate::INVERSE_ATTR_MAP + when Sec::Classes::GENERIC + Item::INVERSE_ATTR_MAP + when Sec::Classes::IDENTITY + Identity::INVERSE_ATTR_MAP + when Sec::Classes::INTERNET + Item::INVERSE_ATTR_MAP + when Sec::Classes::KEY + Key::INVERSE_ATTR_MAP + end + + @conditions.each do |key, value| + key_cf = inverse_attributes[key] || INVERSE_ATTR_MAP[key] + if key_cf.nil? + raise "Unknown search key: #{key}. Type: #{@kind}. Please look at the class's ATTR_MAP constant for accepted keys" + end + if value.nil? + raise "Nil search values are not accepted" + end + + query[key_cf] = value.to_cf + end + + query + end end end diff --git a/lib/keychain/sec.rb b/lib/keychain/sec.rb index bd1317e..38fb4c7 100644 --- a/lib/keychain/sec.rb +++ b/lib/keychain/sec.rb @@ -64,25 +64,6 @@ module Sec attach_variable 'kSecValueData', :pointer attach_variable 'kSecUseKeychain', :pointer - # Query options for use with SecCopyMatching, SecItemUpdate - # - module Query - #key identifying the class of an item (kSecClass) - CLASS = CF::Base.typecast(Sec.kSecClass) - #key speciying the list of keychains to search (kSecMatchSearchList) - SEARCH_LIST = CF::Base.typecast(Sec.kSecMatchSearchList) - #key indicating the list of specific keychain items to the scope the search to - ITEM_LIST = CF::Base.typecast(Sec.kSecMatchItemList) - #key indicating whether to return attributes (kSecReturnAttributes) - RETURN_ATTRIBUTES = CF::Base.typecast(Sec.kSecReturnAttributes) - #key indicating whether to return the SecKeychainItemRef (kSecReturnRef) - RETURN_REF = CF::Base.typecast(Sec.kSecReturnRef) - #key indicating whether to return the password data (kSecReturnData) - RETURN_DATA = CF::Base.typecast(Sec.kSecReturnData) - #key indicating which keychain to use for the operation (kSecUseKeychain) - KEYCHAIN = CF::Base.typecast(Sec.kSecUseKeychain) - end - # defines constants for use as the class of an item module Classes # constant identifying certificates (kSecClassCertificate) diff --git a/spec/keychain_spec.rb b/spec/keychain_spec.rb index db5b812..2a2f8a8 100644 --- a/spec/keychain_spec.rb +++ b/spec/keychain_spec.rb @@ -197,7 +197,7 @@ context 'when the limit is option is set' do it 'should limit the return set' do - expect(Keychain.send(subject).where(search_arguments_with_multiple_results).limit(1).all.length).to eq(1) + expect(Keychain.send(subject).where(search_arguments_with_multiple_results.merge(:limit => 1)).all.length).to eq(1) end end From 3aed2879e406b9284942364ea2c27d2f42152bda Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 1 Aug 2017 15:35:15 -0700 Subject: [PATCH 16/22] Add a couple of missing attributes. --- lib/keychain/certificate.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/keychain/certificate.rb b/lib/keychain/certificate.rb index 883350d..c73fc27 100644 --- a/lib/keychain/certificate.rb +++ b/lib/keychain/certificate.rb @@ -139,12 +139,14 @@ class Certificate < Sec::Base register_type 'SecCertificate' ATTR_MAP = {CF::Base.typecast(Sec::kSecAttrAccessGroup) => :access_group, + CF::Base.typecast(Sec::kSecAttrAccessible) => :accessible, CF::Base.typecast(Sec::kSecAttrCertificateType) => :certificate_type, CF::Base.typecast(Sec::kSecAttrCertificateEncoding) => :certificate_encoding, CF::Base.typecast(Sec::kSecAttrLabel) => :label, CF::Base.typecast(Sec::kSecAttrSubject) => :subject, CF::Base.typecast(Sec::kSecAttrIssuer) => :issuer, CF::Base.typecast(Sec::kSecAttrSerialNumber) => :serial_number, + CF::Base.typecast(Sec::kSecAttrSynchronizable) => :synchronizable, CF::Base.typecast(Sec::kSecAttrSubjectKeyID) => :subject_key_id, CF::Base.typecast(Sec::kSecAttrPublicKeyHash) => :public_key_hash} From 38bc125b9343f34b10fff11251cf09069187b029 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 1 Aug 2017 15:35:23 -0700 Subject: [PATCH 17/22] Formatting. --- lib/keychain/item.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/keychain/item.rb b/lib/keychain/item.rb index 69b98c5..d0be644 100644 --- a/lib/keychain/item.rb +++ b/lib/keychain/item.rb @@ -1,4 +1,3 @@ - module Sec attach_function 'SecKeychainItemDelete', [:pointer], :osstatus attach_function 'SecKeychainItemCopyKeychain', [:pointer, :pointer], :osstatus From 4c8c5c41a8a2fefea7772b5fbbb1f8e219476386 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 1 Aug 2017 15:35:32 -0700 Subject: [PATCH 18/22] Add helper method for querying certificates. --- lib/keychain/keychain.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/keychain/keychain.rb b/lib/keychain/keychain.rb index 15a4c2b..9098477 100644 --- a/lib/keychain/keychain.rb +++ b/lib/keychain/keychain.rb @@ -115,6 +115,13 @@ def generic_passwords Scope.new(Sec::Classes::GENERIC, self) end + # Returns a scope for the certificates contained in this keychain + # + # @return [Keychain::Scope] a new scope object + def certificates + Scope.new(Sec::Classes::CERTIFICATE, self) + end + # Returns a scope for the identities (certificates and private keys) contained in this keychain # # @return [Keychain::Scope] a new scope object From 187c8c82cf275bb5d6d57c2002edd82e29db89e1 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 1 Aug 2017 15:35:46 -0700 Subject: [PATCH 19/22] Add in needed require. --- lib/keychain.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/keychain.rb b/lib/keychain.rb index 8ce8290..fe0be7b 100644 --- a/lib/keychain.rb +++ b/lib/keychain.rb @@ -1,4 +1,5 @@ require 'ffi' +require 'set' require 'corefoundation' require 'keychain/sec' require 'keychain/access' From 8652747337b93a0da856ac8e8f7d0538e54a629d Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 1 Aug 2017 15:35:58 -0700 Subject: [PATCH 20/22] Add rake tasks for packaging gem. --- Rakefile | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 8e2175c..5c12864 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,15 @@ +require 'rubygems/package_task' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new('spec') -task :default => :spec \ No newline at end of file +task :default => :spec + +# Read the spec file +spec = Gem::Specification.load('keychain.gemspec') + +# Setup gem package task +Gem::PackageTask.new(spec) do |pkg| + pkg.package_dir = 'pkg' + pkg.need_tar = false +end From 1b48ace3366f2efadde78e41618e6ea8f9cd5c91 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Tue, 1 Aug 2017 15:36:12 -0700 Subject: [PATCH 21/22] Update dependencies and versions. --- keychain.gemspec | 2 +- lib/keychain/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/keychain.gemspec b/keychain.gemspec index 06a36bd..585438a 100644 --- a/keychain.gemspec +++ b/keychain.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |s| s.summary = %q{Ruby wrapper for OS X's keychain} s.required_ruby_version = '>= 1.9.2' - s.add_runtime_dependency "ffi" + s.add_runtime_dependency "ffi", ">= 1.9.18" s.add_runtime_dependency "corefoundation", "~>0.2.0" s.add_development_dependency "rspec", '>= 3.6.0' s.add_development_dependency "rake", '>= 12.0.0' diff --git a/lib/keychain/version.rb b/lib/keychain/version.rb index ca287e0..f81b213 100644 --- a/lib/keychain/version.rb +++ b/lib/keychain/version.rb @@ -1,4 +1,4 @@ module Keychain # The current version string - VERSION = '0.3.1' + VERSION = '0.4.0' end From 607933a775566fde2747a3ba20911e0dc086f2f7 Mon Sep 17 00:00:00 2001 From: Charlie Savage Date: Mon, 25 Sep 2017 00:44:57 -0700 Subject: [PATCH 22/22] By default trust the application importing the keychain item. --- lib/keychain/keychain.rb | 2 +- lib/keychain/trusted_application.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/keychain/keychain.rb b/lib/keychain/keychain.rb index 9098477..d91befe 100644 --- a/lib/keychain/keychain.rb +++ b/lib/keychain/keychain.rb @@ -143,7 +143,7 @@ def keys # permitted to access imported items # @return [Array ] List of imported keychain objects, # each of which may be a SecCertificate, SecKey, or SecIdentity instance - def import(input, app_paths = []) + def import(input, app_paths = [nil]) input = input.read if input.is_a? IO apps = app_paths.map do |path| diff --git a/lib/keychain/trusted_application.rb b/lib/keychain/trusted_application.rb index 9c1ae0f..5baad0c 100644 --- a/lib/keychain/trusted_application.rb +++ b/lib/keychain/trusted_application.rb @@ -4,7 +4,8 @@ class TrustedApplication < Sec::Base def self.create_from_path(path) trusted_app_buffer = FFI::MemoryPointer.new(:pointer) - status = Sec.SecTrustedApplicationCreateFromPath(path.encode(Encoding::UTF_8), trusted_app_buffer) + path = path ? path.encode(Encoding::UTF_8) : nil + status = Sec.SecTrustedApplicationCreateFromPath(path, trusted_app_buffer) Sec.check_osstatus(status) self.new(trusted_app_buffer.read_pointer).release_on_gc end