Live video on the AT Protocol
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}