better implementation

This commit is contained in:
Hailey
2024-04-14 23:27:27 -07:00
parent 71f1e44653
commit 2ada0cbd0d
12 changed files with 136 additions and 62 deletions
+4 -6
View File
@@ -1,6 +1,4 @@
export * from './src-old/crypto-subtle' export * from './src/oauth-client-react-native'
export * from './src-old/jose-key' export * from './src/react-native-crypto-implementation'
export * from './src-old/rn-crypto-key' export * from './src/react-native-key'
export * from './src-old/rn-oauth-client-factory' export * from './src/react-native-store-with-key'
export * from './src-old/rn-oauth-database'
export * from './src-old/util.web'
@@ -1,5 +1,6 @@
import CryptoKit import CryptoKit
import JOSESwift import JOSESwift
import ExpoModulesCore
class CryptoUtil { class CryptoUtil {
// The equivalent of crypto.subtle.digest() with JS on web // The equivalent of crypto.subtle.digest() with JS on web
@@ -13,8 +14,8 @@ class CryptoUtil {
return Data(bytes) return Data(bytes)
} }
public static func generateKeyPair(kid: String?) throws -> (publicJWK: JWK, privateJWK: JWK)? { public static func generateKeyPair() throws -> JWKPair? {
let keyIdString = kid ?? UUID().uuidString let keyIdString = UUID().uuidString
let privateKey = P256.Signing.PrivateKey() let privateKey = P256.Signing.PrivateKey()
let publicKey = privateKey.publicKey let publicKey = privateKey.publicKey
@@ -23,10 +24,27 @@ class CryptoUtil {
let y = publicKey.x963Representation[33...].base64URLEncodedString() let y = publicKey.x963Representation[33...].base64URLEncodedString()
let d = privateKey.rawRepresentation.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 publicJWK = JWK(
let privateJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, d: d, alg: "ES256") 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: "") return self.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
} }
} }
extension String {
func toField() -> Field<String> {
return Field(wrappedValue: self)
}
func toNullableField() -> Field<String?> {
return Field(wrappedValue: self)
}
}
@@ -14,22 +14,24 @@ public class ExpoBlueskyOAuthClientModule: Module {
return CryptoUtil.getRandomValues(byteLength: byteLength) return CryptoUtil.getRandomValues(byteLength: byteLength)
} }
AsyncFunction ("generateKeyPair") { (kid: String?, promise: Promise) in AsyncFunction ("generateJwk") { (algo: String?, promise: Promise) in
let keypair = try? CryptoUtil.generateKeyPair(kid: kid) if algo != "ES256" {
promise.reject("GenerateKeyError", "Algorithim not supported.")
return
}
guard let keypair = keypair else { let keypair = try? CryptoUtil.generateKeyPair()
guard keypair != nil else {
promise.reject("GenerateKeyError", "Error generating JWK.") promise.reject("GenerateKeyError", "Error generating JWK.")
return return
} }
promise.resolve([ promise.resolve(keypair)
"publicKey": keypair.publicJWK.toJson(),
"privateKey": keypair.privateJWK.toJson()
])
} }
AsyncFunction("createJwt") { (jwk: String, header: String, payload: String, promise: Promise) in AsyncFunction("createJwt") { (header: JWTHeader, payload: JWTPayload, jwk: JWK, promise: Promise) in
guard let jwt = JWTUtil.createJwt(jwk, header: header, payload: payload) else { guard let jwt = JWTUtil.createJwt(header: header, payload: payload, jwk: jwk) else {
promise.reject("JWTError", "Error creating JWT.") promise.reject("JWTError", "Error creating JWT.")
return return
} }
@@ -1,4 +1,5 @@
import ExpoModulesCore import ExpoModulesCore
import JOSESwift
struct JWK : Record { struct JWK : Record {
@Field @Field
@@ -25,6 +26,17 @@ struct JWK : Record {
func toField() -> Field<JWK> { func toField() -> Field<JWK> {
return Field(wrappedValue: self) 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 { struct JWKPair : Record {
@@ -1,4 +1,5 @@
import ExpoModulesCore import ExpoModulesCore
import JOSESwift
struct JWTHeader : Record { struct JWTHeader : Record {
@Field @Field
@@ -6,8 +7,6 @@ struct JWTHeader : Record {
@Field @Field
var jku: String? var jku: String?
@Field @Field
var jwk: JWK
@Field
var kid: String? var kid: String?
@Field @Field
var x5u: String? var x5u: String?
@@ -21,6 +20,14 @@ struct JWTHeader : Record {
var cty: String? var cty: String?
@Field @Field
var crit: String? var crit: String?
func toField() -> Field<JWTHeader> {
return Field(wrappedValue: self)
}
func toJWSHeader() throws -> JWSHeader? {
return JWSHeader(try JSONSerialization.data(withJSONObject: self.toDictionary()))
}
} }
struct JWTPayload : Record { struct JWTPayload : Record {
@@ -106,6 +113,14 @@ struct JWTPayload : Record {
var address: JWTPayloadAddress? var address: JWTPayloadAddress?
@Field @Field
var authorization_details: JWTPayloadAuthorizationDetails? var authorization_details: JWTPayloadAuthorizationDetails?
func toField() -> Field<JWTPayload> {
return Field(wrappedValue: self)
}
func toPayload() throws -> Payload {
return Payload(try JSONSerialization.data(withJSONObject: self.toDictionary()))
}
} }
struct JWTPayloadCNF : Record { struct JWTPayloadCNF : Record {
@@ -121,6 +136,10 @@ struct JWTPayloadCNF : Record {
var jkt: String? var jkt: String?
@Field @Field
var osc: String? var osc: String?
func toField() -> Field<JWTPayloadCNF> {
return Field(wrappedValue: self)
}
} }
struct JWTPayloadAddress : Record { struct JWTPayloadAddress : Record {
@@ -136,6 +155,10 @@ struct JWTPayloadAddress : Record {
var postal_code: String? var postal_code: String?
@Field @Field
var country: String? var country: String?
func toField() -> Field<JWTPayloadAddress> {
return Field(wrappedValue: self)
}
} }
struct JWTPayloadAuthorizationDetails : Record { struct JWTPayloadAuthorizationDetails : Record {
@@ -151,4 +174,8 @@ struct JWTPayloadAuthorizationDetails : Record {
var identifier: String? var identifier: String?
@Field @Field
var privileges: [String]? var privileges: [String]?
func toField() -> Field<JWTPayloadAuthorizationDetails> {
return Field(wrappedValue: self)
}
} }
@@ -1,21 +1,6 @@
import JOSESwift import JOSESwift
class JWTUtil { 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? { static func jsonToPublicKey(_ jwkString: String) throws -> SecKey? {
guard let jsonData = jwkString.data(using: .utf8), guard let jsonData = jwkString.data(using: .utf8),
let jwk = try? JSONDecoder().decode(ECPublicKey.self, from: jsonData), let jwk = try? JSONDecoder().decode(ECPublicKey.self, from: jsonData),
@@ -46,12 +31,12 @@ class JWTUtil {
return JWSHeader(headerData) return JWSHeader(headerData)
} }
public static func createJwt(_ jwkString: String, header headerString: String, payload payloadString: String) -> String? { public static func createJwt(header: JWTHeader, payload: JWTPayload, jwk: JWK) -> String? {
guard let key = try? jsonToPrivateKey(jwkString), guard let header = try? header.toJWSHeader(),
let payload = payloadStringToPayload(payloadString), let payload = try? payload.toPayload(),
let header = headerStringToPayload(headerString) let key = try? jwk.toSecKey()
else else {
{ print("didn't have one")
return nil return nil
} }
@@ -1,5 +1,5 @@
import {requireNativeModule} from 'expo-modules-core' import {requireNativeModule} from 'expo-modules-core'
import {Jwk, Jwt} from '@atproto/jwk' import {Jwk, Jwt, Key} from '@atproto/jwk'
const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') 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) * @throws if the algorithm is not supported ("sha256" must be supported)
*/ */
digest(_bytes: Uint8Array, _algorithm: string): Uint8Array { async digest(_bytes: Uint8Array, _algorithm: string): Promise<Uint8Array> {
throw new Error(LINKING_ERROR) 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) * @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) throw new Error(LINKING_ERROR)
}, },
createJwt(_header: unknown, _payload: unknown, _jwk: unknown): Jwt { async createJwt(
_header: unknown,
_payload: unknown,
_jwk: unknown,
): Promise<Jwt> {
throw new Error(LINKING_ERROR) throw new Error(LINKING_ERROR)
}, },
verifyJwt( async verifyJwt(
_token: Jwt, _token: Jwt,
_jwk: Jwk, _jwk: Jwk,
): { ): Promise<{
payload: Record<string, unknown> payload: Record<string, unknown>
protectedHeader: Record<string, unknown> protectedHeader: Record<string, unknown>
} { }> {
throw new Error(LINKING_ERROR) throw new Error(LINKING_ERROR)
}, },
} }
@@ -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 {OauthClientReactNative} from './oauth-client-react-native'
import {ReactNativeKey} from './react-native-key' import {ReactNativeKey} from './react-native-key'
export class ReactNativeCryptoImplementation implements CryptoImplementaton { export class ReactNativeCryptoImplementation implements CryptoImplementation {
async createKey(algs: string[]): Promise<Key> { async createKey(algs: string[]): Promise<Key> {
const bytes = await this.getRandomValues(12) const bytes = await this.getRandomValues(12)
const kid = Array.from(bytes, byteToHex).join('') const kid = Array.from(bytes, byteToHex).join('')
return ReactNativeKey.generate(kid, algs) return await ReactNativeKey.generate(kid, algs)
} }
async getRandomValues(length: number): Promise<Uint8Array> { async getRandomValues(length: number): Promise<Uint8Array> {
@@ -19,7 +19,12 @@ export class ReactNativeKey extends Key {
try { try {
// Note: OauthClientReactNative.generatePrivateJwk should throw if it // Note: OauthClientReactNative.generatePrivateJwk should throw if it
// doesn't support the algorithm. // 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' const use = jwk.use || 'sig'
return new ReactNativeKey(jwkValidator.parse({...jwk, use, kid})) return new ReactNativeKey(jwkValidator.parse({...jwk, use, kid}))
} catch { } catch {
@@ -1,8 +1,8 @@
import {GenericStore, Value} from '@atproto/caching' import {GenericStore, Value} from '@atproto/caching'
import {Jwk} from '@atproto/jwk' import {Jwk} from '@atproto/jwk'
import {ReactNativeKey} from './react-native-key.js' import {ReactNativeKey} from './react-native-key'
import {ReactNativeStore} from './react-native-store.js' import {ReactNativeStore} from './react-native-store'
type ExposedValue = Value & {dpopKey: ReactNativeKey} type ExposedValue = Value & {dpopKey: ReactNativeKey}
type StoredValue<V extends ExposedValue> = Omit<V, 'dpopKey'> & { type StoredValue<V extends ExposedValue> = Omit<V, 'dpopKey'> & {
@@ -20,6 +20,6 @@ export class ReactNativeStore<V extends Value>
} }
async del(key: string): Promise<void> { async del(key: string): Promise<void> {
await Storage.delete(key) await Storage.removeItem(key)
} }
} }
+19 -5
View File
@@ -19,7 +19,7 @@ import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' 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 {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA'
import {HomeHeader} from '../com/home/HomeHeader' import {HomeHeader} from '../com/home/HomeHeader'
@@ -54,13 +54,27 @@ function HomeScreenReady({
}) { }) {
React.useEffect(() => { React.useEffect(() => {
;(async () => { ;(async () => {
const key = await RnCryptoKey.generate(undefined, ['ES256'], false) const key = await ReactNativeKey.generate('test', ['ES256'])
console.log('public', key.publicJwk) console.log(key.privateJwk)
const jwt = await key.createJwt( const jwt = await key.createJwt(
{alg: 'ES256', kid: key.kid}, {
{sub: 'test'}, alg: 'ES256',
kid: key.kid,
},
{
sub: 'test',
},
) )
console.log(jwt) console.log(jwt)
// console.log('public', key.publicJwk)
// const jwt = await key.createJwt(
// {alg: 'ES256', kid: key.kid},
// {sub: 'test'},
// )
// console.log(jwt)
})() })()
}, []) }, [])