Live video on the AT Protocol
at next 202 lines 5.7 kB view raw
1import { SimpleStore } from "@atproto-labs/simple-store"; 2import { JoseKey } from "@atproto/jwk-jose"; 3import { 4 InternalStateData, 5 Jwk, 6 OAuthClient, 7 OAuthClientFetchMetadataOptions, 8 OAuthClientOptions, 9 OAuthSession, 10 Session, 11 SessionStore, 12 StateStore, 13} from "@atproto/oauth-client"; 14import { JWK } from "jose"; 15import QuickCrypto from "react-native-quick-crypto"; 16import { 17 CryptoKey, 18 SubtleAlgorithm, 19} from "react-native-quick-crypto/lib/typescript/src/keys"; 20import { JoseKeyStore, SQLiteKVStore } from "./sqlite-keystore"; 21 22export type ReactNativeOAuthClientOptions = Omit< 23 OAuthClientOptions, 24 // Provided by this lib 25 | "runtimeImplementation" 26 // Provided by this lib but can be overridden 27 | "sessionStore" 28 | "stateStore" 29> & { 30 sessionStore?: SessionStore; 31 stateStore?: StateStore; 32 didStore?: SimpleStore<string, string>; 33}; 34 35export type ReactNativeOAuthClientFromMetadataOptions = 36 OAuthClientFetchMetadataOptions & 37 Omit<ReactNativeOAuthClientOptions, "clientMetadata">; 38 39export class ReactNativeOAuthClient extends OAuthClient { 40 didStore: SimpleStore<string, string>; 41 42 static async fromClientId( 43 options: ReactNativeOAuthClientFromMetadataOptions, 44 ) { 45 const clientMetadata = await OAuthClient.fetchMetadata(options); 46 return new ReactNativeOAuthClient({ ...options, clientMetadata }); 47 } 48 49 constructor({ 50 fetch, 51 responseMode = "query", 52 53 ...options 54 }: ReactNativeOAuthClientOptions) { 55 if (!options.stateStore) { 56 options.stateStore = new JoseKeyStore<InternalStateData>( 57 new SQLiteKVStore("state"), 58 ); 59 } 60 if (!options.sessionStore) { 61 options.sessionStore = new JoseKeyStore<Session>( 62 new SQLiteKVStore("session"), 63 ); 64 } 65 if (!options.didStore) { 66 options.didStore = new SQLiteKVStore("did"); 67 } 68 super({ 69 ...options, 70 71 sessionStore: options.sessionStore, 72 stateStore: options.stateStore, 73 fetch, 74 responseMode, 75 runtimeImplementation: { 76 createKey: async (algs): Promise<JoseKey> => { 77 const errors: unknown[] = []; 78 for (const alg of algs) { 79 try { 80 let subtle = QuickCrypto?.webcrypto?.subtle; 81 const subalg = toSubtleAlgorithm(alg); 82 const keyPair = (await subtle.generateKey(subalg, true, [ 83 "sign", 84 "verify", 85 ])) as CryptoKeyPair; 86 87 const ex = (await subtle.exportKey( 88 "jwk", 89 keyPair.privateKey as unknown as CryptoKey, 90 )) as JWK; 91 ex.alg = alg; 92 // these have trailing periods sometimes for some reason 93 for (const k of ["x", "y", "d"]) { 94 if (ex[k].endsWith(".")) { 95 ex[k] = ex[k].slice(0, -1); 96 } 97 } 98 99 // RNQC doesn't give us a kid, so let's do a quick hash of the key 100 const kid = QuickCrypto.createHash("sha256") 101 .update(JSON.stringify(ex)) 102 .digest("hex"); 103 const use = "sig"; 104 105 const newKey = { 106 ...ex, 107 kid, 108 use, 109 }; 110 111 let joseKey: JoseKey | null = null; 112 try { 113 joseKey = new JoseKey(newKey as Jwk); 114 } catch (err) { 115 throw new Error(`error creating jose key: ${err}`); 116 } 117 118 return joseKey; 119 } catch (err) { 120 errors.push(err); 121 } 122 } 123 throw new Error( 124 `None of the algorithms worked: ${errors.join(", ")}`, 125 ); 126 }, 127 getRandomValues: (length) => 128 new Uint8Array(QuickCrypto.randomBytes(length)), 129 digest: (bytes, algorithm) => 130 QuickCrypto.createHash(algorithm.name) 131 .update(bytes as unknown as ArrayBuffer) 132 .digest(), 133 }, 134 clientMetadata: options.clientMetadata, 135 }); 136 this.didStore = options.didStore; 137 } 138 139 async init(refresh?: boolean) { 140 const sub = await this.didStore.get(`(sub)`); 141 if (sub) { 142 try { 143 const session = await this.restore(sub, refresh); 144 return { session }; 145 } catch (err) { 146 this.didStore.del(`(sub)`); 147 throw err; 148 } 149 } 150 } 151 152 async callback(params: URLSearchParams): Promise<{ 153 session: OAuthSession; 154 state: string | null; 155 }> { 156 const { session, state } = await super.callback(params); 157 await this.didStore.set(`(sub)`, session.sub); 158 return { session, state }; 159 } 160} 161 162export function toSubtleAlgorithm( 163 alg: string, 164 crv?: string, 165 options?: { modulusLength?: number }, 166): SubtleAlgorithm { 167 switch (alg) { 168 case "PS256": 169 case "PS384": 170 case "PS512": 171 return { 172 name: "RSA-PSS", 173 hash: `SHA-${alg.slice(-3) as "256" | "384" | "512"}`, 174 modulusLength: options?.modulusLength ?? 2048, 175 publicExponent: new Uint8Array([0x01, 0x00, 0x01]), 176 }; 177 case "RS256": 178 case "RS384": 179 case "RS512": 180 return { 181 name: "RSASSA-PKCS1-v1_5", 182 hash: `SHA-${alg.slice(-3) as "256" | "384" | "512"}`, 183 modulusLength: options?.modulusLength ?? 2048, 184 publicExponent: new Uint8Array([0x01, 0x00, 0x01]), 185 }; 186 case "ES256": 187 case "ES384": 188 return { 189 name: "ECDSA", 190 namedCurve: `P-${alg.slice(-3) as "256" | "384"}`, 191 }; 192 case "ES512": 193 return { 194 name: "ECDSA", 195 namedCurve: "P-521", 196 }; 197 default: 198 // https://github.com/w3c/webcrypto/issues/82#issuecomment-849856773 199 200 throw new TypeError(`Unsupported alg "${alg}"`); 201 } 202}