Skip to content

Commit 62636f8

Browse files
committed
Added authentication token provider support
1 parent d138af6 commit 62636f8

14 files changed

+859
-161
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//
2+
// CertificateProvider.swift
3+
// SwiftyAPNS
4+
//
5+
// Created by Sergii Tkachenko on 11.08.2020.
6+
// Copyright © 2020 Sergii Tkachenko. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
private enum APNSCertificateProviderError: LocalizedError {
12+
13+
case badUrl
14+
case encodePayload
15+
case parseResponce
16+
case emptyData
17+
18+
public var errorDescription: String? {
19+
switch self {
20+
case .badUrl: return
21+
"The url was invalid"
22+
case .encodePayload: return
23+
"Can't encode payload"
24+
case .parseResponce: return
25+
"Can't parse responce"
26+
case .emptyData: return
27+
"Empty data"
28+
}
29+
}
30+
}
31+
32+
open class APNSCertificateProvider: NSObject, APNSSendMessageProtocol {
33+
34+
private var identity: SecIdentity
35+
private var sesion: URLSession?
36+
37+
public init(identity: SecIdentity, sandbox: Bool = true, configuration: URLSessionConfiguration = URLSessionConfiguration.default, qeue: OperationQueue = OperationQueue.main) {
38+
self.identity = identity
39+
super.init()
40+
self.sesion = URLSession.init(configuration: configuration, delegate: self, delegateQueue: qeue)
41+
}
42+
43+
public func push(_ notification: APNSNotification, completion: @escaping (Result<APNSResponse, Error>) -> Void) {
44+
45+
let options = notification.options
46+
var components = URLComponents()
47+
components.scheme = "https"
48+
components.host = options.url
49+
components.path = "/3/device/\(notification.token)"
50+
guard let url = components.url else {
51+
completion(.failure(APNSCertificateProviderError.badUrl))
52+
return
53+
}
54+
55+
var request = URLRequest.init(url: url)
56+
request.httpMethod = "POST"
57+
if let port = options.port {
58+
components.port = port.rawValue
59+
}
60+
request.applyOptions(options)
61+
62+
let encoder = JSONEncoder()
63+
encoder.outputFormatting = .prettyPrinted
64+
do {
65+
let payload = try encoder.encode(notification.payload)
66+
request.httpBody = payload
67+
} catch {
68+
completion(.failure(APNSCertificateProviderError.encodePayload))
69+
return
70+
}
71+
72+
let task = self.sesion?.dataTask(with: request) { (data, responce, error) in
73+
if let error = error {
74+
completion(.failure(error))
75+
} else if let responce = responce as? HTTPURLResponse, let data = data {
76+
if let apnsStatus = APNSStatus(code: responce.statusCode),
77+
let apnsId = responce.allHeaderFields["apns-id"] as? String
78+
{
79+
let decoder = JSONDecoder()
80+
let reason = try? decoder.decode(APNSError.self, from: data)
81+
let apnsResponce = APNSResponse(status: apnsStatus, apnsId: apnsId, reason: reason)
82+
completion(.success(apnsResponce))
83+
} else {
84+
completion(.failure(APNSCertificateProviderError.parseResponce))
85+
}
86+
} else {
87+
completion(.failure(APNSCertificateProviderError.emptyData))
88+
}
89+
}
90+
task?.resume()
91+
}
92+
}
93+
94+
extension APNSCertificateProvider: URLSessionDelegate {
95+
public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
96+
if let error = error {
97+
print("Error: \(error)")
98+
}
99+
}
100+
101+
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
102+
var certificate: SecCertificate?
103+
_ = withUnsafeMutablePointer(to: &certificate) {
104+
SecIdentityCopyCertificate(self.identity, UnsafeMutablePointer($0))
105+
}
106+
107+
var certificates = [SecCertificate]()
108+
if let cert = certificate {
109+
certificates.append(cert)
110+
}
111+
112+
let cred = URLCredential.init(identity: self.identity, certificates: certificates, persistence: .forSession)
113+
completionHandler(.useCredential, cred)
114+
}
115+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//
2+
// Data+Extensions.swift
3+
// SwiftyAPNS
4+
//
5+
// Created by Sergii Tkachenko on 12.08.2020.
6+
// Copyright © 2020 Sergii Tkachenko. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public typealias ASN1 = Data
12+
13+
public enum ASN1Error: LocalizedError {
14+
15+
case invalidAsn1
16+
17+
public var errorDescription: String? {
18+
switch self {
19+
case .invalidAsn1:
20+
return "The ASN.1 data has invalid format."
21+
}
22+
}
23+
}
24+
25+
extension ASN1 {
26+
private indirect enum ASN1Element {
27+
case seq(elements: [ASN1Element])
28+
case integer(int: Int)
29+
case bytes(data: Data)
30+
case constructed(tag: Int, elem: ASN1Element)
31+
case unknown
32+
}
33+
34+
public func toECKeyData() throws -> ECKeyData {
35+
let (result, _) = self.toASN1Element()
36+
37+
guard case let ASN1Element.seq(elements: es) = result,
38+
case let ASN1Element.bytes(data: privateOctest) = es[2] else {
39+
throw ASN1Error.invalidAsn1
40+
}
41+
42+
let (octest, _) = privateOctest.toASN1Element()
43+
guard case let ASN1Element.seq(elements: seq) = octest,
44+
case let ASN1Element.bytes(data: privateKeyData) = seq[1],
45+
case let ASN1Element.constructed(tag: _, elem: publicElement) = seq[3],
46+
case let ASN1Element.bytes(data: publicKeyData) = publicElement else {
47+
throw ASN1Error.invalidAsn1
48+
}
49+
50+
let keyData = (publicKeyData.drop(while: { $0 == 0x00}) + privateKeyData)
51+
return keyData
52+
}
53+
54+
// SecKeyCreateSignature seems to sometimes return a leading zero; strip it out
55+
private func dropLeadingBytes() -> Data {
56+
if self.count == 33 {
57+
return self.dropFirst()
58+
}
59+
return self
60+
}
61+
62+
/// Convert an ASN.1 format EC signature returned by commoncrypto into a raw 64bit signature
63+
public func toRawSignature() throws -> Data {
64+
let (result, _) = self.toASN1Element()
65+
66+
guard case let ASN1Element.seq(elements: es) = result,
67+
case let ASN1Element.bytes(data: sigR) = es[0],
68+
case let ASN1Element.bytes(data: sigS) = es[1] else {
69+
throw ASN1Error.invalidAsn1
70+
}
71+
72+
let rawSig = sigR.dropLeadingBytes() + sigS.dropLeadingBytes()
73+
return rawSig
74+
}
75+
76+
private func readLength() -> (Int, Int) {
77+
if self[0] & 0x80 == 0x00 { // short form
78+
return (Int(self[0]), 1)
79+
} else {
80+
let lenghOfLength = Int(self[0] & 0x7F)
81+
var result: Int = 0
82+
for i in 1..<(1 + lenghOfLength) {
83+
result = 256 * result + Int(self[i])
84+
}
85+
return (result, 1 + lenghOfLength)
86+
}
87+
}
88+
89+
private func toASN1Element() -> (ASN1Element, Int) {
90+
guard self.count >= 2 else {
91+
// format error
92+
return (.unknown, self.count)
93+
}
94+
95+
switch self[0] {
96+
case 0x30: // sequence
97+
let (length, lengthOfLength) = self.advanced(by: 1).readLength()
98+
var result: [ASN1Element] = []
99+
var subdata = self.advanced(by: 1 + lengthOfLength)
100+
var alreadyRead = 0
101+
102+
while alreadyRead < length {
103+
let (e, l) = subdata.toASN1Element()
104+
result.append(e)
105+
subdata = subdata.count > l ? subdata.advanced(by: l) : Data()
106+
alreadyRead += l
107+
}
108+
return (.seq(elements: result), 1 + lengthOfLength + length)
109+
110+
case 0x02: // integer
111+
let (length, lengthOfLength) = self.advanced(by: 1).readLength()
112+
if (length < 8) {
113+
var result: Int = 0
114+
let subdata = self.advanced(by: 1 + lengthOfLength)
115+
// ignore negative case
116+
for i in 0..<length {
117+
result = 256 * result + Int(subdata[i])
118+
}
119+
return (.integer(int: result), 1 + lengthOfLength + length)
120+
}
121+
// number is too large to fit in Int; return the bytes
122+
return (.bytes(data: self.subdata(in: (1 + lengthOfLength) ..< (1 + lengthOfLength + length))), 1 + lengthOfLength + length)
123+
124+
125+
case let s where (s & 0xe0) == 0xa0: // constructed
126+
let tag = Int(s & 0x1f)
127+
let (length, lengthOfLength) = self.advanced(by: 1).readLength()
128+
let subdata = self.advanced(by: 1 + lengthOfLength)
129+
let (e, _) = subdata.toASN1Element()
130+
return (.constructed(tag: tag, elem: e), 1 + lengthOfLength + length)
131+
132+
default: // octet string
133+
let (length, lengthOfLength) = self.advanced(by: 1).readLength()
134+
return (.bytes(data: self.subdata(in: (1 + lengthOfLength) ..< (1 + lengthOfLength + length))), 1 + lengthOfLength + length)
135+
}
136+
}
137+
}
138+
139+
public typealias ECKeyData = Data
140+
141+
extension ECKeyData {
142+
public func toPrivateKey() throws -> ECPrivateKey {
143+
var error: Unmanaged<CFError>? = nil
144+
guard let privateKey =
145+
SecKeyCreateWithData(self as CFData,
146+
[kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
147+
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
148+
kSecAttrKeySizeInBits: 256] as CFDictionary,
149+
&error) else {
150+
throw error!.takeRetainedValue()
151+
}
152+
return privateKey
153+
}
154+
}
155+
156+
extension Data {
157+
func base64EncodedURLString() -> String {
158+
return base64EncodedString()
159+
.replacingOccurrences(of: "=", with: "")
160+
.replacingOccurrences(of: "+", with: "-")
161+
.replacingOccurrences(of: "/", with: "_")
162+
}
163+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// SecIdentity+Extensions.swift
3+
// SwiftyAPNS
4+
//
5+
// Created by Sergii Tkachenko on 12.08.2020.
6+
// Copyright © 2020 Sergii Tkachenko. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
extension SecIdentity {
12+
public func name() -> String {
13+
var result = ""
14+
var certificate: SecCertificate?
15+
_ = withUnsafeMutablePointer(to: &certificate) {
16+
SecIdentityCopyCertificate(self, UnsafeMutablePointer($0))
17+
}
18+
19+
if let cert = certificate {
20+
var cfName: CFString?
21+
_ = withUnsafeMutablePointer(to: &cfName) {
22+
SecCertificateCopyCommonName(cert, UnsafeMutablePointer($0))
23+
}
24+
if let neme = cfName {
25+
result = neme as String
26+
} else if let description = SecCertificateCopyLongDescription(nil, cert, nil) {
27+
result = description as String
28+
}
29+
}
30+
return result
31+
}
32+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// SecKey+Extensions.swift
3+
// SwiftyAPNS
4+
//
5+
// Created by Sergii Tkachenko on 12.08.2020.
6+
// Copyright © 2020 Sergii Tkachenko. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import CommonCrypto
11+
12+
public typealias ECPrivateKey = SecKey
13+
14+
public enum ECPrivateKeyError: LocalizedError {
15+
16+
case digestDataCorruption
17+
case keyNotSupportES256Signing
18+
19+
public var errorDescription: String? {
20+
switch self {
21+
case .digestDataCorruption:
22+
return "Internal error."
23+
case .keyNotSupportES256Signing:
24+
return "The private key does not support ES256 signing"
25+
}
26+
}
27+
}
28+
29+
extension ECPrivateKey {
30+
public func es256Sign(digest: String) throws -> String {
31+
guard let message = digest.data(using: .utf8) else {
32+
throw ECPrivateKeyError.digestDataCorruption
33+
}
34+
35+
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
36+
CC_SHA256((message as NSData).bytes, CC_LONG(message.count), &hash)
37+
38+
let digestData = Data(hash)
39+
let algorithm = SecKeyAlgorithm.ecdsaSignatureDigestX962SHA256
40+
guard SecKeyIsAlgorithmSupported(self, .sign, algorithm) else {
41+
throw ECPrivateKeyError.keyNotSupportES256Signing
42+
}
43+
44+
var error: Unmanaged<CFError>? = nil
45+
guard let signature = SecKeyCreateSignature(self, algorithm, digestData as CFData, &error) else {
46+
throw error!.takeRetainedValue()
47+
}
48+
49+
let rawSignature = try (signature as ASN1).toRawSignature()
50+
return rawSignature.base64EncodedURLString()
51+
}
52+
}

0 commit comments

Comments
 (0)