| 1 | /** |
| 2 | * Defines a platform-independent service for cryptographic operations. |
| 3 | * |
| 4 | * Runtime packages provide concrete implementations backed by the host |
| 5 | * platform's cryptography APIs. This module defines the service interface and a |
| 6 | * constructor from random-byte and digest primitives. The service provides |
| 7 | * secure random bytes and numbers, UUIDv4 and UUIDv7 generation, shuffling, and |
| 8 | * SHA message digests. |
| 9 | * |
| 10 | * @since 4.0.0 |
| 11 | */ |
| 12 | import * as Context from "./Context.ts" |
| 13 | import * as Effect from "./Effect.ts" |
| 14 | import * as random from "./internal/random.ts" |
| 15 | import * as Ulid from "./internal/ulid.ts" |
| 16 | import * as Uuid from "./internal/uuid.ts" |
| 17 | import * as PlatformError from "./PlatformError.ts" |
| 18 | |
| 19 | const TypeId = "~effect/Crypto" |
| 20 | |
| 21 | /** |
| 22 | * Digest algorithms supported by the platform `Crypto` service. |
| 23 | * |
| 24 | * **Gotchas** |
| 25 | * |
| 26 | * SHA-1 is included for interoperability with existing protocols. Do not use |
| 27 | * SHA-1 for new security-sensitive designs. |
| 28 | * |
| 29 | * **Example** (Using a digest algorithm) |
| 30 | * |
| 31 | * ```ts import.meta.vitest |
| 32 | * import { Crypto } from "effect" |
| 33 | * |
| 34 | * const algorithm: Crypto.DigestAlgorithm = "SHA-256" |
| 35 | * ``` |
| 36 | * |
| 37 | * @category models |
| 38 | * @since 4.0.0 |
| 39 | */ |
| 40 | export type DigestAlgorithm = "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512" |
| 41 | |
| 42 | /** |
| 43 | * Platform-agnostic cryptographic operations. |
| 44 | * |
| 45 | * **Details** |
| 46 | * |
| 47 | * `Crypto` implementations must use cryptographically secure platform APIs. |
| 48 | * The random generator helpers are derived by the `make` constructor from |
| 49 | * the random methods on this service. |
| 50 | * |
| 51 | * **Example** (Using cryptographic operations) |
| 52 | * |
| 53 | * ```ts import.meta.vitest |
| 54 | * import { Crypto, Effect, Layer } from "effect" |
| 55 | * |
| 56 | * const TestCrypto = Layer.succeed( |
| 57 | * Crypto.Crypto, |
| 58 | * Crypto.make({ |
| 59 | * randomBytes: (size) => new Uint8Array(size), |
| 60 | * digest: (_algorithm, data) => Effect.succeed(data) |
| 61 | * }) |
| 62 | * ) |
| 63 | * |
| 64 | * const program = Effect.gen(function*() { |
| 65 | * const crypto = yield* Crypto.Crypto |
| 66 | * const bytes = yield* crypto.randomBytes(16) |
| 67 | * const uuidv4 = yield* crypto.randomUUIDv4 |
| 68 | * const hash = yield* crypto.digest("SHA-256", bytes) |
| 69 | * return [bytes.length, uuidv4.length, hash.length] |
| 70 | * }) |
| 71 | * |
| 72 | * await Effect.runPromise(Effect.provide(program, TestCrypto)) // => [16, 36, 16] |
| 73 | * ``` |
| 74 | * |
| 75 | * @category services |
| 76 | * @since 4.0.0 |
| 77 | */ |
| 78 | export interface Crypto { |
| 79 | readonly [TypeId]: typeof TypeId |
| 80 | |
| 81 | /** |
| 82 | * Generates a random integer in the range Number.MIN_SAFE_INTEGER to |
| 83 | * Number.MAX_SAFE_INTEGER (both inclusive). |
| 84 | */ |
| 85 | nextIntUnsafe(): number |
| 86 | |
| 87 | /** |
| 88 | * Generates a random number in the range 0 (inclusive) to 1 (exclusive). |
| 89 | */ |
| 90 | nextDoubleUnsafe(): number |
| 91 | |
| 92 | /** |
| 93 | * Generates cryptographically secure random bytes. |
| 94 | */ |
| 95 | randomBytes(size: number): Effect.Effect<Uint8Array, PlatformError.PlatformError> |
| 96 | |
| 97 | /** |
| 98 | * Computes a cryptographic digest for the supplied data. |
| 99 | */ |
| 100 | digest( |
| 101 | algorithm: DigestAlgorithm, |
| 102 | data: Uint8Array |
| 103 | ): Effect.Effect<Uint8Array, PlatformError.PlatformError> |
| 104 | |
| 105 | /** |
| 106 | * Generates a cryptographically secure random number between 0 (inclusive) |
| 107 | * and 1 (exclusive). |
| 108 | */ |
| 109 | readonly random: Effect.Effect<number> |
| 110 | |
| 111 | /** |
| 112 | * Generates a cryptographically secure random boolean. |
| 113 | */ |
| 114 | readonly randomBoolean: Effect.Effect<boolean> |
| 115 | |
| 116 | /** |
| 117 | * Generates a cryptographically secure random integer between |
| 118 | * `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER` (both inclusive). |
| 119 | */ |
| 120 | readonly randomInt: Effect.Effect<number> |
| 121 | |
| 122 | /** |
| 123 | * Generates a cryptographically secure random number between `min` |
| 124 | * (inclusive) and `max` (exclusive). |
| 125 | */ |
| 126 | randomBetween(min: number, max: number): Effect.Effect<number> |
| 127 | |
| 128 | /** |
| 129 | * Generates a cryptographically secure random integer between `min` and `max`. |
| 130 | * |
| 131 | * **Details** |
| 132 | * |
| 133 | * The lower bound is rounded up with `Math.ceil` and the upper bound is |
| 134 | * rounded down with `Math.floor`. By default the range is inclusive; set |
| 135 | * `options.halfOpen: true` to exclude the upper bound. |
| 136 | */ |
| 137 | randomIntBetween(min: number, max: number, options?: { |
| 138 | readonly halfOpen?: boolean | undefined |
| 139 | }): Effect.Effect<number> |
| 140 | |
| 141 | /** |
| 142 | * Uses the cryptographically secure random generator to shuffle the supplied |
| 143 | * iterable. |
| 144 | */ |
| 145 | randomShuffle<A>(elements: Iterable<A>): Effect.Effect<Array<A>> |
| 146 | |
| 147 | /** |
| 148 | * Generates a cryptographically secure UUIDv4 string. |
| 149 | */ |
| 150 | readonly randomUUIDv4: Effect.Effect<string, PlatformError.PlatformError> |
| 151 | |
| 152 | /** |
| 153 | * Generates a cryptographically secure UUIDv7 string. |
| 154 | */ |
| 155 | readonly randomUUIDv7: Effect.Effect<string, PlatformError.PlatformError> |
| 156 | |
| 157 | /** |
| 158 | * Generates a cryptographically secure ULID string. |
| 159 | * |
| 160 | * **Details** |
| 161 | * |
| 162 | * ULIDs contain 26 uppercase Crockford base32 characters. The first 10 encode |
| 163 | * the `Clock` timestamp in milliseconds; the remaining 16 encode 80 random |
| 164 | * bits. ULIDs sort by timestamp, with no ordering guarantee within the same |
| 165 | * millisecond. |
| 166 | * |
| 167 | * Timestamp normalization matches UUIDv7: fractions are truncated, values are |
| 168 | * clamped to the 48-bit range, and `NaN` encodes as zero. |
| 169 | */ |
| 170 | readonly randomULID: Effect.Effect<string, PlatformError.PlatformError> |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * Service tag for platform cryptography. |
| 175 | * |
| 176 | * **When to use** |
| 177 | * |
| 178 | * Use when you need to provide or retrieve the full platform cryptography |
| 179 | * service from an effect's context. |
| 180 | * |
| 181 | * **Details** |
| 182 | * |
| 183 | * Providing this service supplies platform-agnostic cryptographic operations |
| 184 | * such as hashing, UUID generation, and secure random values. |
| 185 | * |
| 186 | * @see {@link make} for constructing a Crypto service from primitive operations |
| 187 | * |
| 188 | * @category services |
| 189 | * @since 4.0.0 |
| 190 | */ |
| 191 | export const Crypto: Context.Service<Crypto, Crypto> = Context.Service("effect/Crypto") |
| 192 | |
| 193 | /** |
| 194 | * Creates a `Crypto` service from the primitive implementation, deriving the |
| 195 | * random generator helpers and UUID generation from those primitives. |
| 196 | * |
| 197 | * **When to use** |
| 198 | * |
| 199 | * Use to build a Crypto service for a platform integration, test layer, or |
| 200 | * custom runtime from primitive random-byte and digest operations. |
| 201 | * |
| 202 | * **Details** |
| 203 | * |
| 204 | * The constructor derives random numbers, booleans, integer ranges, shuffling, |
| 205 | * and UUID generation from `impl.randomBytes`. Digest operations delegate to |
| 206 | * `impl.digest`. |
| 207 | * |
| 208 | * **Gotchas** |
| 209 | * |
| 210 | * `impl.randomBytes` must return cryptographically secure bytes of the |
| 211 | * requested length. UUID formatting mutates the byte array returned for UUID |
| 212 | * generation, so the implementation should return a fresh array for each call. |
| 213 | * |
| 214 | * **Example** (Creating a Crypto service) |
| 215 | * |
| 216 | * ```ts import.meta.vitest |
| 217 | * import { Crypto, Effect } from "effect" |
| 218 | * |
| 219 | * const testCrypto = Crypto.make({ |
| 220 | * randomBytes: (size) => new Uint8Array(size), |
| 221 | * digest: (_algorithm, data) => Effect.succeed(data) |
| 222 | * }) |
| 223 | * |
| 224 | * await Effect.runPromise(testCrypto.randomBytes(4)) // => new Uint8Array([0, 0, 0, 0]) |
| 225 | * ``` |
| 226 | * |
| 227 | * @category constructors |
| 228 | * @since 4.0.0 |
| 229 | */ |
| 230 | export const make = ( |
| 231 | impl: { |
| 232 | readonly randomBytes: (size: number) => Uint8Array |
| 233 | readonly digest: ( |
| 234 | algorithm: DigestAlgorithm, |
| 235 | data: Uint8Array |
| 236 | ) => Effect.Effect<Uint8Array, PlatformError.PlatformError> |
| 237 | } |
| 238 | ): Crypto => { |
| 239 | const randomBytesUnsafe = impl.randomBytes |
| 240 | |
| 241 | const randomBytes: Crypto["randomBytes"] = (size) => Effect.map(validateSize("randomBytes", size), randomBytesUnsafe) |
| 242 | |
| 243 | const readUint53 = (bytes: Uint8Array): number => |
| 244 | ((bytes[0] & 0x1f) * 2 ** 48) + (bytes[1] * 2 ** 40) + (bytes[2] * 2 ** 32) + |
| 245 | (bytes[3] * 2 ** 24) + (bytes[4] * 2 ** 16) + (bytes[5] * 2 ** 8) + bytes[6] |
| 246 | |
| 247 | const nextDoubleUnsafe = (): number => readUint53(randomBytesUnsafe(7)) / 2 ** 53 |
| 248 | |
| 249 | const nextIntUnsafe = (): number => { |
| 250 | while (true) { |
| 251 | const bytes = randomBytesUnsafe(7) |
| 252 | const value = readUint53(bytes) |
| 253 | if ((bytes[0] & 0x20) === 0) { |
| 254 | return value + Number.MIN_SAFE_INTEGER |
| 255 | } |
| 256 | if (value < Number.MAX_SAFE_INTEGER) { |
| 257 | return value + 1 |
| 258 | } |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | return Crypto.of({ |
| 263 | [TypeId]: TypeId, |
| 264 | randomBytes, |
| 265 | nextDoubleUnsafe, |
| 266 | nextIntUnsafe, |
| 267 | digest: impl.digest, |
| 268 | random: Effect.sync(() => nextDoubleUnsafe()), |
| 269 | randomBoolean: Effect.sync(() => nextDoubleUnsafe() > 0.5), |
| 270 | randomInt: Effect.sync(() => nextIntUnsafe()), |
| 271 | randomBetween: (min, max) => Effect.sync(() => random.nextBetween(min, max, nextDoubleUnsafe())), |
| 272 | randomIntBetween(min, max, options) { |
| 273 | const extra = options?.halfOpen === true ? 0 : 1 |
| 274 | return Effect.sync(() => { |
| 275 | const minInt = Math.ceil(min) |
| 276 | const maxInt = Math.floor(max) |
| 277 | return Math.floor(nextDoubleUnsafe() * (maxInt - minInt + extra)) + minInt |
| 278 | }) |
| 279 | }, |
| 280 | randomShuffle: (elements) => |
| 281 | Effect.sync(() => { |
| 282 | const buffer = Array.from(elements) |
| 283 | for (let i = buffer.length - 1; i >= 1; i = i - 1) { |
| 284 | const index = Math.min(i, Math.floor(nextDoubleUnsafe() * (i + 1))) |
| 285 | const value = buffer[i]! |
| 286 | buffer[i] = buffer[index]! |
| 287 | buffer[index] = value |
| 288 | } |
| 289 | return buffer |
| 290 | }), |
| 291 | randomUUIDv4: Effect.sync(() => Uuid.v4String(randomBytesUnsafe(16))), |
| 292 | randomUUIDv7: Effect.clockWith((clock) => |
| 293 | Effect.succeed(Uuid.v7String(clock.currentTimeMillisUnsafe(), randomBytesUnsafe(16))) |
| 294 | ), |
| 295 | randomULID: Effect.clockWith((clock) => |
| 296 | Effect.succeed(Ulid.ulidString(clock.currentTimeMillisUnsafe(), randomBytesUnsafe(10))) |
| 297 | ) |
| 298 | }) |
| 299 | } |
| 300 | |
| 301 | const validateSize = (method: string, size: number): Effect.Effect<number, PlatformError.PlatformError> => |
| 302 | Number.isSafeInteger(size) && size >= 0 |
| 303 | ? Effect.succeed(size) |
| 304 | : Effect.fail(PlatformError.badArgument({ |
| 305 | module: "Crypto", |
| 306 | method, |
| 307 | description: "size must be a non-negative safe integer" |
| 308 | })) |