diff --git a/Package.resolved b/Package.resolved index 8486532ba..f5c2c0f0e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/outfoxx/PotentCodables.git", "state": { "branch": null, - "revision": "e7241008d1d52e106f84674161652e12cf8dfc32", - "version": "3.0.1" + "revision": "9c5adf2563d12ce0a3cd1c19efa19f7f8d8ca078", + "version": "3.0.3" } }, { @@ -37,6 +37,15 @@ "version": "2.1.1" } }, + { + "package": "swift-algorithms", + "repositoryURL": "https://github.com/apple/swift-algorithms", + "state": { + "branch": null, + "revision": "b14b7f4c528c942f121c8b860b9410b2bf57825e", + "version": "1.0.0" + } + }, { "package": "swift-collections", "repositoryURL": "https://github.com/apple/swift-collections.git", diff --git a/Package.swift b/Package.swift index ee83f6044..0cdb06868 100644 --- a/Package.swift +++ b/Package.swift @@ -16,8 +16,9 @@ let package = Package( targets: ["Shield", "ShieldSecurity", "ShieldCrypto", "ShieldOID", "ShieldPKCS", "ShieldX509", "ShieldX500"]), ], dependencies: [ - .package(url: "https://github.com/outfoxx/PotentCodables.git", from: "3.0.0"), + .package(url: "https://github.com/outfoxx/PotentCodables.git", from: "3.0.3"), .package(url: "https://github.com/sharplet/Regex.git", from: "2.1.0"), + .package(name: "Algorithms", url: "https://github.com/apple/swift-algorithms", from: "1.0.0"), ], targets: [ .target( @@ -37,11 +38,11 @@ let package = Package( ), .target( name: "ShieldPKCS", - dependencies: ["ShieldX500", "PotentCodables"] + dependencies: ["ShieldX509", "PotentCodables"] ), .target( name: "ShieldX509", - dependencies: ["ShieldCrypto", "ShieldX500", "ShieldOID", "ShieldPKCS", "PotentCodables"] + dependencies: ["ShieldCrypto", "ShieldX500", "ShieldOID", "PotentCodables", "Algorithms"] ), .target( name: "ShieldCrypto" diff --git a/Sources/ShieldCrypto/PBKDF.swift b/Sources/ShieldCrypto/PBKDF.swift index d1e6aaf31..6986f63fa 100644 --- a/Sources/ShieldCrypto/PBKDF.swift +++ b/Sources/ShieldCrypto/PBKDF.swift @@ -45,13 +45,23 @@ public enum PBKDF { self.name = name } + @available(*, deprecated, message: "Use hmacSha1") public static let sha1 = PsuedoRandomAlgorithm(rawValue: UInt32(kCCPRFHmacAlgSHA1), name: "SHA1") + public static let hmacSha1 = PsuedoRandomAlgorithm(rawValue: UInt32(kCCPRFHmacAlgSHA1), name: "SHA1") + @available(*, deprecated, message: "Use hmacSha224") public static let sha224 = PsuedoRandomAlgorithm(rawValue: UInt32(kCCPRFHmacAlgSHA224), name: "SHA224") + public static let hmacSha224 = PsuedoRandomAlgorithm(rawValue: UInt32(kCCPRFHmacAlgSHA224), name: "SHA224") + @available(*, deprecated, message: "Use hmacSha256") public static let sha256 = PsuedoRandomAlgorithm(rawValue: UInt32(kCCPRFHmacAlgSHA256), name: "SHA256") + public static let hmacSha256 = PsuedoRandomAlgorithm(rawValue: UInt32(kCCPRFHmacAlgSHA256), name: "SHA256") + @available(*, deprecated, message: "Use hmacSha384") public static let sha384 = PsuedoRandomAlgorithm(rawValue: UInt32(kCCPRFHmacAlgSHA384), name: "SHA384") + public static let hmacSha384 = PsuedoRandomAlgorithm(rawValue: UInt32(kCCPRFHmacAlgSHA384), name: "SHA384") + @available(*, deprecated, message: "Use hmacSha512") public static let sha512 = PsuedoRandomAlgorithm(rawValue: UInt32(kCCPRFHmacAlgSHA512), name: "SHA512") + public static let hmacSha512 = PsuedoRandomAlgorithm(rawValue: UInt32(kCCPRFHmacAlgSHA512), name: "SHA512") - public static let allCases: [PsuedoRandomAlgorithm] = [.sha1, .sha224, .sha256, .sha384, .sha512] + public static let allCases: [PsuedoRandomAlgorithm] = [.hmacSha1, .hmacSha224, .hmacSha256, .hmacSha384, .hmacSha512] public var description: String { return name @@ -97,7 +107,7 @@ public enum PBKDF { saltLength: Int, keyLength: Int, using algorithm: Algorithm = .pbkdf2, - psuedoRandomAlgorithm: PsuedoRandomAlgorithm = .sha512, + psuedoRandomAlgorithm: PsuedoRandomAlgorithm = .hmacSha512, taking: TimeInterval ) throws -> Int { let rounds = CCCalibratePBKDF( diff --git a/Sources/ShieldOID/ISO-ITU.swift b/Sources/ShieldOID/ISO-ITU.swift index 79447c49b..9e3b7ece3 100644 --- a/Sources/ShieldOID/ISO-ITU.swift +++ b/Sources/ShieldOID/ISO-ITU.swift @@ -11,7 +11,7 @@ import Foundation // swiftformat:disable consecutiveSpaces -// swiftlint:disable type_name identifier_name +// swiftlint:disable type_name identifier_name nesting /// Areas of joint work between ISO/IEC (International Organization for Standardization/International Electrotechnical Commission) /// and ITU-T (International Telecommunication Union - Telecommunication standardization sector), and other international work @@ -26,7 +26,7 @@ public struct iso_itu: OIDBranch { public struct ds: OIDBranch { public static let id: UInt64 = 5 public static let names = ["ds"] - internal static let children: [OIDNode.Type] = [attributeType.self, certificateExtension.self] + internal static let children: [OIDNode.Type] = [attributeType.self, certificateExtension.self, algorithm.self] public enum attributeType: OID, CaseIterable, OIDLeaf { public static let id: UInt64 = 4 @@ -193,5 +193,153 @@ public struct iso_itu: OIDBranch { case noRevAvail = "2.5.29.56" case acceptablePrivilegePolicies = "2.5.29.57" } + + public struct algorithm: OIDBranch { + public static let id: UInt64 = 44 + public static let names = ["algorithm"] + internal static let children: [OIDNode.Type] = [aes.self] + + public enum aes: OID, CaseIterable, OIDLeaf { + public static let id: UInt64 = 2 + public static let names = ["aes"] + internal static let children: [OIDNode.Type] = [] + + case aes_cbc_128 = "2.5.44.2.1" + case aes_cbc_192 = "2.5.44.2.2" + case aes_cbc_256 = "2.5.44.2.3" + + case aes_ofb_128 = "2.5.44.2.5" + case aes_ofb_192 = "2.5.44.2.6" + case aes_ofb_256 = "2.5.44.2.7" + + case aes_cfb_128 = "2.5.44.2.9" + case aes_cfb_192 = "2.5.44.2.10" + case aes_cfb_256 = "2.5.44.2.11" + + case aes_gcm_128 = "2.5.44.2.17" + case aes_gcm_192 = "2.5.44.2.18" + case aes_gcm_256 = "2.5.44.2.19" + + case aes_gcm_siv_128 = "2.5.44.2.21" + case aes_gcm_siv_192 = "2.5.44.2.22" + case aes_gcm_siv_256 = "2.5.44.2.23" + + case aes_ccm_128 = "2.5.44.2.25" + case aes_ccm_192 = "2.5.44.2.26" + case aes_ccm_256 = "2.5.44.2.27" + + case aes_gmac_128 = "2.5.44.2.29" + case aes_gmac_192 = "2.5.44.2.30" + case aes_gmac_256 = "2.5.44.2.31" + } + } + } + + public struct country: OIDBranch { + public static let id: UInt64 = 16 + public static let names = ["country"] + internal static let children: [OIDNode.Type] = [us.self] + + public struct us: OIDBranch { + public static let id: UInt64 = 840 + public static let names = ["us"] + internal static let children: [OIDNode.Type] = [organization.self] + + public struct organization: OIDBranch { + public static let id: UInt64 = 1 + public static let names = ["organization"] + internal static let children: [OIDNode.Type] = [gov.self] + + public struct gov: OIDBranch { + public static let id: UInt64 = 101 + public static let names = ["gov"] + internal static let children: [OIDNode.Type] = [csor.self] + + public struct csor: OIDBranch { + public static let id: UInt64 = 3 + public static let names = ["country"] + internal static let children: [OIDNode.Type] = [nistAlgorithms.self] + + public struct nistAlgorithms: OIDBranch { + public static let id: UInt64 = 4 + public static let names = ["nistAlgorithms"] + internal static let children: [OIDNode.Type] = [aes.self, hashAlgs.self] + + public enum aes: OID, CaseIterable, OIDLeaf { + public static let id: UInt64 = 1 + public static let names = ["aes"] + internal static let children: [OIDNode.Type] = [] + + case aes128_ECB = "2.16.840.1.101.3.4.1.1" + case aes128_CBC_PAD = "2.16.840.1.101.3.4.1.2" + case aes128_OFB = "2.16.840.1.101.3.4.1.3" + case aes128_CFB = "2.16.840.1.101.3.4.1.4" + case aes128_wrap = "2.16.840.1.101.3.4.1.5" + case aes128_GCM = "2.16.840.1.101.3.4.1.6" + case aes128_CCM = "2.16.840.1.101.3.4.1.7" + case aes128_wrap_pad = "2.16.840.1.101.3.4.1.8" + case aes128_GMAC = "2.16.840.1.101.3.4.1.9" + + case aes192_ECB = "2.16.840.1.101.3.4.1.21" + case aes192_CBC_PAD = "2.16.840.1.101.3.4.1.22" + case aes192_OFB = "2.16.840.1.101.3.4.1.23" + case aes192_CFB = "2.16.840.1.101.3.4.1.24" + case aes192_wrap = "2.16.840.1.101.3.4.1.25" + case aes192_GCM = "2.16.840.1.101.3.4.1.26" + case aes192_CCM = "2.16.840.1.101.3.4.1.27" + case aes192_wrap_pad = "2.16.840.1.101.3.4.1.28" + case aes192_GMAC = "2.16.840.1.101.3.4.1.29" + + case aes256_ECB = "2.16.840.1.101.3.4.1.41" + case aes256_CBC_PAD = "2.16.840.1.101.3.4.1.42" + case aes256_OFB = "2.16.840.1.101.3.4.1.43" + case aes256_CFB = "2.16.840.1.101.3.4.1.44" + case aes256_wrap = "2.16.840.1.101.3.4.1.45" + case aes256_GCM = "2.16.840.1.101.3.4.1.46" + case aes256_CCM = "2.16.840.1.101.3.4.1.47" + case aes256_wrap_pad = "2.16.840.1.101.3.4.1.48" + case aes256_GMAC = "2.16.840.1.101.3.4.1.49" + } + + public enum hashAlgs: OID, CaseIterable, OIDLeaf { + public static let id: UInt64 = 2 + public static let names = ["hashAlgs", "hashalgs"] + internal static let children: [OIDNode.Type] = [] + + case sha256 = "2.16.840.1.101.3.4.2.1" + case sha384 = "2.16.840.1.101.3.4.2.2" + case sha512 = "2.16.840.1.101.3.4.2.3" + case sha224 = "2.16.840.1.101.3.4.2.4" + case sha512_224 = "2.16.840.1.101.3.4.2.5" + case sha512_256 = "2.16.840.1.101.3.4.2.6" + + case sha3_224 = "2.16.840.1.101.3.4.2.7" + case sha3_256 = "2.16.840.1.101.3.4.2.8" + case sha3_384 = "2.16.840.1.101.3.4.2.9" + case sha3_512 = "2.16.840.1.101.3.4.2.10" + + case shake128 = "2.16.840.1.101.3.4.2.11" + case shake256 = "2.16.840.1.101.3.4.2.12" + + case hmacWithSHA3_224 = "2.16.840.1.101.3.4.2.13" + case hmacWithSHA3_256 = "2.16.840.1.101.3.4.2.14" + case hmacWithSHA3_384 = "2.16.840.1.101.3.4.2.15" + case hmacWithSHA3_512 = "2.16.840.1.101.3.4.2.16" + + case shake128_len = "2.16.840.1.101.3.4.2.17" + case shake256_len = "2.16.840.1.101.3.4.2.18" + + + case kmac128 = "2.16.840.1.101.3.4.2.19" + case kmac256 = "2.16.840.1.101.3.4.2.20" + + case KMACXOF128 = "2.16.840.1.101.3.4.2.21" + case KACXOF256 = "2.16.840.1.101.3.4.2.22" + } + } + } + } + } + } } } diff --git a/Sources/ShieldOID/ISO.swift b/Sources/ShieldOID/ISO.swift index f73feb0e6..9b90f7ab0 100644 --- a/Sources/ShieldOID/ISO.swift +++ b/Sources/ShieldOID/ISO.swift @@ -12,7 +12,7 @@ import Foundation import PotentASN1 // swiftformat:disable consecutiveSpaces -// swiftlint:disable type_name nesting +// swiftlint:disable type_name identifier_name nesting /// International Organization for Standardization (ISO) /// @@ -58,6 +58,14 @@ public struct iso: OIDBranch { case sha224WithRSAEncryption = "1.2.840.113549.1.1.14" } + public enum pkcs5: OID, CaseIterable, OIDLeaf { + public static let id: UInt64 = 5 + public static let names = ["pkcs-5"] + + case pbkdf2 = "1.2.840.113549.1.5.12" + case pbes2 = "1.2.840.113549.1.5.13" + } + public enum pkcs9: OID, CaseIterable, OIDLeaf { public static let id: UInt64 = 9 public static let names = ["pkcs-9"] @@ -74,6 +82,36 @@ public struct iso: OIDBranch { case extensionRequest = "1.2.840.113549.1.9.14" } } + + public enum digestAlgorithm: OID, CaseIterable, OIDLeaf { + public static let id: UInt64 = 2 + public static let names = ["digestAlgorithm"] + + case hmacWithSHA1 = "1.2.840.113549.2.7" + case hmacWithSHA224 = "1.2.840.113549.2.8" + case hmacWithSHA256 = "1.2.840.113549.2.9" + case hmacWithSHA384 = "1.2.840.113549.2.10" + case hmacWithSHA512 = "1.2.840.113549.2.11" + case hhmacWithSHA512_224 = "1.2.840.113549.2.12" + case hhmacWithSHA512_256 = "1.2.840.113549.2.13" + } + + public enum encryptionAlgorithm: OID, CaseIterable, OIDLeaf { + public static let id: UInt64 = 3 + public static let names = ["encryptionAlgorithm", "encryptionalgorithm"] + + case rc2CBC = "1.2.840.113549.3.2" + case rc2ECB = "1.2.840.113549.3.3" + case rc4 = "1.2.840.113549.3.4" + case rc4WithMAC = "1.2.840.113549.3.5" + case desxCBC = "1.2.840.113549.3.6" + case desEDE3CBC = "1.2.840.113549.3.7" + case rc5CBC = "1.2.840.113549.3.8" + case rc5CBCPad = "1.2.840.113549.3.9" + case desCDMF = "1.2.840.113549.3.10" + case desEDE3 = "1.2.840.113549.3.17" + } + } public struct ansix962: OIDBranch { diff --git a/Sources/ShieldPKCS/Moved.swift b/Sources/ShieldPKCS/Moved.swift new file mode 100644 index 000000000..c6caf17c1 --- /dev/null +++ b/Sources/ShieldPKCS/Moved.swift @@ -0,0 +1,26 @@ +// +// Moved.swift +// Shield +// +// Copyright © 2019 Outfox, inc. +// +// +// Distributed under the MIT License, See LICENSE for details. +// + +import ShieldX509 +import PotentASN1 + +// The following types have been moved to ShieldX509 due to issues with circular references + +public typealias RSAPrivateKey = ShieldX509.RSAPrivateKey +public typealias RSAPublicKey = ShieldX509.RSAPublicKey +public typealias ECParameters = ShieldX509.ECParameters + +public extension Schemas { + static let RSAPrivateKey = ShieldX509.Schemas.RSAPrivateKey + static let RSAPrivateKeyOtherPrimeInfos = ShieldX509.Schemas.RSAPrivateKeyOtherPrimeInfos + static let RSAPrivateKeyOtherPrimeInfo = ShieldX509.Schemas.RSAPrivateKeyOtherPrimeInfo + static let RSAPublicKey = ShieldX509.Schemas.RSAPublicKey + static let ECParameters = ShieldX509.Schemas.ECParameters +} diff --git a/Sources/ShieldSecurity/SecKeyPair.swift b/Sources/ShieldSecurity/SecKeyPair.swift index 38bfed10d..f17bd925b 100644 --- a/Sources/ShieldSecurity/SecKeyPair.swift +++ b/Sources/ShieldSecurity/SecKeyPair.swift @@ -8,24 +8,32 @@ // Distributed under the MIT License, See LICENSE for details. // -import CryptoKit +import Algorithms import Foundation import PotentASN1 import Security import ShieldCrypto -import ShieldPKCS +import ShieldOID +import ShieldX509 /// Public and private key of an asymmetric key pair. /// public struct SecKeyPair { - /// Default final key length for PBKDF generated export keys. + /// Default final key size for PBKDF generated export keys. /// /// ## See Also /// - ``export(password:derivedKeyLength:keyDerivationTiming:)`` /// - public static let exportDerivedKeyLengthDefault = 32 + public static let exportDerivedKeySizeDefault: ExportKeySize = .bits256 + + /// Default final psuedorandom algorthm for PBKDF generated export keys. + /// + /// ## See Also + /// - ``export(password:derivedKeyLength:keyDerivationTiming:)`` + /// + public static let exportPsuedoRandomAlgorithmDefault: PBKDF.PsuedoRandomAlgorithm = .hmacSha512 /// Default PBKDF generation time for generated export keys. /// @@ -40,6 +48,7 @@ public struct SecKeyPair { case noMatchingKey case itemAddFailed case itemDeleteFailed + case invalidEncodedPrivateKey public static func build(error: Error, message: String, status: OSStatus) -> NSError { let error = error as NSError @@ -292,25 +301,10 @@ public struct SecKeyPair { } #endif - - /// Structure representing keys exported using ``export(password:derivedKeyLength:keyDerivationTiming:)``. - /// - public struct ExportedKey: Codable, SchemaSpecified { - - public static let asn1Schema: Schema = - .sequence([ - "keyType": .integer(), - "exportKeyLength": .integer(), - "exportKeyRounds": .integer(), - "exportKeySalt": .octetString(), - "keyMaterial": .octetString(), - ]) - - public var keyType: SecKeyType - public var exportKeyLength: UInt64 - public var exportKeyRounds: UInt64 - public var exportKeySalt: Data - public var keyMaterial: Data + public enum ExportKeySize: Int { + case bits128 = 16 + case bits192 = 24 + case bits256 = 32 } /// Encrypt and encode the key pair's private key using PBKDF. @@ -323,56 +317,71 @@ public struct SecKeyPair { /// /// - Parameters: /// - password: Password use for key encryption. - /// - derivedKeyLength: PBKDF target key length. + /// - derivedKeySize: PBKDF target key size. /// - keyDerivationTiming: Time PBKDF function should take to generate encryption key. /// - Returns: Encoded encrypted key and PBKDF paraemters. /// public func export( password: String, - derivedKeyLength: Int = exportDerivedKeyLengthDefault, + derivedKeySize: ExportKeySize = exportDerivedKeySizeDefault, + psuedoRandomAlgorithm: PBKDF.PsuedoRandomAlgorithm = exportPsuedoRandomAlgorithmDefault, keyDerivationTiming: TimeInterval = exportKeyDerivationTimingDefault ) throws -> Data { + // Derive key from password + let passwordData = password.data(using: String.Encoding.utf8)! - let exportKeySalt = try Random.generate(count: derivedKeyLength) + let exportKeySalt = try Random.generate(count: derivedKeySize.rawValue) let exportKeyRounds = try PBKDF.calibrate( passwordLength: passwordData.count, saltLength: exportKeySalt.count, - keyLength: derivedKeyLength, + keyLength: derivedKeySize.rawValue, using: .pbkdf2, - psuedoRandomAlgorithm: .sha512, + psuedoRandomAlgorithm: psuedoRandomAlgorithm, taking: keyDerivationTiming ) let exportKey = try PBKDF.derive( - length: derivedKeyLength, + length: derivedKeySize.rawValue, from: passwordData, salt: exportKeySalt, using: .pbkdf2, - psuedoRandomAlgorithm: .sha512, + psuedoRandomAlgorithm: psuedoRandomAlgorithm, rounds: exportKeyRounds ) - let keyMaterial = try encodedPrivateKey() + // Encode and encrypt PKCS#8 PrivateKeyInfo - let encryptedKeyBox = try AES.GCM.seal(keyMaterial, using: SymmetricKey(data: exportKey)) + let encodedPrivateKey = try privateKey.encodePKCS8() - guard let encryptedKeyMaterial = encryptedKeyBox.combined else { - fatalError("Combined sealed box should be available") - } + let encryptedPrivateKeyIV = try Random.generate(count: 16) - let keyType = try privateKey.keyType() + let encryptedPrivateKey = try Cryptor.crypt(encodedPrivateKey, + operation: .encrypt, + using: .aes, + options: .pkcs7Padding, + key: exportKey, + iv: encryptedPrivateKeyIV) - return try ASN1Encoder.encode(ExportedKey( - keyType: keyType, - exportKeyLength: UInt64(derivedKeyLength), - exportKeyRounds: UInt64(exportKeyRounds), - exportKeySalt: exportKeySalt, - keyMaterial: encryptedKeyMaterial - )) + // Build PKCS#8 EncryptedPrivateKeyInfo + + let encryptedPrivateKeyInfo = + try EncryptedPrivateKeyInfo.build(encryptedData: encryptedPrivateKey, + pbkdf2Salt: exportKeySalt, + pbkdf2IterationCount: UInt64(exportKeyRounds), + pbkdf2KeyLength: UInt64(derivedKeySize.rawValue), + pbkdf2Prf: psuedoRandomAlgorithm.prfAlgorithm.oid, + aesEncryptionScheme: derivedKeySize.aesCBCAlgorithm.oid, + aesIV: encryptedPrivateKeyIV) + + let encryptedPrivateKeyInfoData = try ASN1Encoder.encode(encryptedPrivateKeyInfo) + + printPEM(data: encryptedPrivateKeyInfoData, type: "ENCRYPTED PRIVATE KEY") + + return encryptedPrivateKeyInfoData } /// Decode and decrypt a previously exported private key. @@ -387,21 +396,59 @@ public struct SecKeyPair { /// public static func `import`(fromData data: Data, withPassword password: String) throws -> SecKeyPair { - let info = try ASN1Decoder.decode(ExportedKey.self, from: data) + typealias Nist = iso_itu.country.us.organization.gov.csor.nistAlgorithms + typealias RSADSI = iso.memberBody.us.rsadsi + typealias PKCS = RSADSI.pkcs + let supportedEncOids = [Nist.aes.aes128_CBC_PAD.oid, Nist.aes.aes192_CBC_PAD.oid, Nist.aes.aes256_CBC_PAD.oid] - let exportKey = try PBKDF.derive( - length: Int(info.exportKeyLength), + let info = try ASN1.Decoder.decode(EncryptedPrivateKeyInfo.self, from: data) + + // Convert and validate requirements (PBKDF2 and AES-CBC-PAD encryption) + + guard + info.encryptionAlgorithm.algorithm == RSADSI.pkcs.pkcs5.pbes2.oid, + let encAlgParams = try? info.encryptionAlgorithm.parameters.map({ try ASN1.Decoder.decodeTree(PBES2Params.self, from: $0) }), + encAlgParams.keyDerivationFunc.algorithm == PKCS.pkcs5.pbkdf2.oid, + let pbkdf2Params = try? encAlgParams.keyDerivationFunc.parameters.map({ try ASN1.Decoder.decodeTree(PBKDF2Params.self, from: $0) }), + supportedEncOids.contains(encAlgParams.encryptionScheme.algorithm), + let aesIV = encAlgParams.encryptionScheme.parameters?.octetStringValue + else { + throw Error.invalidEncodedPrivateKey + } + + // Derive import key from password and PBKDF2 params in encrypted PKCS#8 data + + let importKey = try PBKDF.derive( + length: Int(pbkdf2Params.keyLength), from: password.data(using: .utf8)!, - salt: info.exportKeySalt, + salt: pbkdf2Params.salt, using: .pbkdf2, - psuedoRandomAlgorithm: .sha512, - rounds: Int(info.exportKeyRounds) + psuedoRandomAlgorithm: try PBKDF.PsuedoRandomAlgorithm.from(oid: pbkdf2Params.prf.algorithm), + rounds: Int(pbkdf2Params.iterationCount) ) - let keyMaterial = try AES.GCM.open(AES.GCM.SealedBox(combined: info.keyMaterial), - using: SymmetricKey(data: exportKey)) + // Decrypt & decode PKCS#8 PrivateKeyInfo + + let privateKeyInfoData = try Cryptor.crypt(info.encryptedData, + operation: .decrypt, + using: .aes, + options: .pkcs7Padding, + key: importKey, + iv: aesIV) - return try Self(type: info.keyType, privateKeyData: keyMaterial) + let privateKeyInfo: PrivateKeyInfo + do { + privateKeyInfo = try ASN1.Decoder.decode(PrivateKeyInfo.self, from: privateKeyInfoData) + } + catch { + throw SecKeyPair.Error.invalidEncodedPrivateKey + } + + // Convert to SecKey decode params + + let (keyType, keyDecodeData) = try SecKey.extractDecodeParams(privateKeyInfo: privateKeyInfo) + + return try Self(type: keyType, privateKeyData: keyDecodeData) } } @@ -427,3 +474,204 @@ extension SecKeyPair: Codable { } } + +private extension SecKey { + + static func extractDecodeParams(privateKeyInfo: PrivateKeyInfo) throws -> (SecKeyType, Data) { + + let keyType: SecKeyType + let importKeyData: Data + switch privateKeyInfo.privateKeyAlgorithm.algorithm { + case iso.memberBody.us.rsadsi.pkcs.pkcs1.rsaEncryption.oid: + keyType = .rsa + importKeyData = privateKeyInfo.privateKey + + case iso.memberBody.us.ansix962.keyType.ecPublicKey.oid: + let ecPrivateKey = try ASN1.Decoder.decode(ECPrivateKey.self, from: privateKeyInfo.privateKey) + guard let publicKey = ecPrivateKey.publicKey else { + throw SecKeyPair.Error.invalidEncodedPrivateKey + } + + keyType = .ec + importKeyData = publicKey.bytes + ecPrivateKey.privateKey + + default: + throw AlgorithmIdentifier.Error.unsupportedAlgorithm + } + + return (keyType, importKeyData) + } + + func encodePKCS8() throws -> Data { + let privateKeyInfo: PrivateKeyInfo + switch try keyType() { + case .rsa: + privateKeyInfo = try generateRSAPrivateKeyInfo() + + case .ec: + privateKeyInfo = try generateECPrivateKeyInfo() + } + + let encoded = try ASN1.Encoder.encode(privateKeyInfo) + + printPEM(data: encoded, type: "PRIVATE KEY") + + return encoded + } + + private func generateRSAPrivateKeyInfo() throws -> PrivateKeyInfo { + let encodedPrivateKey = try encode() + + printPEM(data: encodedPrivateKey, type: "RSA PRIVATE KEY") + + return PrivateKeyInfo(privateKeyAlgorithm: .init(algorithm: iso.memberBody.us.rsadsi.pkcs.pkcs1.rsaEncryption.oid), + privateKey: encodedPrivateKey) + } + + private func generateECPrivateKeyInfo() throws -> PrivateKeyInfo { + let encodedPrivateKey = try encode() + + let (curveOID, keyNumberSize) = try getECCurveAndNumberSize() + + let parts = encodedPrivateKey.dropFirst().chunks(ofCount: keyNumberSize) + if parts.count != 3 { + throw SecKeyPair.Error.invalidEncodedPrivateKey + } + + let encodedPublicKey = Data(encodedPrivateKey.prefix(1) + parts.dropLast().joined()) + + let privateKey = ECPrivateKey(version: .one, + privateKey: parts.last!, + parameters: curveOID, + publicKey: BitString(bytes: encodedPublicKey)) + + let privateKeyData = try ASN1.Encoder.encode(privateKey) + + printPEM(data: privateKeyData, type: "EC PRIVATE KEY") + + + return PrivateKeyInfo(version: .zero, + privateKeyAlgorithm: .init(algorithm: iso.memberBody.us.ansix962.keyType.ecPublicKey.oid, + parameters: ASN1.objectIdentifier(curveOID.fields)), + privateKey: privateKeyData) + } + + func getECCurveAndNumberSize() throws -> (OID, Int) { + + switch try attributes()[kSecAttrKeySizeInBits as String] as? Int ?? 0 { + case 192: + // P-192, secp192r1 + return (iso.memberBody.us.ansix962.curves.prime.prime192v1.oid, 24) + case 256: + // P-256, secp256r1 + return (iso.memberBody.us.ansix962.curves.prime.prime256v1.oid, 32) + case 384: + // P-384, secp384r1 + return (iso.org.certicom.curve.ansip384r1.oid, 48) + case 521: + // P-521, secp521r1 + return (iso.org.certicom.curve.ansip521r1.oid, 66) + default: + throw AlgorithmIdentifier.Error.unsupportedECKeySize + } + } + +} + +private extension PBKDF.PsuedoRandomAlgorithm { + + var prfAlgorithm: iso.memberBody.us.rsadsi.digestAlgorithm { + typealias Algs = iso.memberBody.us.rsadsi.digestAlgorithm + + switch self { + case .hmacSha1: + return Algs.hmacWithSHA1 + case .hmacSha224: + return Algs.hmacWithSHA224 + case .hmacSha256: + return Algs.hmacWithSHA256 + case .hmacSha384: + return Algs.hmacWithSHA384 + case .hmacSha512: + return Algs.hmacWithSHA512 + default: + fatalError("Unsupported PBKDF Psuedo Random Algorithm") + } + } + + static func from(oid: OID) throws -> Self { + typealias Algs = iso.memberBody.us.rsadsi.digestAlgorithm + + switch oid { + case Algs.hmacWithSHA1.oid: + return .hmacSha1 + case Algs.hmacWithSHA224.oid: + return .hmacSha224 + case Algs.hmacWithSHA256.oid: + return .hmacSha256 + case Algs.hmacWithSHA384.oid: + return .hmacSha384 + case Algs.hmacWithSHA512.oid: + return .hmacSha512 + default: + throw SecKeyPair.Error.invalidEncodedPrivateKey + } + } + +} + +private extension SecKeyPair.ExportKeySize { + + var aesCBCAlgorithm: iso_itu.country.us.organization.gov.csor.nistAlgorithms.aes { + typealias AES = iso_itu.country.us.organization.gov.csor.nistAlgorithms.aes + + switch self { + case .bits128: + return AES.aes128_CBC_PAD + case .bits192: + return AES.aes192_CBC_PAD + case .bits256: + return AES.aes256_CBC_PAD + } + } + +} + +private extension EncryptedPrivateKeyInfo { + + static func build( + encryptedData: Data, + pbkdf2Salt: Data, + pbkdf2IterationCount: UInt64, + pbkdf2KeyLength: UInt64, + pbkdf2Prf: OID, + aesEncryptionScheme: OID, + aesIV: Data + ) throws -> EncryptedPrivateKeyInfo { + typealias PKCS = iso.memberBody.us.rsadsi.pkcs.pkcs5 + + let pbkdf2Params = PBKDF2Params(salt: pbkdf2Salt, + iterationCount: pbkdf2IterationCount, + keyLength: pbkdf2KeyLength, + prf: .init(algorithm: pbkdf2Prf)) + + let encAlgParams = PBES2Params(keyDerivationFunc: .init(algorithm: PKCS.pbkdf2.oid, + parameters: try ASN1.Encoder.encodeTree(pbkdf2Params)), + encryptionScheme: .init(algorithm: aesEncryptionScheme, + parameters: .octetString(aesIV))) + + let encAlgId = AlgorithmIdentifier(algorithm: PKCS.pbes2.oid, + parameters: try ASN1.Encoder.encodeTree(encAlgParams)) + + return EncryptedPrivateKeyInfo(encryptionAlgorithm: encAlgId, + encryptedData: encryptedData) + } +} + +private func printPEM(data: Data, type: String) { + + let base64 = data.base64EncodedString() + let pemBase64 = base64.chunks(ofCount: 64).joined(separator: "\n") + + print("-----BEGIN \(type)-----\n\(pemBase64)\n-----END \(type)-----") +} diff --git a/Sources/ShieldPKCS/ECParameters.swift b/Sources/ShieldX509/ECParameters.swift similarity index 100% rename from Sources/ShieldPKCS/ECParameters.swift rename to Sources/ShieldX509/ECParameters.swift diff --git a/Sources/ShieldX509/ECPrivateKey.swift b/Sources/ShieldX509/ECPrivateKey.swift new file mode 100644 index 000000000..c4b4b8ef8 --- /dev/null +++ b/Sources/ShieldX509/ECPrivateKey.swift @@ -0,0 +1,51 @@ +// +// ECPrivateKey.swift +// Shield +// +// Copyright © 2019 Outfox, inc. +// +// +// Distributed under the MIT License, See LICENSE for details. +// + +import Foundation +import PotentASN1 + + +public struct ECPrivateKey: Equatable, Hashable, Codable { + + public enum Version: Int, CaseIterable, Equatable, Hashable, Codable { + case one = 1 + } + + public var version: Version + public var privateKey: Data + public var parameters: ECParameters? + public var publicKey: BitString? + + public init(version: Version = .one, privateKey: Data, parameters: ECParameters? = nil, publicKey: BitString? = nil) { + self.version = version + self.privateKey = privateKey + self.parameters = parameters + self.publicKey = publicKey + } + +} + +extension ECPrivateKey: SchemaSpecified { + + public static var asn1Schema: Schema { Schemas.ECPrivateKey } + +} + +public extension Schemas { + + static let ECPrivateKey: Schema = + .sequence([ + "version": .version(.integer(allowed: 1 ..< 2)), + "privateKey": .octetString(), + "parameters": .optional(.explicit(0, ECParameters)), + "publicKey": .optional(.explicit(1, .bitString())), + ]) + +} diff --git a/Sources/ShieldX509/EncryptedPrivateKeyInfo.swift b/Sources/ShieldX509/EncryptedPrivateKeyInfo.swift new file mode 100644 index 000000000..33bf59a70 --- /dev/null +++ b/Sources/ShieldX509/EncryptedPrivateKeyInfo.swift @@ -0,0 +1,46 @@ +// +// EncryptedPrivateKeyInfo.swift +// Shield +// +// Copyright © 2019 Outfox, inc. +// +// +// Distributed under the MIT License, See LICENSE for details. +// + +import Foundation +import PotentASN1 +import ShieldOID + +public struct EncryptedPrivateKeyInfo: Equatable, Hashable, Codable { + + public var encryptionAlgorithm: AlgorithmIdentifier + public var encryptedData: Data + + public init(encryptionAlgorithm: AlgorithmIdentifier, encryptedData: Data) { + self.encryptionAlgorithm = encryptionAlgorithm + self.encryptedData = encryptedData + } + +} + +extension EncryptedPrivateKeyInfo: SchemaSpecified { + + public static var asn1Schema: Schema { Schemas.EncryptedPrivateKeyInfo } + +} + +public extension Schemas { + + static let EncryptedPrivateKeyInfoAlgorithms: Schema.DynamicMap = [ + iso.memberBody.us.rsadsi.pkcs.pkcs1.rsaEncryption.asn1: .null, + iso.memberBody.us.ansix962.keyType.ecPublicKey.asn1: ECParameters, + ] + + static let EncryptedPrivateKeyInfo: Schema = + .sequence([ + "encryptionAlgorithm": algorithmIdentifier(EncryptedPrivateKeyInfoAlgorithms), + "encryptedData": .octetString(), + ]) + +} diff --git a/Sources/ShieldX509/PBES2Params.swift b/Sources/ShieldX509/PBES2Params.swift new file mode 100644 index 000000000..2f14b6ad7 --- /dev/null +++ b/Sources/ShieldX509/PBES2Params.swift @@ -0,0 +1,92 @@ +// +// PBES.swift +// Shield +// +// Copyright © 2019 Outfox, inc. +// +// +// Distributed under the MIT License, See LICENSE for details. +// + +import BigInt +import Foundation +import PotentASN1 +import ShieldOID + +public struct PBES2Params: Equatable, Hashable, Codable { + + public var keyDerivationFunc: AlgorithmIdentifier + public var encryptionScheme: AlgorithmIdentifier + + public init(keyDerivationFunc: AlgorithmIdentifier, encryptionScheme: AlgorithmIdentifier) { + self.keyDerivationFunc = keyDerivationFunc + self.encryptionScheme = encryptionScheme + } +} + +extension PBES2Params: SchemaSpecified { + + public static var asn1Schema: Schema { Schemas.PBES2Params } + +} + +public extension Schemas { + + private typealias RSADSIDigAlgs = iso.memberBody.us.rsadsi.digestAlgorithm + private typealias RSADSIEncAlgs = iso.memberBody.us.rsadsi.encryptionAlgorithm + private typealias NISTAlgs = iso_itu.country.us.organization.gov.csor.nistAlgorithms.aes + + static let PBES2ParamsKeyDerivationFuncAlgorithms: Schema.DynamicMap = [ + iso.memberBody.us.rsadsi.pkcs.pkcs5.pbkdf2.asn1: PBKDF2Params + ] + + static let PBES2ParamsEncryptionSchemeAlgorithms: Schema.DynamicMap = [ + RSADSIEncAlgs.rc2CBC.asn1: .null, + RSADSIEncAlgs.rc2ECB.asn1: .null, + RSADSIEncAlgs.rc4.asn1: .null, + RSADSIEncAlgs.rc4WithMAC.asn1: .null, + RSADSIEncAlgs.desxCBC.asn1: .null, + RSADSIEncAlgs.desEDE3CBC.asn1: .null, + RSADSIEncAlgs.rc5CBC.asn1: .null, + RSADSIEncAlgs.rc5CBCPad.asn1: .null, + RSADSIEncAlgs.desCDMF.asn1: .null, + RSADSIEncAlgs.desEDE3.asn1: .null, + + NISTAlgs.aes128_ECB.asn1: .null, + NISTAlgs.aes128_CBC_PAD.asn1: .null, + NISTAlgs.aes128_OFB.asn1: .null, + NISTAlgs.aes128_CFB.asn1: .null, + NISTAlgs.aes128_wrap.asn1: .null, + NISTAlgs.aes128_GCM.asn1: .null, + NISTAlgs.aes128_CCM.asn1: .null, + NISTAlgs.aes128_wrap_pad.asn1: .null, + NISTAlgs.aes128_GMAC.asn1: .null, + + NISTAlgs.aes192_ECB.asn1: .null, + NISTAlgs.aes192_CBC_PAD.asn1: .null, + NISTAlgs.aes192_OFB.asn1: .null, + NISTAlgs.aes192_CFB.asn1: .null, + NISTAlgs.aes192_wrap.asn1: .null, + NISTAlgs.aes192_GCM.asn1: .null, + NISTAlgs.aes192_CCM.asn1: .null, + NISTAlgs.aes192_wrap_pad.asn1: .null, + NISTAlgs.aes192_GMAC.asn1: .null, + + NISTAlgs.aes256_ECB.asn1: .null, + NISTAlgs.aes256_CBC_PAD.asn1: .null, + NISTAlgs.aes256_OFB.asn1: .null, + NISTAlgs.aes256_CFB.asn1: .null, + NISTAlgs.aes256_wrap.asn1: .null, + NISTAlgs.aes256_GCM.asn1: .null, + NISTAlgs.aes256_CCM.asn1: .null, + NISTAlgs.aes256_wrap_pad.asn1: .null, + NISTAlgs.aes256_GMAC.asn1: .null, + ] + + static let PBES2Params: Schema = + .sequence([ + "keyDerivationFunc": algorithmIdentifier(PBES2ParamsKeyDerivationFuncAlgorithms), + "encryptionScheme": algorithmIdentifier(PBES2ParamsEncryptionSchemeAlgorithms), + ]) + +} diff --git a/Sources/ShieldX509/PBKDF2Params.swift b/Sources/ShieldX509/PBKDF2Params.swift new file mode 100644 index 000000000..58cf7fbec --- /dev/null +++ b/Sources/ShieldX509/PBKDF2Params.swift @@ -0,0 +1,57 @@ +// +// PBKDF2Params.swift +// Shield +// +// Copyright © 2019 Outfox, inc. +// +// +// Distributed under the MIT License, See LICENSE for details. +// + +import Foundation +import PotentASN1 +import ShieldOID + +public struct PBKDF2Params: Equatable, Hashable, Codable { + + public var salt: Data + public var iterationCount: UInt64 + public var keyLength: UInt64 + public var prf: AlgorithmIdentifier + + public init(salt: Data, iterationCount: UInt64, keyLength: UInt64, prf: AlgorithmIdentifier) { + self.salt = salt + self.iterationCount = iterationCount + self.keyLength = keyLength + self.prf = prf + } + +} + +extension PBKDF2Params: SchemaSpecified { + + public static var asn1Schema: Schema { Schemas.PBKDF2Params } + +} + +public extension Schemas { + + private typealias DigAlgs = iso.memberBody.us.rsadsi.digestAlgorithm + + private static let PRFAglorithms: Schema.DynamicMap = [ + DigAlgs.hmacWithSHA1.asn1: .null, + DigAlgs.hmacWithSHA224.asn1: .null, + DigAlgs.hmacWithSHA256.asn1: .null, + DigAlgs.hmacWithSHA384.asn1: .null, + DigAlgs.hmacWithSHA512.asn1: .null, + ] + + static let PBKDF2Params: Schema = + .sequence([ + "salt": .choiceOf([.octetString(), .objectIdentifier()]), + "iterationCount": .integer(), + "keyLength": .optional(.integer()), + "prf": algorithmIdentifier(PRFAglorithms), + ]) + +} diff --git a/Sources/ShieldX509/PrivateKeyInfo.swift b/Sources/ShieldX509/PrivateKeyInfo.swift new file mode 100644 index 000000000..87ce59c5a --- /dev/null +++ b/Sources/ShieldX509/PrivateKeyInfo.swift @@ -0,0 +1,53 @@ +// +// PrivateKeyInfo.swift +// Shield +// +// Copyright © 2019 Outfox, inc. +// +// +// Distributed under the MIT License, See LICENSE for details. +// + +import Foundation +import ShieldOID +import PotentASN1 + +public struct PrivateKeyInfo: Equatable, Hashable, Codable { + + public enum Version: Int, CaseIterable, Equatable, Hashable, Codable { + case zero = 0 + } + + public var version: Version + public var privateKeyAlgorithm: AlgorithmIdentifier + public var privateKey: Data + + public init(version: Version = .zero, privateKeyAlgorithm: AlgorithmIdentifier, privateKey: Data) { + self.version = version + self.privateKeyAlgorithm = privateKeyAlgorithm + self.privateKey = privateKey + } + +} + +extension PrivateKeyInfo: SchemaSpecified { + public static var asn1Schema: Schema { Schemas.PrivateKeyInfo } +} + +public extension Schemas { + + static let PrivateKeyInfoAlgorithms: Schema.DynamicMap = [ + iso.memberBody.us.rsadsi.pkcs.pkcs1.rsaEncryption.asn1: .null, + iso.memberBody.us.ansix962.keyType.ecPublicKey.asn1: ECParameters, + ] + + static let PrivateKeyInfoVersion: Schema = .integer(allowed: 0 ..< 1) + + static let PrivateKeyInfo: Schema = + .sequence([ + "version": .version(PrivateKeyInfoVersion), + "privateKeyAlgorithm": algorithmIdentifier(PrivateKeyInfoAlgorithms), + "privateKey": .octetString(), + ]) + +} diff --git a/Sources/ShieldPKCS/RSAPrivateKey.swift b/Sources/ShieldX509/RSAPrivateKey.swift similarity index 73% rename from Sources/ShieldPKCS/RSAPrivateKey.swift rename to Sources/ShieldX509/RSAPrivateKey.swift index 03677ce18..b2b56f353 100644 --- a/Sources/ShieldPKCS/RSAPrivateKey.swift +++ b/Sources/ShieldX509/RSAPrivateKey.swift @@ -35,6 +35,30 @@ public struct RSAPrivateKey: Equatable, Hashable, Codable { public var exponent2: ASN1.Integer public var coefficient: ASN1.Integer public var otherPrimeInfos: [OtherPrimeInfo]? + + public init( + version: Version, + modulus: ASN1.Integer, + publicExponent: ASN1.Integer, + privateExponent: ASN1.Integer, + prime1: ASN1.Integer, + prime2: ASN1.Integer, + exponent1: ASN1.Integer, + exponent2: ASN1.Integer, + coefficient: ASN1.Integer, + otherPrimeInfos: [OtherPrimeInfo]? = nil + ) { + self.version = version + self.modulus = modulus + self.publicExponent = publicExponent + self.privateExponent = privateExponent + self.prime1 = prime1 + self.prime2 = prime2 + self.exponent1 = exponent1 + self.exponent2 = exponent2 + self.coefficient = coefficient + self.otherPrimeInfos = otherPrimeInfos + } } diff --git a/Sources/ShieldPKCS/RSAPublicKey.swift b/Sources/ShieldX509/RSAPublicKey.swift similarity index 100% rename from Sources/ShieldPKCS/RSAPublicKey.swift rename to Sources/ShieldX509/RSAPublicKey.swift diff --git a/Sources/ShieldX509/TBSCertificate.swift b/Sources/ShieldX509/TBSCertificate.swift index 1324753a1..db43895c2 100644 --- a/Sources/ShieldX509/TBSCertificate.swift +++ b/Sources/ShieldX509/TBSCertificate.swift @@ -11,7 +11,6 @@ import Foundation import PotentASN1 import ShieldOID -import ShieldPKCS import ShieldX500 @@ -91,7 +90,7 @@ public extension Schemas { static let PKInfoAlgorithms: Schema.DynamicMap = [ iso.memberBody.us.rsadsi.pkcs.pkcs1.rsaEncryption.asn1: .null, - iso.memberBody.us.ansix962.keyType.ecPublicKey.asn1: ShieldPKCS.Schemas.ECParameters, + iso.memberBody.us.ansix962.keyType.ecPublicKey.asn1: ECParameters, ] static let SignatureAlgorithms: Schema.DynamicMap = [ diff --git a/Tests/SecIdentityTests.swift b/Tests/SecIdentityTests.swift index 1e0dc70c9..5b8fab885 100644 --- a/Tests/SecIdentityTests.swift +++ b/Tests/SecIdentityTests.swift @@ -39,7 +39,7 @@ class SecIdentityTests: XCTestCase { kSecClass as String: kSecClassCertificate, kSecMatchItemList as String: [cert] as CFArray, kSecMatchLimit as String: kSecMatchLimitOne, - ] as CFDictionary) + ] as [String: Any] as CFDictionary) } // Ensure all went well diff --git a/Tests/SecKeyPairTests.swift b/Tests/SecKeyPairTests.swift index 9d97765c9..55133df92 100644 --- a/Tests/SecKeyPairTests.swift +++ b/Tests/SecKeyPairTests.swift @@ -162,7 +162,7 @@ class SecKeyPairTests: XCTestCase { } #endif - func testImportExport() throws { + func testImportExportRSA() throws { let exportedKeyData = try rsaKeyPair.export(password: "123") @@ -195,6 +195,15 @@ class SecKeyPairTests: XCTestCase { } + func testImportExportEC() throws { + + let exportedKeyData = try ecKeyPair.export(password: "123") + + _ = try SecKeyPair.import(fromData: exportedKeyData, withPassword: "123") + + XCTAssertThrowsError(try SecKeyPair.import(fromData: exportedKeyData, withPassword: "456")) + } + func testCodable() throws { let rsaData = try JSONEncoder().encode(rsaKeyPair)