diff --git a/docs/pages/reference/main/addToDate.md b/docs/pages/reference/main/addToDate.md new file mode 100644 index 0000000..53398ff --- /dev/null +++ b/docs/pages/reference/main/addToDate.md @@ -0,0 +1,27 @@ +--- +title: "addToDate()" +--- + +# `addToDate()` + +Creates a new `Date` by adding the provided time-span to the one provided. Supports negative time spans. + +## Definition + +```ts +//$ TimeSpan=/reference/main/TimeSpan +function createDate(date: Date, timeSpan: $$TimeSpan): Date; +``` + +### Parameters + +- `date` +- `timeSpan` + +## Example + +```ts +import { addToDate, TimeSpan } from "oslo"; + +const tomorrow = addToDate(new Date(), new TimeSpan(1, "d")); +``` diff --git a/docs/pages/reference/main/createDate.md b/docs/pages/reference/main/createDate.md deleted file mode 100644 index 2822660..0000000 --- a/docs/pages/reference/main/createDate.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: "createDate()" ---- - -# `createDate()` - -Creates a new `Date` by adding the provided time-span to the current time. Mostly for defining expiration times. Supports negative time span. - -## Definition - -```ts -//$ TimeSpan=/reference/main/TimeSpan -function createDate(timeSpan: $$TimeSpan): Date; -``` - -### Parameters - -- `timeSpan` - -## Example - -```ts -import { createDate, TimeSpan } from "oslo"; - -const tomorrow = createDate(new TimeSpan(1, "d")); -``` diff --git a/docs/pages/reference/main/index.md b/docs/pages/reference/main/index.md index 9c8b88c..c20a549 100644 --- a/docs/pages/reference/main/index.md +++ b/docs/pages/reference/main/index.md @@ -8,7 +8,7 @@ Provides basic utilities used by other modules. ## Functions -- [`createDate()`](/reference/main/createDate) +- [`addToDate()`](/reference/main/addToDate) - [`isWithinExpirationDate()`](/reference/main/isWithinExpirationDate) ## Classes diff --git a/src/crypto/bytes.test.ts b/src/crypto/bytes.test.ts new file mode 100644 index 0000000..9177fdd --- /dev/null +++ b/src/crypto/bytes.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from "vitest"; +import { constantTimeEqual } from "./bytes.js"; + +test("compareBytes()", () => { + const randomBytes = new Uint8Array(32); + crypto.getRandomValues(randomBytes); + expect(constantTimeEqual(randomBytes, randomBytes)).toBe(true); + const anotherRandomBytes = new Uint8Array(32); + crypto.getRandomValues(anotherRandomBytes); + expect(constantTimeEqual(randomBytes, anotherRandomBytes)).toBe(false); + expect(constantTimeEqual(new Uint8Array(0), new Uint8Array(1))).toBe(false); +}); diff --git a/src/crypto/bytes.ts b/src/crypto/bytes.ts new file mode 100644 index 0000000..1553775 --- /dev/null +++ b/src/crypto/bytes.ts @@ -0,0 +1,10 @@ +export function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) { + return false; + } + let c = 0; + for (let i = 0; i < a.length; i++) { + c |= a[i]! ^ b[i]!; + } + return c === 0; +} diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 4c5a25a..3841011 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -1,6 +1,4 @@ -export { ECDSA } from "./ecdsa.js"; -export { HMAC } from "./hmac.js"; -export { RSASSAPKCS1v1_5, RSASSAPSS } from "./rsa.js"; +export { ECDSA, HMAC, RSASSAPKCS1v1_5, RSASSAPSS } from "./signing-algorithm/index.js"; export { sha1, sha256, sha384, sha512 } from "./sha/index.js"; export { random, @@ -9,21 +7,6 @@ export { alphabet, generateRandomBoolean } from "./random.js"; +export { constantTimeEqual } from "./bytes.js"; -export type { ECDSACurve } from "./ecdsa.js"; - -export interface KeyPair { - publicKey: Uint8Array; - privateKey: Uint8Array; -} - -export function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean { - if (a.length !== b.length) { - return false; - } - let c = 0; - for (let i = 0; i < a.length; i++) { - c |= a[i]! ^ b[i]!; - } - return c === 0; -} +export type { SigningAlgorithm, KeyPair, ECDSACurve } from "./signing-algorithm/index.js"; diff --git a/src/crypto/ecdsa.test.ts b/src/crypto/signing-algorithm/ecdsa.test.ts similarity index 93% rename from src/crypto/ecdsa.test.ts rename to src/crypto/signing-algorithm/ecdsa.test.ts index 90f471f..3894575 100644 --- a/src/crypto/ecdsa.test.ts +++ b/src/crypto/signing-algorithm/ecdsa.test.ts @@ -1,8 +1,8 @@ import { describe, test, expect } from "vitest"; -import { ECDSA } from "./index.js"; +import { ECDSA } from "../index.js"; import type { ECDSACurve } from "./ecdsa.js"; -import type { SHAHash } from "./sha/index.js"; +import type { SHAHash } from "../sha/index.js"; interface TestCase { hash: SHAHash; diff --git a/src/crypto/ecdsa.ts b/src/crypto/signing-algorithm/ecdsa.ts similarity index 90% rename from src/crypto/ecdsa.ts rename to src/crypto/signing-algorithm/ecdsa.ts index 5d654a2..840b802 100644 --- a/src/crypto/ecdsa.ts +++ b/src/crypto/signing-algorithm/ecdsa.ts @@ -1,9 +1,9 @@ -import type { KeyPair } from "./index.js"; -import type { SHAHash } from "./sha/index.js"; +import type { SHAHash } from "../sha/index.js"; +import type { SigningAlgorithm, KeyPair } from "./shared.js"; export type ECDSACurve = "P-256" | "P-384" | "P-521"; -export class ECDSA { +export class ECDSA implements SigningAlgorithm { private hash: SHAHash; private curve: ECDSACurve; diff --git a/src/crypto/hmac.test.ts b/src/crypto/signing-algorithm/hmac.test.ts similarity index 91% rename from src/crypto/hmac.test.ts rename to src/crypto/signing-algorithm/hmac.test.ts index 15df261..cc4a8e1 100644 --- a/src/crypto/hmac.test.ts +++ b/src/crypto/signing-algorithm/hmac.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "vitest"; -import { HMAC } from "./index.js"; +import { HMAC } from "../index.js"; -import type { SHAHash } from "./sha/index.js"; +import type { SHAHash } from "../sha/index.js"; interface TestCase { hash: SHAHash; diff --git a/src/crypto/hmac.ts b/src/crypto/signing-algorithm/hmac.ts similarity index 87% rename from src/crypto/hmac.ts rename to src/crypto/signing-algorithm/hmac.ts index 87760ee..0dce771 100644 --- a/src/crypto/hmac.ts +++ b/src/crypto/signing-algorithm/hmac.ts @@ -1,6 +1,7 @@ -import type { SHAHash } from "./sha/index.js"; +import type { SigningAlgorithm } from "./shared.js"; +import type { SHAHash } from "../sha/index.js"; -export class HMAC { +export class HMAC implements SigningAlgorithm { private hash: SHAHash; constructor(hash: SHAHash) { this.hash = hash; diff --git a/src/crypto/signing-algorithm/index.ts b/src/crypto/signing-algorithm/index.ts new file mode 100644 index 0000000..4dfc260 --- /dev/null +++ b/src/crypto/signing-algorithm/index.ts @@ -0,0 +1,7 @@ +export { ECDSA } from "./ecdsa.js"; +export { HMAC } from "./hmac.js"; +export { RSASSAPKCS1v1_5, RSASSAPSS } from "./rsa.js"; + +export type { ECDSACurve } from "./ecdsa.js"; + +export type { SigningAlgorithm, KeyPair } from "./shared.js"; diff --git a/src/crypto/rsa.test.ts b/src/crypto/signing-algorithm/rsa.test.ts similarity index 97% rename from src/crypto/rsa.test.ts rename to src/crypto/signing-algorithm/rsa.test.ts index 2018278..cb58c3e 100644 --- a/src/crypto/rsa.test.ts +++ b/src/crypto/signing-algorithm/rsa.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "vitest"; import { RSASSAPKCS1v1_5, RSASSAPSS } from "./rsa.js"; -import type { SHAHash } from "./sha/index.js"; +import type { SHAHash } from "../sha/index.js"; interface TestCase { hash: SHAHash; diff --git a/src/crypto/rsa.ts b/src/crypto/signing-algorithm/rsa.ts similarity index 90% rename from src/crypto/rsa.ts rename to src/crypto/signing-algorithm/rsa.ts index 6003e36..b03bb12 100644 --- a/src/crypto/rsa.ts +++ b/src/crypto/signing-algorithm/rsa.ts @@ -1,7 +1,7 @@ -import type { KeyPair } from "./index.js"; -import type { SHAHash } from "./sha/index.js"; +import type { KeyPair, SigningAlgorithm } from "./shared.js"; +import type { SHAHash } from "../sha/index.js"; -export class RSASSAPKCS1v1_5 { +export class RSASSAPKCS1v1_5 implements SigningAlgorithm { private hash: SHAHash; constructor(hash: SHAHash) { this.hash = hash; @@ -132,12 +132,12 @@ export class RSASSAPSS { return signature; } - public async generateKeyPair(modulusLength?: 2048 | 4096): Promise { + public async generateKeyPair(options?: { modulusLength?: 2048 | 4096 }): Promise { const cryptoKeyPair = await crypto.subtle.generateKey( { name: "RSA-PSS", hash: this.hash, - modulusLength: modulusLength ?? 2048, + modulusLength: options?.modulusLength ?? 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]) }, true, diff --git a/src/crypto/signing-algorithm/shared.ts b/src/crypto/signing-algorithm/shared.ts new file mode 100644 index 0000000..d5bb062 --- /dev/null +++ b/src/crypto/signing-algorithm/shared.ts @@ -0,0 +1,9 @@ +export interface SigningAlgorithm { + sign(key: Uint8Array, data: Uint8Array): Promise; + verify(key: Uint8Array, signature: Uint8Array, data: Uint8Array): Promise; +} + +export interface KeyPair { + publicKey: Uint8Array; + privateKey: Uint8Array; +} diff --git a/src/index.ts b/src/index.ts index 903b792..ea26155 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,46 +1,3 @@ -export type TimeSpanUnit = "ms" | "s" | "m" | "h" | "d" | "w"; +export { TimeSpan, isWithinExpirationDate, addToDate } from "./time.js"; -export class TimeSpan { - constructor(value: number, unit: TimeSpanUnit) { - this.value = value; - this.unit = unit; - } - - public value: number; - public unit: TimeSpanUnit; - - public milliseconds(): number { - if (this.unit === "ms") { - return this.value; - } - if (this.unit === "s") { - return this.value * 1000; - } - if (this.unit === "m") { - return this.value * 1000 * 60; - } - if (this.unit === "h") { - return this.value * 1000 * 60 * 60; - } - if (this.unit === "d") { - return this.value * 1000 * 60 * 60 * 24; - } - return this.value * 1000 * 60 * 60 * 24 * 7; - } - - public seconds(): number { - return this.milliseconds() / 1000; - } - - public transform(x: number): TimeSpan { - return new TimeSpan(Math.round(this.milliseconds() * x), "ms"); - } -} - -export function isWithinExpirationDate(date: Date): boolean { - return Date.now() < date.getTime(); -} - -export function createDate(timeSpan: TimeSpan): Date { - return new Date(Date.now() + timeSpan.milliseconds()); -} +export type { TimeSpanUnit } from "./time.js"; diff --git a/src/jwt/index.test.ts b/src/jwt/index.test.ts index 1309a70..4f07496 100644 --- a/src/jwt/index.test.ts +++ b/src/jwt/index.test.ts @@ -1,9 +1,9 @@ import { describe, test, expect } from "vitest"; import { createJWT, parseJWT, validateJWT } from "./index.js"; -import { HMAC } from "../crypto/hmac.js"; -import { ECDSA } from "../crypto/ecdsa.js"; -import { RSASSAPKCS1v1_5, RSASSAPSS } from "../crypto/rsa.js"; +import { HMAC } from "../crypto/signing-algorithm/hmac.js"; +import { ECDSA } from "../crypto/signing-algorithm/ecdsa.js"; +import { RSASSAPKCS1v1_5, RSASSAPSS } from "../crypto/signing-algorithm/rsa.js"; import { TimeSpan } from "../index.js"; test.each(["ES256", "ES384", "ES512"] as const)( diff --git a/src/jwt/index.ts b/src/jwt/index.ts index c0df005..577271e 100644 --- a/src/jwt/index.ts +++ b/src/jwt/index.ts @@ -3,6 +3,7 @@ import { base64url } from "../encoding/index.js"; import { isWithinExpirationDate } from "../index.js"; import type { TimeSpan } from "../index.js"; +import type { SigningAlgorithm } from "../crypto/index.js"; export type JWTAlgorithm = | "HS256" @@ -235,7 +236,7 @@ export interface JWT extends JWTProperties { parts: [header: string, payload: string, signature: string]; } -function getAlgorithm(algorithm: JWTAlgorithm): ECDSA | HMAC | RSASSAPKCS1v1_5 | RSASSAPSS { +function getAlgorithm(algorithm: JWTAlgorithm): SigningAlgorithm { if (algorithm === "ES256" || algorithm === "ES384" || algorithm === "ES512") { return new ECDSA(ecdsaDictionary[algorithm].hash, ecdsaDictionary[algorithm].curve); } diff --git a/src/oauth2/index.ts b/src/oauth2/index.ts index 9f2e1f6..1ef9a3a 100644 --- a/src/oauth2/index.ts +++ b/src/oauth2/index.ts @@ -1,6 +1,6 @@ import { sha256 } from "../crypto/index.js"; import { base64, base64url } from "../encoding/index.js"; -import { createDate, TimeSpan } from "../index.js"; +import { TimeSpan, addToDate } from "../index.js"; export class OAuth2Client { public clientId: string; @@ -237,7 +237,7 @@ export class OAuth2TokenRevocationClient { const retryAfterNumber = parseInt(retryAfterHeader); if (!Number.isNaN(retryAfterNumber)) { throw new OAuth2TokenRevocationRetryError({ - retryAfter: createDate(new TimeSpan(retryAfterNumber, "s")) + retryAfter: addToDate(new Date(), new TimeSpan(retryAfterNumber, "s")) }); } const retryAfterDate = parseDateString(retryAfterHeader); @@ -303,15 +303,6 @@ export interface TokenResponseBody { scope?: string; } -export interface OAuth2Endpoints { - authorizeEndpoint: string; - tokenEndpoint: string; -} - -export interface OAuth2EndpointsWithTokenRevocation extends OAuth2Endpoints { - tokenRevocationEndpoint: string; -} - function parseDateString(dateString: string): Date | null { try { return new Date(dateString); diff --git a/src/otp/hotp.ts b/src/otp/hotp.ts index 69c818d..9a523c0 100644 --- a/src/otp/hotp.ts +++ b/src/otp/hotp.ts @@ -1,5 +1,5 @@ import { bigEndian } from "../binary/uint.js"; -import { HMAC } from "../crypto/hmac.js"; +import { HMAC } from "../crypto/signing-algorithm/hmac.js"; export async function generateHOTP( key: Uint8Array, diff --git a/src/time.ts b/src/time.ts new file mode 100644 index 0000000..be22358 --- /dev/null +++ b/src/time.ts @@ -0,0 +1,46 @@ +export type TimeSpanUnit = "ms" | "s" | "m" | "h" | "d" | "w"; + +export class TimeSpan { + constructor(value: number, unit: TimeSpanUnit) { + this.value = value; + this.unit = unit; + } + + public value: number; + public unit: TimeSpanUnit; + + public milliseconds(): number { + if (this.unit === "ms") { + return this.value; + } + if (this.unit === "s") { + return this.value * 1000; + } + if (this.unit === "m") { + return this.value * 1000 * 60; + } + if (this.unit === "h") { + return this.value * 1000 * 60 * 60; + } + if (this.unit === "d") { + return this.value * 1000 * 60 * 60 * 24; + } + return this.value * 1000 * 60 * 60 * 24 * 7; + } + + public seconds(): number { + return this.milliseconds() / 1000; + } + + public transform(x: number): TimeSpan { + return new TimeSpan(Math.round(this.milliseconds() * x), "ms"); + } +} + +export function isWithinExpirationDate(date: Date): boolean { + return Date.now() < date.getTime(); +} + +export function addToDate(date: Date, timeSpan: TimeSpan): Date { + return new Date(date.getTime() + timeSpan.milliseconds()); +}