-
Notifications
You must be signed in to change notification settings - Fork 4
/
index.ts
204 lines (196 loc) · 6.12 KB
/
index.ts
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
import { createCipheriv, createDecipheriv, randomBytes } from "crypto"
export type Message = Buffer | string
export interface Secret {
message: Message
passphrase: string
}
export type Kdf = (passphrase: string, salt: string) => Promise<Buffer>
export interface Block {
salt: Buffer
iv: Buffer
headers: Buffer
data: Buffer
}
const validateSecrets = (secrets: Secret[]) => {
if (!(secrets instanceof Array) || secrets.length === 0) {
return false
}
for (const secret of secrets) {
if (!secret.message || !secret.passphrase) {
return false
}
}
return true
}
/**
* Get data length of message
* @param message message
* @returns data length in bytes
*/
export const getDataLength = (message: Message) => {
const key = randomBytes(32)
const iv = randomBytes(12)
const cipher = createCipheriv("aes-256-gcm", key, iv)
const enciphered = cipher.update(message)
const encipheredFinal = Buffer.concat([enciphered, cipher.final()])
const authTag = cipher.getAuthTag()
const buffer = Buffer.concat([encipheredFinal, iv, authTag])
return buffer.length
}
/**
* Encrypt secrets using Blockcrypt
* @param secrets secrets
* @param kdf key derivation function
* @param headersLength optional, headers length in increments of `8` bytes (defaults to `64`)
* @param dataLength optional, data length in increments of `8` bytes (defaults to first secret ciphertext buffer length * 2 rounded to nearest upper increment of `64` bytes)
* @param salt optional, salt used for deterministic unit tests
* @param iv optional, initialization vector used for deterministic unit tests
* @returns block
*/
export const encrypt = async (
secrets: Secret[],
kdf: Kdf,
headersLength?: number,
dataLength?: number,
salt?: Buffer,
iv?: Buffer
): Promise<Block> => {
if (!validateSecrets(secrets)) {
throw new Error("Invalid secrets")
}
if (headersLength && headersLength % 8 !== 0) {
throw new Error("Invalid headers length")
} else if (!headersLength) {
headersLength = 64
}
if (dataLength && dataLength % 8 !== 0) {
throw new Error("Invalid data length")
}
if (!salt) {
salt = randomBytes(16)
}
if (!iv) {
iv = randomBytes(16)
}
let headersBuffers: Buffer[] = []
let dataBuffers: Buffer[] = []
let dataStart = 0
for (const [index, secret] of secrets.entries()) {
const key = await kdf(secret.passphrase, salt.toString("base64"))
const dataIv = randomBytes(12)
const dataCipher = createCipheriv("aes-256-gcm", key, dataIv)
const dataEnciphered = dataCipher.update(secret.message)
const dataEncipheredFinal = Buffer.concat([
dataEnciphered,
dataCipher.final(),
])
const dataAuthTag = dataCipher.getAuthTag()
const dataEncipheredFinalLength = dataEncipheredFinal.length
const headersCipher = createCipheriv("aes-256-cbc", key, iv)
const headersEnciphered = headersCipher.update(
`${dataStart}:${dataEncipheredFinalLength}`
)
const headersEncipheredFinal = Buffer.concat([
headersEnciphered,
headersCipher.final(),
])
headersBuffers.push(headersEncipheredFinal)
const dataBuffer = Buffer.concat([dataEncipheredFinal, dataIv, dataAuthTag])
dataBuffers.push(dataBuffer)
dataStart += dataBuffer.length
if (!dataLength && index === 0) {
dataLength = Math.ceil((dataBuffer.length * 2) / 64) * 64
}
}
let data = Buffer.concat(dataBuffers)
const unpaddedDataLength = data.length
if (unpaddedDataLength > dataLength) {
throw new Error("Data too long for data length")
}
dataBuffers.push(randomBytes(dataLength - unpaddedDataLength))
data = Buffer.concat(dataBuffers)
let headers = Buffer.concat(headersBuffers)
const unpaddedHeadersLength = headers.length
if (unpaddedHeadersLength > headersLength) {
throw new Error("Headers too long for headers length")
}
headersBuffers.push(randomBytes(headersLength - unpaddedHeadersLength))
headers = Buffer.concat(headersBuffers)
return {
salt: salt,
iv: iv,
headers: headers,
data: data,
}
}
/**
* Decrypt secret encrypted using Blockcrypt
* @param passphrase passphrase
* @param salt salt
* @param iv initialization vector
* @param headers headers
* @param data data
* @param kdf key derivation function
* @returns message
*/
export const decrypt = async (
passphrase: string,
salt: Buffer,
iv: Buffer,
headers: Buffer,
data: Buffer,
kdf: Kdf
): Promise<Buffer> => {
const key = await kdf(passphrase, salt.toString("base64"))
let headerStart = 0
let header: string | null = null
while (headerStart < headers.length) {
for (let headerEnd = headers.length; headerEnd > headerStart; headerEnd--) {
try {
const headersDecipher = createDecipheriv("aes-256-cbc", key, iv)
const headersDeciphered = headersDecipher.update(
headers.subarray(headerStart, headerEnd)
)
const headerDecipheredFinal = Buffer.concat([
headersDeciphered,
headersDecipher.final(),
])
const string = headerDecipheredFinal.toString()
if (string.match(/^[0-9]+:[0-9]+$/)) {
header = string
}
if (header) {
break
}
} catch (error) {}
}
if (header) {
break
}
headerStart++
}
if (!header) {
throw new Error("Header not found")
}
const [dataEncipheredFinalStart, dataEncipheredFinalLength] =
header.split(":")
const dataEncipheredFinalEnd =
parseInt(dataEncipheredFinalStart) + parseInt(dataEncipheredFinalLength)
const dataEncipheredFinal = data.subarray(
parseInt(dataEncipheredFinalStart),
dataEncipheredFinalEnd
)
const dataIvStart = dataEncipheredFinalEnd
const dataIvEnd = dataIvStart + 12
const dataIv = data.subarray(dataIvStart, dataIvEnd)
const dataAuthTagStart = dataIvEnd
const dataAuthTag = data.subarray(dataAuthTagStart, dataAuthTagStart + 16)
const dataDecipher = createDecipheriv("aes-256-gcm", key, dataIv)
dataDecipher.setAuthTag(dataAuthTag)
const dataDeciphered = dataDecipher.update(dataEncipheredFinal)
const dataDecipheredFinal = Buffer.concat([
dataDeciphered,
dataDecipher.final(),
])
return dataDecipheredFinal
}