From 2ada0cbd0dd07432b7d9c76917d4672ed908738b Mon Sep 17 00:00:00 2001 From: Hailey Date: Sun, 14 Apr 2024 23:27:27 -0700 Subject: [PATCH] better implementation --- modules/expo-bluesky-oauth-client/index.ts | 10 ++--- .../ios/CryptoUtil.swift | 37 ++++++++++++++++--- .../ios/ExpoBlueskyOAuthClientModule.swift | 20 +++++----- .../expo-bluesky-oauth-client/ios/JWK.swift | 12 ++++++ .../ios/{JWTHeader.swift => JWT.swift} | 31 +++++++++++++++- .../ios/JWTUtil.swift | 27 +++----------- .../src/oauth-client-react-native.ts | 18 +++++---- .../src/react-native-crypto-implementation.ts | 6 +-- .../src/react-native-key.ts | 7 +++- .../src/react-native-store-with-key.ts | 4 +- .../src/react-native-store.ts | 2 +- src/view/screens/Home.tsx | 24 +++++++++--- 12 files changed, 136 insertions(+), 62 deletions(-) rename modules/expo-bluesky-oauth-client/ios/{JWTHeader.swift => JWT.swift} (76%) diff --git a/modules/expo-bluesky-oauth-client/index.ts b/modules/expo-bluesky-oauth-client/index.ts index 05f53f9803..79e975ebc9 100644 --- a/modules/expo-bluesky-oauth-client/index.ts +++ b/modules/expo-bluesky-oauth-client/index.ts @@ -1,6 +1,4 @@ -export * from './src-old/crypto-subtle' -export * from './src-old/jose-key' -export * from './src-old/rn-crypto-key' -export * from './src-old/rn-oauth-client-factory' -export * from './src-old/rn-oauth-database' -export * from './src-old/util.web' +export * from './src/oauth-client-react-native' +export * from './src/react-native-crypto-implementation' +export * from './src/react-native-key' +export * from './src/react-native-store-with-key' diff --git a/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift b/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift index c9b95f81b9..fd20643f9a 100644 --- a/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift +++ b/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift @@ -1,5 +1,6 @@ import CryptoKit import JOSESwift +import ExpoModulesCore class CryptoUtil { // The equivalent of crypto.subtle.digest() with JS on web @@ -13,8 +14,8 @@ class CryptoUtil { return Data(bytes) } - public static func generateKeyPair(kid: String?) throws -> (publicJWK: JWK, privateJWK: JWK)? { - let keyIdString = kid ?? UUID().uuidString + public static func generateKeyPair() throws -> JWKPair? { + let keyIdString = UUID().uuidString let privateKey = P256.Signing.PrivateKey() let publicKey = privateKey.publicKey @@ -23,10 +24,27 @@ class CryptoUtil { 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 (publicJWK, privateJWK) + return JWKPair(privateKey: privateJWK.toField(), publicKey: publicJWK.toField()) } } @@ -35,3 +53,12 @@ extension Data { return self.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "") } } + +extension String { + func toField() -> Field { + return Field(wrappedValue: self) + } + func toNullableField() -> Field { + return Field(wrappedValue: self) + } +} diff --git a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift index 729194916a..510d60fdc6 100644 --- a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift +++ b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift @@ -14,22 +14,24 @@ public class ExpoBlueskyOAuthClientModule: Module { return CryptoUtil.getRandomValues(byteLength: byteLength) } - AsyncFunction ("generateKeyPair") { (kid: String?, promise: Promise) in - let keypair = try? CryptoUtil.generateKeyPair(kid: kid) + AsyncFunction ("generateJwk") { (algo: String?, promise: Promise) in + if algo != "ES256" { + promise.reject("GenerateKeyError", "Algorithim not supported.") + return + } + + let keypair = try? CryptoUtil.generateKeyPair() - guard let keypair = keypair else { + guard keypair != nil else { promise.reject("GenerateKeyError", "Error generating JWK.") return } - promise.resolve([ - "publicKey": keypair.publicJWK.toJson(), - "privateKey": keypair.privateJWK.toJson() - ]) + promise.resolve(keypair) } - AsyncFunction("createJwt") { (jwk: String, header: String, payload: String, promise: Promise) in - guard let jwt = JWTUtil.createJwt(jwk, header: header, payload: payload) else { + AsyncFunction("createJwt") { (header: JWTHeader, payload: JWTPayload, jwk: JWK, promise: Promise) in + guard let jwt = JWTUtil.createJwt(header: header, payload: payload, jwk: jwk) else { promise.reject("JWTError", "Error creating JWT.") return } diff --git a/modules/expo-bluesky-oauth-client/ios/JWK.swift b/modules/expo-bluesky-oauth-client/ios/JWK.swift index 41d9522aa0..e9d80fe3b4 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWK.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWK.swift @@ -1,4 +1,5 @@ import ExpoModulesCore +import JOSESwift struct JWK : Record { @Field @@ -25,6 +26,17 @@ struct JWK : Record { func toField() -> Field { return Field(wrappedValue: self) } + + func toSecKey() 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 + } } struct JWKPair : Record { diff --git a/modules/expo-bluesky-oauth-client/ios/JWTHeader.swift b/modules/expo-bluesky-oauth-client/ios/JWT.swift similarity index 76% rename from modules/expo-bluesky-oauth-client/ios/JWTHeader.swift rename to modules/expo-bluesky-oauth-client/ios/JWT.swift index c80ce57f1e..b6c27a8c66 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWTHeader.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWT.swift @@ -1,4 +1,5 @@ import ExpoModulesCore +import JOSESwift struct JWTHeader : Record { @Field @@ -6,8 +7,6 @@ struct JWTHeader : Record { @Field var jku: String? @Field - var jwk: JWK - @Field var kid: String? @Field var x5u: String? @@ -21,6 +20,14 @@ struct JWTHeader : Record { 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 { @@ -106,6 +113,14 @@ struct JWTPayload : Record { 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 { @@ -121,6 +136,10 @@ struct JWTPayloadCNF : Record { var jkt: String? @Field var osc: String? + + func toField() -> Field { + return Field(wrappedValue: self) + } } struct JWTPayloadAddress : Record { @@ -136,6 +155,10 @@ struct JWTPayloadAddress : Record { var postal_code: String? @Field var country: String? + + func toField() -> Field { + return Field(wrappedValue: self) + } } struct JWTPayloadAuthorizationDetails : Record { @@ -151,4 +174,8 @@ struct JWTPayloadAuthorizationDetails : Record { var identifier: String? @Field var privileges: [String]? + + func toField() -> Field { + return Field(wrappedValue: self) + } } diff --git a/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift b/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift index d3dcdcee3d..fc10136c86 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift @@ -1,21 +1,6 @@ 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 { - let jsonData = jwkString.data(using: .utf8)! - let jwk = try! JSONDecoder().decode(ECPrivateKey.self, from: jsonData) -// let key = try! jwk.converted(to: SecKey.self) - print("Error creating JWK from JWK string \(jwkString).") - 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), @@ -46,12 +31,12 @@ class JWTUtil { return JWSHeader(headerData) } - public static func createJwt(_ jwkString: String, header headerString: String, payload payloadString: String) -> String? { - guard let key = try? jsonToPrivateKey(jwkString), - let payload = payloadStringToPayload(payloadString), - let header = headerStringToPayload(headerString) - else - { + 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.toSecKey() + else { + print("didn't have one") return nil } 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 4ce738202e..7fd1889d44 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} from '@atproto/jwk' +import {Jwk, Jwt, Key} from '@atproto/jwk' const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') @@ -14,7 +14,7 @@ export const OauthClientReactNative = (NativeModule as null) || { /** * @throws if the algorithm is not supported ("sha256" must be supported) */ - digest(_bytes: Uint8Array, _algorithm: string): Uint8Array { + async digest(_bytes: Uint8Array, _algorithm: string): Promise { throw new Error(LINKING_ERROR) }, @@ -24,21 +24,25 @@ export const OauthClientReactNative = (NativeModule as null) || { * * @throws if the algorithm is not supported ("ES256" must be supported) */ - generateJwk(_algo: string): Jwk { + async generateJwk(_algo: string): Promise<{publicKey: Key; privateKey: Key}> { throw new Error(LINKING_ERROR) }, - createJwt(_header: unknown, _payload: unknown, _jwk: unknown): Jwt { + async createJwt( + _header: unknown, + _payload: unknown, + _jwk: unknown, + ): Promise { throw new Error(LINKING_ERROR) }, - verifyJwt( + async verifyJwt( _token: Jwt, _jwk: Jwk, - ): { + ): Promise<{ payload: Record protectedHeader: Record - } { + }> { throw new Error(LINKING_ERROR) }, } diff --git a/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts b/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts index 757e5615a0..26f58dc7b2 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts @@ -1,13 +1,13 @@ -import {CryptoImplementaton, DigestAlgorithm, Key} from '@atproto/oauth-client' +import {CryptoImplementation, DigestAlgorithm, Key} from '@atproto/oauth-client' import {OauthClientReactNative} from './oauth-client-react-native' import {ReactNativeKey} from './react-native-key' -export class ReactNativeCryptoImplementation implements CryptoImplementaton { +export class ReactNativeCryptoImplementation implements CryptoImplementation { async createKey(algs: string[]): Promise { const bytes = await this.getRandomValues(12) const kid = Array.from(bytes, byteToHex).join('') - return ReactNativeKey.generate(kid, algs) + return await ReactNativeKey.generate(kid, algs) } async getRandomValues(length: number): Promise { 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 03d27db691..d4b8ef355c 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,12 @@ export class ReactNativeKey extends Key { try { // Note: OauthClientReactNative.generatePrivateJwk should throw if it // doesn't support the algorithm. - const jwk = await OauthClientReactNative.generateJwk(algo) + const res = await OauthClientReactNative.generateJwk(algo) + const jwk = jwkValidator.parse({ + ...res.privateKey, + key_ops: ['sign', 'verify'], + kid, + }) const use = jwk.use || 'sig' return new ReactNativeKey(jwkValidator.parse({...jwk, use, kid})) } catch { diff --git a/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts b/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts index b65e9a46ed..435e566cf5 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts @@ -1,8 +1,8 @@ import {GenericStore, Value} from '@atproto/caching' import {Jwk} from '@atproto/jwk' -import {ReactNativeKey} from './react-native-key.js' -import {ReactNativeStore} from './react-native-store.js' +import {ReactNativeKey} from './react-native-key' +import {ReactNativeStore} from './react-native-store' type ExposedValue = Value & {dpopKey: ReactNativeKey} type StoredValue = Omit & { diff --git a/modules/expo-bluesky-oauth-client/src/react-native-store.ts b/modules/expo-bluesky-oauth-client/src/react-native-store.ts index 518a72243d..0a3d186d07 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-store.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-store.ts @@ -20,6 +20,6 @@ export class ReactNativeStore } async del(key: string): Promise { - await Storage.delete(key) + await Storage.removeItem(key) } } diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index a97f2d2986..b201db08a9 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -19,7 +19,7 @@ import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' -import {RnCryptoKey} from '../../../modules/expo-bluesky-oauth-client' +import {ReactNativeKey} from '../../../modules/expo-bluesky-oauth-client' import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA' import {HomeHeader} from '../com/home/HomeHeader' @@ -54,13 +54,27 @@ function HomeScreenReady({ }) { React.useEffect(() => { ;(async () => { - const key = await RnCryptoKey.generate(undefined, ['ES256'], false) - console.log('public', key.publicJwk) + const key = await ReactNativeKey.generate('test', ['ES256']) + console.log(key.privateJwk) + const jwt = await key.createJwt( - {alg: 'ES256', kid: key.kid}, - {sub: 'test'}, + { + alg: 'ES256', + kid: key.kid, + }, + { + sub: 'test', + }, ) + console.log(jwt) + + // console.log('public', key.publicJwk) + // const jwt = await key.createJwt( + // {alg: 'ES256', kid: key.kid}, + // {sub: 'test'}, + // ) + // console.log(jwt) })() }, [])