Skip to content

Commit

Permalink
Fix SHA256 and SHA512 hash algorithms format
Browse files Browse the repository at this point in the history
- Update JWK types
  • Loading branch information
modscleo4 committed Mar 17, 2024
1 parent e64b433 commit 4d659c1
Show file tree
Hide file tree
Showing 14 changed files with 276 additions and 209 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "midori",
"version": "1.0.0",
"version": "0.1.0",
"description": "Midori is a Node.js web API framework with minimal dependencies and based on PSR ideas.",
"type": "module",
"keywords": [
Expand Down
6 changes: 4 additions & 2 deletions src/hash/SHA256.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import Hash from "./Hash.js";
/**
* SHA256 Hash implementation.
*
* Format: `$5$<digest>$<iterations>$<cost>$<salt>$<hash>`
* Format: `$5$<salt>$<hash>`
*
* Note: This should not be used for new applications.
*/

export default class SHA256 extends Hash {
Expand All @@ -35,7 +37,7 @@ export default class SHA256 extends Hash {
}

override verify(hash: string, data: string | Buffer): boolean {
const [, version, salt] = hash.split('$', 7);
const [, version, salt] = hash.split('$', 4);
if (version !== SHA256.version) {
return false;
}
Expand Down
6 changes: 4 additions & 2 deletions src/hash/SHA512.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import Hash from "./Hash.js";
/**
* SHA512 Hash implementation.
*
* Format: `$6$<digest>$<iterations>$<cost>$<salt>$<hash>`
* Format: `$6$<salt>$<hash>`
*
* Note: This should not be used for new applications.
*/

export default class SHA512 extends Hash {
Expand All @@ -35,7 +37,7 @@ export default class SHA512 extends Hash {
}

override verify(hash: string, data: string | Buffer): boolean {
const [, version, salt] = hash.split('$', 7);
const [, version, salt] = hash.split('$', 4);
if (version !== SHA512.version) {
return false;
}
Expand Down
8 changes: 4 additions & 4 deletions src/http/Request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default class Request<T = unknown> extends IncomingMessage {
init(app: Application) {
this.#config = app.config.get(RequestConfigProvider);

const url = new URL(this.url ?? '', `http://${this.headers.host}`);
const url = new URL(this.url ?? '', `${this.headers['x-forwarded-proto'] ?? 'http'}://${this.headers.host}`);

this.#query = url.searchParams;
this.#path = url.pathname;
Expand Down Expand Up @@ -86,8 +86,8 @@ export default class Request<T = unknown> extends IncomingMessage {
return;
}

this.headers.cookie.split(';').forEach((cookie) => {
const [key, value] = cookie.split('=');
this.headers.cookie.split(';').forEach(cookie => {
const [key, value] = cookie.split('=', 2);
this.#cookies.set(key.trim(), value.trim());
});
}
Expand All @@ -99,7 +99,7 @@ export default class Request<T = unknown> extends IncomingMessage {
}

const mimeTypes = this.headers.accept.split(',').map((algorithm) => {
const [mimeType, q] = algorithm.split(';');
const [mimeType, q] = algorithm.split(';', 2);
return {
mimeType: mimeType.trim(),
q: q ? parseFloat(q.replace('q=', '')) : 1,
Expand Down
78 changes: 47 additions & 31 deletions src/jwt/JWT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { readFileSync } from "node:fs";
import { createPrivateKey, createPublicKey } from "node:crypto";

import { Payload as JWKPayload, PayloadEC, PayloadRSA, PayloadSymmetric } from "../util/jwk.js";
import { SymmetricKey, JWK, RSAPrivateKey, ECPrivateKey, BaseKey } from "../util/jwk.js";
import { Payload as JWTPayload } from "../util/jwt.js";
import { signJWT, verifyJWS, JWSAlgorithm } from "../util/jws.js";
import { cekLength, decryptJWE, encryptJWT, JWEAlgorithm, JWEEncryption } from "../util/jwe.js";
Expand All @@ -30,12 +30,12 @@ import { JWTConfig } from "../providers/JWTConfigProvider.js";
export default class JWT {
#sign?: {
alg: JWSAlgorithm;
key: JWKPayload;
key: JWK;
};
#encrypt?: {
alg: JWEAlgorithm;
enc: JWEEncryption;
key: JWKPayload;
key: JWK;
};

constructor(config: JWTConfig) {
Expand All @@ -56,27 +56,35 @@ export default class JWT {
throw new Error('Secret is required for this algorithm');
}

const baseKey: JWKPayload = {
const key: JWK | null = ([JWSAlgorithm.HS256, JWSAlgorithm.HS384, JWSAlgorithm.HS512].includes(alg) ? <SymmetricKey> {
kty: 'oct',
use: 'sig',
key_ops: ['sign', 'verify'],
alg,
kid: generateUUID(),
};

const key: JWKPayload = ([JWSAlgorithm.HS256, JWSAlgorithm.HS384, JWSAlgorithm.HS512].includes(alg) ? <PayloadSymmetric> {
...baseKey,

k: Buffer.from(secret!, 'hex').toString('base64url'),
} : [JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512, JWSAlgorithm.PS256, JWSAlgorithm.PS384, JWSAlgorithm.PS512].includes(alg) ? <PayloadRSA> {
...baseKey,
} : [JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512, JWSAlgorithm.PS256, JWSAlgorithm.PS384, JWSAlgorithm.PS512].includes(alg) ? <RSAPrivateKey> {
kty: 'RSA',
use: 'sig',
key_ops: ['sign', 'verify'],
alg,
kid: generateUUID(),

...createPrivateKey(privateKey!).export({ format: 'jwk' })
} : [JWSAlgorithm.ES256, JWSAlgorithm.ES384, JWSAlgorithm.ES512].includes(alg) ? <PayloadEC> {
...baseKey,
} : [JWSAlgorithm.ES256, JWSAlgorithm.ES384, JWSAlgorithm.ES512].includes(alg) ? <ECPrivateKey> {
kty: 'EC',
use: 'sig',
key_ops: ['sign', 'verify'],
alg,
kid: generateUUID(),

...createPrivateKey(privateKey!).export({ format: 'jwk' })
} : baseKey);
} : null);

if (!key) {
throw new Error('Invalid algorithm');
}

this.#sign = {
alg,
Expand Down Expand Up @@ -115,27 +123,35 @@ export default class JWT {
throw new Error(`Secret must be ${cekLength(enc) / 8} bytes long for this algorithm`);
}

const baseKey: JWKPayload = {
const key: JWK | null = ([JWEAlgorithm.A128KW, JWEAlgorithm.A192KW, JWEAlgorithm.A256KW, JWEAlgorithm.dir].includes(alg) ? <SymmetricKey> {
kty: 'oct',
use: 'sig',
key_ops: ['sign', 'verify'],
use: 'enc',
key_ops: ['encrypt', 'decrypt'],
alg,
kid: generateUUID(),
};

const key: JWKPayload = ([JWEAlgorithm.A128KW, JWEAlgorithm.A192KW, JWEAlgorithm.A256KW, JWEAlgorithm.dir].includes(alg) ? <PayloadSymmetric> {
...baseKey,

k: Buffer.from(secret!, 'hex').toString('base64url'),
} : [JWEAlgorithm.RSA1_5, JWEAlgorithm['RSA-OAEP'], JWEAlgorithm['RSA-OAEP-256']].includes(alg) ? <PayloadRSA> {
...baseKey,
} : [JWEAlgorithm.RSA1_5, JWEAlgorithm['RSA-OAEP'], JWEAlgorithm['RSA-OAEP-256']].includes(alg) ? <RSAPrivateKey> {
kty: 'RSA',
use: 'enc',
key_ops: ['encrypt', 'decrypt'],
alg,
kid: generateUUID(),

...createPrivateKey(privateKey!).export({ format: 'jwk' })
} : [JWEAlgorithm['ECDH-ES'], JWEAlgorithm['ECDH-ES+A128KW'], JWEAlgorithm['ECDH-ES+A192KW'], JWEAlgorithm['ECDH-ES+A256KW']].includes(alg) ? <PayloadEC> {
...baseKey,
} : [JWEAlgorithm['ECDH-ES'], JWEAlgorithm['ECDH-ES+A128KW'], JWEAlgorithm['ECDH-ES+A192KW'], JWEAlgorithm['ECDH-ES+A256KW']].includes(alg) ? <ECPrivateKey> {
kty: 'EC',
use: 'enc',
key_ops: ['encrypt', 'decrypt'],
alg,
kid: generateUUID(),

...createPrivateKey(privateKey!).export({ format: 'jwk' })
} : baseKey);
} : null);

if (!key) {
throw new Error('Invalid algorithm');
}

this.#encrypt = {
alg,
Expand Down Expand Up @@ -185,19 +201,19 @@ export default class JWT {
}
}

getPublicKeys(): JWKPayload[] {
const keys: JWKPayload[] = [];
getPublicKeys(): BaseKey[] {
const keys: BaseKey[] = [];

if (this.#sign?.key) {
if (this.#sign?.key && this.#sign.key.kty !== 'oct') { // DO NOT export symmetric keys (oct)
const key = createPublicKey(createPrivateKey({ key: this.#sign.key, format: 'jwk' }));

keys.push({ ...key.export({ format: 'jwk' }), use: 'sig', kid: this.#sign.key.kid });
keys.push({ ...key.export({ format: 'jwk' }), kty: this.#sign.key.kty, use: 'sig', kid: this.#sign.key.kid });
}

if (this.#encrypt?.key) {
if (this.#encrypt?.key && this.#encrypt.key.kty !== 'oct') { // DO NOT export symmetric keys (oct)
const key = createPublicKey(createPrivateKey({ key: this.#encrypt.key, format: 'jwk' }));

keys.push({ ...key.export({ format: 'jwk' }), use: 'enc', kid: this.#encrypt.key.kid });
keys.push({ ...key.export({ format: 'jwk' }), kty: this.#encrypt.key.kty, use: 'enc', kid: this.#encrypt.key.kid });
}

return keys;
Expand Down
2 changes: 2 additions & 0 deletions src/jwt/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { default as JWT } from './JWT.js';
export { RSAPublicKey, ECPublicKey, SymmetricKey, JWK, BaseKey } from '../util/jwk.js';
export { Payload as JWTPayload } from '../util/jwt.js';
16 changes: 8 additions & 8 deletions src/middlewares/ContentLengthMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ export default class ContentLengthMiddleware extends Middleware {
if (!res.isStream) { // Calculate Content-Length for non-stream responses
res.headers.set('Content-Length', res.length);
} else if (req.method === 'HEAD') { // Calculate Content-Length for HEAD requests
await new Promise<void>((resolve, reject) => {
let bodyLength = 0;
const body = res.body;
const bodyLength = await new Promise<number>((resolve, reject) => {
let len = 0;

body.on('data', chunk => bodyLength += chunk.length);
body.on('close', () => {
res.headers.set('Content-Length', bodyLength);
resolve();
res.body.on('data', chunk => len += chunk.length);
res.body.on('close', () => {
resolve(len);
});
body.on('error', reject);
res.body.on('error', reject);
});

res.headers.set('Content-Length', bodyLength);
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/middlewares/PublicPathMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
*/

import { existsSync, statSync } from "node:fs";
import { EStatusCode } from "../http/EStatusCode.js";
import { join, normalize } from "node:path";

import { EStatusCode } from "../http/EStatusCode.js";
import Middleware from "../http/Middleware.js";
import Request from "../http/Request.js";
import Response from "../http/Response.js";
Expand Down Expand Up @@ -48,6 +49,11 @@ export class PublicPathMiddleware extends Middleware {
return await next(req);
}

// Don't allow path traversal
if (req.path.includes('..')) {
return Response.redirect(normalize(req.path));
}

const indexFiles = this.options?.indexFiles ?? ['index.html'];

// If the request ends with a slash, try to find an index file
Expand All @@ -74,7 +80,7 @@ export class PublicPathMiddleware extends Middleware {
/** @internal */
async tryFile(path: string): Promise<Response | false> {
// Try to find a matching file in the public directory
const filename = this.options?.path + (!path.startsWith('/') ? '/' : '') + (path.endsWith('/') ? path.substring(0, path.length - 1) : path);
const filename = join(this.options?.path!, normalize(path));

// If the file exists, return it
if (existsSync(filename) && statSync(filename).isFile()) {
Expand Down
10 changes: 5 additions & 5 deletions src/util/crypt/ecdh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
import { createECDH, createPrivateKey, createPublicKey, generateKeyPairSync } from "node:crypto";

import { privateKeyToRaw, publicKeyToRaw } from "../asn1.js";
import { PayloadEC } from "../jwk.js";
import { ECPublicKey, ECPrivateKey } from "../jwk.js";

/**
* Elliptic Curve Diffie-Hellman, as used by JWE.
*/
export default class ECDH {
static generateEphemeralKey(crv: string): PayloadEC {
static generateEphemeralKey(crv: string): ECPrivateKey {
const { publicKey, privateKey } = generateKeyPairSync('ec', {
publicKeyEncoding: {
format: 'pem',
Expand All @@ -36,17 +36,17 @@ export default class ECDH {
namedCurve: crv,
});

return createPrivateKey(privateKey).export({ format: 'jwk' }) as PayloadEC;
return createPrivateKey(privateKey).export({ format: 'jwk' }) as ECPrivateKey;
}

/**
* Derives a shared secret from a private and a public key. Uses the crypto.subtle API.
*/
static deriveSharedSecret(privateKey: PayloadEC, publicKey: PayloadEC): Buffer {
static deriveSharedSecret(privateKey: ECPrivateKey, publicKey: ECPublicKey): Buffer {
const privKey = createPrivateKey({ format: 'jwk', key: privateKey });
const pubKey = createPublicKey({ format: 'jwk', key: publicKey });

const ecdh = createECDH(ECDH.getCurveName(privateKey.crv!));
const ecdh = createECDH(ECDH.getCurveName(privateKey.crv));
ecdh.setPrivateKey(privateKeyToRaw(privKey));

return ecdh.computeSecret(publicKeyToRaw(pubKey));
Expand Down
Loading

0 comments on commit 4d659c1

Please sign in to comment.