better layout
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export * from './src/crypto-subtle'
|
export * from './src-old/crypto-subtle'
|
||||||
export * from './src/jose-key'
|
export * from './src-old/jose-key'
|
||||||
export * from './src/rn-crypto-key'
|
export * from './src-old/rn-crypto-key'
|
||||||
export * from './src/rn-oauth-client-factory'
|
export * from './src-old/rn-oauth-client-factory'
|
||||||
export * from './src/rn-oauth-database'
|
export * from './src-old/rn-oauth-database'
|
||||||
export * from './src/util.web'
|
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