From 7ed21e448d982a0e6a6965f3d094ea379b12dd3c Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 03:52:10 -0700 Subject: [PATCH] rely on zod to remove os specific restraints --- .../ios/CryptoUtil.swift | 30 +-- .../ios/ExpoBlueskyOAuthClientModule.swift | 4 +- .../expo-bluesky-oauth-client/ios/JWK.swift | 76 +++----- .../expo-bluesky-oauth-client/ios/JWT.swift | 179 +----------------- .../ios/JWTUtil.swift | 60 ++++-- .../src/oauth-client-react-native.ts | 10 +- .../src/react-native-key.ts | 17 +- 7 files changed, 91 insertions(+), 285 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift b/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift index fd20643f9a..a3c26069ae 100644 --- a/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift +++ b/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift @@ -14,7 +14,7 @@ class CryptoUtil { return Data(bytes) } - public static func generateKeyPair() throws -> JWKPair? { + public static func generateKeyPair() throws -> [String:String] { let keyIdString = UUID().uuidString let privateKey = P256.Signing.PrivateKey() @@ -23,28 +23,14 @@ class CryptoUtil { let x = publicKey.x963Representation[1..<33].base64URLEncodedString() let y = publicKey.x963Representation[33...].base64URLEncodedString() let d = privateKey.rawRepresentation.base64URLEncodedString() + + let publicJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, alg: "ES256") + let privateJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, d: d, alg: "ES256") - let publicJWK = JWK( - alg: "ES256".toField(), - kty: "EC".toField(), - crv: "P-256".toNullableField(), - x: x.toNullableField(), - y: y.toNullableField(), - use: "sig".toNullableField(), - kid: keyIdString.toNullableField() - ) - let privateJWK = JWK( - alg: "ES256".toField(), - kty: "EC".toField(), - crv: "P-256".toNullableField(), - x: x.toNullableField(), - y: y.toNullableField(), - d: d.toNullableField(), - use: "sig".toNullableField(), - kid: keyIdString.toNullableField() - ) - - return JWKPair(privateKey: privateJWK.toField(), publicKey: publicJWK.toField()) + return [ + "privateKey": privateJWK.toJson(), + "publicKey": publicJWK.toJson() + ] } } diff --git a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift index 702e56253a..386b8d6430 100644 --- a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift +++ b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift @@ -34,7 +34,7 @@ public class ExpoBlueskyOAuthClientModule: Module { promise.resolve(keypair) } - AsyncFunction("createJwt") { (header: JWTHeader, payload: JWTPayload, jwk: JWK, promise: Promise) in + AsyncFunction("createJwt") { (header: String, payload: String, jwk: String, promise: Promise) in guard let jwt = JWTUtil.createJwt(header: header, payload: payload, jwk: jwk) else { promise.reject("JWTError", "Error creating JWT.") return @@ -42,7 +42,7 @@ public class ExpoBlueskyOAuthClientModule: Module { promise.resolve(jwt) } - AsyncFunction("verifyJwt") { (token: String, jwk: JWK, promise: Promise) in + AsyncFunction("verifyJwt") { (token: String, jwk: String, promise: Promise) in promise.resolve(JWTUtil.verifyJwt(token: token, jwk: jwk)) } } diff --git a/modules/expo-bluesky-oauth-client/ios/JWK.swift b/modules/expo-bluesky-oauth-client/ios/JWK.swift index daa7206914..72148a9cbf 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWK.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWK.swift @@ -1,58 +1,32 @@ import ExpoModulesCore import JOSESwift -struct JWK : Record { - @Field - var alg: String - @Field - var kty: String - @Field - var crv: String? - @Field - var x: String? - @Field - var y: String? - @Field - var e: String? - @Field - var n: String? - @Field +struct JWK { + let kty: String + let use: String + let crv: String + let kid: String + let x: String + let y: String var d: String? - @Field - var use: String? - @Field - var kid: String? + let alg: String - func toField() -> Field { - return Field(wrappedValue: self) - } - - func toPrivateSecKey() throws -> SecKey? { - let jsonData = try JSONSerialization.data(withJSONObject: self.toDictionary()) - guard let jwk = try? JSONDecoder().decode(ECPrivateKey.self, from: jsonData), - let key = try? jwk.converted(to: SecKey.self) - else { - print("Error creating SecKey.") - return nil - } - return key - } - - func toPublicSecKey() throws -> SecKey? { - let jsonData = try JSONSerialization.data(withJSONObject: self.toDictionary()) - guard let jwk = try? JSONDecoder().decode(ECPublicKey.self, from: jsonData), - let key = try? jwk.converted(to: SecKey.self) - else { - print("Error creating SecKey.") - return nil - } - return key - } -} + func toJson() -> String { + var dict: [String: Any] = [ + "kty": kty, + "use": use, + "crv": crv, + "kid": kid, + "x": x, + "y": y, + "alg": alg, + ] -struct JWKPair : Record { - @Field - var privateKey: JWK - @Field - var publicKey: JWK + if let d = d { + dict["d"] = d + } + + let jsonData = try! JSONSerialization.data(withJSONObject: dict, options: []) + return String(data: jsonData, encoding: .utf8)! + } } diff --git a/modules/expo-bluesky-oauth-client/ios/JWT.swift b/modules/expo-bluesky-oauth-client/ios/JWT.swift index 8ce014048a..18c68cbf04 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWT.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWT.swift @@ -1,184 +1,9 @@ import ExpoModulesCore import JOSESwift -struct JWTHeader : Record { - @Field - var alg: String = "ES256" - @Field - var jku: String? - @Field - var jwk: JWK - @Field - var kid: String? - @Field - var typ: String? - @Field - var cty: String? - @Field - var crit: String? - - func toField() -> Field { - return Field(wrappedValue: self) - } - - func toJWSHeader() throws -> JWSHeader? { - return JWSHeader(try JSONSerialization.data(withJSONObject: self.toDictionary())) - } -} - -struct JWTPayload : Record { - @Field - var iss: String? - @Field - var aud: String? - @Field - var sub: String? - @Field - var exp: Int? - @Field - var nbr: Int? - @Field - var iat: Int? - @Field - var jti: String? - @Field - var htm: String? - @Field - var htu: String? - @Field - var ath: String? - @Field - var acr: String? - @Field - var azp: String? - @Field - var amr: String? - @Field - var cnf: JWTPayloadCNF? - @Field - var client_id: String? - @Field - var scope: String? - @Field - var nonce: String? - @Field - var at_hash: String? - @Field - var c_hash: String? - @Field - var s_hash: String? - @Field - var auth_time: Int? - @Field - var name: String? - @Field - var family_name: String? - @Field - var given_name: String? - @Field - var middle_name: String? - @Field - var nickname: String? - @Field - var preferred_username: String? - @Field - var gender: String? - @Field - var picture: String? - @Field - var profile: String? - @Field - var website: String? - @Field - var birthdate: String? - @Field - var zoneinfo: String? - @Field - var locale: String? - @Field - var updated_at: Int? - @Field - var email: String? - @Field - var email_verified: Bool? - @Field - var phone_number: String? - @Field - var phone_number_verified: Bool? - @Field - var address: JWTPayloadAddress? - @Field - var authorization_details: JWTPayloadAuthorizationDetails? - - func toField() -> Field { - return Field(wrappedValue: self) - } - - func toPayload() throws -> Payload { - return Payload(try JSONSerialization.data(withJSONObject: self.toDictionary())) - } -} - -struct JWTPayloadCNF : Record { - @Field - var kid: String? - @Field - var jwk: JWK? - @Field - var jwe: String? - @Field - var jku: String? - @Field - var jkt: String? - @Field - var osc: String? - - func toField() -> Field { - return Field(wrappedValue: self) - } -} - -struct JWTPayloadAddress : Record { - @Field - var formatted: String? - @Field - var street_address: String? - @Field - var locality: String? - @Field - var region: String? - @Field - var postal_code: String? - @Field - var country: String? - - func toField() -> Field { - return Field(wrappedValue: self) - } -} - -struct JWTPayloadAuthorizationDetails : Record { - @Field - var type: String - @Field - var locations: [String]? - @Field - var actions: [String]? - @Field - var datatypes: [String]? - @Field - var identifier: String? - @Field - var privileges: [String]? - - func toField() -> Field { - return Field(wrappedValue: self) - } -} - struct JWTVerifyResponse : Record { @Field - var protectedHeader: JWTHeader + var payload: String? @Field - var payload: String + var protectedHeader: String? } diff --git a/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift b/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift index f14d2a113d..5997f587f1 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift @@ -2,6 +2,18 @@ import ExpoModulesCore import JOSESwift class JWTUtil { + static func jsonToPrivateKey(_ jwkString: String) throws -> SecKey? { + guard let jsonData = jwkString.data(using: .utf8), + let jwk = try? JSONDecoder().decode(ECPrivateKey.self, from: jsonData), + let key = try? jwk.converted(to: SecKey.self) + else { + print("Error creating JWK from JWK string.") + return nil + } + + return key + } + static func jsonToPublicKey(_ jwkString: String) throws -> SecKey? { guard let jsonData = jwkString.data(using: .utf8), let jwk = try? JSONDecoder().decode(ECPublicKey.self, from: jsonData), @@ -32,12 +44,11 @@ class JWTUtil { return JWSHeader(headerData) } - public static func createJwt(header: JWTHeader, payload: JWTPayload, jwk: JWK) -> String? { - guard let header = try? header.toJWSHeader(), - let payload = try? payload.toPayload(), - let key = try? jwk.toPrivateSecKey() + public static func createJwt(header: String, payload: String, jwk: String) -> String? { + guard let header = headerStringToPayload(header), + let payload = payloadStringToPayload(payload), + let key = try? jsonToPrivateKey(jwk) else { - print("didn't have one") return nil } @@ -53,8 +64,8 @@ class JWTUtil { return jws.compactSerializedString } - public static func verifyJwt(token: String, jwk: JWK) -> JWTVerifyResponse? { - guard let key = try? jwk.toPublicSecKey(), + public static func verifyJwt(token: String, jwk: String) -> [String: Any]? { + guard let key = try? jsonToPublicKey(jwk), let jws = try? JWS(compactSerialization: token), let verifier = Verifier(verifyingAlgorithm: .ES256, key: key), let validation = try? jws.validate(using: verifier) @@ -63,24 +74,33 @@ class JWTUtil { } let header = validation.header - let serializedHeader = JWTHeader( - alg: "ES256", - jku: Field(wrappedValue: header.jku?.absoluteString), - kid: Field(wrappedValue:header.kid), - typ: Field(wrappedValue: header.typ), - cty: Field(wrappedValue: header.cty), - crit: Field(wrappedValue: header.cty) - ) - let payload = String(data: validation.payload.data(), encoding: .utf8) guard let payload = payload else { return nil } - return JWTVerifyResponse( - protectedHeader: serializedHeader.toField(), - payload: payload.toField() - ) + var protectedHeader: [String:Any] = [:] + protectedHeader["alg"] = "ES256" + if header.jku != nil { + protectedHeader["jku"] = header.jku?.absoluteString + } + if header.kid != nil { + protectedHeader["kid"] = header.kid + } + if header.typ != nil { + protectedHeader["typ"] = header.typ + } + if header.cty != nil { + protectedHeader["cty"] = header.cty + } + if header.crit != nil { + protectedHeader["crit"] = header.crit + } + + return [ + "payload": payload, + "protectedHeader": protectedHeader + ] } } diff --git a/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts b/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts index 2816b7055a..cf29ac193e 100644 --- a/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts +++ b/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts @@ -1,5 +1,5 @@ import {requireNativeModule} from 'expo-modules-core' -import {Jwk, Jwt, Key} from '@atproto/jwk' +import {Jwk, Jwt} from '@atproto/jwk' const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') @@ -24,7 +24,9 @@ export const OauthClientReactNative = (NativeModule as null) || { * * @throws if the algorithm is not supported ("ES256" must be supported) */ - async generateJwk(_algo: string): Promise<{publicKey: Key; privateKey: Key}> { + async generateJwk( + _algo: string, + ): Promise<{publicKey: string; privateKey: string}> { throw new Error(LINKING_ERROR) }, @@ -40,8 +42,8 @@ export const OauthClientReactNative = (NativeModule as null) || { _token: Jwt, _jwk: Jwk, ): Promise<{ - payload: string // this is a JSON response to make Swift a bit easier to work with - protectedHeader: Record + payload: string + protectedHeader: string }> { throw new Error(LINKING_ERROR) }, diff --git a/modules/expo-bluesky-oauth-client/src/react-native-key.ts b/modules/expo-bluesky-oauth-client/src/react-native-key.ts index 6d93b0cfdb..d13754ddce 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-key.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-key.ts @@ -19,7 +19,7 @@ export class ReactNativeKey extends Key { // Note: OauthClientReactNative.generatePrivateJwk should throw if it // doesn't support the algorithm. const res = await OauthClientReactNative.generateJwk(algo) - const jwk = res.privateKey + const jwk = JSON.parse(res.privateKey) as Record const use = jwk.use || 'sig' return new ReactNativeKey(jwkValidator.parse({...jwk, use, kid})) } catch { @@ -31,7 +31,11 @@ export class ReactNativeKey extends Key { } async createJwt(header: JwtHeader, payload: JwtPayload): Promise { - return OauthClientReactNative.createJwt(header, payload, this.jwk) + return await OauthClientReactNative.createJwt( + JSON.stringify(header), + JSON.stringify(payload), + JSON.stringify(this.jwk), + ) } async verifyJwt< @@ -40,16 +44,11 @@ export class ReactNativeKey extends Key { >(token: Jwt, options?: VerifyOptions): Promise> { const result = await OauthClientReactNative.verifyJwt(token, this.jwk) - // TODO see if we can make these `undefined` or maybe update zod to allow `nullable()` let payloadParsed = JSON.parse(result.payload) - payloadParsed = Object.fromEntries( - Object.entries(payloadParsed as object).filter(([_, v]) => v !== null), - ) const payload = jwtPayloadSchema.parse(payloadParsed) - // We don't need to validate this, because the native types ensure it is correct. But this is a TODO - // for the same reason above - const protectedHeader = result.protectedHeader + let protectedHeaderParsed = JSON.parse(result.protectedHeader) + const protectedHeader = jwtPayloadSchema.parse(protectedHeaderParsed) if (options?.audience != null) { const audience = Array.isArray(options.audience)