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 */
12import * as Context from "./Context.ts"
13import * as Effect from "./Effect.ts"
14import * as random from "./internal/random.ts"
15import * as Ulid from "./internal/ulid.ts"
16import * as Uuid from "./internal/uuid.ts"
17import * as PlatformError from "./PlatformError.ts"
18
19const 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 */
40export 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 */
78export 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 */
191export 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 */
230export 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
301const 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 }))