diff --git a/.gitignore b/.gitignore index 8615121..f36a59c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,26 @@ # Xcode # -build/ -*.pbxuser -!default.pbxuser + +# Exclude the build directory +build/* + +# Exclude temp nibs and swap files +*~.nib +*.swp + +# Exclude OS X folder attributes +.DS_Store + +# Exclude user-specific XCode 3 and 4 files +*.mode1 *.mode1v3 -!default.mode1v3 *.mode2v3 -!default.mode2v3 +*.perspective *.perspectivev3 -!default.perspectivev3 +*.pbxuser +*.xcworkspace xcuserdata -*.xccheckout -*.moved-aside -DerivedData -*.hmap -*.ipa -*.xcuserstate + # CocoaPods # diff --git a/Build-Phases/common-crypto.sh b/Build-Phases/common-crypto.sh new file mode 100755 index 0000000..2524659 --- /dev/null +++ b/Build-Phases/common-crypto.sh @@ -0,0 +1,28 @@ +# Credits to @radianttap https://github.com/radianttap/JSONWebToken.swift/blob/master/LICENSE + +COMMON_CRYPTO_DIR="${SDKROOT}/usr/include/CommonCrypto" +if [ -f "${COMMON_CRYPTO_DIR}/module.modulemap" ] +then + echo "CommonCrypto already exists, skipping" +else + # This if-statement means we'll only run the main script if the + # CommonCrypto.framework directory doesn't exist because otherwise + # the rest of the script causes a full recompile for anything + # where CommonCrypto is a dependency + # Do a "Clean Build Folder" to remove this directory and trigger + # the rest of the script to run + FRAMEWORK_DIR="${BUILT_PRODUCTS_DIR}/CommonCrypto.framework" + + if [ -d "${FRAMEWORK_DIR}" ]; then + echo "${FRAMEWORK_DIR} already exists, so skipping the rest of the script." + exit 0 + fi + + mkdir -p "${FRAMEWORK_DIR}/Modules" + echo "module CommonCrypto [system] { + header \"${SDKROOT}/usr/include/CommonCrypto/CommonCrypto.h\" + export * + }" >> "${FRAMEWORK_DIR}/Modules/module.modulemap" + + ln -sf "${SDKROOT}/usr/include/CommonCrypto" "${FRAMEWORK_DIR}/Headers" +fi \ No newline at end of file diff --git a/JWT workspace.xcworkspace/contents.xcworkspacedata b/JWT workspace.xcworkspace/contents.xcworkspacedata index 3030947..1e0b833 100644 --- a/JWT workspace.xcworkspace/contents.xcworkspacedata +++ b/JWT workspace.xcworkspace/contents.xcworkspacedata @@ -2,10 +2,7 @@ - - + location = "group:JWT-Playground.playground"> diff --git a/JWT workspace.xcworkspace/xcshareddata/JWT workspace.xcscmblueprint b/JWT workspace.xcworkspace/xcshareddata/JWT workspace.xcscmblueprint new file mode 100644 index 0000000..c6ae699 --- /dev/null +++ b/JWT workspace.xcworkspace/xcshareddata/JWT workspace.xcscmblueprint @@ -0,0 +1,30 @@ +{ + "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "D13A0161F3961F3380472D7F6128C9B2159DADA6", + "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { + + }, + "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { + "D13A0161F3961F3380472D7F6128C9B2159DADA6" : 0, + "CD87797B9D09AEEE7FEB1D43B38DA32D51BC7F68" : 0 + }, + "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "FC5C1AE1-1DF3-43CF-89D3-D0C72142FD62", + "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { + "D13A0161F3961F3380472D7F6128C9B2159DADA6" : "swift-jwt\/", + "CD87797B9D09AEEE7FEB1D43B38DA32D51BC7F68" : "swift-sodium\/" + }, + "DVTSourceControlWorkspaceBlueprintNameKey" : "JWT workspace", + "DVTSourceControlWorkspaceBlueprintVersion" : 204, + "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "JWT workspace.xcworkspace", + "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ + { + "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:ziogaschr\/swift-sodium.git", + "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", + "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "CD87797B9D09AEEE7FEB1D43B38DA32D51BC7F68" + }, + { + "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:stannie\/swift-jwt.git", + "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", + "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "D13A0161F3961F3380472D7F6128C9B2159DADA6" + } + ] +} \ No newline at end of file diff --git a/JWT-Playground.playground/Contents.swift b/JWT-Playground.playground/Contents.swift new file mode 100644 index 0000000..7a91b66 --- /dev/null +++ b/JWT-Playground.playground/Contents.swift @@ -0,0 +1,79 @@ +/*: + ### Table of Contents + + 1. [HS256 Load](HS256 Load) + 2. [HS256 Dump](HS256 Dump) + 3. [Ed25519 Dump](Ed25519 Dump) + */ + +import SwiftJWT + +/*: + ### HS256 Load + Example: load a HS256 signed JWT from a string, and generate a new one with a new password + */ + +let jwt_str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InBob25lIiwicGhvbmVfbnVtYmVyIjoiKzE2NTAyODU1NjAwIiwidHlwZSI6ImxhbmRsaW5lIiwiYXVkIjoiaHR0cHM6Ly9hdXRoZW50aXEuY29tIn0.CAqNpbmOA9lz9aq7Sp1NqqbdLJARmFKY3L7CKcgXLNU" + +var jwt = JWT(algorithms: ["none", "HS256"]) + +do { + try jwt.loads(jwt_str, key: "secret") + print(jwt.header) + print(jwt.body) + + let new_jwt = try jwt.dumps("geheim") + + assert(jwt_str != new_jwt) + +} catch { + print(error) + print("Validate of SignedJWT failed") +} + + +/*: + ### HS256 Dump + Example: dump a HS256 signed JWT from a dictionary + */ + +jwt = JWT(header: ["alg":"HS256"], + body: [ + "sub": "1234567890", + "name": "John Doe", + "admin": true + ], algorithms: nil) + +do { + print(try jwt.dumps("secret")) + +} catch { + print(error) + print("Validate of SignedJWT failed") +} + + +/*: + ### Ed25519 Load + Example: load an Ed25519 signed JWT from a string + */ + +let jwt_ed = "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiSldUIiwia2lkIjoiWE43VnBFWDF1Q3h4aHZ3VXVhY1lodVU5dDZ1eGdMYWhSaUxlU0VIRU5payJ9.eyJmb28iOiJiYXIifQ.a2dDcKXByKxiouOLnXUm7YUKHMGOU3yn_g91C90e8YmKjlF1_9ylAKukfMm6Y6WS3dZp2ysaglzzTnVxnRYyDQ" +let sk = "YHWUUc0P6SY46WaDdnssE8NpFsQQxJrvmdOrpU9X0wU" +let pk = "XN7VpEX1uCxxhvwUuacYhuU9t6uxgLahRiLeSEHENik" + + +jwt = JWTNaCl(algorithms: ["Ed25519"]) + +do { + try jwt.loads(jwt_ed, verify: false, mandatory: ["foo"]) + + print(jwt.header) + print(jwt.body) + +} catch { + print(error) + print("Validate of SignedJWT failed") +} + + diff --git a/JWT-Playground.playground/section-1.swift b/JWT-Playground.playground/section-1.swift deleted file mode 100644 index 7756ab4..0000000 --- a/JWT-Playground.playground/section-1.swift +++ /dev/null @@ -1,27 +0,0 @@ -// Playground - noun: a place where people can play - -import SwiftJWT - -// Example: load a HS256 signed JWT from a sting, and generate a new one with a new password - -let jwt_str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InBob25lIiwicGhvbmVfbnVtYmVyIjoiKzE2NTAyODU1NjAwIiwidHlwZSI6ImxhbmRsaW5lIiwiYXVkIjoiaHR0cHM6Ly9hdXRoZW50aXEuY29tIn0.CAqNpbmOA9lz9aq7Sp1NqqbdLJARmFKY3L7CKcgXLNU" - -var jwt = JWT(algorithms: ["none","HS256"]) -var j = jwt.loads(jwt_str, key: "secret") -println(jwt.dumps("geheim")) - -let jwt_ed = "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiSldUIiwia2lkIjoiWE43VnBFWDF1Q3h4aHZ3VXVhY1lodVU5dDZ1eGdMYWhSaUxlU0VIRU5payJ9.eyJmb28iOiJiYXIifQ.a2dDcKXByKxiouOLnXUm7YUKHMGOU3yn_g91C90e8YmKjlF1_9ylAKukfMm6Y6WS3dZp2ysaglzzTnVxnRYyDQ" -let sk = "YHWUUc0P6SY46WaDdnssE8NpFsQQxJrvmdOrpU9X0wU" -let pk = "XN7VpEX1uCxxhvwUuacYhuU9t6uxgLahRiLeSEHENik" -jwt = JWTNaCl(algorithms: ["Ed25519","none"]) -jwt.loads(jwt_ed) -jwt.header["alg"] = "none" -println(jwt.dumps()!) - -jwt = JWT(header: ["alg":"HS256"], - body: [ - "sub": "1234567890", - "name": "John Doe", - "admin": true - ], algorithms: nil) -println(jwt.dumps("secret")!) diff --git a/JWT/CommonCrypto/CommonCrypto.h b/JWT/CommonCrypto/CommonCrypto.h deleted file mode 100644 index 8e6e747..0000000 --- a/JWT/CommonCrypto/CommonCrypto.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// CommonCrypto.h -// CommonCrypto -// -// Created by Stan P. van de Burgt on 3/31/15. -// Copyright (c) 2015 RoundZero bv. All rights reserved. -// - -#import - -//! Project version number for CommonCrypto. -FOUNDATION_EXPORT double CommonCryptoVersionNumber; - -//! Project version string for CommonCrypto. -FOUNDATION_EXPORT const unsigned char CommonCryptoVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/JWT/CommonCrypto/CommonCrypto.xcconfig b/JWT/CommonCrypto/CommonCrypto.xcconfig deleted file mode 100644 index 2806757..0000000 --- a/JWT/CommonCrypto/CommonCrypto.xcconfig +++ /dev/null @@ -1,15 +0,0 @@ -// -// CommonCrypto.xcconfig -// JWT -// -// Created by Stan P. van de Burgt on 3/31/15. -// Copyright (c) 2015 RoundZero bv. All rights reserved. -// - - -// See http://stackoverflow.com/questions/25248598/importing-commoncrypto-in-a-swift-framework (answer from stephencelis) on how to link -// and http://stackoverflow.com/questions/24099520/commonhmac-in-swift (sample code) - -MODULEMAP_FILE[sdk=iphoneos*] = $(SRCROOT)/CommonCrypto/iphoneos.modulemap -MODULEMAP_FILE[sdk=iphonesimulator*] = $(SRCROOT)/CommonCrypto/iphonesimulator.modulemap -MODULEMAP_FILE[sdk=macosx*] = $(SRCROOT)/CommonCrypto/macosx.modulemap diff --git a/JWT/CommonCrypto/Info.plist b/JWT/CommonCrypto/Info.plist deleted file mode 100644 index 679f1f0..0000000 --- a/JWT/CommonCrypto/Info.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - nl.roundzero.$(PRODUCT_NAME:rfc1034identifier) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/JWT/CommonCrypto/iphoneos.modulemap b/JWT/CommonCrypto/iphoneos.modulemap deleted file mode 100644 index 125b772..0000000 --- a/JWT/CommonCrypto/iphoneos.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CommonCrypto [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/CommonCrypto/CommonCrypto.h" - export * -} diff --git a/JWT/CommonCrypto/iphonesimulator.modulemap b/JWT/CommonCrypto/iphonesimulator.modulemap deleted file mode 100644 index 835d686..0000000 --- a/JWT/CommonCrypto/iphonesimulator.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CommonCrypto [system] { - header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include/CommonCrypto/CommonCrypto.h" - export * -} diff --git a/JWT/CommonCrypto/macosx.modulemap b/JWT/CommonCrypto/macosx.modulemap deleted file mode 100644 index fc7fb2a..0000000 --- a/JWT/CommonCrypto/macosx.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CommonCrypto [system] { - header "/usr/include/CommonCrypto/CommonCrypto.h" - export * -} diff --git a/JWT/CommonCryptoTests/CommonCryptoTests.swift b/JWT/CommonCryptoTests/CommonCryptoTests.swift deleted file mode 100644 index 78a1147..0000000 --- a/JWT/CommonCryptoTests/CommonCryptoTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// CommonCryptoTests.swift -// CommonCryptoTests -// -// Created by Stan P. van de Burgt on 3/31/15. -// Copyright (c) 2015 RoundZero bv. All rights reserved. -// - -import UIKit -import XCTest - -class CommonCryptoTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testExample() { - // This is an example of a functional test case. - XCTAssert(true, "Pass") - } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measureBlock() { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/JWT/CommonCryptoTests/Info.plist b/JWT/CommonCryptoTests/Info.plist deleted file mode 100644 index 0fc1bd4..0000000 --- a/JWT/CommonCryptoTests/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - nl.roundzero.$(PRODUCT_NAME:rfc1034identifier) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - - diff --git a/JWT/JWT/Extensions/Data+JWT.swift b/JWT/JWT/Extensions/Data+JWT.swift new file mode 100644 index 0000000..a0d0bf3 --- /dev/null +++ b/JWT/JWT/Extensions/Data+JWT.swift @@ -0,0 +1,24 @@ +// +// NSData+JWT.swift +// SwiftJWT +// +// Created by Chris Ziogas on 05/11/15. +// Copyright © 2015 RoundZero bv. All rights reserved. +// + +import Foundation + +extension Data { + // MARK: - base64 extensions + func base64SafeUrlEncode(_ options: Data.Base64EncodingOptions = []) -> String { + // regular base64 encoding + var s = self.base64EncodedString(options: options) + + // s = s.substringToIndex(s.endIndex.predecessor()) // remove last char + s = s.replacingOccurrences(of: "=", with: "") // Remove any trailing '='s + s = s.replacingOccurrences(of: "+", with: "-") // 62nd char of encoding + s = s.replacingOccurrences(of: "/", with: "_") // 63rd char of encoding + + return s + } +} diff --git a/JWT/JWT/Extensions/String+JWT.swift b/JWT/JWT/Extensions/String+JWT.swift new file mode 100644 index 0000000..9c8f800 --- /dev/null +++ b/JWT/JWT/Extensions/String+JWT.swift @@ -0,0 +1,29 @@ +// +// String+JWT.swift +// SwiftJWT +// +// Created by Chris Ziogas on 05/11/15. +// Copyright © 2015 RoundZero bv. All rights reserved. +// + +import Foundation + +extension String { + + // MARK: - base64 extensions + func base64SafeUrlDecode(_ options: Data.Base64DecodingOptions = []) -> Data? { + var s = self + + s = s.replacingOccurrences(of: "-", with: "+") // 62nd char of encoding + s = s.replacingOccurrences(of: "_", with: "/") // 63rd char of encoding + + switch (s.count % 4) { // Pad with trailing '='s + case 0: break; // No pad chars in this case + case 2: s += "=="; break; // Two pad chars + case 3: s += "="; break; // One pad char + default: print("Illegal base64url string!") + } + + return Data(base64Encoded: s, options: options) + } +} diff --git a/JWT/JWT/Info.plist b/JWT/JWT/Info.plist index 679f1f0..d3de8ee 100644 --- a/JWT/JWT/Info.plist +++ b/JWT/JWT/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - nl.roundzero.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/JWT/JWT/JWTNaCl.swift b/JWT/JWT/JWTNaCl.swift new file mode 100644 index 0000000..0b1c3f1 --- /dev/null +++ b/JWT/JWT/JWTNaCl.swift @@ -0,0 +1,266 @@ +// +// JWTNaCl.swift +// +// Stan P. van de Burgt +// stan@vandeburgt.com +// (c) RoundZero 2015 +// Project Authentiq + +import Sodium +import CommonCrypto + +// MARK: - NaCl signatures +// subclass with additional sign/verify for "Ed25519" signatures + +public enum JWTNaClError: Error { + case invalidKid + case invalidSub +} + +public class JWTNaCl: JWT { + + public func _kid(_ key: Data) -> String { + return key.base64SafeUrlEncode() + } + + override func implemented(_ algorithm: String?) -> Bool { + let algorithms = ["Ed25519"] + for alg in algorithms { + if alg == algorithm { + return true + } + } + return super.implemented(algorithm) // not implemented here, so try parent + } + + override func signature(_ msg: Data, algorithm: String, key: Data?) -> String? { + if algorithm == "Ed25519" { + return msg.nacl_signature(key!) // will crash on nil key + } + else { + return super.signature(msg, algorithm: algorithm, key: key) + } + } + + override func verify_signature(_ msg: Data, signature: String, algorithm: String, key: Data? = nil) -> Bool { + if algorithm == "Ed25519" { + if key == nil { + // use the "kid" field as base64 key string as key if no key provided. + if let kid = header["kid"] as? String, + let b64key = kid.base64SafeUrlDecode() { + return msg.nacl_verify(signature, key: b64key) + } + } + else { + return msg.nacl_verify(signature, key: key!) + } + } + else { + return super.verify_signature(msg, signature: signature, algorithm: algorithm, key: key) + } + return false + } + + override open func verify_content() throws { + + // kid is not optional when using NaCl + // TIP: use guard statement + guard let kid = self.header["kid"] as? String, + let kidData = kid.base64SafeUrlDecode(), + kidData.count == 32 + else { + throw JWTNaClError.invalidKid + } + + if let sub = self.body["sub"] as? String, + let id = sub.base64SafeUrlDecode(), + id.count != 32 + { + throw JWTNaClError.invalidSub + } + + try super.verify_content() // run the parent tests too + } +} + +// MARK: - HMACAlgorithm +// See http://stackoverflow.com/questions/25248598/importing-commoncrypto-in-a-swift-framework (answer by stephencelis) on how to import + +// import CommonCrypto + +// See: http://stackoverflow.com/questions/24099520/commonhmac-in-swift (answer by hdost) on HMAC signing. +// note that MD5, SHA1 and SHA224 are not used as JWT algorithms + +enum HMACAlgorithm { + case md5, sha1, sha224, sha256, sha384, sha512 + + func toCCEnum() -> 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) + } + + func digestLength() -> Int { + var result: CInt = 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) + } +} + +// See http://stackoverflow.com/questions/21724337/signing-and-verifying-on-ios-using-rsa on RSA signing +extension Data { + func base64digest(_ algorithm: HMACAlgorithm, key: Data) -> String! { + let digestLen = algorithm.digestLength() + + var digest = Data(count: digestLen) + _ = digest.withUnsafeMutableBytes { mutableBytes in + key.withUnsafeBytes { keybytes in + self.withUnsafeBytes { bytes in + + // Inspired by: http://stackoverflow.com/questions/24099520/commonhmac-in-swift (answer by hdost) + // CCHmac(algorithm: algorithm.toCCEnum(), key: keyData, keyLength: keyLen, data: data, dataLength: dataLen, macOut: result) + CCHmac(algorithm.toCCEnum(), keybytes, key.count, bytes, self.count, mutableBytes) + } + } + } + + // TODO: catch errors and throw? + + return digest.base64SafeUrlEncode() + } + + func nacl_signature(_ key: Data) -> String! { + // key is privkey + + let sodium = Sodium() + if let sig = sodium.sign.signature(message: Bytes(self), + secretKey: Bytes(key)) + { + return Data(bytes: sig).base64SafeUrlEncode() + } + return nil + } + + func nacl_verify(_ signature: String, key: Data) -> Bool { + // key is pubkey + + let sodium = Sodium() + if let sig_raw = signature.base64SafeUrlDecode() { + return sodium.sign.verify(message: Bytes(self), + publicKey: Bytes(key), + signature: Bytes(sig_raw)) + } + return false + } + + // based on http://stackoverflow.com/questions/21724337/signing-and-verifying-on-ios-using-rsa + + func rsa_signature(_ algorithm: HMACAlgorithm, key: Data) -> String? { + // key is privkey, in raw format + let privkey: SecKey? = nil // TODO: get the SecKey format of the (private) key + let msglen = CC_LONG(self.count) + let digestlen = algorithm.digestLength() + + var digest = Data(count: digestlen) + _ = digest.withUnsafeMutableBytes { mutableBytes in + self.withUnsafeBytes { bytes in + // TODO: test on nil return? + CC_SHA256(bytes, msglen, mutableBytes) + } + } + + var padding: SecPadding + + switch algorithm { + case .sha256: // TODO: change to HS256, ... ? + padding = SecPadding.PKCS1SHA256 + case .sha384: + padding = SecPadding.PKCS1SHA384 + case .sha512: + padding = SecPadding.PKCS1SHA512 + default: + return nil + } + + var sig = Data(count: digestlen) + var siglen: Int = digestlen + let status = sig.withUnsafeMutableBytes { mutableBytes in + digest.withUnsafeBytes { digestbytes in + SecKeyRawSign(privkey!, padding, digestbytes, digestlen, mutableBytes, &siglen) + } + } + + // OSStatus SecKeyRawSign(key: SecKey!, padding: SecPadding, dataToSign: UnsafePointer, dataToSignLen: Int, sig: UnsafeMutablePointer, sigLen: UnsafeMutablePointer) + if status == errSecSuccess { + // use siglen in/out parameter to set the actual lenght of sig + sig.count = siglen + return (sig as Data).base64SafeUrlEncode() + } + return nil + } + + func rsa_verify(_ algorithm: HMACAlgorithm, signature: String, key: Data) -> Bool { + // key is pubkey, in raw format + let pubkey: SecKey? = nil /// TODO: get the SecKey format of the (public) key + let msglen = CC_LONG(self.count) + let digestlen = algorithm.digestLength() + + var digest = Data(count: digestlen) + _ = digest.withUnsafeMutableBytes { mutableBytes in + self.withUnsafeBytes { bytes in + // TODO: test on nil return? + CC_SHA256(bytes, msglen, mutableBytes) + } + } + + var padding: SecPadding + + switch algorithm { + case .sha256: // TODO: change to HS256, ... ? + padding = SecPadding.PKCS1SHA256 + case .sha384: + padding = SecPadding.PKCS1SHA384 + case .sha512: + padding = SecPadding.PKCS1SHA512 + default: + return false + } + + let sig_raw = signature.data(using: String.Encoding.utf8)! + let sigbytes = (sig_raw as NSData).bytes.bindMemory(to: UInt8.self, capacity: sig_raw.count) + let siglen = sig_raw.count + + // OSStatus SecKeyRawVerify(key: SecKey!, padding: SecPadding, signedData: UnsafePointer, signedDataLen: Int, sig: UnsafePointer, sigLen: Int) + let status = digest.withUnsafeBytes { digestbytes in + SecKeyRawVerify(pubkey!, padding, digestbytes, digestlen, sigbytes, siglen) + } + + return status == errSecSuccess + } +} diff --git a/JWT/JWT/SwiftJWT.h b/JWT/JWT/SwiftJWT.h index 3c20f27..c293889 100644 --- a/JWT/JWT/SwiftJWT.h +++ b/JWT/JWT/SwiftJWT.h @@ -13,7 +13,3 @@ FOUNDATION_EXPORT double SwiftJWTVersionNumber; //! Project version string for SwiftJWT. FOUNDATION_EXPORT const unsigned char SwiftJWTVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/JWT/JWT/SwiftJWT.swift b/JWT/JWT/SwiftJWT.swift new file mode 100644 index 0000000..445a16e --- /dev/null +++ b/JWT/JWT/SwiftJWT.swift @@ -0,0 +1,319 @@ +// +// SwiftJWT.swift +// +// Stan P. van de Burgt +// stan@vandeburgt.com +// (c) RoundZero 2015 +// Project Authentiq + +import Foundation + +public enum JWTError: Error { + case notValid + case loadFailed + case decodeFailed + + case algorithmIsNotWhitelisted + case verifyFailed + case mandatoryClaimMissing + case dumpFailed + + case expiredIAT + case expiredNBF + case expiredEXP +} + +public class JWT { + // https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-4 + // base class; supports alg: none, HS256, HS384, HS512 + // TODO: add support for RS256, RS384, RS512 (almost there!) + // TODO: add support for PS256, PS384, PS512 + + public var header: [AnyHashable: Any] = ["alg": "none", "typ": "JWT"] { + // JWT header + didSet { + if header["alg"] as? String == nil { + self.header["alg"] = "none" // if not present, insert alg + } + } + } + public var body: [AnyHashable: Any] = [:] // JWT payload + var algorithms: [String] = [] // algorithms that are valid on loads(), dumps() and setting 'alg' header + + public required init(algorithms: [String]) { + self.algorithms = implemented(algorithms) // only add algoritms that are implemented() + } + + // public init(header: [String: AnyObject], body: [String: AnyObject]) { + // self.init(header: header, body: body, algorithms: nil) { + // } + + public init(header: [AnyHashable: Any], body: [AnyHashable: Any], algorithms: [String]? = nil) { + self.header = header + self.body = body + if header["alg"] as? String == nil { + self.header["alg"] = "none" // if not present, insert 'alg' + } + if let alg = algorithms { + // TODO: decide if this was smart, as it could introduce a vulnerability for the caller + self.algorithms = implemented(alg) // only add algoritms that are implemented() + } + else { + self.algorithms = [self.header["alg"] as! String] + } + if header["typ"] as? String == nil { + self.header["typ"] = "JWT" // if not present, insert 'typ' element + } + } + + public func loads(_ jwt: String, key: Data? = nil, verify: Bool = true, mandatory: [String] = []) throws { + + // load a JWT string into this object + var sig = "" + + // clear object properties + self.header = [:] + self.body = [:] + + // split JWT string into parts: header, body, optional signature + let parts: [String] = jwt.components(separatedBy: ".") + switch parts.count { + case 2: break + case 3: sig = parts[2] + default: + throw JWTError.notValid + } + + // decode the header (a URL-safe, base 64 encoded JSON dict) from 1st part + guard let hdr_data = parts[0].base64SafeUrlDecode(), + let hdr = try JSONSerialization.jsonObject(with: hdr_data, options: JSONSerialization.ReadingOptions(rawValue: 0)) as? [AnyHashable: Any] + else { + throw JWTError.decodeFailed + } + + // check that "alg" header is on whitelist (and thus implemented) ; even if verify == false + guard let algorithm = hdr["alg"] as? String, + self.whitelisted(algorithm) + else { + throw JWTError.algorithmIsNotWhitelisted + } + + // decode the body (a URL-safe base 64 encoded JSON dict) from the 2nd part + guard let body_data = parts[1].base64SafeUrlDecode(), + let payload = try JSONSerialization.jsonObject(with: body_data, options: JSONSerialization.ReadingOptions(rawValue: 0)) as? [AnyHashable: Any] + else { + throw JWTError.decodeFailed + } + + // check if mandatory claims are set + // either in "header" or "body" + if mandatory.count > 0 { + for claim in mandatory { + if header[claim] == nil && payload[claim] == nil { + throw JWTError.mandatoryClaimMissing + } + } + } + + // all went well so far, so let's set the object properties + // TODO: set properties even later (but are needed by verification methods now) + self.header = hdr + self.body = payload + + if verify { + // verify the signature, a URL-safe base64 encoded string + let hdr_body: String = parts[0] + "." + parts[1] // header & body of a JWT + let data = hdr_body.data(using: String.Encoding.utf8)! + if self.verify_signature(data, signature: sig, algorithm: algorithm, key: key) == false { + self.header = [:]; self.body = [:] // reset + + throw JWTError.verifyFailed + } + + // verify content fields + do { + try self.verify_content() + + } catch { + self.header = [:]; self.body = [:] // reset + + throw error + } + } + } + + // convenience method for plain strings as key + public func loads(_ jwt: String, key: String, verify: Bool = true, mandatory: [String] = []) throws { + let key_raw = key.data(using: .utf8)! + try loads(jwt, key: key_raw, verify: verify, mandatory: mandatory) + } + + // convenience method for base64 strings as key + public func loads(_ jwt: String, b64key: String, verify: Bool = true, mandatory: [String] = []) throws { + let key_raw = b64key.base64SafeUrlDecode() + try loads(jwt, key: key_raw, verify: verify, mandatory: mandatory) + } + + public func dumps(_ key: Data? = nil, jti_len: UInt = 16) throws -> String { + + // create a JWT string from this object + // TODO: some way to indicate that some fields should be generated, next to jti; e.g. nbf and iat + var payload = self.body + // if 'jti' (the nonce) not present in body, and it is requested (jti_len > 0), set one + if payload["jti"] as? String == nil && jti_len > 0 { + // generate a random string (nonce) of length jti_len for body item 'jti' + // https://developer.apple.com/library/ios/documentation/Security/Reference/RandomizationReference/index.html + + var bytes = Data(count: Int(jti_len)) + let count = bytes.count + let result = bytes.withUnsafeMutableBytes { mutableBytes in + SecRandomCopyBytes(kSecRandomDefault, count, mutableBytes) + } + + if result == errSecSuccess { + payload["jti"] = bytes.base64SafeUrlEncode() as NSString + } else { + throw JWTError.dumpFailed + } + } + // TODO: set iat, nbf in payload here if not set & requested? + do { + let h = try JSONSerialization.data(withJSONObject: self.header, options: []) + var data = h.base64SafeUrlEncode() + + let b = try JSONSerialization.data(withJSONObject: payload, options: []) + data = data + "." + b.base64SafeUrlEncode() + // check that "alg" header is on whitelist (and thus implemented) + let alg = self.header["alg"] as? String + if !self.whitelisted(alg) { + throw JWTError.algorithmIsNotWhitelisted + } + + let data_raw = data.data(using: String.Encoding.utf8)! + if let sig = self.signature(data_raw, algorithm: alg!, key: key) { + return data + "." + sig + } + + } catch { + // no need to handle something here + // JSON Encode might throw here + } + + throw JWTError.dumpFailed + } + + // convenience method for plain strings as key + public func dumps(_ key: String, jti_len: UInt = 16) throws -> String { + let key_raw = key.data(using: String.Encoding.utf8)! + return try dumps(key_raw, jti_len: jti_len) + } + + func whitelisted(_ algorithm: String?) -> Bool { + for alg in self.algorithms { + if alg == algorithm { + return true + } + } + return false + } + + func implemented(_ algorithm: String?) -> Bool { + let algorithms = ["none", "HS256", "HS384", "HS512"] + // TODO: add RS256, RS384, RS512, PS256, PS384, PS512 when rsa_* methods below are done + for alg in algorithms { + if alg == algorithm { + return true + } + } + return false + } + + func implemented(_ algorithms: [String]) -> [String] { + var result: [String] = [] + for alg in algorithms { + if implemented(alg) { + result.append(alg) + } + } + return result + } + + func signature(_ msg: Data, algorithm: String, key: Data?) -> String? { + // internal function to compute the signature (third) part of a JWT + if let key_raw = key { + switch algorithm { + case "HS256": return msg.base64digest(HMACAlgorithm.sha256, key: key_raw) + case "HS384": return msg.base64digest(HMACAlgorithm.sha384, key: key_raw) + case "HS512": return msg.base64digest(HMACAlgorithm.sha512, key: key_raw) + case "RS256": return msg.rsa_signature(HMACAlgorithm.sha256, key: key_raw) + case "RS384": return msg.rsa_signature(HMACAlgorithm.sha384, key: key_raw) + case "RS512": return msg.rsa_signature(HMACAlgorithm.sha512, key: key_raw) + case "PS256": return msg.rsa_signature(HMACAlgorithm.sha256, key: key_raw) // TODO: convert PS to RS key + case "PS384": return msg.rsa_signature(HMACAlgorithm.sha384, key: key_raw) + case "PS512": return msg.rsa_signature(HMACAlgorithm.sha512, key: key_raw) + default: return nil + } + } + else { + return algorithm == "none" ? "" : nil + } + } + + func verify_signature(_ msg: Data, signature: String, algorithm: String, key: Data? = nil) -> Bool { + // internal function to verify the signature (third) part of a JWT + if key == nil && algorithm != "none" { + return false + } + switch algorithm { + case "none": return signature == "" // if "none" then the signature shall be empty + case "HS256": return msg.base64digest(HMACAlgorithm.sha256, key: key!) == signature + case "HS384": return msg.base64digest(HMACAlgorithm.sha384, key: key!) == signature + case "HS512": return msg.base64digest(HMACAlgorithm.sha512, key: key!) == signature + case "RS256": return msg.rsa_verify(HMACAlgorithm.sha256, signature: signature, key: key!) + case "RS384": return msg.rsa_verify(HMACAlgorithm.sha384, signature: signature, key: key!) + case "RS512": return msg.rsa_verify(HMACAlgorithm.sha512, signature: signature, key: key!) + case "PS256": return msg.rsa_verify(HMACAlgorithm.sha256, signature: signature, key: key!) // TODO: convert PS to RS key + case "PS384": return msg.rsa_verify(HMACAlgorithm.sha384, signature: signature, key: key!) + case "PS512": return msg.rsa_verify(HMACAlgorithm.sha512, signature: signature, key: key!) + default: return false + } + } + + // TODO: some way to enforce that e.g. iat and nbf are present + // TODO: verification of iss and aud when given in loads() + public func verify_content() throws { + // internal function to verify the content (header and body) parts of a JWT + let date = Date() + let now = UInt(date.timeIntervalSince1970) + + guard let typ = self.header["typ"] as? String, + typ == "JWT" + else { + throw JWTError.notValid // 'typ' shall be 'JWT' + } + + if let exp = self.body["exp"] as? UInt, + now > exp + { + + // TODO: also false if "exp" is not of type UInt + throw JWTError.expiredEXP + } + if let nbf = self.body["nbf"] as? UInt, + now < nbf + { + + // TODO: also false if "nbf" is not of type UInt + throw JWTError.expiredNBF + } + if let iat = self.body["iat"] as? UInt, + now < iat + { + + // TODO: also false if "iat" is not of type UInt + throw JWTError.expiredIAT + } + } +} + +// END diff --git a/JWT/JWTTests/Info.plist b/JWT/JWTTests/Info.plist index 0fc1bd4..ba72822 100644 --- a/JWT/JWTTests/Info.plist +++ b/JWT/JWTTests/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - nl.roundzero.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/JWT/JWTTests/JWTTests.swift b/JWT/JWTTests/JWTTests.swift index f79a5b0..99d2f54 100644 --- a/JWT/JWTTests/JWTTests.swift +++ b/JWT/JWTTests/JWTTests.swift @@ -26,18 +26,24 @@ class JWTTests: XCTestCase { // This is an example of a functional test case. let expected_jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoZWxsbyI6IndvcmxkIn0.lnneNaoem98xYFES3mi2CJJjnMONuWAu-FTWB3XJN14" let jwt = JWT(header: ["alg":"HS256"], body: ["hello":"world"], algorithms: ["HS256","HS512","RS512"]) - let s = jwt.dumps("secret", jti_len: 0) // without jti to enable replay - XCTAssert(s != nil, "JWT.dumps() failed") - XCTAssert(s! == expected_jwt, "JWT.dumps() unexpected jwt") + let s = try! jwt.dumps("secret", jti_len: 0) // without jti to enable replay + XCTAssert(s == expected_jwt, "JWT.dumps() unexpected jwt") } func test_NaCl_JWT_from_string() { let jwt_ed = "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiSldUIiwia2lkIjoiWE43VnBFWDF1Q3h4aHZ3VXVhY1lodVU5dDZ1eGdMYWhSaUxlU0VIRU5payJ9.eyJmb28iOiJiYXIifQ.a2dDcKXByKxiouOLnXUm7YUKHMGOU3yn_g91C90e8YmKjlF1_9ylAKukfMm6Y6WS3dZp2ysaglzzTnVxnRYyDQ" - let sk = "YHWUUc0P6SY46WaDdnssE8NpFsQQxJrvmdOrpU9X0wU" + // let sk = "YHWUUc0P6SY46WaDdnssE8NpFsQQxJrvmdOrpU9X0wU" let pk = "XN7VpEX1uCxxhvwUuacYhuU9t6uxgLahRiLeSEHENik" let jwt = JWTNaCl(algorithms: ["Ed25519"]) - XCTAssert(jwt.loads(jwt_ed, b64key: pk, verify: true), "loading a NaCl signed JWT and specified public (verification) key") - XCTAssert(jwt.loads(jwt_ed, key: nil, verify: true), "loading a NaCl signed JWT and with implicit (verification) key 'kid'") + + XCTempAssertNoThrowError("loading a NaCl signed JWT and specified public (verification) key") { + try jwt.loads(jwt_ed, b64key: pk, verify: true) + } + + XCTempAssertNoThrowError("loading a NaCl signed JWT and with implicit (verification) key 'kid'") { + try jwt.loads(jwt_ed, key: nil, verify: true) + } + let kid = jwt.header["kid"] as? String XCTAssert(kid != nil && kid! == pk, "mismatch PK / kid in loaded JWT") } @@ -45,98 +51,158 @@ class JWTTests: XCTestCase { func test_HS256_JWT_from_string() { let jwt_hs256 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFCR293UEhjSXRwb3ZlVnpyclFzU25rNjVjX3FoS3ZmamQtNHd5UFVmVVEifQ.eyJwaG9uZV9udW1iZXIiOiIrMzA2OTQ3ODk4NjA1Iiwic2NvcGUiOiJwaG9uZSIsImF1ZCI6Imh0dHBzOi8vNS1kb3QtYXV0aGVudGlxaW8uYXBwc3BvdC5jb20iLCJzdWIiOiJhQkdvd1BIY0l0cG92ZVZ6cnJRc1NuazY1Y19xaEt2ZmpkLTR3eVBVZlVRIiwidHlwZSI6Im1vYmlsZSJ9.qrq-939iZydNFdNsTosbSteghjc2VcK9EZVklxfQgiU" let jwt = JWT(algorithms: ["HS256","HS512","RS512"]) - XCTAssert(jwt.loads(jwt_hs256, key: "secret", verify: true), "loading a HS256 signed JWT and specified (verification) hash") + XCTempAssertNoThrowError("loading a HS256 signed JWT and specified (verification) hash") { + try jwt.loads(jwt_hs256, key: "secret", verify: true) + } + XCTAssert(jwt.body["phone_number"] as? String == "+306947898605", "wrong 'phone' scope in loaded JWT") - var jwtn = JWTNaCl(algorithms: ["HS512","RS512"]) - XCTAssert(jwt.loads(jwt_hs256, key: "secret", verify: true), "loading a HS256 signed JWT in a NaCl subclass should work too") + let jwtn = JWTNaCl(algorithms: ["HS256","HS512","RS512"]) + XCTempAssertNoThrowError("loading a HS256 signed JWT in a NaCl subclass should work too") { + try jwtn.loads(jwt_hs256, key: "secret", verify: true) + } } func test_random_string_as_JWT() { - var error: NSError? - var jwt = JWT(algorithms: ["HS512","RS512"]) - XCTAssert(jwt.loads("randomstring", verify: false, error: &error) == false, "random string should not load") + let jwt = JWT(algorithms: ["HS512","RS512"]) + + XCTAssertThrowsSpecificError(JWTError.notValid) { + try jwt.loads("randomstring", verify: false) + } XCTAssert(jwt.body.count == 0, "loading garbage should leave body empty") } func test_alg_none_with_sig() { let jwt_none_sig = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.qrq-939iZydNFdNsTosbSteghjc2VcK9EZVklxfQgiU" var jwt = JWT(algorithms: ["none","HS512","RS512"]) - XCTAssert(jwt.loads(jwt_none_sig, verify: true) == false, "alg=none with a signature is not valid") + + XCTAssertThrowsSpecificError(JWTError.verifyFailed) { + try jwt.loads(jwt_none_sig, verify: true) + } + XCTAssert(jwt.body.count == 0, "loading garbage should leave body empty") + jwt = JWT(algorithms: ["HS512","RS512"]) - XCTAssert(jwt.loads(jwt_none_sig, verify: false) == false, "none is not on whitelist") + + XCTAssertThrowsSpecificError(JWTError.algorithmIsNotWhitelisted) { + try jwt.loads(jwt_none_sig, verify: false) + } } func test_timestamps() { - let date = NSDate() + let date = Date() let now = UInt(date.timeIntervalSince1970) let jwt = JWT(algorithms: ["none","HS512","RS512"]) - var jwt_dated = JWT(algorithms: ["none","HS512","RS512"]) + let jwt_dated = JWT(algorithms: ["none","HS512","RS512"]) var s = "" jwt_dated.body["exp"] = now-100 - s = jwt_dated.dumps()! - XCTAssert(jwt.loads(s, verify: true) == false, "exp in past \(s)") + s = try! jwt_dated.dumps() + XCTAssertThrowsSpecificError(JWTError.expiredEXP) { + try jwt.loads(s, verify: true) + // "exp in past \(s)" + } + jwt_dated.body["exp"] = now+100 // and leave it there for next tests - s = jwt_dated.dumps()! - XCTAssert(jwt.loads(s, verify: true) == true, "exp in future \(s)") + s = try! jwt_dated.dumps() + XCTempAssertNoThrowError("exp in future \(s)") { + try jwt.loads(s, verify: true) + } jwt_dated.body["nbf"] = now+100 - s = jwt_dated.dumps()! - XCTAssert(jwt.loads(s, verify: true) == false, "nbf in future \(s)") + s = try! jwt_dated.dumps() + XCTAssertThrowsSpecificError(JWTError.expiredNBF) { + try jwt.loads(s, verify: true) + // "nbf in future \(s)" + } + jwt_dated.body["nbf"] = now-100 // and leave it there for next tests - s = jwt_dated.dumps()! - XCTAssert(jwt.loads(s, verify: true) == true, "nbf in past \(s)") + s = try! jwt_dated.dumps() + XCTempAssertNoThrowError("nbf in past \(s)") { + try jwt.loads(s, verify: true) + } + jwt_dated.body["iat"] = now+100 - s = jwt_dated.dumps()! - XCTAssert(jwt.loads(s, verify: true) == false, "iat in future \(s)") + s = try! jwt_dated.dumps() + XCTAssertThrowsSpecificError(JWTError.expiredIAT) { + try jwt.loads(s, verify: true) + // "iat in future \(s)" + } + jwt_dated.body["iat"] = now-100 // and leave it there for next tests - s = jwt_dated.dumps()! - XCTAssert(jwt.loads(s, verify: true) == true, "iat in past \(s)") + s = try! jwt_dated.dumps() + XCTempAssertNoThrowError("iat in past \(s)") { + try jwt.loads(s, verify: true) + } } func test_NaCl_JWT_load_dump_load() { let jwt_ed = "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiSldUIiwia2lkIjoiYUJHb3dQSGNJdHBvdmVWenJyUXNTbms2NWNfcWhLdmZqZC00d3lQVWZVUSJ9.eyJwaG9uZV9udW1iZXIiOiIrMzA2OTQ3ODk4NjA1Iiwic2NvcGUiOiJwaG9uZSIsImF1ZCI6Imh0dHBzOlwvXC81LWRvdC1hdXRoZW50aXFpby5hcHBzcG90LmNvbSIsInN1YiI6ImFCR293UEhjSXRwb3ZlVnpyclFzU25rNjVjX3FoS3ZmamQtNHd5UFVmVVEiLCJ0eXBlIjoibW9iaWxlIn0.kD4YcuAb7v3cxlRZTrUbew1lWiY3G8uEmRguizy1KJs" // generate keys - let sodium = Sodium()! - let seed = NSMutableData(length: sodium.sign.SeedBytes)! - SecRandomCopyBytes(kSecRandomDefault, sodium.sign.SeedBytes, UnsafeMutablePointer(seed.mutableBytes)) - let kpp = sodium.sign.keyPair(seed: seed) + let sodium = Sodium() + + let kpp = sodium.sign.keyPair() XCTAssert(kpp != nil, "Key pair generation") let kp = kpp! var jwt = JWTNaCl(algorithms: ["Ed25519"]) - XCTAssert(jwt.loads(jwt_ed, key: kp.publicKey, verify: true) == false, "NaCl JWT should not validate with wrong key") + XCTAssertThrowsSpecificError(JWTError.verifyFailed) { + try jwt.loads(jwt_ed, + key: Data(bytes: kp.publicKey), + verify: true) + // "NaCl JWT should not validate with wrong key") + } + // but is still loaded (DO WE WANT THAT?) NOW FAILS jwt = JWTNaCl(header: ["alg":"Ed25519","kid":"XN7VpEX1uCxxhvwUuacYhuU9t6uxgLahRiLeSEHENik"], body: ["hello":"world"], algorithms: ["Ed25519"]) - let jwt_str = jwt.dumps(key: kp.secretKey)! // valid Ed25519 signed token - XCTAssert(jwt.loads(jwt_str, verify: true) == false, "verify a generated JWT with wrong kid when signed with fresh key") - XCTAssert(jwt.loads(jwt_str, key: kp.publicKey, verify: true), "verify a generated JWT with its public key") + let jwt_str = try! jwt.dumps(Data(bytes: kp.secretKey)) // valid Ed25519 signed token + XCTAssertThrowsSpecificError(JWTError.verifyFailed) { + try jwt.loads(jwt_str, verify: true) + //"verify a generated JWT with wrong kid when signed with fresh key") + } + + XCTempAssertNoThrowError("verify a generated JWT with its public key") { + try jwt.loads(jwt_str, + key: Data(bytes: kp.publicKey), + verify: true) + } } func test_NaCl_JWT_loadHS256_dumpEd() { let jwt_hs256 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFCR293UEhjSXRwb3ZlVnpyclFzU25rNjVjX3FoS3ZmamQtNHd5UFVmVVEifQ.eyJwaG9uZV9udW1iZXIiOiIrMzA2OTQ3ODk4NjA1Iiwic2NvcGUiOiJwaG9uZSIsImF1ZCI6Imh0dHBzOi8vNS1kb3QtYXV0aGVudGlxaW8uYXBwc3BvdC5jb20iLCJzdWIiOiJhQkdvd1BIY0l0cG92ZVZ6cnJRc1NuazY1Y19xaEt2ZmpkLTR3eVBVZlVRIiwidHlwZSI6Im1vYmlsZSJ9.qrq-939iZydNFdNsTosbSteghjc2VcK9EZVklxfQgiU" // generate keys - let sodium = Sodium()! - let seed = NSMutableData(length: sodium.sign.SeedBytes)! - SecRandomCopyBytes(kSecRandomDefault, sodium.sign.SeedBytes, UnsafeMutablePointer(seed.mutableBytes)) - let kpp = sodium.sign.keyPair(seed: seed) - XCTAssert(kpp != nil, "Key pair generation") - let kp = kpp! + let sodium = Sodium() + + guard let kp = sodium.sign.keyPair() else { + XCTFail("Key pair generation") + return + } - var jwtn = JWTNaCl(algorithms: ["Ed25519","HS256"]) - XCTAssert(jwtn.loads(jwt_hs256, key: "secret", verify: true), "could not load a HS256 JWT") - XCTAssert(jwtn.dumps("secret") != nil, "could not stringify a HS256 JWT") + let jwtn = JWTNaCl(algorithms: ["Ed25519","HS256"]) + XCTempAssertNoThrowError("could not load a HS256 JWT") { + try jwtn.loads(jwt_hs256, key: "secret", verify: true) + } + + XCTempAssertNoThrowError("could not stringify a HS256 JWT") { + _ = try jwtn.dumps("secret") + } + jwtn.header["alg"] = "Ed25519" // set alg to NaCl type tokens - jwtn.dumps("secret") // THIS -> nil ; WHY? - let myjwt = jwtn.dumps(key: kp.secretKey) // valid Ed25519 signed token - XCTAssert(myjwt != nil, "could not stringify an Ed25519 JWT") - XCTAssert(jwtn.loads(myjwt!, key: kp.publicKey, verify: true) == true, "loading of generated Ed25519 JWT failed") + // try! jwtn.dumps("secret") // THIS -> nil ; WHY? + + XCTempAssertNoThrowError("could not stringify a HS256 JWT") { + let myjwt = try jwtn.dumps(Data(bytes: kp.secretKey)) // valid Ed25519 signed token + + XCTempAssertNoThrowError("loading of generated Ed25519 JWT failed") { + try jwtn.loads(myjwt, + key: Data(bytes: kp.publicKey), + verify: true) + } + } } func testPerformanceVerify() { // This is an example of a performance test case. - let jwt_str = "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiSldUIiwia2lkIjoiYUJHb3dQSGNJdHBvdmVWenJyUXNTbms2NWNfcWhLdmZqZC00d3lQVWZVUSJ9.eyJwaG9uZV9udW1iZXIiOiIrMzA2OTQ3ODk4NjA1Iiwic2NvcGUiOiJwaG9uZSIsImF1ZCI6Imh0dHBzOlwvXC81LWRvdC1hdXRoZW50aXFpby5hcHBzcG90LmNvbSIsInN1YiI6ImFCR293UEhjSXRwb3ZlVnpyclFzU25rNjVjX3FoS3ZmamQtNHd5UFVmVVEiLCJ0eXBlIjoibW9iaWxlIn0.kD4YcuAb7v3cxlRZTrUbew1lWiY3G8uEmRguizy1KJs" - var jwt = JWTNaCl(algorithms: ["none","HS512","RS512"]) - self.measureBlock() { - let ok = jwt.loads(jwt_str, verify: true) + let jwt_ed = "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiSldUIiwia2lkIjoiWE43VnBFWDF1Q3h4aHZ3VXVhY1lodVU5dDZ1eGdMYWhSaUxlU0VIRU5payJ9.eyJmb28iOiJiYXIifQ.a2dDcKXByKxiouOLnXUm7YUKHMGOU3yn_g91C90e8YmKjlF1_9ylAKukfMm6Y6WS3dZp2ysaglzzTnVxnRYyDQ" + let pk = "XN7VpEX1uCxxhvwUuacYhuU9t6uxgLahRiLeSEHENik" + let jwt = JWTNaCl(algorithms: ["Ed25519"]) + self.measure() { + let _ = try! jwt.loads(jwt_ed, b64key: pk, verify: true) } } - } diff --git a/JWT/JWTTests/XCTAssert+JWT.swift b/JWT/JWTTests/XCTAssert+JWT.swift new file mode 100644 index 0000000..fe9dfc2 --- /dev/null +++ b/JWT/JWTTests/XCTAssert+JWT.swift @@ -0,0 +1,40 @@ +// +// XCTAssert+JWT.swift +// SwiftJWT +// +// Created by Chris Ziogas on 05/11/15. +// Copyright © 2015 RoundZero bv. All rights reserved. +// + +import XCTest + +func XCTempAssertNoThrowError(_ message: String = "", file: StaticString = #file, line: UInt = #line, _ block: () throws -> ()) +{ + do {try block()} + catch + { + let msg = (message == "") ? "Tested block threw unexpected error." : message + XCTFail(msg, file: file, line: line) + } +} + +// assert if a method throws the expected ErrorType +func XCTAssertThrowsSpecificError(_ kind: Error, _ message: String = "", file: StaticString = #file, line: UInt = #line, _ block: () throws -> ()) +{ + do + { + try block() + + let msg = (message == "") ? "Tested block did not throw expected \(kind) error." : message + XCTFail(msg, file: file, line: line) + } + catch let error as NSError + { + let expected = kind as NSError + if ((error.domain != expected.domain) || (error.code != expected.code)) + { + let msg = (message == "") ? "Tested block threw \(error), not expected \(kind) error." : message + XCTFail(msg, file: file, line: line) + } + } +} diff --git a/JWT/SwiftJWT.xcodeproj/project.pbxproj b/JWT/SwiftJWT.xcodeproj/project.pbxproj index 1d98012..501c49f 100644 --- a/JWT/SwiftJWT.xcodeproj/project.pbxproj +++ b/JWT/SwiftJWT.xcodeproj/project.pbxproj @@ -7,13 +7,14 @@ objects = { /* Begin PBXBuildFile section */ + DE183DF71BEB4EB4000D4544 /* String+JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE183DF61BEB4EB4000D4544 /* String+JWT.swift */; }; + DE183DF91BEB4EDC000D4544 /* Data+JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE183DF81BEB4EDC000D4544 /* Data+JWT.swift */; }; + DE183DFC1BEB61AD000D4544 /* XCTAssert+JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE183DFB1BEB61AD000D4544 /* XCTAssert+JWT.swift */; }; + DE183DFE1BEB774D000D4544 /* JWTNaCl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE183DFD1BEB774D000D4544 /* JWTNaCl.swift */; }; F672EDE71ACB44520020B9BA /* SwiftJWT.h in Headers */ = {isa = PBXBuildFile; fileRef = F672EDE61ACB44520020B9BA /* SwiftJWT.h */; settings = {ATTRIBUTES = (Public, ); }; }; F672EDED1ACB44520020B9BA /* SwiftJWT.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F672EDE11ACB44520020B9BA /* SwiftJWT.framework */; }; F672EDF41ACB44520020B9BA /* JWTTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F672EDF31ACB44520020B9BA /* JWTTests.swift */; }; F672EDFE1ACB44650020B9BA /* SwiftJWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = F672EDFD1ACB44650020B9BA /* SwiftJWT.swift */; }; - F6C51F921ACB912800B00CF7 /* CommonCrypto.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6C51F871ACB912800B00CF7 /* CommonCrypto.framework */; }; - F6C51F991ACB912800B00CF7 /* CommonCryptoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6C51F981ACB912800B00CF7 /* CommonCryptoTests.swift */; }; - F6C51FA71ACBC65400B00CF7 /* CommonCrypto.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6C51F871ACB912800B00CF7 /* CommonCrypto.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -24,37 +25,20 @@ remoteGlobalIDString = F672EDE01ACB44520020B9BA; remoteInfo = JWT; }; - F6C51F931ACB912800B00CF7 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = F672EDD81ACB44520020B9BA /* Project object */; - proxyType = 1; - remoteGlobalIDString = F6C51F861ACB912800B00CF7; - remoteInfo = CommonCrypto; - }; - F6C51FA51ACBC64400B00CF7 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = F672EDD81ACB44520020B9BA /* Project object */; - proxyType = 1; - remoteGlobalIDString = F6C51F861ACB912800B00CF7; - remoteInfo = CommonCrypto; - }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + DE183DF61BEB4EB4000D4544 /* String+JWT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "String+JWT.swift"; path = "Extensions/String+JWT.swift"; sourceTree = ""; }; + DE183DF81BEB4EDC000D4544 /* Data+JWT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Data+JWT.swift"; path = "Extensions/Data+JWT.swift"; sourceTree = ""; }; + DE183DFB1BEB61AD000D4544 /* XCTAssert+JWT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCTAssert+JWT.swift"; sourceTree = ""; }; + DE183DFD1BEB774D000D4544 /* JWTNaCl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWTNaCl.swift; sourceTree = ""; }; F672EDE11ACB44520020B9BA /* SwiftJWT.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftJWT.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F672EDE51ACB44520020B9BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F672EDE61ACB44520020B9BA /* SwiftJWT.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftJWT.h; sourceTree = ""; }; F672EDEC1ACB44520020B9BA /* SwiftJWTTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftJWTTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F672EDF21ACB44520020B9BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F672EDF31ACB44520020B9BA /* JWTTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWTTests.swift; sourceTree = ""; }; - F672EDFD1ACB44650020B9BA /* SwiftJWT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwiftJWT.swift; path = ../../SwiftJWT.swift; sourceTree = ""; }; - F6C51F871ACB912800B00CF7 /* CommonCrypto.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CommonCrypto.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F6C51F8A1ACB912800B00CF7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F6C51F911ACB912800B00CF7 /* CommonCryptoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CommonCryptoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F6C51F971ACB912800B00CF7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F6C51F981ACB912800B00CF7 /* CommonCryptoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonCryptoTests.swift; sourceTree = ""; }; - F6C51FA01ACB916B00B00CF7 /* CommonCrypto.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = CommonCrypto.xcconfig; sourceTree = ""; }; - F6C51FA81ACBC67300B00CF7 /* libcommonCrypto.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libcommonCrypto.dylib; path = usr/lib/system/libcommonCrypto.dylib; sourceTree = SDKROOT; }; + F672EDFD1ACB44650020B9BA /* SwiftJWT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftJWT.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -62,7 +46,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F6C51FA71ACBC65400B00CF7 /* CommonCrypto.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -74,31 +57,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F6C51F831ACB912800B00CF7 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F6C51F8E1ACB912800B00CF7 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F6C51F921ACB912800B00CF7 /* CommonCrypto.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + DE183DFA1BEB5CC1000D4544 /* Extensions */ = { + isa = PBXGroup; + children = ( + DE183DF61BEB4EB4000D4544 /* String+JWT.swift */, + DE183DF81BEB4EDC000D4544 /* Data+JWT.swift */, + ); + name = Extensions; + sourceTree = ""; + }; F672EDD71ACB44520020B9BA = { isa = PBXGroup; children = ( F672EDE31ACB44520020B9BA /* JWT */, F672EDF01ACB44520020B9BA /* JWTTests */, - F6C51F881ACB912800B00CF7 /* CommonCrypto */, - F6C51F951ACB912800B00CF7 /* CommonCryptoTests */, F672EDE21ACB44520020B9BA /* Products */, ); sourceTree = ""; @@ -108,8 +83,6 @@ children = ( F672EDE11ACB44520020B9BA /* SwiftJWT.framework */, F672EDEC1ACB44520020B9BA /* SwiftJWTTests.xctest */, - F6C51F871ACB912800B00CF7 /* CommonCrypto.framework */, - F6C51F911ACB912800B00CF7 /* CommonCryptoTests.xctest */, ); name = Products; sourceTree = ""; @@ -119,6 +92,8 @@ children = ( F672EDE61ACB44520020B9BA /* SwiftJWT.h */, F672EDFD1ACB44650020B9BA /* SwiftJWT.swift */, + DE183DFD1BEB774D000D4544 /* JWTNaCl.swift */, + DE183DFA1BEB5CC1000D4544 /* Extensions */, F672EDE41ACB44520020B9BA /* Supporting Files */, ); path = JWT; @@ -136,6 +111,7 @@ isa = PBXGroup; children = ( F672EDF31ACB44520020B9BA /* JWTTests.swift */, + DE183DFB1BEB61AD000D4544 /* XCTAssert+JWT.swift */, F672EDF11ACB44520020B9BA /* Supporting Files */, ); path = JWTTests; @@ -149,41 +125,6 @@ name = "Supporting Files"; sourceTree = ""; }; - F6C51F881ACB912800B00CF7 /* CommonCrypto */ = { - isa = PBXGroup; - children = ( - F6C51FA81ACBC67300B00CF7 /* libcommonCrypto.dylib */, - F6C51F891ACB912800B00CF7 /* Supporting Files */, - ); - path = CommonCrypto; - sourceTree = ""; - }; - F6C51F891ACB912800B00CF7 /* Supporting Files */ = { - isa = PBXGroup; - children = ( - F6C51F8A1ACB912800B00CF7 /* Info.plist */, - F6C51FA01ACB916B00B00CF7 /* CommonCrypto.xcconfig */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - F6C51F951ACB912800B00CF7 /* CommonCryptoTests */ = { - isa = PBXGroup; - children = ( - F6C51F981ACB912800B00CF7 /* CommonCryptoTests.swift */, - F6C51F961ACB912800B00CF7 /* Supporting Files */, - ); - path = CommonCryptoTests; - sourceTree = ""; - }; - F6C51F961ACB912800B00CF7 /* Supporting Files */ = { - isa = PBXGroup; - children = ( - F6C51F971ACB912800B00CF7 /* Info.plist */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -195,13 +136,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F6C51F841ACB912800B00CF7 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ @@ -209,6 +143,7 @@ isa = PBXNativeTarget; buildConfigurationList = F672EDF71ACB44520020B9BA /* Build configuration list for PBXNativeTarget "SwiftJWT" */; buildPhases = ( + AEC84E66214D7FDB00867631 /* Run Script: CommonCrypto */, F672EDDC1ACB44520020B9BA /* Sources */, F672EDDD1ACB44520020B9BA /* Frameworks */, F672EDDE1ACB44520020B9BA /* Headers */, @@ -217,7 +152,6 @@ buildRules = ( ); dependencies = ( - F6C51FA61ACBC64400B00CF7 /* PBXTargetDependency */, ); name = SwiftJWT; productName = JWT; @@ -242,62 +176,23 @@ productReference = F672EDEC1ACB44520020B9BA /* SwiftJWTTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - F6C51F861ACB912800B00CF7 /* CommonCrypto */ = { - isa = PBXNativeTarget; - buildConfigurationList = F6C51F9E1ACB912800B00CF7 /* Build configuration list for PBXNativeTarget "CommonCrypto" */; - buildPhases = ( - F6C51F821ACB912800B00CF7 /* Sources */, - F6C51F831ACB912800B00CF7 /* Frameworks */, - F6C51F841ACB912800B00CF7 /* Headers */, - F6C51F851ACB912800B00CF7 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = CommonCrypto; - productName = CommonCrypto; - productReference = F6C51F871ACB912800B00CF7 /* CommonCrypto.framework */; - productType = "com.apple.product-type.framework"; - }; - F6C51F901ACB912800B00CF7 /* CommonCryptoTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F6C51F9F1ACB912800B00CF7 /* Build configuration list for PBXNativeTarget "CommonCryptoTests" */; - buildPhases = ( - F6C51F8D1ACB912800B00CF7 /* Sources */, - F6C51F8E1ACB912800B00CF7 /* Frameworks */, - F6C51F8F1ACB912800B00CF7 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F6C51F941ACB912800B00CF7 /* PBXTargetDependency */, - ); - name = CommonCryptoTests; - productName = CommonCryptoTests; - productReference = F6C51F911ACB912800B00CF7 /* CommonCryptoTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F672EDD81ACB44520020B9BA /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0620; + LastSwiftUpdateCheck = 0710; + LastUpgradeCheck = 1000; ORGANIZATIONNAME = "RoundZero bv"; TargetAttributes = { F672EDE01ACB44520020B9BA = { CreatedOnToolsVersion = 6.2; + LastSwiftMigration = 0810; }; F672EDEB1ACB44520020B9BA = { CreatedOnToolsVersion = 6.2; - }; - F6C51F861ACB912800B00CF7 = { - CreatedOnToolsVersion = 6.2; - }; - F6C51F901ACB912800B00CF7 = { - CreatedOnToolsVersion = 6.2; + LastSwiftMigration = 0810; }; }; }; @@ -315,8 +210,6 @@ targets = ( F672EDE01ACB44520020B9BA /* SwiftJWT */, F672EDEB1ACB44520020B9BA /* SwiftJWTTests */, - F6C51F861ACB912800B00CF7 /* CommonCrypto */, - F6C51F901ACB912800B00CF7 /* CommonCryptoTests */, ); }; /* End PBXProject section */ @@ -336,28 +229,38 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F6C51F851ACB912800B00CF7 /* Resources */ = { - isa = PBXResourcesBuildPhase; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + AEC84E66214D7FDB00867631 /* Run Script: CommonCrypto */ = { + isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - runOnlyForDeploymentPostprocessing = 0; - }; - F6C51F8F1ACB912800B00CF7 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Script: CommonCrypto"; + outputFileListPaths = ( + ); + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "../Build-Phases/common-crypto.sh\n"; }; -/* End PBXResourcesBuildPhase section */ +/* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F672EDDC1ACB44520020B9BA /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DE183DF91BEB4EDC000D4544 /* Data+JWT.swift in Sources */, + DE183DF71BEB4EB4000D4544 /* String+JWT.swift in Sources */, F672EDFE1ACB44650020B9BA /* SwiftJWT.swift in Sources */, + DE183DFE1BEB774D000D4544 /* JWTNaCl.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -366,21 +269,7 @@ buildActionMask = 2147483647; files = ( F672EDF41ACB44520020B9BA /* JWTTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F6C51F821ACB912800B00CF7 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F6C51F8D1ACB912800B00CF7 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F6C51F991ACB912800B00CF7 /* CommonCryptoTests.swift in Sources */, + DE183DFC1BEB61AD000D4544 /* XCTAssert+JWT.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -392,16 +281,6 @@ target = F672EDE01ACB44520020B9BA /* SwiftJWT */; targetProxy = F672EDEE1ACB44520020B9BA /* PBXContainerItemProxy */; }; - F6C51F941ACB912800B00CF7 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = F6C51F861ACB912800B00CF7 /* CommonCrypto */; - targetProxy = F6C51F931ACB912800B00CF7 /* PBXContainerItemProxy */; - }; - F6C51FA61ACBC64400B00CF7 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = F6C51F861ACB912800B00CF7 /* CommonCrypto */; - targetProxy = F6C51FA51ACBC64400B00CF7 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -413,21 +292,33 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 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_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; ENABLE_STRICT_OBJC_MSGSEND = YES; + 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", @@ -459,13 +350,23 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 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_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -474,6 +375,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; @@ -483,6 +385,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.2; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -493,6 +396,7 @@ F672EDF81ACB44520020B9BA /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -500,18 +404,17 @@ INFOPLIST_FILE = JWT/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(SDKROOT)/usr/lib/system", - ); + PRODUCT_BUNDLE_IDENTIFIER = "nl.roundzero.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = SwiftJWT; SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; }; name = Debug; }; F672EDF91ACB44520020B9BA /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -519,116 +422,36 @@ INFOPLIST_FILE = JWT/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(SDKROOT)/usr/lib/system", - ); + PRODUCT_BUNDLE_IDENTIFIER = "nl.roundzero.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = SwiftJWT; SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; }; name = Release; }; F672EDFB1ACB44520020B9BA /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - FRAMEWORK_SEARCH_PATHS = ( - "$(SDKROOT)/Developer/Library/Frameworks", - "$(inherited)", - ); GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = JWTTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "nl.roundzero.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = SwiftJWTTests; + SWIFT_VERSION = 4.0; }; name = Debug; }; F672EDFC1ACB44520020B9BA /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - FRAMEWORK_SEARCH_PATHS = ( - "$(SDKROOT)/Developer/Library/Frameworks", - "$(inherited)", - ); INFOPLIST_FILE = JWTTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "nl.roundzero.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = SwiftJWTTests; - }; - name = Release; - }; - F6C51F9A1ACB912800B00CF7 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = F6C51FA01ACB916B00B00CF7 /* CommonCrypto.xcconfig */; - buildSettings = { - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - INFOPLIST_FILE = CommonCrypto/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(SDKROOT)/usr/lib/system", - ); - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - }; - name = Debug; - }; - F6C51F9B1ACB912800B00CF7 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = F6C51FA01ACB916B00B00CF7 /* CommonCrypto.xcconfig */; - buildSettings = { - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = CommonCrypto/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(SDKROOT)/usr/lib/system", - ); - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - }; - name = Release; - }; - F6C51F9C1ACB912800B00CF7 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - FRAMEWORK_SEARCH_PATHS = ( - "$(SDKROOT)/Developer/Library/Frameworks", - "$(inherited)", - ); - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - INFOPLIST_FILE = CommonCryptoTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - F6C51F9D1ACB912800B00CF7 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - FRAMEWORK_SEARCH_PATHS = ( - "$(SDKROOT)/Developer/Library/Frameworks", - "$(inherited)", - ); - INFOPLIST_FILE = CommonCryptoTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; }; name = Release; }; @@ -662,24 +485,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F6C51F9E1ACB912800B00CF7 /* Build configuration list for PBXNativeTarget "CommonCrypto" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F6C51F9A1ACB912800B00CF7 /* Debug */, - F6C51F9B1ACB912800B00CF7 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F6C51F9F1ACB912800B00CF7 /* Build configuration list for PBXNativeTarget "CommonCryptoTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F6C51F9C1ACB912800B00CF7 /* Debug */, - F6C51F9D1ACB912800B00CF7 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = F672EDD81ACB44520020B9BA /* Project object */; diff --git a/README.md b/README.md index 2fd3f5b..c1e63dc 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,23 @@ Swift Framework for JWT (JSON Web Token) Created for Authentiq ID +## Installation + +### Available in CocoaPods + +SwiftJWT is available through [CocoaPods](http://cocoapods.org). To install it, simply add the following line to your Podfile: + +```objc +pod "SwiftJWT" +``` + +In case you want to use the project with `Ed25519` algorithm support, you can install it using: + +```objc +pod "SwiftJWT/with-ed25519" +``` + + ## Examples ```Swift diff --git a/SwiftJWT.podspec b/SwiftJWT.podspec index 71e9c85..b55ffbe 100644 --- a/SwiftJWT.podspec +++ b/SwiftJWT.podspec @@ -1,24 +1,39 @@ Pod::Spec.new do |s| s.name = "SwiftJWT" - s.version = "0.1" + s.version = "0.6" s.summary = "a JSON Web Token implementation in Swift on iOS & OSX" s.homepage = "git://github.com/stannie/swift-jwt" s.license = { :type => "MIT", :file => 'LICENSE' } s.authors = { "Stan P. van de Burgt" => "stan@vandeburgt.com" } s.social_media_url = 'https://twitter.com/stannie' - s.ios.deployment_target = "8.0" + s.ios.deployment_target = "9.0" s.osx.deployment_target = "10.9" - s.source = { :git => "git://github.com/stannie/swift-jwt.git", :tag => "0.4"} - # https://github.com/stannie/swift-jwt.git git@github.com:stannie/swift-jwt.git - s.source_files = "*.swift" + # s.source = { :git => "git://github.com/stannie/swift-jwt.git", :tag => s.version} + s.source = { :git => "git://github.com/stannie/swift-jwt.git", :branch => "master"} s.requires_arc = true - # needs https://github.com/jedisct1/swift-sodium - # but only for the JWTNaCl sub class - # s.dependency "Sodium", "0" - # also needs CommonCrypto # see http://stackoverflow.com/questions/25248598/importing-commoncrypto-in-a-swift-framework # (answer by stephencelis) on how to import + # s.frameworks = "CommonCrypto" + + s.preserve_paths = "Build-Phases/*.sh" + s.script_phase = { :name => "CommonCrypto", :script => "sh $SRCROOT/SwiftJWT/Build-Phases/common-crypto.sh", :execution_position => :before_compile } + + s.default_subspec = 'Core' + + s.subspec 'Core' do |core| + core.source_files = "JWT/JWT/**/*.{swift,h}" + + # exclude ed25519 code + core.exclude_files = "**/JWTNaCl.swift" + end + + s.subspec 'with-ed25519' do |ed25519| + ed25519.source_files = "JWT/JWT/**/*.{swift,h}" + + # needed only for the JWTNaCl sub class + s.dependency "Sodium" + end end diff --git a/SwiftJWT.swift b/SwiftJWT.swift deleted file mode 100644 index 5b70e37..0000000 --- a/SwiftJWT.swift +++ /dev/null @@ -1,547 +0,0 @@ -// -// SwiftJWT.swift -// -// Stan P. van de Burgt -// stan@vandeburgt.com -// (c) RoundZero 2015 -// Project Authentiq - -import Foundation - -public class JWT { - // https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-4 - // base class; supports alg: none, HS256, HS384, HS512 - // TODO: add support for RS256, RS384, RS512 (almost there!) - // TODO: add support for PS256, PS384, PS512 - - public var header: [String: AnyObject] = ["alg": "none", "typ": "JWT", ] { - // JWT header - didSet { - if header["alg"] as? String == nil { - self.header["alg"] = "none" // if not present, insert alg - } - } - } - public var body: [String: AnyObject] = [:] // JWT payload - var algorithms: [String] = [] // algorithms that are valid on loads(), dumps() and setting 'alg' header - - public init(algorithms: [String]) { - self.algorithms = implemented(algorithms) // only add algoritms that are implemented() - } - -// public init(header: [String: AnyObject], body: [String: AnyObject]) { -// self.init(header: header, body: body, algorithms: nil) { -// } - - public init(header: [String: AnyObject], body: [String: AnyObject], algorithms: [String]?) { - self.header = header - self.body = body - if header["alg"] as? String == nil { - self.header["alg"] = "none" // if not present, insert 'alg' - } - if let alg = algorithms { - // TODO: decide if this was smart, as it could introduce a vulnerability for the caller - self.algorithms = implemented(alg) // only add algoritms that are implemented() - } - else { - self.algorithms = [self.header["alg"] as! String] - } - if header["typ"] as? String == nil { - self.header["typ"] = "JWT" // if not present, insert 'typ' element - } - } - - public func loads(jwt: String, key: NSData? = nil, verify: Bool = true, mandatory: [String] = [], error: NSErrorPointer = nil) -> Bool { - // load a JWT string into this object - var sig = "" - var hdr: [String: AnyObject]? - var payload: [String: AnyObject]? - var algorithm: String? - - // clear object properties - self.header = [:] - self.body = [:] - - // split JWT string into parts: header, body, optional signature - let parts: [String] = jwt.componentsSeparatedByString(".") - switch parts.count { - case 2: break - case 3: sig = parts[2] - default: - if error != nil { - error.memory = NSError(domain: "JWT", code: 1, userInfo: nil) // TODO: actual error details - } - return false - } - - // decode the header (a URL-safe, base 64 encoded JSON dict) from 1st part - let hdr_data = parts[0].base64SafeUrlDecode() - hdr = NSJSONSerialization.JSONObjectWithData(hdr_data, options: NSJSONReadingOptions(0), error: error) as? [String: AnyObject] - if hdr != nil { - // check that "alg" header is on whitelist (and thus implemented) ; even if verify == false - algorithm = hdr!["alg"] as? String - if !self.whitelisted(algorithm) { - return false // TODO: populate NSError - } - } - else { - return false // TODO: populate NSError - } - // decode the body (a URL-safe base 64 encoded JSON dict) from the 2nd part - if parts.count > 1 { - let body_data = parts[1].base64SafeUrlDecode() - payload = NSJSONSerialization.JSONObjectWithData(body_data, options: NSJSONReadingOptions(0), error: error) as? [String: AnyObject] - if payload == nil { - return false // TODO: populate NSError - } - } - else { - return false // TODO: populate NSError - } - - // all went well so far, so let's set the object properties - // TODO: set properties even later (but are needed by verification methods now) - self.header = hdr! - self.body = payload! - - if verify { - // verify the signature, a URL-safe base64 encoded string - let hdr_body: String = parts[0] + "." + parts[1] // header & body of a JWT - let data = hdr_body.dataUsingEncoding(NSUTF8StringEncoding)! - if self.verify_signature(data, signature: sig, algorithm: algorithm!, key: key) == false { - self.header = [:]; self.body = [:] // reset - return false // TODO: populate NSError - } - // verify content fields - if self.verify_content() == false { - self.header = [:]; self.body = [:] // reset - return false // TODO: populate NSError - } - } - for fld in mandatory { - if self.body[fld] == nil { - // not present, but was mandatory - self.header = [:]; self.body = [:] // reset - return false // TODO: populate NSError - } - } - return true - } - - // convenience method for plain strings as key - public func loads(jwt: String, key: String, verify: Bool = true, mandatory: [String] = [], error: NSErrorPointer = nil) -> Bool { - let key_raw = key.dataUsingEncoding(NSUTF8StringEncoding)! - return loads(jwt, key: key_raw, verify: verify, mandatory: mandatory, error: error) - } - - // convenience method for base64 strings as key - public func loads(jwt: String, b64key: String, verify: Bool = true, mandatory: [String] = [], error: NSErrorPointer = nil) -> Bool { - let key_raw = b64key.base64SafeUrlDecode() - return loads(jwt, key: key_raw, verify: verify, mandatory: mandatory, error: error) - } - - public func dumps(key: NSData? = nil, jti_len: UInt = 16, error: NSErrorPointer = nil) -> String? { - // create a JWT string from this object - // TODO: some way to indicate that some fields should be generated, next to jti; e.g. nbf and iat - var payload = self.body - // if 'jti' (the nonce) not present in body, and it is requested (jti_len > 0), set one - if payload["jti"] as? String == nil && jti_len > 0 { - // generate a random string (nonce) of length jti_len for body item 'jti' - // https://developer.apple.com/library/ios/documentation/Security/Reference/RandomizationReference/index.html - var bytes = NSMutableData(length: Int(jti_len))! - // SecRandomCopyBytes(rnd: SecRandomRef, count: Int, bytes: UnsafeMutablePointer) - SecRandomCopyBytes(kSecRandomDefault, Int(jti_len), UnsafeMutablePointer(bytes.mutableBytes)) - payload["jti"] = bytes.base64SafeUrlEncode() - } - // TODO: set iat, nbf in payload here if not set & requested? - if let h = NSJSONSerialization.dataWithJSONObject(self.header, options: nil, error: error) { - var data = h.base64SafeUrlEncode() - if let b = NSJSONSerialization.dataWithJSONObject(payload, options: nil, error: error) { - data = data + "." + b.base64SafeUrlEncode() - // check that "alg" header is on whitelist (and thus implemented) - let alg = self.header["alg"] as? String - if self.whitelisted(alg) { - let data_raw = data.dataUsingEncoding(NSUTF8StringEncoding)! - if let sig = self.signature(data_raw, algorithm: alg!, key: key) { - return data + "." + sig - } - } - } - } - return nil // TODO: populate NSError - } - - // convenience method for plain strings as key - public func dumps(key: String, jti_len: UInt = 16, error: NSErrorPointer = nil) -> String? { - let key_raw = key.dataUsingEncoding(NSUTF8StringEncoding)! - return dumps(key: key_raw, jti_len: jti_len, error: error) - } - - func whitelisted(algorithm: String?) -> Bool { - for alg in self.algorithms { - if alg == algorithm { - return true - } - } - return false - } - - func implemented(algorithm: String?) -> Bool { - let algorithms = ["none", "HS256", "HS384", "HS512"] - // TODO: add RS256, RS384, RS512, PS256, PS384, PS512 when rsa_* methods below are done - for alg in algorithms { - if alg == algorithm { - return true - } - } - return false - } - - func implemented(algorithms: [String]) -> [String] { - var result: [String] = [] - for alg in algorithms { - if implemented(alg) { - result.append(alg) - } - } - return result - } - - func signature(msg: NSData, algorithm: String, key: NSData?) -> String? { - // internal function to compute the signature (third) part of a JWT - if let key_raw = key { - switch algorithm { - case "HS256": return msg.base64digest(HMACAlgorithm.SHA256, key: key_raw) - case "HS384": return msg.base64digest(HMACAlgorithm.SHA384, key: key_raw) - case "HS512": return msg.base64digest(HMACAlgorithm.SHA512, key: key_raw) - case "RS256": return msg.rsa_signature(HMACAlgorithm.SHA256, key: key_raw) - case "RS384": return msg.rsa_signature(HMACAlgorithm.SHA384, key: key_raw) - case "RS512": return msg.rsa_signature(HMACAlgorithm.SHA512, key: key_raw) - case "PS256": return msg.rsa_signature(HMACAlgorithm.SHA256, key: key_raw) // TODO: convert PS to RS key - case "PS384": return msg.rsa_signature(HMACAlgorithm.SHA384, key: key_raw) - case "PS512": return msg.rsa_signature(HMACAlgorithm.SHA512, key: key_raw) - default: return nil - } - } - else { - return algorithm == "none" ? "" : nil - } - } - - func verify_signature(msg: NSData, signature: String, algorithm: String, key: NSData? = nil) -> Bool { - // internal function to verify the signature (third) part of a JWT - if key == nil && algorithm != "none" { - return false - } - switch algorithm { - case "none": return signature == "" // if "none" then the signature shall be empty - case "HS256": return msg.base64digest(HMACAlgorithm.SHA256, key: key!) == signature - case "HS384": return msg.base64digest(HMACAlgorithm.SHA384, key: key!) == signature - case "HS512": return msg.base64digest(HMACAlgorithm.SHA512, key: key!) == signature - case "RS256": return msg.rsa_verify(HMACAlgorithm.SHA256, signature: signature, key: key!) - case "RS384": return msg.rsa_verify(HMACAlgorithm.SHA384, signature: signature, key: key!) - case "RS512": return msg.rsa_verify(HMACAlgorithm.SHA512, signature: signature, key: key!) - case "PS256": return msg.rsa_verify(HMACAlgorithm.SHA256, signature: signature, key: key!) // TODO: convert PS to RS key - case "PS384": return msg.rsa_verify(HMACAlgorithm.SHA384, signature: signature, key: key!) - case "PS512": return msg.rsa_verify(HMACAlgorithm.SHA512, signature: signature, key: key!) - default: return false - } - } - - // TODO: some way to enforce that e.g. iat and nbf are present - // TODO: verification of iss and aud when given in loads() - func verify_content() -> Bool { - // internal function to verify the content (header and body) parts of a JWT - let date = NSDate() - let now = UInt(date.timeIntervalSince1970) - - if let typ = self.header["typ"] as? String { - if typ != "JWT" { return false } // 'typ' shall be 'JWT' - } - else { - return false // 'typ' shall be present - } - if let exp = self.body["exp"] as? UInt { - if now > exp { return false } // TODO: also false if "exp" is not of type UInt - } - if let nbf = self.body["nbf"] as? UInt { - if now < nbf { return false } // TODO: also false if "nbf" is not of type UInt - } - if let iat = self.body["iat"] as? UInt { - if now < iat { return false } // TODO: also false if "iat" is not of type UInt - } - return true - } -} - -// MARK: - NaCl signatures -// subclass with additional sign/verify for "Ed25519" signatures -// TODO: move following class and nacl_* extentions to NSData to JWTNaCl.swift ? - -public class JWTNaCl: JWT { - - public func _kid(key: NSData) -> String { - return key.base64SafeUrlEncode() - } - - override func implemented(algorithm: String?) -> Bool { - let algorithms = ["Ed25519"] - for alg in algorithms { - if alg == algorithm { - return true - } - } - return super.implemented(algorithm) // not implemented here, so try parent - } - - override func signature(msg: NSData, algorithm: String, key: NSData?) -> String? { - if algorithm == "Ed25519" { - return msg.nacl_signature(key!) // will crash on nil key - } - else { - return super.signature(msg, algorithm: algorithm, key: key) - } - } - - override func verify_signature(msg: NSData, signature: String, algorithm: String, key: NSData? = nil) -> Bool { - if algorithm == "Ed25519" { - if key == nil { - // use the "kid" field as base64 key string as key if no key provided. - if let kid = header["kid"] as? String { - return msg.nacl_verify(signature, key: kid.base64SafeUrlDecode()) - } - } - else { - return msg.nacl_verify(signature, key: key!) - } - } - else { - return super.verify_signature(msg, signature: signature, algorithm: algorithm, key: key) - } - return false - } - - override func verify_content() -> Bool { - if let kid = self.header["kid"] as? String { - let pk = kid.base64SafeUrlDecode(nil) - if pk == nil || pk!.length != 32 { - return false - } - } - else { - return false // kid is not optional when using NaCl - } - if let sub = self.body["sub"] as? String { - let id = sub.base64SafeUrlDecode(nil) - if id == nil || id!.length != 32 { - return false - } - } - return super.verify_content() // run the parent tests too - } -} - -// MARK: - base64 extensions - -extension String { - func base64SafeUrlDecode() -> NSData { // TODO : add ! or ? - return self.base64SafeUrlDecode(nil) - } - - func base64SafeUrlDecode(options: NSDataBase64DecodingOptions) -> NSData! { // TODO: better use ? instead of ! ?? - var s: String = self; - - s = s.stringByReplacingOccurrencesOfString("-", withString: "+") // 62nd char of encoding - s = s.stringByReplacingOccurrencesOfString("_", withString: "/") // 63rd char of encoding - - // Pad with trailing '='s - switch (count(s) % 4) { - case 0: break // No pad chars in this case - case 2: s += "==" // Two pad chars - case 3: s += "=" // One pad char - default: return nil - } - - return NSData(base64EncodedString: s, options: options)! - } -} - -extension NSData { - func base64SafeUrlEncode() -> String { - return self.base64SafeUrlEncode(nil) - } - - func base64SafeUrlEncode(options: NSDataBase64EncodingOptions) -> String { - var s: String = self.base64EncodedStringWithOptions(options) - if let idx = find(s, "=") { - s = s.substringToIndex(idx) // remove trailing '='s - } - s = s.stringByReplacingOccurrencesOfString("/", withString: "_") // 63rd char of encoding - s = s.stringByReplacingOccurrencesOfString("+", withString: "-") // 62nd char of encoding - return s - } -} - -// MARK: - HMACAlgorithm -// See http://stackoverflow.com/questions/25248598/importing-commoncrypto-in-a-swift-framework (answer by stephencelis) on how to import - -import CommonCrypto - -// See: http://stackoverflow.com/questions/24099520/commonhmac-in-swift (answer by hdost) on HMAC signing. -// note that MD5, SHA1 and SHA224 are not used as JWT algorithms - -enum HMACAlgorithm { - case MD5, SHA1, SHA224, SHA256, SHA384, SHA512 - - func toCCEnum() -> 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) - } - - func digestLength() -> Int { - var result: CInt = 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) - } -} - -// See http://stackoverflow.com/questions/21724337/signing-and-verifying-on-ios-using-rsa on RSA signing - - -import Sodium // swift wrapper of LibSodium, a NaCl implementation https://github.com/jedisct1/swift-sodium - -extension NSData { - - func base64digest(algorithm: HMACAlgorithm, key: NSData) -> String! { - let digestLen = algorithm.digestLength() - let result = UnsafeMutablePointer.alloc(digestLen) - - // Inspired by: http://stackoverflow.com/questions/24099520/commonhmac-in-swift (answer by hdost) - // CCHmac(algorithm: algorithm.toCCEnum(), key: keyData, keyLength: keyLen, data: data, dataLength: dataLen, macOut: result) - CCHmac(algorithm.toCCEnum(), key.bytes, key.length, self.bytes, self.length, result) - let hdata = NSData(bytes: result, length: digestLen) - result.destroy() - - return hdata.base64SafeUrlEncode() - } - - func nacl_signature(key: NSData) -> String! { - // key is privkey - let sodium = Sodium() - if let sig = sodium?.sign.signature(self, secretKey: key) { - return sig.base64SafeUrlEncode() - } - return nil - } - func nacl_verify(signature: String, key: NSData) -> Bool { - // key is pubkey - if let sodium = Sodium() { - let sig_raw = signature.base64SafeUrlDecode() - return sodium.sign.verify(self, publicKey: key, signature: sig_raw) - } - return false - } - - // based on http://stackoverflow.com/questions/21724337/signing-and-verifying-on-ios-using-rsa - - func rsa_signature(algorithm: HMACAlgorithm, key: NSData) -> String? { - // key is privkey, in raw format - let privkey: SecKey? = nil // TODO: get the SecKey format of the (private) key - let msgbytes = UnsafePointer(self.bytes) - let msglen = CC_LONG(self.length) - let digestlen = algorithm.digestLength() - var digest = NSMutableData(length: digestlen)! - let digestbytes = UnsafeMutablePointer(digest.mutableBytes) - var padding: SecPadding - - switch algorithm { - case .SHA256: // TODO: change to HS256, ... ? - CC_SHA256(msgbytes, msglen, digestbytes) // TODO: test on nil return? - padding = SecPadding(kSecPaddingPKCS1SHA256) - case .SHA384: - CC_SHA256(msgbytes, msglen, digestbytes) - padding = SecPadding(kSecPaddingPKCS1SHA384) - case .SHA512: - CC_SHA256(msgbytes, msglen, digestbytes) - padding = SecPadding(kSecPaddingPKCS1SHA512) - default: - return nil - } - - var sig = NSMutableData(length: digestlen)! - var sigbytes = UnsafeMutablePointer(sig.mutableBytes) // or UnsafeMutablePointer.alloc(Int(digestLen)) ? - var siglen: Int = digestlen - - // OSStatus SecKeyRawSign(key: SecKey!, padding: SecPadding, dataToSign: UnsafePointer, dataToSignLen: Int, sig: UnsafeMutablePointer, sigLen: UnsafeMutablePointer) - let status = SecKeyRawSign(privkey!, padding, digestbytes, digestlen, sigbytes, &siglen) - if status == errSecSuccess { - // use siglen in/out parameter to set the actual lenght of sig - sig.length = siglen - return sig.base64SafeUrlEncode() - } - return nil - } - - func rsa_verify(algorithm: HMACAlgorithm, signature: String, key: NSData) -> Bool { - // key is pubkey, in raw format - let pubkey: SecKey? = nil /// TODO: get the SecKey format of the (public) key - let msgbytes = UnsafePointer(self.bytes) - let msglen = CC_LONG(self.length) - let digestlen = algorithm.digestLength() - var digest = NSMutableData(length: digestlen)! - let digestbytes = UnsafeMutablePointer(digest.mutableBytes) - var padding: SecPadding - - switch algorithm { - case .SHA256: // TODO: change to HS256, ... - CC_SHA256(msgbytes, msglen, digestbytes) // TODO: test on nil return? - padding = SecPadding(kSecPaddingPKCS1SHA256) - case .SHA384: - CC_SHA256(msgbytes, msglen, digestbytes) - padding = SecPadding(kSecPaddingPKCS1SHA384) - case .SHA512: - CC_SHA256(msgbytes, msglen, digestbytes) - padding = SecPadding(kSecPaddingPKCS1SHA512) - default: - return false - } - - let sig_raw = signature.dataUsingEncoding(NSUTF8StringEncoding)! - let sigbytes = UnsafePointer(sig_raw.bytes) - let siglen = sig_raw.length - - // OSStatus SecKeyRawVerify(key: SecKey!, padding: SecPadding, signedData: UnsafePointer, signedDataLen: Int, sig: UnsafePointer, sigLen: Int) - let status = SecKeyRawVerify(pubkey!, padding, digestbytes, digestlen, sigbytes, siglen) - - return status == errSecSuccess - } -} - -// END