Skip to content

Commit

Permalink
chore: Allow decoding keys and fields from Base64 with invalid charac…
Browse files Browse the repository at this point in the history
…ters

chore: Minor refactors
  • Loading branch information
amosavian committed Jun 4, 2024
1 parent 7708001 commit 5fb3cda
Show file tree
Hide file tree
Showing 14 changed files with 82 additions and 55 deletions.
2 changes: 1 addition & 1 deletion Sources/JWSETKit/Base/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public struct JSONWebValueStorage: Codable, Hashable, ExpressibleByDictionaryLit
if urlEncoded {
return Data(urlBase64Encoded: value)
} else {
return Data(base64Encoded: value)
return Data(base64Encoded: value, options: [.ignoreUnknownCharacters])
}
}
set {
Expand Down
19 changes: 16 additions & 3 deletions Sources/JWSETKit/Base/StorageField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ extension Data: JSONWebFieldEncodable, JSONWebFieldDecodable {
case let value as Data:
return value
case let value as String:
return Data(urlBase64Encoded: value) ?? Data(base64Encoded: value, options: [.ignoreUnknownCharacters])
return Data(urlBase64Encoded: value)
default:
return nil
}
Expand All @@ -84,8 +84,21 @@ extension Date: JSONWebFieldEncodable, JSONWebFieldDecodable {
}

static func castValue(_ value: Any?) -> Self? {
(value as? NSNumber)
.map { Date(timeIntervalSince1970: $0.doubleValue) }
switch value {
case let value as NSNumber:
return Date(timeIntervalSince1970: value.doubleValue)
case let value as Date:
return value
case let value as String:
if let value = Double(value) {
return Date(timeIntervalSince1970: value)
} else if let value = ISO8601DateFormatter().date(from: value) {
return value
}
return nil
default:
return nil
}
}
}

