diff --git a/JWT.xcodeproj/project.pbxproj b/JWT.xcodeproj/project.pbxproj index 86147ea..159ef93 100644 --- a/JWT.xcodeproj/project.pbxproj +++ b/JWT.xcodeproj/project.pbxproj @@ -357,7 +357,7 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0700; + LastUpgradeCheck = 0800; ORGANIZATIONNAME = Cocode; TargetAttributes = { 279D639B1AD07FFF0024E2BC = { @@ -532,8 +532,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; @@ -542,6 +544,7 @@ ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -579,8 +582,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; @@ -589,6 +594,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -599,6 +605,7 @@ MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_NAME = "$(PROJECT_NAME)"; SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 3.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -676,7 +683,7 @@ CD9B62211C7753D8005D4844 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -695,7 +702,7 @@ CD9B62221C7753D8005D4844 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -715,6 +722,7 @@ CD9B62331C7753EC005D4844 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -733,6 +741,7 @@ CD9B62341C7753EC005D4844 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -752,6 +761,7 @@ CD9B62451C7753FB005D4844 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -770,6 +780,7 @@ CD9B62461C7753FB005D4844 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; diff --git a/JWT.xcodeproj/xcshareddata/xcschemes/JWT-OSX.xcscheme b/JWT.xcodeproj/xcshareddata/xcschemes/JWT-OSX.xcscheme index 1d4884e..ff0db9d 100644 --- a/JWT.xcodeproj/xcshareddata/xcschemes/JWT-OSX.xcscheme +++ b/JWT.xcodeproj/xcshareddata/xcschemes/JWT-OSX.xcscheme @@ -1,6 +1,6 @@ String { +func base64encode(_ input: Data) -> String { let data = input.base64EncodedData(options: NSData.Base64EncodingOptions(rawValue: 0)) let string = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as! String return string @@ -12,7 +11,7 @@ func base64encode(_ input:Data) -> String { } /// URI Safe base64 decode -func base64decode(_ input:String) -> Data? { +func base64decode(_ input: String) -> Data? { let rem = input.characters.count % 4 var ending = "" diff --git a/Sources/Claims.swift b/Sources/Claims.swift index 2b3f33d..9a817b7 100644 --- a/Sources/Claims.swift +++ b/Sources/Claims.swift @@ -1,53 +1,42 @@ import Foundation -func validateClaims(_ payload:Payload, audience:String?, issuer:String?) -> InvalidToken? { - return validateIssuer(payload, issuer: issuer) ?? validateAudience(payload, audience: audience) ?? - validateDate(payload, key: "exp", comparison: .orderedAscending, failure: .expiredSignature, decodeError: "Expiration time claim (exp) must be an integer") ?? - validateDate(payload, key: "nbf", comparison: .orderedDescending, failure: .immatureSignature, decodeError: "Not before claim (nbf) must be an integer") ?? - validateDate(payload, key: "iat", comparison: .orderedDescending, failure: .invalidIssuedAt, decodeError: "Issued at claim (iat) must be an integer") +func validateClaims(payload: Payload, audience: String?, issuer: String?) throws { + try validateIssuer(payload: payload, issuer: issuer) + try validateAudience(payload: payload, audience: audience) + try validateDate(payload: payload, key: "exp", comparison: .orderedAscending, failure: .expiredSignature) + try validateDate(payload: payload, key: "nbf", comparison: .orderedDescending, failure: .immatureSignature) + try validateDate(payload: payload, key: "iat", comparison: .orderedDescending, failure: .invalidIssuedAt) } -func validateAudience(_ payload:Payload, audience:String?) -> InvalidToken? { - if let audience = audience { - if let aud = payload["aud"] as? [String] { - if !aud.contains(audience) { - return .invalidAudience - } - } else if let aud = payload["aud"] as? String { - if aud != audience { - return .invalidAudience - } - } else { - return .decodeError("Invalid audience claim, must be a string or an array of strings") - } +func validateAudience(payload: Payload, audience: String?) throws { + guard let audience = audience else { + return } - return nil -} + guard let audiences = payload.audiences else { + throw InvalidToken.decodeError("Invalid audience claim, must be a string or an array of strings") + } -func validateIssuer(_ payload:Payload, issuer:String?) -> InvalidToken? { - if let issuer = issuer { - if let iss = payload["iss"] as? String { - if iss != issuer { - return .invalidIssuer - } - } else { - return .invalidIssuer - } + if !audiences.contains(audience) { + throw InvalidToken.invalidAudience } +} - return nil +func validateIssuer(payload: Payload, issuer: String?) throws { + if let issuer = issuer, issuer != payload.issuer { + throw InvalidToken.invalidIssuer + } } -func validateDate(_ payload:Payload, key:String, comparison:ComparisonResult, failure:InvalidToken, decodeError:String) -> InvalidToken? { - if let timestamp = payload[key] as? TimeInterval ?? (payload[key] as? NSString)?.doubleValue as TimeInterval? { - let date = Date(timeIntervalSince1970: timestamp) - if date.compare(Date()) == comparison { - return failure +func validateDate(payload: Payload, key: String, comparison: ComparisonResult, failure: InvalidToken) throws { + guard let validationDate: Date = payload[key] else { + if payload.store[key] != nil { + throw InvalidToken.decodeError("Incorrect date claim value for key: '\(key)'") } - } else if payload[key] != nil { - return .decodeError(decodeError) + return } - return nil + if validationDate.compare(Date()) == comparison { + throw failure + } } diff --git a/Sources/Decode.swift b/Sources/Decode.swift index 4ec9642..311fd72 100644 --- a/Sources/Decode.swift +++ b/Sources/Decode.swift @@ -1,8 +1,7 @@ import Foundation - /// Failure reasons from decoding a JWT -public enum InvalidToken : CustomStringConvertible, Error { +public enum InvalidToken: CustomStringConvertible, Error { /// Decoding the JWT itself failed case decodeError(String) @@ -45,39 +44,30 @@ public enum InvalidToken : CustomStringConvertible, Error { } } - /// Decode a JWT -public func decode(_ jwt:String, algorithms:[Algorithm], verify:Bool = true, audience:String? = nil, issuer:String? = nil) throws -> Payload { - switch load(jwt) { - case let .success(header, payload, signature, signatureInput): - if verify { - if let failure = validateClaims(payload, audience: audience, issuer: issuer) ?? verifySignature(algorithms, header: header, signingInput: signatureInput, signature: signature) { - throw failure - } - } +public func decode(_ jwt: String, algorithms: [Algorithm], verify: Bool = true, audience: String? = nil, issuer: String? = nil) throws -> Payload { - return payload - case .failure(let failure): - throw failure + let (header, payload, signature, signatureInput) = try load(jwt) + if verify { + try validateClaims(payload: payload, audience: audience, issuer: issuer) + try verifySignature(algorithms, header: header, signingInput: signatureInput, signature: signature) } + return payload } /// Decode a JWT -public func decode(_ jwt:String, algorithm:Algorithm, verify:Bool = true, audience:String? = nil, issuer:String? = nil) throws -> Payload { +public func decode(_ jwt: String, algorithm: Algorithm, verify: Bool = true, audience: String? = nil, issuer: String? = nil) throws -> Payload { return try decode(jwt, algorithms: [algorithm], verify: verify, audience: audience, issuer: issuer) } // MARK: Parsing a JWT -enum LoadResult { - case success(header:Payload, payload:Payload, signature:Data, signatureInput:String) - case failure(InvalidToken) -} +typealias LoadedToken = (header: Payload, payload: Payload, signature: Data, signatureInput: String) -func load(_ jwt:String) -> LoadResult { +func load(_ jwt: String) throws -> LoadedToken { let segments = jwt.components(separatedBy: ".") - if segments.count != 3 { - return .failure(.decodeError("Not enough segments")) + guard segments.count == 3 else { + throw InvalidToken.decodeError("Not enough segments") } let headerSegment = segments[0] @@ -85,47 +75,41 @@ func load(_ jwt:String) -> LoadResult { let signatureSegment = segments[2] let signatureInput = "\(headerSegment).\(payloadSegment)" - let headerData = base64decode(headerSegment) - if headerData == nil { - return .failure(.decodeError("Header is not correctly encoded as base64")) + guard let headerData = base64decode(headerSegment) else { + throw InvalidToken.decodeError("Header is not correctly encoded as base64") } - let header = (try? JSONSerialization.jsonObject(with: headerData!, options: JSONSerialization.ReadingOptions(rawValue: 0))) as? Payload - if header == nil { - return .failure(.decodeError("Invalid header")) + guard let header = try Payload(jsonData: headerData) else { + throw InvalidToken.decodeError("Invalid header") } - let payloadData = base64decode(payloadSegment) - if payloadData == nil { - return .failure(.decodeError("Payload is not correctly encoded as base64")) + guard let payloadData = base64decode(payloadSegment) else { + throw InvalidToken.decodeError("Payload is not correctly encoded as base64") } - let payload = (try? JSONSerialization.jsonObject(with: payloadData!, options: JSONSerialization.ReadingOptions(rawValue: 0))) as? Payload - if payload == nil { - return .failure(.decodeError("Invalid payload")) + guard let payload = try Payload(jsonData: payloadData) else { + throw InvalidToken.decodeError("Invalid payload") } - let signature = base64decode(signatureSegment) - if signature == nil { - return .failure(.decodeError("Signature is not correctly encoded as base64")) + guard let signature = base64decode(signatureSegment) else { + throw InvalidToken.decodeError("Signature is not correctly encoded as base64") } - return .success(header:header!, payload:payload!, signature:signature!, signatureInput:signatureInput) + return (header: header, payload: payload, signature: signature, signatureInput: signatureInput) } // MARK: Signature Verification -func verifySignature(_ algorithms:[Algorithm], header:Payload, signingInput:String, signature:Data) -> InvalidToken? { - if let alg = header["alg"] as? String { - let matchingAlgorithms = algorithms.filter { algorithm in algorithm.description == alg } - let results = matchingAlgorithms.map { algorithm in algorithm.verify(signingInput, signature: signature) } - let successes = results.filter { $0 } - if successes.count > 0 { - return nil - } - - return .invalidAlgorithm +func verifySignature(_ algorithms: [Algorithm], header: Payload, signingInput: String, signature: Data) throws { + guard let algorithmDescription: String = header["alg"] else { + throw InvalidToken.decodeError("Missing Algorithm") } - return .decodeError("Missing Algorithm") + let verifiedAlgorithmsMatchingDescription = algorithms + .filter { algorithm in algorithm.description == algorithmDescription } + .filter { algorithm in algorithm.verify(signingInput, signature: signature) } + + if verifiedAlgorithmsMatchingDescription.count == 0 { + throw InvalidToken.invalidAlgorithm + } } diff --git a/Sources/JWT.swift b/Sources/JWT.swift index 84f4809..9ebd7d3 100644 --- a/Sources/JWT.swift +++ b/Sources/JWT.swift @@ -1,10 +1,8 @@ import Foundation import CryptoSwift -public typealias Payload = [String: Any] - /// The supported Algorithms -public enum Algorithm : CustomStringConvertible { +public enum Algorithm: CustomStringConvertible { /// No Algorithm, i-e, insecure case none @@ -31,10 +29,10 @@ public enum Algorithm : CustomStringConvertible { } /// Sign a message using the algorithm - func sign(_ message:String) -> String { - func signHS(_ key: Data, variant:CryptoSwift.HMAC.Variant) -> String { + func sign(_ message: String) -> String { + func signHS(_ key: Data, variant: CryptoSwift.HMAC.Variant) -> String { let messageData = message.data(using: String.Encoding.utf8, allowLossyConversion: false)! - let mac = HMAC(key: key.bytes, variant:variant) + let mac = HMAC(key: key.bytes, variant: variant) let result: [UInt8] do { result = try mac.authenticate(messageData.bytes) @@ -60,106 +58,189 @@ public enum Algorithm : CustomStringConvertible { } /// Verify a signature for a message using the algorithm - func verify(_ message:String, signature:Data) -> Bool { + func verify(_ message: String, signature: Data) -> Bool { return sign(message) == base64encode(signature) } } -// MARK: Encoding - -/*** Encode a payload - - parameter payload: The payload to sign - - parameter algorithm: The algorithm to sign the payload with - - returns: The JSON web token as a String -*/ -public func encode(_ payload:Payload, algorithm:Algorithm) -> String { - func encodeJSON(_ payload:Payload) -> String? { - if let data = try? JSONSerialization.data(withJSONObject: payload, options: JSONSerialization.WritingOptions(rawValue: 0)) { - return base64encode(data) - } - - return nil +/// Encode a payload +/// +/// - parameter payload: The payload to sign +/// - parameter algorithm: The algorithm to sign the payload with +/// +/// - throws: when serialization fails +/// +/// - returns: The JSON web token as a String +public func encode(_ payload: Payload, algorithm: Algorithm) throws -> String { + func encodeJSON(_ payload: Payload) throws -> String { + let data = try JSONSerialization.data(withJSONObject: payload.store) + return base64encode(data) } - let header = encodeJSON(["typ": "JWT" as AnyObject, "alg": algorithm.description as AnyObject])! - let payload = encodeJSON(payload)! + let header = try encodeJSON(["typ": "JWT", "alg": algorithm.description]) + let payload = try encodeJSON(payload) let signingInput = "\(header).\(payload)" let signature = algorithm.sign(signingInput) return "\(signingInput).\(signature)" } -open class PayloadBuilder { - var payload = Payload() +public struct Payload: ExpressibleByDictionaryLiteral { + + typealias BackingStore = [String: Any] + + var store: BackingStore = [:] + + // TODO: this is unsafe because it allows invalid JSON values + public init(dictionaryLiteral elements: (String, Any)...) { + for (key, value) in elements { + store[key] = value + } + } + + init?(jsonData: Data) throws { + guard let store = try JSONSerialization.jsonObject(with: jsonData) as? BackingStore else { + return nil + } + self.store = store + } - open var issuer:String? { + public var issuer: String? { get { - return payload["iss"] as? String + return self["iss"] } set { - payload["iss"] = newValue as AnyObject? + self["iss"] = newValue } } - open var audience:String? { + public var audience: String? { get { - return payload["aud"] as? String + return self["aud"] } set { - payload["aud"] = newValue as AnyObject? + self["aud"] = newValue } } - open var expiration:Date? { + public var audiences: [String]? { get { - if let expiration = payload["exp"] as? TimeInterval { - return Date(timeIntervalSince1970: expiration) - } + return self["aud"] ?? audience.map { [$0] } + } + set { + self["aud"] = newValue + } + } - return nil + public var expiration: Date? { + get { + return self["exp"] } set { - payload["exp"] = newValue?.timeIntervalSince1970 as AnyObject? + self["exp"] = newValue } } - open var notBefore:Date? { + public var notBefore: Date? { get { - if let notBefore = payload["nbf"] as? TimeInterval { - return Date(timeIntervalSince1970: notBefore) - } + return self["nbf"] + } + set { + self["nbf"] = newValue + } + } - return nil + public var issuedAt: Date? { + get { + return self["iat"] } set { - payload["nbf"] = newValue?.timeIntervalSince1970 as AnyObject? + self["iat"] = newValue } } - open var issuedAt:Date? { + subscript(key: String) -> Int? { get { - if let issuedAt = payload["iat"] as? TimeInterval { - return Date(timeIntervalSince1970: issuedAt) - } + return store[key] as? Int + } + set { + store[key] = newValue + } + } - return nil + subscript(key: String) -> [Int]? { + get { + return store[key] as? [Int] } set { - payload["iat"] = newValue?.timeIntervalSince1970 as AnyObject? + store[key] = newValue } } - open subscript(key: String) -> Any { + + subscript(key: String) -> UInt? { get { - return payload[key] + return store[key] as? UInt } set { - payload[key] = newValue + store[key] = newValue } } -} -public func encode(_ algorithm:Algorithm, closure:((PayloadBuilder) -> ())) -> String { - let builder = PayloadBuilder() - closure(builder) - return encode(builder.payload, algorithm: algorithm) + subscript(key: String) -> [UInt]? { + get { + return store[key] as? [UInt] + } + set { + store[key] = newValue + } + } + + subscript(key: String) -> Double? { + get { + return store[key] as? Double + } + set { + store[key] = newValue + } + } + + subscript(key: String) -> [Double]? { + get { + return store[key] as? [Double] + } + set { + store[key] = newValue + } + } + + public subscript(key: String) -> Date? { + get { + guard let timeInterval = store[key] as? TimeInterval ?? + (store[key] as? NSString)?.doubleValue else { + return nil + } + return Date.init(timeIntervalSince1970: timeInterval) + } + set { + store[key] = newValue?.timeIntervalSince1970 + } + } + + public subscript(key: String) -> String? { + get { + return store[key] as? String + } + set { + store[key] = newValue + } + } + + public subscript(key: String) -> [String]? { + get { + return store[key] as? [String] + } + set { + store[key] = newValue + } + } }