Skip to content

Commit

Permalink
decent base
Browse files Browse the repository at this point in the history
  • Loading branch information
haileyok committed Apr 9, 2024
1 parent ec58082 commit 1461c0a
Show file tree
Hide file tree
Showing 45 changed files with 2,276 additions and 74 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@
"@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.6.4",
"@miblanchard/react-native-slider": "^2.3.1",
"@pagopa/io-react-native-jwt": "^1.1.0",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-masked-view/masked-view": "0.3.0",
Expand Down Expand Up @@ -133,6 +132,7 @@
"expo-web-browser": "~12.8.2",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
"jose": "^5.2.4",
"js-sha256": "^0.9.0",
"jwt-decode": "^4.0.0",
"lande": "^1.0.10",
Expand Down
44 changes: 40 additions & 4 deletions src/oauth-client-temp/client/crypto-implementation.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,47 @@
import {Key} from '../jwk'
import {CryptoKey} from '#/oauth-client-temp/client/key'
import {Key} from '#/oauth-client-temp/jwk'

// TODO this might not be necessary with this setup, we will see

export type DigestAlgorithm = {
name: 'sha256' | 'sha384' | 'sha512'
}

export type {Key}
export class CryptoImplementation {
public async createKey(algs: string[]): Promise<Key> {
return CryptoKey.generate(undefined, algs)
}

getRandomValues(byteLength: number): Uint8Array {
const bytes = new Uint8Array(byteLength)
// TODO REVIEW POLYFILL
// @ts-ignore Polyfilled
return crypto.getRandomValues(bytes)
}

async digest(
bytes: Uint8Array,
algorithm: DigestAlgorithm,
): Promise<Uint8Array> {
// TODO REVIEW POLYFILL
// @ts-ignore Polyfilled
const buffer = await this.crypto.subtle.digest(
digestAlgorithmToSubtle(algorithm),
bytes,
)
return new Uint8Array(buffer)
}
}

export interface CryptoImplementation {
createKey(algs: )
// TODO OAUTH types
// @ts-ignore
function digestAlgorithmToSubtle({name}: DigestAlgorithm): AlgorithmIdentifier {
switch (name) {
case 'sha256':
case 'sha384':
case 'sha512':
return `SHA-${name.slice(-3)}`
default:
throw new Error(`Unknown hash algorithm ${name}`)
}
}
173 changes: 173 additions & 0 deletions src/oauth-client-temp/client/crypto-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import {b64uEncode} from '#/oauth-client-temp/b64'
import {
JwtHeader,
JwtPayload,
Key,
unsafeDecodeJwt,
} from '#/oauth-client-temp/jwk'
import {CryptoImplementation, DigestAlgorithm} from './crypto-implementation'

export class CryptoWrapper {
constructor(protected implementation: CryptoImplementation) {}

public async generateKey(algs: string[]): Promise<Key> {
return this.implementation.createKey(algs)
}

public async sha256(text: string): Promise<string> {
// TODO REVIEW POLYFILL
// @ts-ignore Polyfilled
const bytes = new TextEncoder().encode(text)
const digest = await this.implementation.digest(bytes, {name: 'sha256'})
return b64uEncode(digest)
}

public async generateNonce(length = 16): Promise<string> {
const bytes = await this.implementation.getRandomValues(length)
return b64uEncode(bytes)
}

public async validateIdTokenClaims(
token: string,
state: string,
nonce: string,
code?: string,
accessToken?: string,
): Promise<{
header: JwtHeader
payload: JwtPayload
}> {
// It's fine to use unsafeDecodeJwt here because the token was received from
// the server's token endpoint. The following checks are to ensure that the
// oauth flow was indeed initiated by the client.
const {header, payload} = unsafeDecodeJwt(token)
if (!payload.nonce || payload.nonce !== nonce) {
throw new TypeError('Nonce mismatch')
}
if (payload.c_hash) {
await this.validateHashClaim(payload.c_hash, code, header)
}
if (payload.s_hash) {
await this.validateHashClaim(payload.s_hash, state, header)
}
if (payload.at_hash) {
await this.validateHashClaim(payload.at_hash, accessToken, header)
}
return {header, payload}
}

private async validateHashClaim(
claim: unknown,
source: unknown,
header: {alg: string; crv?: string},
): Promise<void> {
if (typeof claim !== 'string' || !claim) {
throw new TypeError(`string "_hash" claim expected`)
}
if (typeof source !== 'string' || !source) {
throw new TypeError(`string value expected`)
}
const expected = await this.generateHashClaim(source, header)
if (expected !== claim) {
throw new TypeError(`"_hash" does not match`)
}
}

protected async generateHashClaim(
source: string,
header: {alg: string; crv?: string},
) {
const algo = getHashAlgo(header)
// TODO REVIEW POLYFILL
// @ts-ignore Polyfilled
const bytes = new TextEncoder().encode(source)
const digest = await this.implementation.digest(bytes, algo)
return b64uEncode(digest.slice(0, digest.length / 2))
}

public async generatePKCE(byteLength?: number) {
const verifier = await this.generateVerifier(byteLength)
return {
verifier,
challenge: await this.sha256(verifier),
method: 'S256',
}
}

// TODO OAUTH types
public async calculateJwkThumbprint(jwk: any) {
const components = extractJktComponents(jwk)
const data = JSON.stringify(components)
return this.sha256(data)
}

/**
* @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.1}
* @note It is RECOMMENDED that the output of a suitable random number generator
* be used to create a 32-octet sequence. The octet sequence is then
* base64url-encoded to produce a 43-octet URL safe string to use as the code
* verifier.
*/
protected async generateVerifier(byteLength = 32) {
if (byteLength < 32 || byteLength > 96) {
throw new TypeError('Invalid code_verifier length')
}
const bytes = await this.implementation.getRandomValues(byteLength)
return b64uEncode(bytes)
}
}

function getHashAlgo(header: {alg: string; crv?: string}): DigestAlgorithm {
switch (header.alg) {
case 'HS256':
case 'RS256':
case 'PS256':
case 'ES256':
case 'ES256K':
return {name: 'sha256'}
case 'HS384':
case 'RS384':
case 'PS384':
case 'ES384':
return {name: 'sha384'}
case 'HS512':
case 'RS512':
case 'PS512':
case 'ES512':
return {name: 'sha512'}
case 'EdDSA':
switch (header.crv) {
case 'Ed25519':
return {name: 'sha512'}
default:
throw new TypeError('unrecognized or invalid EdDSA curve provided')
}
default:
throw new TypeError('unrecognized or invalid JWS algorithm provided')
}
}

// TODO OAUTH types
function extractJktComponents(jwk: {[x: string]: any; kty: any}) {
// TODO OAUTH types
const get = (field: string) => {
const value = jwk[field]
if (typeof value !== 'string' || !value) {
throw new TypeError(`"${field}" Parameter missing or invalid`)
}
return value
}

switch (jwk.kty) {
case 'EC':
return {crv: get('crv'), kty: get('kty'), x: get('x'), y: get('y')}
case 'OKP':
return {crv: get('crv'), kty: get('kty'), x: get('x')}
case 'RSA':
return {e: get('e'), kty: get('kty'), n: get('n')}
case 'oct':
return {k: get('k'), kty: get('kty')}
default:
throw new TypeError('"kty" (Key Type) Parameter missing or unsupported')
}
}
Empty file.
Empty file.
78 changes: 78 additions & 0 deletions src/oauth-client-temp/client/key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
fromSubtleAlgorithm,
generateKeypair,
isSignatureKeyPair,
} from '#/oauth-client-temp/client/util'
import {Jwk, jwkSchema} from '#/oauth-client-temp/jwk'
import {JoseKey} from '#/oauth-client-temp/jwk-jose'