Expand Down
6 changes: 2 additions & 4 deletions Sources/JWSETKit/Cryptography/EC/CryptoKitAbstract.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@ extension CryptoECPublicKey {

public static func create(storage: JSONWebValueStorage) throws -> Self {
let keyData = AnyJSONWebKey(storage: storage)
guard let x = keyData.xCoordinate, !x.isEmpty else {
guard let x = keyData.xCoordinate, !x.isEmpty, let y = keyData.yCoordinate, y.count == x.count else {
throw CryptoKitError.incorrectKeySize
}
let y = keyData.yCoordinate ?? .init()
let rawKey = x + y
return try .init(rawRepresentation: rawKey)
return try .init(rawRepresentation: x + y)
}

public func hash(into hasher: inout Hasher) {
Expand Down
17 changes: 17 additions & 0 deletions Sources/JWSETKit/Cryptography/EC/Ed25519.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ extension Curve25519.Signing.PublicKey: CryptoECPublicKey {
result.xCoordinate = rawRepresentation
return result.storage
}


public static func create(storage: JSONWebValueStorage) throws -> Self {
let keyData = AnyJSONWebKey(storage: storage)
guard let x = keyData.xCoordinate, !x.isEmpty else {
throw CryptoKitError.incorrectKeySize
}
return try .init(rawRepresentation: x)
}
}

extension Curve25519.KeyAgreement.PublicKey: CryptoECPublicKey {
Expand All @@ -34,6 +43,14 @@ extension Curve25519.KeyAgreement.PublicKey: CryptoECPublicKey {
result.xCoordinate = rawRepresentation
return result.storage
}

public static func create(storage: JSONWebValueStorage) throws -> Self {
let keyData = AnyJSONWebKey(storage: storage)
guard let x = keyData.xCoordinate, !x.isEmpty else {
throw CryptoKitError.incorrectKeySize
}
return try .init(rawRepresentation: x)
}
}

extension Curve25519.Signing.PublicKey: JSONWebValidatingKey {
Expand Down
2 changes: 1 addition & 1 deletion Sources/JWSETKit/Cryptography/RSA/RSA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ extension _RSA.Signing.PrivateKey: JSONWebKeyImportable, JSONWebKeyExportable {
// PEM is always a valid Bas64.
Data(base64Encoded: pkcs8PEMRepresentation
.components(separatedBy: .whitespacesAndNewlines)
.dropFirst().dropLast().joined()).unsafelyUnwrapped
.dropFirst().dropLast().joined(), options: [.ignoreUnknownCharacters]).unsafelyUnwrapped
}

public init(importing key: Data, format: JSONWebKeyFormat) throws {
Expand Down
40 changes: 22 additions & 18 deletions Sources/JWSETKit/Extensions/Base64.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ extension DataProtocol {
/// - returns: The URL-safe Base-64 encoded data.
public func urlBase64EncodedData() -> Data {
let result = Data(self).base64EncodedData()
.compactMap {
switch $0 {
case UInt8(ascii: "+"):
return UInt8(ascii: "-")
case UInt8(ascii: "/"):
return UInt8(ascii: "_")
case UInt8(ascii: "="):
.compactMap { (byte: UInt8) -> UInt8? in
switch byte {
case "+":
return "-"
case "/":
return "_"
case "=":
return nil
default:
return $0
return byte
}
}
return Data(result)
Expand All @@ -36,26 +36,31 @@ extension DataProtocol {
}
}

extension UInt8: ExpressibleByUnicodeScalarLiteral {
public init(unicodeScalarLiteral value: UnicodeScalar) {
self = .init(value.value)
}
}

extension Data {
/// Initialize a `Data` from a URL-safe Base-64, UTF-8 encoded `Data`.
///
/// Returns nil when the input is not recognized as valid Base-64.
///
/// - parameter urlBase64Encoded: URL-safe Base-64, UTF-8 encoded input data.
/// - parameter options: Decoding options. Default value is `[]`.
public init?(urlBase64Encoded: any DataProtocol) {
var urlBase64Encoded = urlBase64Encoded.compactMap {
switch $0 {
case UInt8(ascii: "-"):
return UInt8(ascii: "+")
case UInt8(ascii: "_"):
return UInt8(ascii: "/")
var urlBase64Encoded = urlBase64Encoded.map { (byte: UInt8) -> UInt8 in
switch byte {
case "-":
return "+"
case "_":
return "/"
default:
return $0
return byte
}
}
if urlBase64Encoded.count % 4 != 0 {
urlBase64Encoded.append(contentsOf: [UInt8](repeating: .init(ascii: "="), count: 4 - urlBase64Encoded.count % 4))
urlBase64Encoded.append(contentsOf: [UInt8](repeating: "=", count: 4 - urlBase64Encoded.count % 4))
}
guard let value = Data(base64Encoded: .init(urlBase64Encoded), options: [.ignoreUnknownCharacters]) else {
return nil
Expand All @@ -68,7 +73,6 @@ extension Data {
///
/// Returns nil when the input is not recognized as valid Base-64.
/// - parameter urlBase64Encoded: The string to parse.
/// - parameter options: Encoding options. Default value is `[]`.
public init?(urlBase64Encoded: String) {
guard let value = Data(urlBase64Encoded: Data(urlBase64Encoded.utf8)) else { return nil }
self = value
Expand Down
11 changes: 3 additions & 8 deletions Sources/JWSETKit/Extensions/LockValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -450,21 +450,16 @@ extension LockedValue: ExpressibleByBooleanLiteral where Value: ExpressibleByBoo
}

public protocol DictionaryInitialzable: ExpressibleByDictionaryLiteral {
init(elements: [(Key, Value)])
init<S>(uniqueKeysWithValues keysAndValues: S) where S: Sequence, S.Element == (Key, Value)
}

extension Dictionary: DictionaryInitialzable {
@inlinable
public init(elements: [(Key, Value)]) {
self.init(uniqueKeysWithValues: elements)
}
}
extension Dictionary: DictionaryInitialzable {}

extension LockedValue: ExpressibleByDictionaryLiteral where Value: ExpressibleByDictionaryLiteral & DictionaryInitialzable, Value.Key: Hashable {
@inlinable
public convenience init(dictionaryLiteral elements: (Value.Key, Value.Value)...) {
let elements = elements.map { ($0, $1) }
self.init(wrappedValue: Value(elements: elements))
self.init(wrappedValue: Value(uniqueKeysWithValues: elements))
}
}

Expand Down
2 changes: 1 addition & 1 deletion Tests/JWSETKitTests/Base/StorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ final class StorageTests: XCTestCase {
XCTAssertEqual(keys[0].keyType, .ellipticCurve)
XCTAssertEqual(
try P256.Signing.PublicKey.create(storage: keys[0].storage).rawRepresentation,
Data(base64Encoded: "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D7gS2XpJFbZiItSs3m9+9Ue6GnvHw/GW2ZZaVtszggXIw==")
"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D7gS2XpJFbZiItSs3m9+9Ue6GnvHw/GW2ZZaVtszggXIw==".decoded
)
XCTAssertEqual(keys[1].keyType, .rsa)
XCTAssert(type(of: keys[0]) == JSONWebECPublicKey.self)
Expand Down
4 changes: 2 additions & 2 deletions Tests/JWSETKitTests/Cryptography/CompressionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import XCTest
@testable import JWSETKit

final class CompressionTests: XCTestCase {
let decompressed = Data("Data compression test. This text must be compressed.".utf8)
let deflateCompressed = Data(base64Encoded: "c0ksSVRIzs8tKEotLs7Mz1MoSS0u0VMIycgsBjIrShRyS4tLFJJS4WpSU/QA")!
let decompressed = "Data compression test. This text must be compressed.".data
let deflateCompressed = "c0ksSVRIzs8tKEotLs7Mz1MoSS0u0VMIycgsBjIrShRyS4tLFJJS4WpSU/QA".decoded

var deflateCompressor: (any JSONWebCompressor.Type)? {
guard JSONWebCompressionAlgorithm.registeredAlgorithms.contains(.deflate) else { return nil }
Expand Down
6 changes: 3 additions & 3 deletions Tests/JWSETKitTests/Cryptography/RFC7520EncryptionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ final class RFC7520EncryptionTests: XCTestCase {
XCTAssertEqual(jwe.header.protected.encoded, header)
XCTAssertEqual(jwe.encryptedKey, "5vUT2WOtQxKWcekM_IzVQwkGgzlFDwPi".decoded)
XCTAssertEqual(jwe.sealed.nonce, "p9pUq6XHY0jfEZIl".decoded)
#if canImport(Compression)
#if canImport(Compression)
// TODO: Replace SWCompression with a library which matches zlib deflate algorithm.
XCTAssertEqual(jwe.sealed.ciphertext, """
HbDtOsdai1oYziSx25KEeTxmwnh8L8jKMFNc1k3zmMI6VB8hry57tDZ61jXyez\
Expand All @@ -336,9 +336,9 @@ final class RFC7520EncryptionTests: XCTestCase {
hpYA7qi3AyijnCJ7BP9rr3U8kxExCpG3mK420TjOw
""".decoded)
XCTAssertEqual(jwe.sealed.tag, "VILuUwuIxaLVmh5X-T7kmA".decoded)
#else
#else
XCTAssertLessThan(jwe.sealed.ciphertext.count, plainText.data.count)
#endif
#endif
XCTAssertEqual(try jwe.decrypt(using: RFC7520ExampleKeys.keyWrapSymmetricKey.key), plainText.data)
}

Expand Down
8 changes: 4 additions & 4 deletions Tests/JWSETKitTests/Cryptography/RSATests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import CommonCrypto
#endif

final class RSATests: XCTestCase {
let privateKeyDER = Data(base64Encoded: """
let privateKeyDER = """
MIIEogIBAAKCAQBc8heBuESxpRARckQCuVNuiLsH5AX73F1lqNxpFsS+GPWl6rrT\
Q0j9Ox/ag2ZwyPby3FtJ11gWT/kDYkjTbqYBCzmzUeGjm3MbuCzErQLPjzGvdUDn\
vCxx8G+uE2uqTdryfEaregUyo69JmucLq3HQ91cHVeLMN/xMvaAu+xEUbZFiDBcV\
Expand All @@ -38,17 +38,17 @@ final class RSATests: XCTestCase {
5mECgYEAiZXJe66KpFZlzomsvoghwjTCdMb0CSzH8DvstGRC7X2QR1s9BhS8pvSS\
1cPwodOyPbUm6t16iYgZN1ibqxWG+TtiftzNzBjrPWGAIWVW+v0uvYBRgdOKbN/c\
nFi6gyV13lGIMMK61gCaEgcgBtJ9hbuIEH6B3a3n8TACIzewfx0=
""")!
""".decoded

let publicKeyDER = Data(base64Encoded: """
let publicKeyDER = """
MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQBc8heBuESxpRARckQCuVNu\
iLsH5AX73F1lqNxpFsS+GPWl6rrTQ0j9Ox/ag2ZwyPby3FtJ11gWT/kDYkjTbqYB\
CzmzUeGjm3MbuCzErQLPjzGvdUDnvCxx8G+uE2uqTdryfEaregUyo69JmucLq3HQ\
91cHVeLMN/xMvaAu+xEUbZFiDBcVwOOWUft7HmrS+QLngCPlTMyU8Q9ISDtEfE/7\
rbIRyO49u56Y9SulTm/aOcQn/1Qgpc5NvHfRnHJ4Y7zclERWhtOLiDlVPIR7JjOM\
g3wVUEK18XPgoKdTxHBLJymLF2dQSQhfhLruUcndV0R9vOdt3kMB0cSo7NzF4Fcb\
AgMBAAE=
""")!
""".decoded

let plaintext = Data("The quick brown fox jumps over the lazy dog.".utf8)

Expand Down
Loading

0 comments on commit 5fb3cda

Please sign in to comment.