Skip to content

Commit

Permalink
Add accessibility support to SecKey, SecKeyPair, SecCertificate
Browse files Browse the repository at this point in the history
… & `SecIdentity`

Allows selecting accessibility (i.e. `kSecAttrAccessible`) when generating or saving items.

As a requirement we add `kSecUseDataProtectionKeychain` to keychain requests. This has the benefit of normalizing keychain access across macOS & iOS, tvOS, watchOS); allowing the removal of the last usage of `#if os(macOS)/#endif` when accessing the keychain.

`SecKey.attributes()` now retrieves _all_ attributes from using `SecItemCopyMatching` instead of  `SecKeyCopyAttributes`; this is backward compatible with previous functionality with _more_ attributes. `SecKey.keyAttributes()` can be used to get previous _smaller_ list of attributes (which may also be faster).
  • Loading branch information
kdubb committed Oct 16, 2023
1 parent a3ed0f8 commit 8a16c9b
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 87 deletions.
2 changes: 1 addition & 1 deletion Sources/ShieldSecurity/AlgorithmIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public extension AlgorithmIdentifier {

case .ec:
let curve: OID
switch try publicKey.attributes()[kSecAttrKeySizeInBits as String] as? Int ?? 0 {
switch try publicKey.keyAttributes()[kSecAttrKeySizeInBits as String] as? Int ?? 0 {
case 192:
// P-192, secp192r1
curve = iso.memberBody.us.ansix962.curves.prime.prime192v1.oid
Expand Down
55 changes: 55 additions & 0 deletions Sources/ShieldSecurity/SecAccessibility.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// SecAccessibility.swift
// Shield
//
// Copyright © 2021 Outfox, inc.
//
//
// Distributed under the MIT License, See LICENSE for details.
//

import Security


public enum SecAccessibility: Equatable {
case `default`
case unlocked(afterFirst: Bool, shared: Bool)
case passcodeEnabled
#if ACCESSIBILITY_ALWAYS_ENABLED
case always(shared: Bool)
#endif
}


extension SecAccessibility {

var attr: Any {

switch self {

#if ACCESSIBILITY_ALWAYS_ENABLED
case .always(shared: true):
return kSecAttrAccessibleAlways as String

case .always(shared: false):
return kSecAttrAccessibleAlwaysThisDeviceOnly as String
#endif

case .unlocked(afterFirst: true, shared: true):
return kSecAttrAccessibleAfterFirstUnlock as String

case .unlocked(afterFirst: true, shared: false):
return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String

case .unlocked(afterFirst: false, shared: true), .default:
return kSecAttrAccessibleWhenUnlocked as String

case .unlocked(afterFirst: false, shared: false):
return kSecAttrAccessibleWhenUnlockedThisDeviceOnly as String

case .passcodeEnabled:
return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly as String
}
}

}
64 changes: 34 additions & 30 deletions Sources/ShieldSecurity/SecCertificate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public enum SecCertificateError: Int, Error {
case trustValidationError = 5
case publicKeyRetrievalFailed = 6
case parsingFailed = 7
case saveDuplicate = 8
}


Expand Down Expand Up @@ -224,49 +225,52 @@ public extension SecCertificate {

func attributes() throws -> [String: Any] {

#if os(iOS) || os(watchOS) || os(tvOS)

let query = [
kSecReturnAttributes as String: kCFBooleanTrue!,
kSecValueRef as String: self,
] as [String: Any] as CFDictionary

var data: CFTypeRef?
let query: [String: Any] = [
kSecReturnAttributes as String: true,
kSecValueRef as String: self,
kSecUseDataProtectionKeychain as String: true
]

let status = SecItemCopyMatching(query as CFDictionary, &data)
if status != errSecSuccess {
throw SecCertificateError.queryFailed
}
var data: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &data)

#elseif os(macOS)
guard status == errSecSuccess, let attrs = data as? [String: Any] else {
throw SecCertificateError.queryFailed
}

let query: [String: Any] = [
kSecReturnAttributes as String: kCFBooleanTrue!,
kSecUseItemList as String: [self] as CFArray,
]
return attrs
}

var data: AnyObject?
func save(accessibility: SecAccessibility = .default) throws {

let status = SecItemCopyMatching(query as CFDictionary, &data)
if status != errSecSuccess {
throw SecCertificateError.queryFailed
}
let query: [String: Any] = [
kSecClass as String: kSecClassCertificate,
kSecAttrLabel as String: UUID().uuidString,
kSecValueRef as String: self,
kSecAttrAccessible as String: accessibility.attr,
kSecUseDataProtectionKeychain as String: true
]

#endif
var data: CFTypeRef?
let status = SecItemAdd(query as CFDictionary, &data)

return data as! [String: Any] // swiftlint:disable:this force_cast
if status == errSecDuplicateItem {
throw SecCertificateError.saveDuplicate
}
else if status != errSecSuccess {
throw SecCertificateError.saveFailed
}
}

func save() throws {
func delete() throws {

let query = [
let query: [String: Any] = [
kSecClass as String: kSecClassCertificate,
kSecValueRef as String: self,
] as [String: Any] as CFDictionary

var data: CFTypeRef?
kSecUseDataProtectionKeychain as String: true
]

let status = SecItemAdd(query, &data)
let status = SecItemDelete(query as CFDictionary)

if status != errSecSuccess {
throw SecCertificateError.saveFailed
Expand Down
47 changes: 26 additions & 21 deletions Sources/ShieldSecurity/SecIdentity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,40 @@ public extension SecIdentity {
case copyCertificateFailed
}

static func create(certificate: SecCertificate, keyPair: SecKeyPair) throws -> SecIdentity {
static func create(
certificate: SecCertificate,
keyPair: SecKeyPair,
accessibility: SecAccessibility = .default
) throws -> SecIdentity {

return try create(certificate: certificate, privateKey: keyPair.privateKey)
return try create(certificate: certificate, privateKey: keyPair.privateKey, accessibility: accessibility)
}

static func create(certificate: SecCertificate, privateKey: SecKey) throws -> SecIdentity {
static func create(
certificate: SecCertificate,
privateKey: SecKey,
accessibility: SecAccessibility = .default
) throws -> SecIdentity {

do {
try privateKey.save()
}
catch SecKey.Error.saveDuplicate {
// Allowable...
do {
try privateKey.save(accessibility: accessibility)
}
catch SecKey.Error.saveDuplicate {
// Allowable...
}

do {
try certificate.save(accessibility: accessibility)
}
catch SecKey.Error.saveDuplicate {
// Allowable...
}
}
catch {
throw Error.saveFailed
}

let query: [String: Any] = [
kSecClass as String: kSecClassCertificate,
kSecAttrLabel as String: UUID().uuidString,
kSecValueRef as String: certificate,
]

var data: CFTypeRef?

let status = SecItemAdd(query as CFDictionary, &data)

if status != errSecSuccess {
try? privateKey.delete()
try? certificate.delete()

throw Error.saveFailed
}

Expand Down
53 changes: 33 additions & 20 deletions Sources/ShieldSecurity/SecKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public extension SecKey {
let query: [String: Any] = [
kSecValueRef as String: self,
kSecReturnPersistentRef as String: kCFBooleanTrue!,
kSecUseDataProtectionKeychain as String: true,
]

var ref: CFTypeRef?
Expand All @@ -80,6 +81,7 @@ public extension SecKey {
let query: [String: Any] = [
kSecValuePersistentRef as String: pref,
kSecReturnRef as String: kCFBooleanTrue!,
kSecUseDataProtectionKeychain as String: true,
]

var ref: CFTypeRef?
Expand Down Expand Up @@ -108,8 +110,8 @@ public extension SecKey {

var error: Unmanaged<CFError>?

guard let key = SecKeyCreateWithData(data as CFData, attrs, &error), error == nil else {
throw error!.takeRetainedValue()
guard let key = SecKeyCreateWithData(data as CFData, attrs, &error) else {
throw error?.takeRetainedValue() ?? Error.importFailed
}

return key
Expand All @@ -120,13 +122,13 @@ public extension SecKey {
var error: Unmanaged<CFError>?

guard let data = SecKeyCopyExternalRepresentation(self, &error) else {
throw error!.takeRetainedValue()
throw error?.takeRetainedValue() ?? Error.exportFailed
}

return data as Data
}

func attributes() throws -> [String: Any] {
func keyAttributes() throws -> [String: Any] {

guard let attrs = SecKeyCopyAttributes(self) as? [String: Any] else {
throw Error.noAttributes
Expand All @@ -135,6 +137,24 @@ public extension SecKey {
return attrs
}

func attributes() throws -> [String: Any] {

let query: [String: Any] = [
kSecReturnAttributes as String: kCFBooleanTrue!,
kSecValueRef as String: self,
kSecUseDataProtectionKeychain as String: true,
]

var data: CFTypeRef?

let status = SecItemCopyMatching(query as CFDictionary, &data)
guard status == errSecSuccess, let attrs = data as? [String: Any] else {
throw Error.build(error: .queryFailed, message: "Unable to load attributes", status: status)
}

return attrs
}

func keyType() throws -> SecKeyType {

let typeStr = try self.type() as CFString
Expand All @@ -148,7 +168,7 @@ public extension SecKey {

func type() throws -> String {

let attrs = try attributes()
let attrs = try keyAttributes()

// iOS 10 SecKeyCopyAttributes returns string values, SecItemCopyMatching returns number values
guard let type = (attrs[kSecAttrKeyType as String] as? NSNumber)?
Expand All @@ -159,14 +179,16 @@ public extension SecKey {
return type
}

func save() throws {
func save(accessibility: SecAccessibility = .default) throws {

let attrs = try attributes()
let attrs = try keyAttributes()

let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrKeyClass as String: attrs[kSecAttrKeyClass as String]!,
kSecValueRef as String: self,
kSecUseDataProtectionKeychain as String: true,
kSecAttrAccessible as String: accessibility.attr,
]

let status = SecItemAdd(query as CFDictionary, nil)
Expand All @@ -190,6 +212,7 @@ public extension SecKey {
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecValuePersistentRef as String: ref,
kSecUseDataProtectionKeychain as String: true,
]

let status = SecItemDelete(query as CFDictionary)
Expand Down Expand Up @@ -235,12 +258,7 @@ public extension SecKey {
var error: Unmanaged<CFError>?

guard let cipherText = SecKeyCreateEncryptedData(self, algorithm, plainText as CFData, &error) else {
if let error = error {
throw error.takeRetainedValue()
}
else {
throw Error.decryptionFailed
}
throw error?.takeRetainedValue() ?? Error.encryptionFailed
}

return cipherText as Data
Expand Down Expand Up @@ -283,12 +301,7 @@ public extension SecKey {
var error: Unmanaged<CFError>?

guard let plainText = SecKeyCreateDecryptedData(self, algorithm, cipherText as CFData, &error) else {
if let error = error {
throw error.takeRetainedValue()
}
else {
throw Error.decryptionFailed
}
throw error?.takeRetainedValue() ?? Error.decryptionFailed
}

return plainText as Data
Expand Down Expand Up @@ -376,7 +389,7 @@ public extension SecKey {
var error: Unmanaged<CFError>?

guard let signature = SecKeyCreateSignature(self, algorithm, data as CFData, &error) else {
throw error!.takeRetainedValue()
throw error?.takeRetainedValue() ?? Error.signFailed
}

return signature as Data
Expand Down
Loading

0 comments on commit 8a16c9b

Please sign in to comment.