export class CryptoKey extends JoseKey {
// static async fromIndexedDB(kid: string, allowedAlgos: string[] = ['ES384']) {
// const cryptoKeyPair = await loadCryptoKeyPair(kid, allowedAlgos)
// return this.fromKeypair(kid, cryptoKeyPair)
// }

static async generate(
// TODO REVIEW POLYFILL
// @ts-ignore Polyfilled
kid: string = crypto.randomUUID(),
allowedAlgos: string[] = ['ES384'],
exportable = false,
) {
const cryptoKeyPair = await generateKeypair(allowedAlgos, exportable)
return this.fromKeypair(kid, cryptoKeyPair)
}

// TODO REVIEW POLYFILL
// @ts-ignore Polyfilled
static async fromKeypair(kid: string, cryptoKeyPair: CryptoKeyPair) {
if (!isSignatureKeyPair(cryptoKeyPair)) {
throw new TypeError('CryptoKeyPair must be compatible with sign/verify')
}

// https://datatracker.ietf.org/doc/html/rfc7517
// > The "use" and "key_ops" JWK members SHOULD NOT be used together; [...]
// > Applications should specify which of these members they use.

// TODO REVIEW POLYFILL
// @ts-ignore Polyfilled
const {key_ops: _, ...jwk} = await crypto.subtle.exportKey(
'jwk',
cryptoKeyPair.privateKey.extractable
? cryptoKeyPair.privateKey
: cryptoKeyPair.publicKey,
)

const use = jwk.use ?? 'sig'
const alg =
jwk.alg ?? fromSubtleAlgorithm(cryptoKeyPair.privateKey.algorithm)

if (use !== 'sig') {
throw new TypeError('Unsupported JWK use')
}

return new CryptoKey(
jwkSchema.parse({...jwk, use, kid, alg}),
cryptoKeyPair,
)
}

// TODO REVIEW POLYFILL
// @ts-ignore Polyfilled
constructor(jwk: Jwk, readonly cryptoKeyPair: CryptoKeyPair) {
super(jwk)
}

get isPrivate() {
return true
}

get privateJwk(): Jwk | undefined {
if (super.isPrivate) return this.jwk
throw new Error('Private Webcrypto Key not exportable')
}

protected async getKey() {
return this.cryptoKeyPair.privateKey
}
}
Empty file.
16 changes: 16 additions & 0 deletions src/oauth-client-temp/client/oauth-callback-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export class OAuthCallbackError extends Error {
static from(err: unknown, params: URLSearchParams, state?: string) {
if (err instanceof OAuthCallbackError) return err
const message = err instanceof Error ? err.message : undefined
return new OAuthCallbackError(params, message, state, err)
}

constructor(
public readonly params: URLSearchParams,
message = params.get('error_description') || 'OAuth callback error',
public readonly state?: string,
cause?: unknown,
) {
super(message, { cause })
}
}
Loading

0 comments on commit 1461c0a

Please sign in to comment.