better layout
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user