better layout

This commit is contained in:
Hailey
2024-04-14 16:26:31 -07:00
parent 00abca970a
commit c97e439a7c
19 changed files with 263 additions and 6 deletions
+6 -6
View File
@@ -1,6 +1,6 @@
export * from './src/crypto-subtle'
export * from './src/jose-key'
export * from './src/rn-crypto-key'
export * from './src/rn-oauth-client-factory'
export * from './src/rn-oauth-database'
export * from './src/util.web'
export * from './src-old/crypto-subtle'
export * from './src-old/jose-key'
export * from './src-old/rn-crypto-key'
export * from './src-old/rn-oauth-client-factory'
export * from './src-old/rn-oauth-database'
export * from './src-old/util.web'
@@ -0,0 +1,44 @@
import {requireNativeModule} from 'expo-modules-core'
import {Jwk, Jwt} from '@atproto/jwk'
const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient')
const LINKING_ERROR =
'The package ExpoBlueskyOAuthClient is not linked. Make sure you have run `expo install expo-bluesky-oauth-client` and rebuilt your app.'
export const OauthClientReactNative = (NativeModule as null) || {
getRandomValues(_length: number): Uint8Array {
throw new Error(LINKING_ERROR)
},
/**
* @throws if the algorithm is not supported ("sha256" must be supported)
*/
digest(_bytes: Uint8Array, _algorithm: string): Uint8Array {
throw new Error(LINKING_ERROR)
},
/**
* Create a private JWK for the given algorithm. The JWK should have a "use"
* an does not need a "kid" property.
*
* @throws if the algorithm is not supported ("ES256" must be supported)
*/
generateJwk(_algo: string): Jwk {
throw new Error(LINKING_ERROR)
},
createJwt(_header: unknown, _payload: unknown, _jwk: unknown): Jwt {
throw new Error(LINKING_ERROR)
},
verifyJwt(
_token: Jwt,
_jwk: Jwk,
): {
payload: Record<string, unknown>
protectedHeader: Record<string, unknown>
} {
throw new Error(LINKING_ERROR)
},
}
@@ -0,0 +1,27 @@
import {CryptoImplementaton, DigestAlgorithm, Key} from '@atproto/oauth-client'
import {OauthClientReactNative} from './oauth-client-react-native'
import {ReactNativeKey} from './react-native-key'
export class ReactNativeCryptoImplementation implements CryptoImplementaton {
async createKey(algs: string[]): Promise<Key> {
const bytes = await this.getRandomValues(12)
const kid = Array.from(bytes, byteToHex).join('')
return ReactNativeKey.generate(kid, algs)
}
async getRandomValues(length: number): Promise<Uint8Array> {
return OauthClientReactNative.getRandomValues(length)
}
async digest(
bytes: Uint8Array,
algorithm: DigestAlgorithm,
): Promise<Uint8Array> {
return OauthClientReactNative.digest(bytes, algorithm.name)
}
}
function byteToHex(b: number): string {
return b.toString(16).padStart(2, '0')
}
@@ -0,0 +1,110 @@
import {
jwkValidator,
Jwt,
JwtHeader,
jwtHeaderSchema,
JwtPayload,
jwtPayloadSchema,
Key,
VerifyOptions,
VerifyPayload,
VerifyResult,
} from '@atproto/jwk'
import {OauthClientReactNative} from './oauth-client-react-native'
export class ReactNativeKey extends Key {
static async generate(kid: string, allowedAlgos: string[]) {
for (const algo of allowedAlgos) {
try {
// Note: OauthClientReactNative.generatePrivateJwk should throw if it
// doesn't support the algorithm.
const jwk = await OauthClientReactNative.generateJwk(algo)
const use = jwk.use || 'sig'
return new ReactNativeKey(jwkValidator.parse({...jwk, use, kid}))
} catch {
// Ignore, try next one
}
}
throw new Error('No supported algorithms')
}
async createJwt(header: JwtHeader, payload: JwtPayload): Promise<Jwt> {
return OauthClientReactNative.createJwt(header, payload, this.jwk)
}
async verifyJwt<
P extends VerifyPayload = JwtPayload,
C extends string = string,
>(token: Jwt, options?: VerifyOptions<C>): Promise<VerifyResult<P, C>> {
const result = await OauthClientReactNative.verifyJwt(token, this.jwk)
const payload = jwtPayloadSchema.parse(result.payload)
const protectedHeader = jwtHeaderSchema.parse(result.protectedHeader)
if (options?.audience != null) {
const audience = Array.isArray(options.audience)
? options.audience
: [options.audience]
if (!audience.includes(payload.aud)) {
throw new Error('Invalid audience')
}
}
if (options?.issuer != null) {
const issuer = Array.isArray(options.issuer)
? options.issuer
: [options.issuer]
if (!issuer.includes(payload.iss)) {
throw new Error('Invalid issuer')
}
}
if (options?.subject != null && payload.sub !== options.subject) {
throw new Error('Invalid subject')
}
if (options?.typ != null && protectedHeader.typ !== options.typ) {
throw new Error('Invalid type')
}
if (options?.requiredClaims != null) {
for (const key of options.requiredClaims) {
if (
!Object.hasOwn(payload, key) ||
(payload as Record<string, unknown>)[key] === undefined
) {
throw new Error(`Missing claim: ${key}`)
}
}
}
if (payload.iat == null) {
throw new Error('Missing issued at')
}
const now = (options?.currentDate?.getTime() ?? Date.now()) / 1e3
const clockTolerance = options?.clockTolerance ?? 0
if (options?.maxTokenAge != null) {
if (payload.iat < now - options.maxTokenAge + clockTolerance) {
throw new Error('Invalid issued at')
}
}
if (payload.nbf != null) {
if (payload.nbf > now - clockTolerance) {
throw new Error('Invalid not before')
}
}
if (payload.exp != null) {
if (payload.exp < now + clockTolerance) {
throw new Error('Invalid expiration')
}
}
return {payload, protectedHeader} as VerifyResult<P, C>
}
}
@@ -0,0 +1,51 @@
import {GenericStore, Value} from '@atproto/caching'
import {Jwk} from '@atproto/jwk'
import {ReactNativeKey} from './react-native-key.js'
import {ReactNativeStore} from './react-native-store.js'
type ExposedValue = Value & {dpopKey: ReactNativeKey}
type StoredValue<V extends ExposedValue> = Omit<V, 'dpopKey'> & {
dpopKey: Jwk
}
/**
* Uses a {@link ReactNativeStore} to store values that contain a
* {@link ReactNativeKey} as `dpopKey` property. This works by serializing the
* {@link Key} to a JWK before storing it, and deserializing it back to a
* {@link ReactNativeKey} when retrieving the value.
*/
export class ReactNativeStoreWithKey<V extends ExposedValue>
implements GenericStore<string, V>
{
internalStore: ReactNativeStore<StoredValue<V>>
constructor(
protected valueExpiresAt: (value: StoredValue<V>) => null | Date,
) {
this.internalStore = new ReactNativeStore(valueExpiresAt)
}
async set(key: string, value: V): Promise<void> {
const {dpopKey, ...rest} = value
if (!dpopKey.privateJwk) throw new Error('dpopKey.privateJwk is required')
await this.internalStore.set(key, {
...rest,
dpopKey: dpopKey.privateJwk,
})
}
async get(key: string): Promise<V | undefined> {
const value = await this.internalStore.get(key)
if (!value) return undefined
return {
...value,
dpopKey: new ReactNativeKey(value.dpopKey),
} as V
}
async del(key: string): Promise<void> {
await this.internalStore.del(key)
}
}
@@ -0,0 +1,25 @@
import {GenericStore, Value} from '@atproto/caching'
import Storage from '@react-native-async-storage/async-storage'
export class ReactNativeStore<V extends Value>
implements GenericStore<string, V>
{
constructor(protected valueExpiresAt: (value: V) => null | Date) {
throw new Error('Not implemented')
}
async get(key: string): Promise<V | undefined> {
const itemJson = await Storage.getItem(key)
if (itemJson == null) return undefined
return JSON.parse(itemJson) as V
}
async set(key: string, value: V): Promise<void> {
await Storage.setItem(key, JSON.stringify(value))
}
async del(key: string): Promise<void> {
await Storage.delete(key)
}
}