Skip to content

Commit

Permalink
feat: Tests for Entities, JWS for binary data
Browse files Browse the repository at this point in the history
chore: Initial articles for docs
fix: JSONWebStorage equality
fix: UUID lowercased
fix: storage handling Locale and array
fix: Stack overflow when updating ProtectedJSONWebContainer
fix: HMAC key size camputation
fix: RSA PKCS#1 encoding
  • Loading branch information
amosavian committed Sep 19, 2023
1 parent b2e3f22 commit dc0444b
Show file tree
Hide file tree
Showing 35 changed files with 1,037 additions and 235 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/docc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ on:
push:
# If you wanted to only trigger this flow on certain branches,
# specify them here in
branches:
- 'main'
# branches:
# - 'main'
# alternatively, you can trigger docs only on new tags pushed:
# tags:
# - '[0-9]+.[0-9]+.[0-9]+'
tags:
- '[0-9]+.[0-9]+.[0-9]+'

# `concurrency` specifices how this action is run.
# Setting concurrency group makes it so that only one build process runs at a time.
Expand Down
2 changes: 1 addition & 1 deletion .swiftformat
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# file options

--exclude Tests,Snapshots,Build,PluginTests
--exclude Snapshots,Build

# format options
--acronyms
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ dependencies: [
]
```

For detailed usage and API documentation, check [the documentation](http://amosavian.github.io/JWSETKit).
## Usage

For detailed usage and API documentation, check [the documentation](https://amosavian.github.io/JWSETKit/documentation/jwsetkit/).
128 changes: 128 additions & 0 deletions Sources/JWSETKit/Base/ProtectedContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// File.swift
//
//
// Created by Amir Abbas Mousavian on 9/19/23.
//

import Foundation

/// Data value that must be protected by JWS.
public protocol ProtectedWebContainer: Hashable {
/// Signed data.
var protected: Data { get set }

init(protected: Data) throws
}

public struct ProtectedDataWebContainer: ProtectedWebContainer, Codable {
public var protected: Data

public init(protected: Data) throws {
self.protected = protected
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

let encoded = try container.decode(String.self)
guard let protected = Data(urlBase64Encoded: encoded) else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Protected is not a valid bas64url."))
}
self.protected = protected
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let encoded = protected.urlBase64EncodedData()
try container.encode(encoded)
}
}

/// A JSON Web Signature/Encryption (JWS/JWE) header or payload with can be signed.
///
/// This cotainer preserves original data to keep consistancy of signature as re-encoding payload
/// may change sorting.
public struct ProtectedJSONWebContainer<Container: JSONWebContainer>: ProtectedWebContainer, Codable {
private var _protected: Data
private var _value: Container

/// Serialized protected data of JOSE.
public var protected: Data {
get {
_protected
}
set {
_protected = newValue
if newValue.isEmpty {
_value.storage = .init()
return
}
do {
_value = try JSONDecoder().decode(Container.self, from: newValue)
} catch {
_protected = .init()
}
}
}

/// Parsed value of data.
public var value: Container {
get {
_value
}
set {
_value = newValue
if _value.storage == .init() {
_protected = .init()
return
}
do {
_protected = try JSONEncoder().encode(newValue)
} catch {
_value = try! .create(storage: .init())
}
}
}

/// Initialized protected container from a JOSE data.
///
/// - Parameter protected: Serialzed json object but **not** in `base64url` .
public init(protected: Data) throws {
self._protected = protected
self._value = try JSONDecoder().decode(Container.self, from: protected)
}

/// Initialized protected container from object.
///
/// - Parameter value: Object that will be presented in `base64url` json.
public init(value: Container) throws {
self._value = value
self._protected = try JSONEncoder().encode(value).urlBase64EncodedData()
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

let encoded = try container.decode(String.self)
guard let protected = Data(urlBase64Encoded: encoded) else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Protected is not a valid bas64url."))
}
self._protected = protected
self._value = try JSONDecoder().decode(Container.self, from: protected)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let encoded = _protected.urlBase64EncodedData()
try container.encode(encoded)
}

public static func == (lhs: Self, rhs: Self) -> Bool {
lhs._protected == rhs._protected
}

public func hash(into hasher: inout Hasher) {
hasher.combine(_protected)
}
}
50 changes: 46 additions & 4 deletions Sources/JWSETKit/Base/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public struct JSONWebValueStorage: Codable, Hashable, ExpressibleByDictionaryLit
/// Returns values of given key.
public subscript<T>(dynamicMember member: String) -> [T] {
get {
self[member]
self[member]
}
set {
self[member] = newValue
Expand Down Expand Up @@ -89,14 +89,16 @@ public struct JSONWebValueStorage: Codable, Hashable, ExpressibleByDictionaryLit
}
}
set {
updateValue(key: member, value: urlEncoded ? newValue?.urlBase64EncodedData() : newValue?.base64EncodedData())
updateValue(key: member, value: urlEncoded ?
(newValue?.urlBase64EncodedData()).map { String(decoding: $0, as: UTF8.self) } :
newValue?.base64EncodedString())
}
}

/// Returns values of given key decoded using base64.
public subscript(_ member: String, urlEncoded: Bool = false) -> [Data] {
get {
guard let values = self[member] as [String]? else { return [] }
let values = self[member] as [String]
if urlEncoded {
return values.compactMap { Data(urlBase64Encoded: $0) }
} else {
Expand All @@ -105,7 +107,7 @@ public struct JSONWebValueStorage: Codable, Hashable, ExpressibleByDictionaryLit
}
set {
self[member] = newValue.compactMap {
urlEncoded ? $0.urlBase64EncodedData() : $0.base64EncodedData()
urlEncoded ? String(decoding: $0.urlBase64EncodedData(), as: UTF8.self) : $0.base64EncodedString()
}
}
}
Expand Down Expand Up @@ -138,6 +140,14 @@ public struct JSONWebValueStorage: Codable, Hashable, ExpressibleByDictionaryLit
try container.encode(claims)
}

public static func == (lhs: JSONWebValueStorage, rhs: JSONWebValueStorage) -> Bool {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let lhs = try? decoder.decode([String: AnyCodable].self, from: encoder.encode(lhs))
let rhs = try? decoder.decode([String: AnyCodable].self, from: encoder.encode(rhs))
return lhs == rhs
}

/// Removes value of given key from storage.
public func contains(key: String) -> Bool {
claims.keys.contains(key)
Expand Down Expand Up @@ -168,6 +178,9 @@ public struct JSONWebValueStorage: Codable, Hashable, ExpressibleByDictionaryLit
case is URL.Type, is NSURL.Type:
return (value as? String)
.map { URL(string: $0) } as? T
case is Locale.Type, is NSLocale.Type:
return (value as? String)
.map { Locale(bcp47: $0) } as? T
case is (any JSONWebKey).Protocol:
guard let data = try? JSONEncoder().encode(AnyCodable(value)) else { return nil }
return try? AnyJSONWebKey.deserialize(data) as? T
Expand Down Expand Up @@ -197,11 +210,40 @@ public struct JSONWebValueStorage: Codable, Hashable, ExpressibleByDictionaryLit

switch value {
case let value as Date:
// Dates in JWT are `NumericDate` which is a JSON numeric value representing
// the number of seconds from 1970-01-01T00:00:00Z UTC until
// the specified UTC date/time, ignoring leap seconds.
claims[key] = .init(Int(value.timeIntervalSince1970))
case let value as UUID:
// Standards such as ITU-T X.667 and RFC 4122 require them to be formatted
// using lower-case letters.
// The NSUUID class and UUID struct use upper-case letters when formatting.
claims[key] = .init(value.uuidString.lowercased())
case let value as Locale:
// Locales in OIDC is formatted using BCP-47 while Apple uses CLDR/ICU formatting.
claims[key] = .init(value.bcp47)
case let value as any Decodable:
claims[key] = .init(value)
default:
assertionFailure("Unknown storage type")
}
}
}

extension Locale {
fileprivate var bcp47: String {
if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) {
return identifier(.bcp47)
} else {
return identifier.replacingOccurrences(of: "_", with: "-")
}
}

fileprivate init(bcp47: String) {
if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) {
self.init(components: .init(identifier: bcp47))
} else {
self.init(identifier: bcp47.replacingOccurrences(of: "-", with: "_"))
}
}
}
76 changes: 0 additions & 76 deletions Sources/JWSETKit/Base/WebContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,79 +54,3 @@ extension JSONWebContainer {
}
}

/// A JSON Web Signature/Encryption (JWS/JWE) header or payload with can be signed.
///
/// This cotainer preserves original data to keep consistancy of signature as re-encoding payload
/// may change sorting.
public struct ProtectedJSONWebContainer<Container: JSONWebContainer>: Codable, Hashable {
/// Serialized protected date of JOSE.
public var protected: Data {
didSet {
if protected.isEmpty {
value.storage = .init()
return
}
do {
value = try JSONDecoder().decode(Container.self, from: protected)
} catch {
protected = .init()
}
}
}

/// Parsed value of data.
public var value: Container {
didSet {
if value.storage == .init() {
protected = .init()
return
}
do {
protected = try JSONEncoder().encode(value)
} catch {
protected = .init()
}
}
}

/// Initialized protected container from a JOSE data.
///
/// - Parameter protected: Serialzed json object but **not** in `base64url` .
public init(protected: Data) throws {
self.protected = protected
self.value = try JSONDecoder().decode(Container.self, from: protected)
}

/// Initialized protected container from object.
///
/// - Parameter value: Object that will be presented in `base64url` json.
public init(value: Container) throws {
self.value = value
self.protected = try JSONEncoder().encode(value).urlBase64EncodedData()
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

let encoded = try container.decode(String.self)
guard let protected = Data(urlBase64Encoded: encoded) else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Protected is not a valid bas64url."))
}
self.protected = protected
self.value = try JSONDecoder().decode(Container.self, from: protected)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let encoded = protected.urlBase64EncodedData()
try container.encode(encoded)
}

public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.protected == rhs.protected
}

public func hash(into hasher: inout Hasher) {
hasher.combine(protected)
}
}
5 changes: 4 additions & 1 deletion Sources/JWSETKit/Cryptography/Algorithms.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,14 @@ extension JSONWebAlgorithm {
/// ECDSA using P-256 and SHA-256.
public static let ecdsaSignatureP256SHA256: Self = "ES256"

/// EdDSA signature algorithms
public static let eddsaSignature: Self = "EdDSA"

/// ECDSA using P-384 and SHA-384.
public static let ecdsaSignatureP384SHA384: Self = "ES384"

/// ECDSA using P-521 and SHA-512.
public static let ecdsaSignatureP512SHA512: Self = "ES512"
public static let ecdsaSignatureP521SHA512: Self = "ES512"

/// RSAES-PKCS1-v1.5
public static let rsaEncryptionPKCS1: Self = "RSA1_5"
Expand Down
2 changes: 1 addition & 1 deletion Sources/JWSETKit/Cryptography/RSA/JWK-RSA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public struct JSONWebRSAPublicKey: JSONWebValidatingKey {
components = [modulus, publicExponent]
}
var result = DER.Serializer()
result.append(components, as: .integer)
try result.append(components, as: .integer)
return Data(result.serializedBytes)
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/JWSETKit/Cryptography/RSA/SecKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,13 +226,13 @@ extension SecKey: JSONWebValidatingKey {
fileprivate static let signingAlgorithms: [JSONWebAlgorithm: SecKeyAlgorithm] = [
.ecdsaSignatureP256SHA256: .ecdsaSignatureMessageX962SHA256,
.ecdsaSignatureP384SHA384: .ecdsaSignatureMessageX962SHA384,
.ecdsaSignatureP512SHA512: .ecdsaSignatureMessageX962SHA512,
.ecdsaSignatureP521SHA512: .ecdsaSignatureMessageX962SHA512,
.rsaSignaturePKCS1v15SHA256: .rsaSignatureMessagePKCS1v15SHA256,
.rsaSignaturePKCS1v15SHA384: .rsaSignatureMessagePKCS1v15SHA384,
.rsaSignaturePKCS1v15SHA512: .rsaSignatureMessagePKCS1v15SHA512,
.rsaSignaturePSSSHA256: .rsaSignatureMessagePSSSHA256,
.rsaSignaturePSSSHA384: .rsaSignatureMessagePSSSHA384,
.rsaSignaturePSSSHA384: .rsaSignatureMessagePSSSHA384,
.rsaSignaturePSSSHA512: .rsaSignatureMessagePSSSHA512,
]

public func verifySignature<S, D>(_ signature: S, for data: D, using algorithm: JSONWebAlgorithm) throws where S: DataProtocol, D: DataProtocol {
Expand Down
Loading

0 comments on commit dc0444b

Please sign in to comment.