Live video on the AT Protocol
at eli/docker-linting 188 lines 5.4 kB view raw
1import { SimpleStore } from "@atproto-labs/simple-store"; 2import { jwkValidator } from "@atproto/jwk"; 3import { JoseKey } from "@atproto/jwk-jose"; 4import { 5 InternalStateData, 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 console.log("GOT HEREEEE!"); 78 const errors: unknown[] = []; 79 for (const alg of algs) { 80 try { 81 let subtle = QuickCrypto?.webcrypto?.subtle; 82 const subalg = toSubtleAlgorithm(alg); 83 const keyPair = (await subtle.generateKey(subalg, true, [ 84 "sign", 85 "verify", 86 ])) as CryptoKeyPair; 87 88 const ex = (await subtle.exportKey( 89 "jwk", 90 keyPair.privateKey as unknown as CryptoKey, 91 )) as JWK; 92 ex.alg = alg; 93 // these have trailing periods sometimes for some reason 94 for (const k of ["x", "y", "d"]) { 95 if (ex[k].endsWith(".")) { 96 ex[k] = ex[k].slice(0, -1); 97 } 98 } 99 100 // RNQC doesn't give us a kid, so let's do a quick hash of the key 101 const kid = QuickCrypto.createHash("sha256") 102 .update(JSON.stringify(ex)) 103 .digest("hex"); 104 const use = "sig"; 105 106 return new JoseKey(jwkValidator.parse({ ...ex, kid, use })); 107 } catch (err) { 108 errors.push(err); 109 } 110 } 111 throw new Error("None of the algorithms worked"); 112 }, 113 getRandomValues: (length) => 114 new Uint8Array(QuickCrypto.randomBytes(length)), 115 digest: (bytes, algorithm) => 116 QuickCrypto.createHash(algorithm.name) 117 .update(bytes as unknown as ArrayBuffer) 118 .digest(), 119 }, 120 clientMetadata: options.clientMetadata, 121 }); 122 this.didStore = options.didStore; 123 } 124 125 async init(refresh?: boolean) { 126 const sub = await this.didStore.get(`(sub)`); 127 if (sub) { 128 try { 129 const session = await this.restore(sub, refresh); 130 return { session }; 131 } catch (err) { 132 this.didStore.del(`(sub)`); 133 throw err; 134 } 135 } 136 } 137 138 async callback(params: URLSearchParams): Promise<{ 139 session: OAuthSession; 140 state: string | null; 141 }> { 142 const { session, state } = await super.callback(params); 143 await this.didStore.set(`(sub)`, session.sub); 144 return { session, state }; 145 } 146} 147 148export function toSubtleAlgorithm( 149 alg: string, 150 crv?: string, 151 options?: { modulusLength?: number }, 152): SubtleAlgorithm { 153 switch (alg) { 154 case "PS256": 155 case "PS384": 156 case "PS512": 157 return { 158 name: "RSA-PSS", 159 hash: `SHA-${alg.slice(-3) as "256" | "384" | "512"}`, 160 modulusLength: options?.modulusLength ?? 2048, 161 publicExponent: new Uint8Array([0x01, 0x00, 0x01]), 162 }; 163 case "RS256": 164 case "RS384": 165 case "RS512": 166 return { 167 name: "RSASSA-PKCS1-v1_5", 168 hash: `SHA-${alg.slice(-3) as "256" | "384" | "512"}`, 169 modulusLength: options?.modulusLength ?? 2048, 170 publicExponent: new Uint8Array([0x01, 0x00, 0x01]), 171 }; 172 case "ES256": 173 case "ES384": 174 return { 175 name: "ECDSA", 176 namedCurve: `P-${alg.slice(-3) as "256" | "384"}`, 177 }; 178 case "ES512": 179 return { 180 name: "ECDSA", 181 namedCurve: "P-521", 182 }; 183 default: 184 // https://github.com/w3c/webcrypto/issues/82#issuecomment-849856773 185 186 throw new TypeError(`Unsupported alg "${alg}"`); 187 } 188}