-
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
235 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import Foundation | ||
import CommonCrypto | ||
|
||
class Cryptor { | ||
|
||
static func hmac(string: String, algorithm: Algorithm, key: String) -> String? { | ||
guard let key = key.data(using: .utf8) else { return nil } | ||
guard let string = string.data(using: .utf8) else { return nil } | ||
|
||
let context = UnsafeMutablePointer<CCHmacContext>.allocate(capacity: 1) | ||
defer { context.deallocate() } | ||
|
||
key.withUnsafeBytes { (buffer: UnsafePointer<UInt8>) in | ||
CCHmacInit(context, algorithm.HMACAlgorithm, buffer, size_t(key.count)) | ||
} | ||
|
||
string.withUnsafeBytes { (buffer: UnsafePointer<UInt8>) in | ||
CCHmacUpdate(context, buffer, size_t(string.count)) | ||
} | ||
|
||
var hmac = Array<UInt8>(repeating: 0, count: Int(algorithm.digestLength)) | ||
CCHmacFinal(context, &hmac) | ||
|
||
return Data(hmac).base64URLEncodedString() | ||
} | ||
|
||
enum Algorithm { | ||
case MD5, SHA1, SHA224, SHA256, SHA384, SHA512 | ||
|
||
var HMACAlgorithm: CCHmacAlgorithm { | ||
var result: Int = 0 | ||
switch self { | ||
case .MD5: result = kCCHmacAlgMD5 | ||
case .SHA1: result = kCCHmacAlgSHA1 | ||
case .SHA224: result = kCCHmacAlgSHA224 | ||
case .SHA256: result = kCCHmacAlgSHA256 | ||
case .SHA384: result = kCCHmacAlgSHA384 | ||
case .SHA512: result = kCCHmacAlgSHA512 | ||
} | ||
return CCHmacAlgorithm(result) | ||
} | ||
|
||
var digestLength: Int { | ||
var result: Int32 = 0 | ||
switch self { | ||
case .MD5: result = CC_MD5_DIGEST_LENGTH | ||
case .SHA1: result = CC_SHA1_DIGEST_LENGTH | ||
case .SHA224: result = CC_SHA224_DIGEST_LENGTH | ||
case .SHA256: result = CC_SHA256_DIGEST_LENGTH | ||
case .SHA384: result = CC_SHA384_DIGEST_LENGTH | ||
case .SHA512: result = CC_SHA512_DIGEST_LENGTH | ||
} | ||
return Int(result) | ||
} | ||
} | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import Foundation | ||
/// Extension for making base64 representations of `Data` safe for | ||
/// transmitting via URL query parameters | ||
extension Data { | ||
|
||
/// Instantiates data by decoding a base64url string into base64 | ||
/// | ||
/// - Parameter string: A base64url encoded string | ||
init?(base64URLEncoded string: String) { | ||
self.init(base64Encoded: string.toggleBase64URLSafe(on: false)) | ||
} | ||
|
||
/// Encodes the string into a base64url safe representation | ||
/// | ||
/// - Returns: A string that is base64 encoded but made safe for passing | ||
/// in as a query parameter into a URL string | ||
func base64URLEncodedString() -> String { | ||
return self.base64EncodedString().toggleBase64URLSafe(on: true) | ||
} | ||
|
||
} | ||
|
||
extension String { | ||
|
||
var base64String: String? { | ||
return data(using: .utf8)?.base64EncodedString() | ||
} | ||
|
||
var base64UrlString: String? { | ||
return data(using: .utf8)?.base64URLEncodedString() | ||
} | ||
|
||
/// Encodes or decodes into a base64url safe representation | ||
/// | ||
/// - Parameter on: Whether or not the string should be made safe for URL strings | ||
/// - Returns: if `on`, then a base64url string; if `off` then a base64 string | ||
func toggleBase64URLSafe(on: Bool) -> String { | ||
if on { | ||
// Make base64 string safe for passing into URL query params | ||
let base64url = self.replacingOccurrences(of: "/", with: "_") | ||
.replacingOccurrences(of: "+", with: "-") | ||
.replacingOccurrences(of: "=", with: "") | ||
return base64url | ||
} else { | ||
// Return to base64 encoding | ||
var base64 = self.replacingOccurrences(of: "_", with: "/") | ||
.replacingOccurrences(of: "-", with: "+") | ||
// Add any necessary padding with `=` | ||
if base64.count % 4 != 0 { | ||
base64.append(String(repeating: "=", count: 4 - base64.count % 4)) | ||
} | ||
return base64 | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import Foundation | ||
|
||
public class JWT { | ||
|
||
public init(alg: JWT.Algorithm = .HS256, secret: String = "") { | ||
self.alg = alg | ||
self.header["alg"] = alg.rawValue | ||
self.header["typ"] = "JWT" | ||
self.secret = secret | ||
} | ||
|
||
public enum Algorithm: String { | ||
case HS256 | ||
case HS384 | ||
case HS512 | ||
|
||
var forCryptor: Cryptor.Algorithm { | ||
switch self { | ||
case .HS256: return .SHA256 | ||
case .HS384: return .SHA384 | ||
case .HS512: return .SHA512 | ||
} | ||
} | ||
} | ||
var alg: Algorithm | ||
|
||
public var header: [String: String] = [:] | ||
public var payload: [String: Any] = [:] | ||
public var secret: String | ||
|
||
public var subject: String? { return payload["sub"] as? String } | ||
public var identifier: String? { return payload["jti"] as? String } | ||
public var issuer: String? { return payload["iss"] as? String } | ||
|
||
public var notValidBefore: Date? { | ||
if let interval = payload["nbf"] as? TimeInterval { | ||
return Date(timeIntervalSince1970: interval) | ||
} | ||
return nil | ||
} | ||
public var issuedAt: Date? { | ||
if let interval = payload["iat"] as? TimeInterval { | ||
return Date(timeIntervalSince1970: interval) | ||
} | ||
return nil | ||
} | ||
public var expiresAt: Date? { | ||
if let interval = payload["exp"] as? TimeInterval { | ||
return Date(timeIntervalSince1970: interval) | ||
} | ||
return nil | ||
} | ||
|
||
public var isExpired: Bool? { | ||
guard let expireDate = expiresAt else { return nil } | ||
return expireDate.compare(Date()) != .orderedDescending ? true : false} | ||
|
||
public var token: String? { | ||
do { | ||
let headerString = try JSONSerialization.data(withJSONObject: header, options: []).base64URLEncodedString() | ||
let payloadString = try JSONSerialization.data(withJSONObject: payload, options: []).base64URLEncodedString() | ||
|
||
let rawSign = "\(headerString).\(payloadString)" | ||
|
||
if let sign = Cryptor.hmac(string: rawSign, algorithm: alg.forCryptor, key: secret) { | ||
return "\(rawSign).\(sign)" | ||
} else { | ||
print("JWT: Can't compute sign.") | ||
return nil | ||
} | ||
} catch { | ||
print(error.localizedDescription) | ||
return nil | ||
} | ||
} | ||
} | ||
|
||
//MARK: - Token decoding | ||
extension JWT { | ||
public convenience init?(token: String) { | ||
let elements = token.split(separator: ".").map({String($0)}) | ||
guard | ||
elements.count == 3 else { | ||
print("JWT: Wrong format!") | ||
return nil | ||
} | ||
|
||
guard let headerData = Data(base64URLEncoded: elements[0]), | ||
let payloadData = Data(base64URLEncoded: elements[1]) else { | ||
print("JWT: Wrong format!") | ||
print("Failed to parse header/payload.") | ||
return nil | ||
} | ||
|
||
do { | ||
guard let header = try JSONSerialization.jsonObject(with: headerData, options: []) as? [String: String], | ||
let payload = try JSONSerialization.jsonObject(with: payloadData, options: []) as? [String: Any] | ||
else { | ||
print("JWT: Failed to parse header/payload.") | ||
return nil | ||
} | ||
|
||
guard let algString = header["alg"] else { | ||
print("JWT: Can't define algorithm.") | ||
return nil | ||
} | ||
guard let alg = Algorithm(rawValue: algString) else { | ||
print("JWT: Alghoritm doesn't support.") | ||
return nil | ||
} | ||
|
||
self.init(alg: alg, secret: "") | ||
self.header = header | ||
self.payload = payload | ||
|
||
} catch { | ||
print(error.localizedDescription) | ||
return nil | ||
} | ||
} | ||
} |