-
-
Notifications
You must be signed in to change notification settings - Fork 558
Expand file tree
/
Copy pathEventLogEncryption.ts
More file actions
145 lines (138 loc) · 4.52 KB
/
EventLogEncryption.ts
File metadata and controls
145 lines (138 loc) · 4.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/**
* @since 1.0.0
*/
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Redacted from "effect/Redacted"
import * as Schema from "effect/Schema"
import { Entry, EntryId, RemoteEntry } from "./EventJournal.js"
import type { Identity } from "./EventLog.js"
/**
* @since 1.0.0
* @category models
*/
export const EncryptedEntry = Schema.Struct({
entryId: EntryId,
encryptedEntry: Schema.Uint8ArrayFromSelf
})
/**
* @since 1.0.0
* @category models
*/
export interface EncryptedRemoteEntry extends Schema.Schema.Type<typeof EncryptedRemoteEntry> {}
/**
* @since 1.0.0
* @category models
*/
export const EncryptedRemoteEntry = Schema.Struct({
sequence: Schema.Number,
iv: Schema.Uint8ArrayFromSelf,
entryId: EntryId,
encryptedEntry: Schema.Uint8ArrayFromSelf
})
/**
* @since 1.0.0
* @category encrytion
*/
export class EventLogEncryption extends Context.Tag("@effect/experimental/EventLogEncryption")<
EventLogEncryption,
{
readonly encrypt: (
identity: typeof Identity.Service,
entries: ReadonlyArray<Entry>
) => Effect.Effect<{
readonly iv: Uint8Array
readonly encryptedEntries: ReadonlyArray<Uint8Array>
}>
readonly decrypt: (
identity: typeof Identity.Service,
entries: ReadonlyArray<EncryptedRemoteEntry>
) => Effect.Effect<Array<RemoteEntry>>
readonly sha256String: (data: Uint8Array) => Effect.Effect<string>
readonly sha256: (data: Uint8Array) => Effect.Effect<Uint8Array>
}
>() {}
/**
* @since 1.0.0
* @category encrytion
*/
export const makeEncryptionSubtle = (crypto: Crypto): Effect.Effect<typeof EventLogEncryption.Service> =>
Effect.sync(() => {
const keyCache = new WeakMap<typeof Identity.Service, CryptoKey>()
const getKey = (identity: typeof Identity.Service) =>
Effect.suspend(() => {
if (keyCache.has(identity)) {
return Effect.succeed(keyCache.get(identity)!)
}
return Effect.promise(() =>
crypto.subtle.importKey(
"raw",
Redacted.value(identity.privateKey) as Uint8Array<ArrayBuffer>,
"AES-GCM",
true,
["encrypt", "decrypt"]
)
).pipe(
Effect.tap((key) => {
keyCache.set(identity, key)
})
)
})
return EventLogEncryption.of({
encrypt: (identity, entries) =>
Effect.gen(function*() {
const data = yield* Effect.orDie(Entry.encodeArray(entries))
const key = yield* getKey(identity)
const iv = crypto.getRandomValues(new Uint8Array(12))
const encryptedEntries = yield* Effect.promise(() =>
Promise.all(
data.map((entry) =>
crypto.subtle.encrypt({ name: "AES-GCM", iv, tagLength: 128 }, key, entry as Uint8Array<ArrayBuffer>)
)
)
)
return {
iv,
encryptedEntries: encryptedEntries.map((entry) => new Uint8Array(entry))
}
}),
decrypt: (identity, entries) =>
Effect.gen(function*() {
const key = yield* getKey(identity)
const decryptedData = (yield* Effect.promise(() =>
Promise.all(entries.map((data) =>
crypto.subtle.decrypt(
{ name: "AES-GCM", iv: data.iv as Uint8Array<ArrayBuffer>, tagLength: 128 },
key,
data.encryptedEntry as Uint8Array<ArrayBuffer>
)
))
)).map((buffer) => new Uint8Array(buffer))
const decoded = yield* Effect.orDie(Entry.decodeArray(decryptedData))
return decoded.map((entry, i) => new RemoteEntry({ remoteSequence: entries[i].sequence, entry }))
}),
sha256: (data) =>
Effect.promise(() => crypto.subtle.digest("SHA-256", data as Uint8Array<ArrayBuffer>)).pipe(
Effect.map((hash) => new Uint8Array(hash))
),
sha256String: (data) =>
Effect.map(
Effect.promise(() => crypto.subtle.digest("SHA-256", data as Uint8Array<ArrayBuffer>)),
(hash) => {
const hashArray = Array.from(new Uint8Array(hash))
const hashHex = hashArray
.map((bytes) => bytes.toString(16).padStart(2, "0"))
.join("")
return hashHex
}
)
})
})
/**
* @since 1.0.0
* @category encrytion
*/
export const layerSubtle: Layer.Layer<EventLogEncryption> = Layer.suspend(() =>
Layer.effect(EventLogEncryption, makeEncryptionSubtle(globalThis.crypto))
)