mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-04-10 17:47:19 +00:00
Merge branch 'main' into patch-1
This commit is contained in:
commit
de4da44524
@ -32,3 +32,9 @@ SecretKit is a collection of protocols describing secrets and stores.
|
|||||||
### Authentication Persistence
|
### Authentication Persistence
|
||||||
|
|
||||||
- ``PersistedAuthenticationContext``
|
- ``PersistedAuthenticationContext``
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
|
||||||
|
- ``KeychainError``
|
||||||
|
- ``SigningError``
|
||||||
|
- ``SecurityError``
|
||||||
|
@ -10,6 +10,7 @@ public class AnySecretStore: SecretStore {
|
|||||||
private let _name: () -> String
|
private let _name: () -> String
|
||||||
private let _secrets: () -> [AnySecret]
|
private let _secrets: () -> [AnySecret]
|
||||||
private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data
|
private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data
|
||||||
|
private let _verify: (Data, Data, AnySecret) throws -> Bool
|
||||||
private let _existingPersistedAuthenticationContext: (AnySecret) -> PersistedAuthenticationContext?
|
private let _existingPersistedAuthenticationContext: (AnySecret) -> PersistedAuthenticationContext?
|
||||||
private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void
|
private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void
|
||||||
private let _reloadSecrets: () -> Void
|
private let _reloadSecrets: () -> Void
|
||||||
@ -23,6 +24,7 @@ public class AnySecretStore: SecretStore {
|
|||||||
_id = { secretStore.id }
|
_id = { secretStore.id }
|
||||||
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
||||||
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
|
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
|
||||||
|
_verify = { try secretStore.verify(signature: $0, for: $1, with: $2.base as! SecretStoreType.SecretType) }
|
||||||
_existingPersistedAuthenticationContext = { secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
|
_existingPersistedAuthenticationContext = { secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
|
||||||
_persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
|
_persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
|
||||||
_reloadSecrets = { secretStore.reloadSecrets() }
|
_reloadSecrets = { secretStore.reloadSecrets() }
|
||||||
@ -51,6 +53,10 @@ public class AnySecretStore: SecretStore {
|
|||||||
try _sign(data, secret, provenance)
|
try _sign(data, secret, provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func verify(signature: Data, for data: Data, with secret: AnySecret) throws -> Bool {
|
||||||
|
try _verify(signature, data, secret)
|
||||||
|
}
|
||||||
|
|
||||||
public func existingPersistedAuthenticationContext(secret: AnySecret) -> PersistedAuthenticationContext? {
|
public func existingPersistedAuthenticationContext(secret: AnySecret) -> PersistedAuthenticationContext? {
|
||||||
_existingPersistedAuthenticationContext(secret)
|
_existingPersistedAuthenticationContext(secret)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
public func KeychainDictionary(_ dictionary: [CFString: Any]) -> CFDictionary {
|
|
||||||
dictionary as CFDictionary
|
|
||||||
}
|
|
71
Sources/Packages/Sources/SecretKit/KeychainTypes.swift
Normal file
71
Sources/Packages/Sources/SecretKit/KeychainTypes.swift
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public typealias SecurityError = Unmanaged<CFError>
|
||||||
|
|
||||||
|
/// Wraps a Swift dictionary in a CFDictionary.
|
||||||
|
/// - Parameter dictionary: The Swift dictionary to wrap.
|
||||||
|
/// - Returns: A CFDictionary containing the keys and values.
|
||||||
|
public func KeychainDictionary(_ dictionary: [CFString: Any]) -> CFDictionary {
|
||||||
|
dictionary as CFDictionary
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension CFError {
|
||||||
|
|
||||||
|
/// The CFError returned when a verification operation fails.
|
||||||
|
static let verifyError = CFErrorCreate(nil, NSOSStatusErrorDomain as CFErrorDomain, CFIndex(errSecVerifyFailed), nil)!
|
||||||
|
|
||||||
|
/// Equality operation that only considers domain and code.
|
||||||
|
static func ~=(lhs: CFError, rhs: CFError) -> Bool {
|
||||||
|
CFErrorGetDomain(lhs) == CFErrorGetDomain(rhs) && CFErrorGetCode(lhs) == CFErrorGetCode(rhs)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around an error code reported by a Keychain API.
|
||||||
|
public struct KeychainError: Error {
|
||||||
|
/// The status code involved, if one was reported.
|
||||||
|
public let statusCode: OSStatus?
|
||||||
|
|
||||||
|
/// Initializes a KeychainError with an optional error code.
|
||||||
|
/// - Parameter statusCode: The status code returned by the keychain operation, if one is applicable.
|
||||||
|
public init(statusCode: OSStatus?) {
|
||||||
|
self.statusCode = statusCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A signing-related error.
|
||||||
|
public struct SigningError: Error {
|
||||||
|
/// The underlying error reported by the API, if one was returned.
|
||||||
|
public let error: SecurityError?
|
||||||
|
|
||||||
|
/// Initializes a SigningError with an optional SecurityError.
|
||||||
|
/// - Parameter statusCode: The SecurityError, if one is applicable.
|
||||||
|
public init(error: SecurityError?) {
|
||||||
|
self.error = error
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension SecretStore {
|
||||||
|
|
||||||
|
/// Returns the appropriate keychian signature algorithm to use for a given secret.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - secret: The secret which will be used for signing.
|
||||||
|
/// - allowRSA: Whether or not RSA key types should be permited.
|
||||||
|
/// - Returns: The appropriate algorithm.
|
||||||
|
func signatureAlgorithm(for secret: SecretType, allowRSA: Bool = false) -> SecKeyAlgorithm {
|
||||||
|
switch (secret.algorithm, secret.keySize) {
|
||||||
|
case (.ellipticCurve, 256):
|
||||||
|
return .ecdsaSignatureMessageX962SHA256
|
||||||
|
case (.ellipticCurve, 384):
|
||||||
|
return .ecdsaSignatureMessageX962SHA384
|
||||||
|
case (.rsa, 1024), (.rsa, 2048):
|
||||||
|
guard allowRSA else { fatalError() }
|
||||||
|
return .rsaSignatureMessagePKCS1v15SHA512
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -64,6 +64,10 @@ extension OpenSSHKeyWriter {
|
|||||||
switch algorithm {
|
switch algorithm {
|
||||||
case .ellipticCurve:
|
case .ellipticCurve:
|
||||||
return "ecdsa-sha2-nistp" + String(describing: length)
|
return "ecdsa-sha2-nistp" + String(describing: length)
|
||||||
|
case .rsa:
|
||||||
|
// All RSA keys use the same 512 bit hash function, per
|
||||||
|
// https://security.stackexchange.com/questions/255074/why-are-rsa-sha2-512-and-rsa-sha2-256-supported-but-not-reported-by-ssh-q-key
|
||||||
|
return "rsa-sha2-512"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,6 +80,9 @@ extension OpenSSHKeyWriter {
|
|||||||
switch algorithm {
|
switch algorithm {
|
||||||
case .ellipticCurve:
|
case .ellipticCurve:
|
||||||
return "nistp" + String(describing: length)
|
return "nistp" + String(describing: length)
|
||||||
|
case .rsa:
|
||||||
|
// All RSA keys use the same 512 bit hash function
|
||||||
|
return "rsa-sha2-512"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ public protocol Secret: Identifiable, Hashable {
|
|||||||
public enum Algorithm: Hashable {
|
public enum Algorithm: Hashable {
|
||||||
|
|
||||||
case ellipticCurve
|
case ellipticCurve
|
||||||
|
case rsa
|
||||||
|
|
||||||
/// Initializes the Algorithm with a secAttr representation of an algorithm.
|
/// Initializes the Algorithm with a secAttr representation of an algorithm.
|
||||||
/// - Parameter secAttr: the secAttr, represented as an NSNumber.
|
/// - Parameter secAttr: the secAttr, represented as an NSNumber.
|
||||||
@ -28,8 +29,19 @@ public enum Algorithm: Hashable {
|
|||||||
switch secAttrString {
|
switch secAttrString {
|
||||||
case kSecAttrKeyTypeEC:
|
case kSecAttrKeyTypeEC:
|
||||||
self = .ellipticCurve
|
self = .ellipticCurve
|
||||||
|
case kSecAttrKeyTypeRSA:
|
||||||
|
self = .rsa
|
||||||
default:
|
default:
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var secAttrKeyType: CFString {
|
||||||
|
switch self {
|
||||||
|
case .ellipticCurve:
|
||||||
|
return kSecAttrKeyTypeEC
|
||||||
|
case .rsa:
|
||||||
|
return kSecAttrKeyTypeRSA
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,14 @@ public protocol SecretStore: ObservableObject, Identifiable {
|
|||||||
/// - Returns: The signed data.
|
/// - Returns: The signed data.
|
||||||
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data
|
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data
|
||||||
|
|
||||||
|
/// Verifies that a signature is valid over a specified payload.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - signature: The signature over the data.
|
||||||
|
/// - data: The data to verify the signature of.
|
||||||
|
/// - secret: The secret whose signature to verify.
|
||||||
|
/// - Returns: Whether the signature was verified.
|
||||||
|
func verify(signature: Data, for data: Data, with secret: SecretType) throws -> Bool
|
||||||
|
|
||||||
/// Checks to see if there is currently a valid persisted authentication for a given secret.
|
/// Checks to see if there is currently a valid persisted authentication for a given secret.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - secret: The ``Secret`` to check if there is a persisted authentication for.
|
/// - secret: The ``Secret`` to check if there is a persisted authentication for.
|
||||||
|
@ -101,7 +101,7 @@ extension SecureEnclave {
|
|||||||
reloadSecretsInternal()
|
reloadSecretsInternal()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
|
||||||
let context: LAContext
|
let context: LAContext
|
||||||
if let existing = persistedAuthenticationContexts[secret], existing.valid {
|
if let existing = persistedAuthenticationContexts[secret], existing.valid {
|
||||||
context = existing.context
|
context = existing.context
|
||||||
@ -138,6 +138,41 @@ extension SecureEnclave {
|
|||||||
return signature as Data
|
return signature as Data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
|
||||||
|
let context = LAContext()
|
||||||
|
context.localizedReason = "verify a signature using secret \"\(secret.name)\""
|
||||||
|
context.localizedCancelTitle = "Deny"
|
||||||
|
let attributes = KeychainDictionary([
|
||||||
|
kSecClass: kSecClassKey,
|
||||||
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||||
|
kSecAttrApplicationLabel: secret.id as CFData,
|
||||||
|
kSecAttrKeyType: Constants.keyType,
|
||||||
|
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
|
||||||
|
kSecAttrApplicationTag: Constants.keyTag,
|
||||||
|
kSecUseAuthenticationContext: context,
|
||||||
|
kSecReturnRef: true
|
||||||
|
])
|
||||||
|
var verifyError: SecurityError?
|
||||||
|
var untyped: CFTypeRef?
|
||||||
|
let status = SecItemCopyMatching(attributes, &untyped)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
guard let untypedSafe = untyped else {
|
||||||
|
throw KeychainError(statusCode: errSecSuccess)
|
||||||
|
}
|
||||||
|
let key = untypedSafe as! SecKey
|
||||||
|
let verified = SecKeyVerifySignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, signature as CFData, &verifyError)
|
||||||
|
if !verified, let verifyError {
|
||||||
|
if verifyError.takeUnretainedValue() ~= .verifyError {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
throw SigningError(error: verifyError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return verified
|
||||||
|
}
|
||||||
|
|
||||||
public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? {
|
public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? {
|
||||||
guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil }
|
guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil }
|
||||||
return persisted
|
return persisted
|
||||||
@ -260,34 +295,12 @@ extension SecureEnclave.Store {
|
|||||||
])
|
])
|
||||||
let status = SecItemAdd(attributes, nil)
|
let status = SecItemAdd(attributes, nil)
|
||||||
if status != errSecSuccess {
|
if status != errSecSuccess {
|
||||||
throw SecureEnclave.KeychainError(statusCode: status)
|
throw KeychainError(statusCode: status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SecureEnclave {
|
|
||||||
|
|
||||||
/// A wrapper around an error code reported by a Keychain API.
|
|
||||||
public struct KeychainError: Error {
|
|
||||||
/// The status code involved, if one was reported.
|
|
||||||
public let statusCode: OSStatus?
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A signing-related error.
|
|
||||||
public struct SigningError: Error {
|
|
||||||
/// The underlying error reported by the API, if one was returned.
|
|
||||||
public let error: SecurityError?
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SecureEnclave {
|
|
||||||
|
|
||||||
public typealias SecurityError = Unmanaged<CFError>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SecureEnclave {
|
extension SecureEnclave {
|
||||||
|
|
||||||
enum Constants {
|
enum Constants {
|
||||||
|
@ -6,9 +6,3 @@
|
|||||||
|
|
||||||
- ``Secret``
|
- ``Secret``
|
||||||
- ``Store``
|
- ``Store``
|
||||||
|
|
||||||
### Errors
|
|
||||||
|
|
||||||
- ``KeychainError``
|
|
||||||
- ``SigningError``
|
|
||||||
- ``SecurityError``
|
|
||||||
|
@ -45,7 +45,7 @@ extension SmartCard {
|
|||||||
fatalError("Keys must be deleted on the smart card.")
|
fatalError("Keys must be deleted on the smart card.")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
|
||||||
guard let tokenID = tokenID else { fatalError() }
|
guard let tokenID = tokenID else { fatalError() }
|
||||||
let context = LAContext()
|
let context = LAContext()
|
||||||
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
|
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
|
||||||
@ -68,26 +68,40 @@ extension SmartCard {
|
|||||||
}
|
}
|
||||||
let key = untypedSafe as! SecKey
|
let key = untypedSafe as! SecKey
|
||||||
var signError: SecurityError?
|
var signError: SecurityError?
|
||||||
let signatureAlgorithm: SecKeyAlgorithm
|
guard let signature = SecKeyCreateSignature(key, signatureAlgorithm(for: secret, allowRSA: true), data as CFData, &signError) else {
|
||||||
switch (secret.algorithm, secret.keySize) {
|
|
||||||
case (.ellipticCurve, 256):
|
|
||||||
signatureAlgorithm = .ecdsaSignatureMessageX962SHA256
|
|
||||||
case (.ellipticCurve, 384):
|
|
||||||
signatureAlgorithm = .ecdsaSignatureMessageX962SHA384
|
|
||||||
default:
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else {
|
|
||||||
throw SigningError(error: signError)
|
throw SigningError(error: signError)
|
||||||
}
|
}
|
||||||
return signature as Data
|
return signature as Data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
|
||||||
|
let attributes = KeychainDictionary([
|
||||||
|
kSecAttrKeyType: secret.algorithm.secAttrKeyType,
|
||||||
|
kSecAttrKeySizeInBits: secret.keySize,
|
||||||
|
kSecAttrKeyClass: kSecAttrKeyClassPublic
|
||||||
|
])
|
||||||
|
var verifyError: SecurityError?
|
||||||
|
let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &verifyError)
|
||||||
|
guard let untypedSafe = untyped else {
|
||||||
|
throw KeychainError(statusCode: errSecSuccess)
|
||||||
|
}
|
||||||
|
let key = untypedSafe as! SecKey
|
||||||
|
let verified = SecKeyVerifySignature(key, signatureAlgorithm(for: secret, allowRSA: true), data as CFData, signature as CFData, &verifyError)
|
||||||
|
if !verified, let verifyError {
|
||||||
|
if verifyError.takeUnretainedValue() ~= .verifyError {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
throw SigningError(error: verifyError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return verified
|
||||||
|
}
|
||||||
|
|
||||||
public func existingPersistedAuthenticationContext(secret: SmartCard.Secret) -> PersistedAuthenticationContext? {
|
public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws {
|
public func persistAuthentication(secret: Secret, forDuration: TimeInterval) throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reloads all secrets from the store.
|
/// Reloads all secrets from the store.
|
||||||
@ -140,7 +154,6 @@ extension SmartCard.Store {
|
|||||||
let attributes = KeychainDictionary([
|
let attributes = KeychainDictionary([
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrTokenID: tokenID,
|
kSecAttrTokenID: tokenID,
|
||||||
kSecAttrKeyType: kSecAttrKeyTypeEC, // Restrict to EC
|
|
||||||
kSecReturnRef: true,
|
kSecReturnRef: true,
|
||||||
kSecMatchLimit: kSecMatchLimitAll,
|
kSecMatchLimit: kSecMatchLimitAll,
|
||||||
kSecReturnAttributes: true
|
kSecReturnAttributes: true
|
||||||
@ -148,7 +161,7 @@ extension SmartCard.Store {
|
|||||||
var untyped: CFTypeRef?
|
var untyped: CFTypeRef?
|
||||||
SecItemCopyMatching(attributes, &untyped)
|
SecItemCopyMatching(attributes, &untyped)
|
||||||
guard let typed = untyped as? [[CFString: Any]] else { return }
|
guard let typed = untyped as? [[CFString: Any]] else { return }
|
||||||
let wrapped: [SmartCard.Secret] = typed.map {
|
let wrapped = typed.map {
|
||||||
let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
|
let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
|
||||||
let tokenID = $0[kSecAttrApplicationLabel] as! Data
|
let tokenID = $0[kSecAttrApplicationLabel] as! Data
|
||||||
let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber)
|
let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber)
|
||||||
@ -164,6 +177,88 @@ extension SmartCard.Store {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Smart Card specific encryption/decryption/verification
|
||||||
|
extension SmartCard.Store {
|
||||||
|
|
||||||
|
/// Encrypts a payload with a specified key.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - data: The payload to encrypt.
|
||||||
|
/// - secret: The secret to encrypt with.
|
||||||
|
/// - Returns: The encrypted data.
|
||||||
|
/// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged.
|
||||||
|
public func encrypt(data: Data, with secret: SecretType) throws -> Data {
|
||||||
|
let context = LAContext()
|
||||||
|
context.localizedReason = "encrypt data using secret \"\(secret.name)\""
|
||||||
|
context.localizedCancelTitle = "Deny"
|
||||||
|
let attributes = KeychainDictionary([
|
||||||
|
kSecAttrKeyType: secret.algorithm.secAttrKeyType,
|
||||||
|
kSecAttrKeySizeInBits: secret.keySize,
|
||||||
|
kSecAttrKeyClass: kSecAttrKeyClassPublic,
|
||||||
|
kSecUseAuthenticationContext: context
|
||||||
|
])
|
||||||
|
var encryptError: SecurityError?
|
||||||
|
let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &encryptError)
|
||||||
|
guard let untypedSafe = untyped else {
|
||||||
|
throw KeychainError(statusCode: errSecSuccess)
|
||||||
|
}
|
||||||
|
let key = untypedSafe as! SecKey
|
||||||
|
guard let signature = SecKeyCreateEncryptedData(key, encryptionAlgorithm(for: secret), data as CFData, &encryptError) else {
|
||||||
|
throw SigningError(error: encryptError)
|
||||||
|
}
|
||||||
|
return signature as Data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypts a payload with a specified key.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - data: The payload to decrypt.
|
||||||
|
/// - secret: The secret to decrypt with.
|
||||||
|
/// - Returns: The decrypted data.
|
||||||
|
/// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged.
|
||||||
|
public func decrypt(data: Data, with secret: SecretType) throws -> Data {
|
||||||
|
guard let tokenID = tokenID else { fatalError() }
|
||||||
|
let context = LAContext()
|
||||||
|
context.localizedReason = "decrypt data using secret \"\(secret.name)\""
|
||||||
|
context.localizedCancelTitle = "Deny"
|
||||||
|
let attributes = KeychainDictionary([
|
||||||
|
kSecClass: kSecClassKey,
|
||||||
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||||
|
kSecAttrApplicationLabel: secret.id as CFData,
|
||||||
|
kSecAttrTokenID: tokenID,
|
||||||
|
kSecUseAuthenticationContext: context,
|
||||||
|
kSecReturnRef: true
|
||||||
|
])
|
||||||
|
var untyped: CFTypeRef?
|
||||||
|
let status = SecItemCopyMatching(attributes, &untyped)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
guard let untypedSafe = untyped else {
|
||||||
|
throw KeychainError(statusCode: errSecSuccess)
|
||||||
|
}
|
||||||
|
let key = untypedSafe as! SecKey
|
||||||
|
var encryptError: SecurityError?
|
||||||
|
guard let signature = SecKeyCreateDecryptedData(key, encryptionAlgorithm(for: secret), data as CFData, &encryptError) else {
|
||||||
|
throw SigningError(error: encryptError)
|
||||||
|
}
|
||||||
|
return signature as Data
|
||||||
|
}
|
||||||
|
|
||||||
|
private func encryptionAlgorithm(for secret: SecretType) -> SecKeyAlgorithm {
|
||||||
|
switch (secret.algorithm, secret.keySize) {
|
||||||
|
case (.ellipticCurve, 256):
|
||||||
|
return .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
|
||||||
|
case (.ellipticCurve, 384):
|
||||||
|
return .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
|
||||||
|
case (.rsa, 1024), (.rsa, 2048):
|
||||||
|
return .rsaEncryptionOAEPSHA512AESGCM
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
extension TKTokenWatcher {
|
extension TKTokenWatcher {
|
||||||
|
|
||||||
/// All available tokens, excluding the Secure Enclave.
|
/// All available tokens, excluding the Secure Enclave.
|
||||||
@ -172,25 +267,3 @@ extension TKTokenWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SmartCard {
|
|
||||||
|
|
||||||
/// A wrapper around an error code reported by a Keychain API.
|
|
||||||
public struct KeychainError: Error {
|
|
||||||
/// The status code involved.
|
|
||||||
public let statusCode: OSStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A signing-related error.
|
|
||||||
public struct SigningError: Error {
|
|
||||||
/// The underlying error reported by the API, if one was returned.
|
|
||||||
public let error: SecurityError?
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SmartCard {
|
|
||||||
|
|
||||||
public typealias SecurityError = Unmanaged<CFError>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -61,8 +61,17 @@ class AgentTests: XCTestCase {
|
|||||||
var rs = r
|
var rs = r
|
||||||
rs.append(s)
|
rs.append(s)
|
||||||
let signature = try! P256.Signing.ECDSASignature(rawRepresentation: rs)
|
let signature = try! P256.Signing.ECDSASignature(rawRepresentation: rs)
|
||||||
let valid = try! P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey).isValidSignature(signature, for: dataToSign)
|
let referenceValid = try! P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey).isValidSignature(signature, for: dataToSign)
|
||||||
XCTAssertTrue(valid)
|
let store = list.stores.first!
|
||||||
|
let derVerifies = try! store.verify(signature: signature.derRepresentation, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa256Secret))
|
||||||
|
let invalidRandomSignature = try? store.verify(signature: "invalid".data(using: .utf8)!, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa256Secret))
|
||||||
|
let invalidRandomData = try? store.verify(signature: signature.derRepresentation, for: "invalid".data(using: .utf8)!, with: AnySecret(Constants.Secrets.ecdsa256Secret))
|
||||||
|
let invalidWrongKey = try? store.verify(signature: signature.derRepresentation, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa384Secret))
|
||||||
|
XCTAssertTrue(referenceValid)
|
||||||
|
XCTAssertTrue(derVerifies)
|
||||||
|
XCTAssert(invalidRandomSignature == false)
|
||||||
|
XCTAssert(invalidRandomData == false)
|
||||||
|
XCTAssert(invalidWrongKey == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Witness protocol
|
// MARK: Witness protocol
|
||||||
|
@ -58,16 +58,30 @@ extension Stub {
|
|||||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate
|
||||||
])
|
])
|
||||||
, nil)!
|
, nil)!
|
||||||
let signatureAlgorithm: SecKeyAlgorithm
|
return SecKeyCreateSignature(privateKey, signatureAlgorithm(for: secret), data as CFData, nil)! as Data
|
||||||
switch secret.keySize {
|
}
|
||||||
case 256:
|
|
||||||
signatureAlgorithm = .ecdsaSignatureMessageX962SHA256
|
public func verify(signature: Data, for data: Data, with secret: Stub.Secret) throws -> Bool {
|
||||||
case 384:
|
let attributes = KeychainDictionary([
|
||||||
signatureAlgorithm = .ecdsaSignatureMessageX962SHA384
|
kSecAttrKeyType: secret.algorithm.secAttrKeyType,
|
||||||
default:
|
kSecAttrKeySizeInBits: secret.keySize,
|
||||||
fatalError()
|
kSecAttrKeyClass: kSecAttrKeyClassPublic
|
||||||
|
])
|
||||||
|
var verifyError: Unmanaged<CFError>?
|
||||||
|
let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &verifyError)
|
||||||
|
guard let untypedSafe = untyped else {
|
||||||
|
throw NSError(domain: "test", code: 0, userInfo: nil)
|
||||||
}
|
}
|
||||||
return SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data
|
let key = untypedSafe as! SecKey
|
||||||
|
let verified = SecKeyVerifySignature(key, signatureAlgorithm(for: secret), data as CFData, signature as CFData, &verifyError)
|
||||||
|
if let verifyError {
|
||||||
|
if verifyError.takeUnretainedValue() ~= .verifyError {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
throw NSError(domain: "test", code: 0, userInfo: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return verified
|
||||||
}
|
}
|
||||||
|
|
||||||
public func existingPersistedAuthenticationContext(secret: Stub.Secret) -> PersistedAuthenticationContext? {
|
public func existingPersistedAuthenticationContext(secret: Stub.Secret) -> PersistedAuthenticationContext? {
|
||||||
|
@ -40,6 +40,10 @@ extension Preview {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func verify(signature data: Data, for signature: Data, with secret: Preview.Secret) throws -> Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
|
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ struct EmptyStoreImmutableView: View {
|
|||||||
VStack {
|
VStack {
|
||||||
Text("No Secrets").bold()
|
Text("No Secrets").bold()
|
||||||
Text("Use your Smart Card's management tool to create a secret.")
|
Text("Use your Smart Card's management tool to create a secret.")
|
||||||
Text("Secretive only supports Elliptic Curve keys.")
|
Text("Secretive supports EC256, EC384, RSA1024, and RSA2048 keys.")
|
||||||
}.frame(maxWidth: .infinity, maxHeight: .infinity)
|
}.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user