1/**
2 * Cryptographic service for encrypted event-log replication.
3 *
4 * `EventLogEncryption` turns local journal entries into encrypted remote
5 * payloads and decrypts encrypted changes received from a server. It also
6 * hashes byte data and creates event-log identities, so remote replication can
7 * use storage or transport that should not see plaintext event data.
8 *
9 * @stability unstable
10 * @since 4.0.0
11 */
12import * as Context from "../Context.ts"
13import * as Effect from "../Effect.ts"
14import * as Layer from "../Layer.ts"
15import * as Redacted from "../Redacted.ts"
16import * as Schema from "../Schema.ts"
17import * as Transferable from "../workers/Transferable.ts"
18import { Entry, EntryId, RemoteEntry } from "./EventJournal.ts"
19import type { Identity } from "./EventLog.ts"
20import { makeGetIdentityRootSecretMaterial } from "./internal/identityRootSecretDerivation.ts"
21
22/**
23 * Schema for an encrypted journal entry paired with its initialization vector
24 * and the id of the original entry.
25 *
26 * @stability unstable
27 * @category models
28 * @since 4.0.0
29 */
30export const EncryptedEntry = Schema.Struct({
31 entryId: EntryId,
32 iv: Transferable.Uint8Array,
33 encryptedEntry: Transferable.Uint8Array
34})
35
36/**
37 * Type of an encrypted remote entry, including its remote sequence number,
38 * initialization vector, entry id, and encrypted entry bytes.
39 *
40 * @stability unstable
41 * @category models
42 * @since 4.0.0
43 */
44export interface EncryptedRemoteEntry extends Schema.Schema.Type<typeof EncryptedRemoteEntry> {}
45
46/**
47 * Schema for encrypted entries exchanged with a remote event-log server.
48 *
49 * @stability unstable
50 * @category models
51 * @since 4.0.0
52 */
53export const EncryptedRemoteEntry = Schema.Struct({
54 sequence: Schema.Natural,
55 iv: Transferable.Uint8Array,
56 entryId: EntryId,
57 encryptedEntry: Transferable.Uint8Array
58})
59
60const toArrayBuffer = (data: Uint8Array): ArrayBuffer => {
61 const buffer = new ArrayBuffer(data.byteLength)
62 new Uint8Array(buffer).set(data)
63 return buffer
64}
65
66const toBufferSource = (data: Uint8Array): ArrayBufferView<ArrayBuffer> => new Uint8Array(toArrayBuffer(data))
67
68/**
69 * Service that provides identity generation, entry
70 * encryption and decryption, and SHA-256 hashing for event-log replication.
71 *
72 * **When to use**
73 *
74 * Use to provide cryptographic operations required by encrypted event-log
75 * replication.
76 *
77 * @stability unstable
78 * @category services
79 * @since 4.0.0
80 */
81export class EventLogEncryption extends Context.Service<EventLogEncryption, {
82 readonly encrypt: (
83 identity: Identity["Service"],
84 entries: ReadonlyArray<Entry>
85 ) => Effect.Effect<
86 ReadonlyArray<{
87 readonly iv: Uint8Array<ArrayBuffer>
88 readonly encryptedEntry: Uint8Array<ArrayBuffer>
89 }>
90 >
91 readonly decrypt: (
92 identity: Identity["Service"],
93 entries: ReadonlyArray<EncryptedRemoteEntry>
94 ) => Effect.Effect<Array<RemoteEntry>>
95 readonly sha256String: (data: Uint8Array) => Effect.Effect<string>
96 readonly sha256: (data: Uint8Array) => Effect.Effect<Uint8Array>
97 readonly generateIdentity: Effect.Effect<Identity["Service"]>
98}>()("effect/eventlog/EventLogEncryption") {}
99
100/**
101 * Creates an `EventLogEncryption` service backed by the Web Crypto `SubtleCrypto`
102 * APIs from the supplied `Crypto` implementation.
103 *
104 * @stability unstable
105 * @category encryption
106 * @since 4.0.0
107 */
108export const makeEncryptionSubtle = (crypto: Crypto): Effect.Effect<EventLogEncryption["Service"]> =>
109 Effect.sync(() => {
110 const getIdentityRootSecretMaterial = makeGetIdentityRootSecretMaterial(crypto)
111
112 return EventLogEncryption.of({
113 encrypt: Effect.fnUntraced(function*(identity, entries) {
114 const data = yield* Effect.orDie(Entry.encodeArray(entries))
115 const key = (yield* getIdentityRootSecretMaterial(identity)).encryptionKey
116 return yield* Effect.promise(() =>
117 Promise.all(
118 data.map((entry) => {
119 const iv = crypto.getRandomValues(new Uint8Array(12))
120 return crypto.subtle.encrypt(
121 { name: "AES-GCM", iv: toBufferSource(iv), tagLength: 128 },
122 key,
123 toBufferSource(entry)
124 ).then((encryptedEntry) => ({ iv, encryptedEntry: new Uint8Array(encryptedEntry) }))
125 })
126 )
127 )
128 }),
129 decrypt: Effect.fnUntraced(function*(identity, entries) {
130 const key = (yield* getIdentityRootSecretMaterial(identity)).encryptionKey
131 const decryptedData = (yield* Effect.promise(() =>
132 Promise.all(entries.map((data) =>
133 crypto.subtle.decrypt(
134 { name: "AES-GCM", iv: toBufferSource(data.iv), tagLength: 128 },
135 key,
136 toBufferSource(data.encryptedEntry)
137 )
138 ))
139 )).map((buffer) => new Uint8Array(buffer))
140 const decoded = yield* Effect.orDie(Entry.decodeArray(decryptedData))
141 return decoded.map((entry, index) => new RemoteEntry({ remoteSequence: entries[index].sequence, entry }))
142 }),
143 sha256: (data) =>
144 Effect.promise(() => crypto.subtle.digest("SHA-256", toArrayBuffer(data))).pipe(
145 Effect.map((hash) => new Uint8Array(hash))
146 ),
147 sha256String: (data) =>
148 Effect.map(
149 Effect.promise(() => crypto.subtle.digest("SHA-256", toArrayBuffer(data))),
150 (hash) => {
151 const hashArray = Array.from(new Uint8Array(hash))
152 const hashHex = hashArray
153 .map((bytes) => bytes.toString(16).padStart(2, "0"))
154 .join("")
155 return hashHex
156 }
157 ),
158 generateIdentity: Effect.sync(() => ({
159 publicKey: crypto.randomUUID(),
160 privateKey: Redacted.make(crypto.getRandomValues(new Uint8Array(32)))
161 }))
162 })
163 })
164
165/**
166 * Provides `EventLogEncryption` using `globalThis.crypto`.
167 *
168 * @stability unstable
169 * @category layers
170 * @since 4.0.0
171 */
172export const layerSubtle: Layer.Layer<EventLogEncryption> = Layer.effect(
173 EventLogEncryption,
174 makeEncryptionSubtle(globalThis.crypto)
175)