ios_client
This commit is contained in:
356
ios_client 0.8.5/Kecalek/Crypto/RSACrypto.swift
Normal file
356
ios_client 0.8.5/Kecalek/Crypto/RSACrypto.swift
Normal file
@@ -0,0 +1,356 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
/// RSA-4096 operations — used for login challenge-response ONLY
|
||||
enum RSACrypto {
|
||||
|
||||
// MARK: - Key Generation
|
||||
|
||||
/// Generate RSA-4096 keypair
|
||||
static func generateKeypair() throws -> (privateKey: SecKey, publicKey: SecKey) {
|
||||
let attributes: [String: Any] = [
|
||||
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
|
||||
kSecAttrKeySizeInBits as String: 4096,
|
||||
]
|
||||
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
|
||||
throw CryptoError.rsaKeyGenerationFailed
|
||||
}
|
||||
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
|
||||
throw CryptoError.rsaKeyGenerationFailed
|
||||
}
|
||||
return (privateKey, publicKey)
|
||||
}
|
||||
|
||||
// MARK: - Serialization
|
||||
|
||||
/// Serialize RSA private key. With password: DER → ECP1. Without: PEM PKCS#8.
|
||||
static func serializePrivateKey(_ key: SecKey, password: Data? = nil) throws -> Data {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let derData = SecKeyCopyExternalRepresentation(key, &error) as Data? else {
|
||||
throw CryptoError.rsaOperationFailed("Failed to export private key")
|
||||
}
|
||||
|
||||
// SecKey exports in PKCS#1 format on iOS — wrap in PKCS#8 for Python compat
|
||||
let pkcs8 = wrapRSAPrivateKeyPKCS8(derData)
|
||||
|
||||
if let password = password {
|
||||
return try KeyEncryption.encrypt(pkcs8, password: password)
|
||||
}
|
||||
|
||||
// PEM encode for Python compatibility
|
||||
return pemEncode(pkcs8, label: "PRIVATE KEY")
|
||||
}
|
||||
|
||||
/// Serialize RSA public key as PEM SubjectPublicKeyInfo (Python-compatible)
|
||||
static func serializePublicKey(_ key: SecKey) throws -> Data {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let derData = SecKeyCopyExternalRepresentation(key, &error) as Data? else {
|
||||
throw CryptoError.rsaOperationFailed("Failed to export public key")
|
||||
}
|
||||
|
||||
// SecKey exports PKCS#1 on iOS — wrap in SubjectPublicKeyInfo
|
||||
let spki = wrapRSAPublicKeySPKI(derData)
|
||||
return pemEncode(spki, label: "PUBLIC KEY")
|
||||
}
|
||||
|
||||
/// Load RSA private key. Auto-detects ECP1 vs PEM format.
|
||||
static func loadPrivateKey(_ data: Data, password: Data? = nil) throws -> SecKey {
|
||||
let derData: Data
|
||||
|
||||
if KeyEncryption.isECP1Format(data) {
|
||||
guard let pwd = password else {
|
||||
throw CryptoError.invalidKeyData("ECP1 key requires password")
|
||||
}
|
||||
let raw = try KeyEncryption.decrypt(data, password: pwd)
|
||||
derData = unwrapPKCS8ToRSAPrivateKey(raw)
|
||||
} else {
|
||||
// PEM format
|
||||
let pem = String(data: data, encoding: .utf8) ?? ""
|
||||
derData = try pemDecode(pem, label: "PRIVATE KEY")
|
||||
.flatMap { unwrapPKCS8ToRSAPrivateKey($0) }
|
||||
?? pemDecode(pem, label: "RSA PRIVATE KEY")
|
||||
?? { throw CryptoError.invalidKeyData("Cannot parse RSA private key PEM") }()
|
||||
}
|
||||
|
||||
let attributes: [String: Any] = [
|
||||
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
|
||||
kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
|
||||
]
|
||||
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let key = SecKeyCreateWithData(derData as CFData, attributes as CFDictionary, &error) else {
|
||||
throw CryptoError.invalidKeyData("Failed to create RSA private key from DER")
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
/// Load RSA public key from PEM
|
||||
static func loadPublicKey(_ pemData: Data) throws -> SecKey {
|
||||
let pem = String(data: pemData, encoding: .utf8) ?? ""
|
||||
|
||||
// Try SubjectPublicKeyInfo (PUBLIC KEY), unwrap to PKCS#1
|
||||
let derData: Data
|
||||
if let spki = pemDecode(pem, label: "PUBLIC KEY") {
|
||||
derData = unwrapSPKIToRSAPublicKey(spki)
|
||||
} else if let pkcs1 = pemDecode(pem, label: "RSA PUBLIC KEY") {
|
||||
derData = pkcs1
|
||||
} else {
|
||||
throw CryptoError.invalidKeyData("Cannot parse RSA public key PEM")
|
||||
}
|
||||
|
||||
let attributes: [String: Any] = [
|
||||
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
|
||||
kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
|
||||
]
|
||||
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let key = SecKeyCreateWithData(derData as CFData, attributes as CFDictionary, &error) else {
|
||||
throw CryptoError.invalidKeyData("Failed to create RSA public key from DER")
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// MARK: - Sign / Verify
|
||||
|
||||
/// Sign data with RSA-PSS SHA-256.
|
||||
/// Note: iOS uses salt_length = hash_length (32). Server must use PSS.AUTO to verify.
|
||||
static func sign(_ privateKey: SecKey, data: Data) throws -> Data {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let signature = SecKeyCreateSignature(
|
||||
privateKey,
|
||||
.rsaSignatureMessagePSSSHA256,
|
||||
data as CFData,
|
||||
&error
|
||||
) as Data? else {
|
||||
throw CryptoError.rsaOperationFailed("RSA signing failed")
|
||||
}
|
||||
return signature
|
||||
}
|
||||
|
||||
/// Verify RSA-PSS SHA-256 signature
|
||||
static func verify(_ publicKey: SecKey, signature: Data, data: Data) -> Bool {
|
||||
SecKeyVerifySignature(
|
||||
publicKey,
|
||||
.rsaSignatureMessagePSSSHA256,
|
||||
data as CFData,
|
||||
signature as CFData,
|
||||
nil
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - RSA-OAEP Encrypt / Decrypt (for device pairing)
|
||||
|
||||
/// Encrypt data with RSA-OAEP SHA-256 using a public key
|
||||
static func encrypt(_ publicKey: SecKey, plaintext: Data) throws -> Data {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let encrypted = SecKeyCreateEncryptedData(
|
||||
publicKey,
|
||||
.rsaEncryptionOAEPSHA256,
|
||||
plaintext as CFData,
|
||||
&error
|
||||
) as Data? else {
|
||||
throw CryptoError.rsaOperationFailed("RSA-OAEP encryption failed")
|
||||
}
|
||||
return encrypted
|
||||
}
|
||||
|
||||
/// Decrypt data with RSA-OAEP SHA-256 using a private key
|
||||
static func decrypt(_ privateKey: SecKey, ciphertext: Data) throws -> Data {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let decrypted = SecKeyCreateDecryptedData(
|
||||
privateKey,
|
||||
.rsaEncryptionOAEPSHA256,
|
||||
ciphertext as CFData,
|
||||
&error
|
||||
) as Data? else {
|
||||
throw CryptoError.rsaOperationFailed("RSA-OAEP decryption failed")
|
||||
}
|
||||
return decrypted
|
||||
}
|
||||
|
||||
/// Generate RSA-2048 keypair (for pairing temp keys — smaller for OAEP payload)
|
||||
static func generateKeypair2048() throws -> (privateKey: SecKey, publicKey: SecKey) {
|
||||
let attributes: [String: Any] = [
|
||||
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
|
||||
kSecAttrKeySizeInBits as String: 2048,
|
||||
]
|
||||
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
|
||||
throw CryptoError.rsaKeyGenerationFailed
|
||||
}
|
||||
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
|
||||
throw CryptoError.rsaKeyGenerationFailed
|
||||
}
|
||||
return (privateKey, publicKey)
|
||||
}
|
||||
|
||||
// MARK: - PEM Helpers
|
||||
|
||||
private static func pemEncode(_ der: Data, label: String) -> Data {
|
||||
let base64 = der.base64EncodedString(options: .lineLength64Characters)
|
||||
let pem = "-----BEGIN \(label)-----\n\(base64)\n-----END \(label)-----\n"
|
||||
return Data(pem.utf8)
|
||||
}
|
||||
|
||||
private static func pemDecode(_ pem: String, label: String) -> Data? {
|
||||
let beginMarker = "-----BEGIN \(label)-----"
|
||||
let endMarker = "-----END \(label)-----"
|
||||
|
||||
guard let beginRange = pem.range(of: beginMarker),
|
||||
let endRange = pem.range(of: endMarker) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let base64String = pem[beginRange.upperBound..<endRange.lowerBound]
|
||||
.replacingOccurrences(of: "\n", with: "")
|
||||
.replacingOccurrences(of: "\r", with: "")
|
||||
.replacingOccurrences(of: " ", with: "")
|
||||
|
||||
return Data(base64Encoded: base64String)
|
||||
}
|
||||
|
||||
// MARK: - ASN.1 PKCS#8 / SPKI Wrappers
|
||||
|
||||
// SecKey on iOS exports RSA keys in PKCS#1 format, but Python expects PKCS#8 / SPKI.
|
||||
// These functions add/remove the ASN.1 wrapping.
|
||||
|
||||
// RSA OID: 1.2.840.113549.1.1.1
|
||||
private static let rsaOID: [UInt8] = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01]
|
||||
private static let nullParam: [UInt8] = [0x05, 0x00]
|
||||
|
||||
/// Wrap PKCS#1 RSA private key in PKCS#8 PrivateKeyInfo envelope
|
||||
private static func wrapRSAPrivateKeyPKCS8(_ pkcs1: Data) -> Data {
|
||||
// PrivateKeyInfo ::= SEQUENCE {
|
||||
// version INTEGER (0),
|
||||
// algorithm AlgorithmIdentifier,
|
||||
// privateKey OCTET STRING (containing PKCS#1 key)
|
||||
// }
|
||||
let version = Data([0x02, 0x01, 0x00]) // INTEGER 0
|
||||
let algorithmSeq = asn1Sequence(Data(rsaOID) + Data(nullParam))
|
||||
let privateKeyOctet = asn1OctetString(pkcs1)
|
||||
return asn1Sequence(version + algorithmSeq + privateKeyOctet)
|
||||
}
|
||||
|
||||
/// Unwrap PKCS#8 to get PKCS#1 RSA private key
|
||||
private static func unwrapPKCS8ToRSAPrivateKey(_ pkcs8: Data) -> Data {
|
||||
// Parse SEQUENCE, skip version + algorithm, extract OCTET STRING
|
||||
guard pkcs8.count > 2 else { return pkcs8 }
|
||||
|
||||
var offset = 0
|
||||
// Outer SEQUENCE
|
||||
guard pkcs8[offset] == 0x30 else { return pkcs8 }
|
||||
offset += 1
|
||||
offset = skipASN1Length(pkcs8, offset: offset)
|
||||
|
||||
// Version INTEGER
|
||||
guard offset < pkcs8.count, pkcs8[offset] == 0x02 else { return pkcs8 }
|
||||
offset += 1
|
||||
let versionLen = readASN1Length(pkcs8, offset: &offset)
|
||||
offset += versionLen
|
||||
|
||||
// Algorithm SEQUENCE
|
||||
guard offset < pkcs8.count, pkcs8[offset] == 0x30 else { return pkcs8 }
|
||||
offset += 1
|
||||
let algoLen = readASN1Length(pkcs8, offset: &offset)
|
||||
offset += algoLen
|
||||
|
||||
// Private key OCTET STRING
|
||||
guard offset < pkcs8.count, pkcs8[offset] == 0x04 else { return pkcs8 }
|
||||
offset += 1
|
||||
let keyLen = readASN1Length(pkcs8, offset: &offset)
|
||||
guard offset + keyLen <= pkcs8.count else { return pkcs8 }
|
||||
return Data(pkcs8[offset..<(offset + keyLen)])
|
||||
}
|
||||
|
||||
/// Wrap PKCS#1 RSA public key in SubjectPublicKeyInfo
|
||||
private static func wrapRSAPublicKeySPKI(_ pkcs1: Data) -> Data {
|
||||
// SubjectPublicKeyInfo ::= SEQUENCE {
|
||||
// algorithm AlgorithmIdentifier,
|
||||
// subjectPublicKey BIT STRING (containing PKCS#1 key)
|
||||
// }
|
||||
let algorithmSeq = asn1Sequence(Data(rsaOID) + Data(nullParam))
|
||||
let bitString = asn1BitString(pkcs1)
|
||||
return asn1Sequence(algorithmSeq + bitString)
|
||||
}
|
||||
|
||||
/// Unwrap SubjectPublicKeyInfo to get PKCS#1 RSA public key
|
||||
private static func unwrapSPKIToRSAPublicKey(_ spki: Data) -> Data {
|
||||
guard spki.count > 2 else { return spki }
|
||||
|
||||
var offset = 0
|
||||
// Outer SEQUENCE
|
||||
guard spki[offset] == 0x30 else { return spki }
|
||||
offset += 1
|
||||
offset = skipASN1Length(spki, offset: offset)
|
||||
|
||||
// Algorithm SEQUENCE
|
||||
guard offset < spki.count, spki[offset] == 0x30 else { return spki }
|
||||
offset += 1
|
||||
let algoLen = readASN1Length(spki, offset: &offset)
|
||||
offset += algoLen
|
||||
|
||||
// BIT STRING
|
||||
guard offset < spki.count, spki[offset] == 0x03 else { return spki }
|
||||
offset += 1
|
||||
let bitLen = readASN1Length(spki, offset: &offset)
|
||||
// Skip the unused bits byte
|
||||
guard offset < spki.count, spki[offset] == 0x00 else { return spki }
|
||||
offset += 1
|
||||
let keyLen = bitLen - 1
|
||||
guard offset + keyLen <= spki.count else { return spki }
|
||||
return Data(spki[offset..<(offset + keyLen)])
|
||||
}
|
||||
|
||||
// MARK: - ASN.1 Primitives
|
||||
|
||||
private static func asn1Length(_ length: Int) -> Data {
|
||||
if length < 0x80 {
|
||||
return Data([UInt8(length)])
|
||||
} else if length <= 0xFF {
|
||||
return Data([0x81, UInt8(length)])
|
||||
} else if length <= 0xFFFF {
|
||||
return Data([0x82, UInt8(length >> 8), UInt8(length & 0xFF)])
|
||||
} else {
|
||||
return Data([0x83, UInt8(length >> 16), UInt8((length >> 8) & 0xFF), UInt8(length & 0xFF)])
|
||||
}
|
||||
}
|
||||
|
||||
private static func asn1Sequence(_ content: Data) -> Data {
|
||||
Data([0x30]) + asn1Length(content.count) + content
|
||||
}
|
||||
|
||||
private static func asn1OctetString(_ content: Data) -> Data {
|
||||
Data([0x04]) + asn1Length(content.count) + content
|
||||
}
|
||||
|
||||
private static func asn1BitString(_ content: Data) -> Data {
|
||||
// BIT STRING: tag + length + unused_bits(0) + content
|
||||
Data([0x03]) + asn1Length(content.count + 1) + Data([0x00]) + content
|
||||
}
|
||||
|
||||
private static func readASN1Length(_ data: Data, offset: inout Int) -> Int {
|
||||
guard offset < data.count else { return 0 }
|
||||
let first = data[offset]
|
||||
offset += 1
|
||||
if first < 0x80 {
|
||||
return Int(first)
|
||||
}
|
||||
let numBytes = Int(first & 0x7F)
|
||||
var length = 0
|
||||
for _ in 0..<numBytes {
|
||||
guard offset < data.count else { return length }
|
||||
length = (length << 8) | Int(data[offset])
|
||||
offset += 1
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
private static func skipASN1Length(_ data: Data, offset: Int) -> Int {
|
||||
var off = offset
|
||||
_ = readASN1Length(data, offset: &off)
|
||||
return off
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user