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