-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtrez-format.js
234 lines (205 loc) · 7.07 KB
/
trez-format.js
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
const createHash = require('create-hash')
const sodium = require('libsodium-wrappers')
const assert = require('assert')
module.exports = {
encrypt,
decrypt,
dissect,
check
}
const defaultTrezorMsg = 'Trez Cypher'
const asserts = true
const keygen = () => sodium.crypto_secretbox_keygen()
/**
@arg {TrezorSession} session
@arg {function} data - returns Buffer or Promise<Buffer> (collects data after the PIN)
@arg {object} config.entropy () => <Buffer> additional entropy
@return {Promise<Buffer>} encrypted data
*/
function encrypt(session, data, config) {
const {
address,
trezorMsg,
askOnEncrypt,
askOnDecrypt,
entropy,
iv,
saveLabel,
} =
Object.assign({
address: [0],
trezorMsg: defaultTrezorMsg,
askOnEncrypt: false,
askOnDecrypt: true,
entropy: null,
iv: undefined,
saveLabel: false,
}, config)
if(!session || typeof session.cipherKeyValue !== 'function') {
throw new TypeError('session parameter is a required Trezor session')
}
if(typeof data !== 'function') {
throw new TypeError('data parameter should be a function')
}
if(entropy && typeof entropy !== 'function') {
throw new TypeError('entropy parameter should be function that returns a Buffer')
}
if(iv) {
iv = toBinaryBuffer(iv, 'iv needs to be a hex string or a buffer')
if(iv.length !== 16) {
throw new TypeError('iv needs to be 16 bytes, instead got ' + iv.length)
}
}
// A 256 bit secret matches the strength of a Trezor's private key.
//
// By encrypting only the secret on the device (not the data), this format
// allows for better and predictable device performance, quick decryption
// checking, and quick decryption key changes (simply re-encrypt the secret).
let secret = Buffer.from(keygen())
if(asserts && secret.length !== 32) {
throw new Error('invalid secret length')
}
return Promise.resolve(data()).then(data => {
if(!Buffer.isBuffer(data)) {
throw new TypeError('data function parameter should return a Buffer or Promise<Buffer>')
}
if(entropy) {
entropyBuf = entropy()
if(!Buffer.isBuffer(entropyBuf)) {
throw new TypeError('entropy parameter should return a Buffer')
}
const h = createHash('sha256')
h.update(secret)
h.update(entropyBuf)
secret = h.digest()
}
// Encrypt only a secret using the device
// then encrypt the data using the secret..
return session.cipherKeyValue(address, trezorMsg, secret,
true/*encrypt*/, askOnEncrypt, askOnDecrypt, iv)
.then(enc => {
const encSecret = Buffer.from(enc.message.value, 'hex')
if(asserts && encSecret.length !== 32) {
throw new Error('invalid secret length')
}
const trezorParams = {
address,
trezorMsg,
encSecret: enc.message.value,
askOnEncrypt,
askOnDecrypt,
iv
}
const encData = secretboxEncrypt(data, secret)
const encrypedDataSha256 = createHash('sha256').update(encData).digest().toString('hex')
const trezorParamsSha256 = createHash('sha256').update(JSON.stringify(trezorParams)).digest().toString('hex')
const validationParams = {
encrypedDataSha256, trezorParamsSha256
}
const headers = JSON.stringify(Object.assign(trezorParams, validationParams), null, 2)
return Buffer.concat([Buffer.from(headers + '\n'), encData])
})
})
}
/**
@arg {Buffer} data
@return {object} - {header: {trezorMsg, encSecret, ..}, dataIndex: number}
@throws {Error} formatting errors
*/
function dissect(data) {
let header, dataIndex
try {
const headerEndIdx = data.indexOf('\n}\n')
const headerJson = data.slice(0, headerEndIdx + 2)
header = JSON.parse(headerJson)
assert(Array.isArray(header.address), 'address')
assert(typeof header.trezorMsg === 'string', 'trezorMsg')
assert(typeof header.encSecret === 'string', 'encSecret')
assert(typeof header.askOnDecrypt === 'boolean', 'askOnDecrypt')
assert(typeof header.askOnEncrypt === 'boolean', 'askOnEncrypt')
dataIndex = headerEndIdx + 3
} catch(error) {
error.message = 'This is not a valid trez file format: ' + error.message
throw error
}
return {header, dataIndex}
}
function check(data) {
let checkHeaders
try {
checkHeaders = dissect(data)
} catch(error) {
return {validData: false, validHeader: false}
}
const {header: {address, trezorMsg, encSecret, askOnEncrypt, askOnDecrypt, iv}} = checkHeaders
const {dataIndex} = checkHeaders
const trezorParams = {address, trezorMsg, encSecret, askOnEncrypt, askOnDecrypt, iv}
const trezorParamsSha256 = createHash('sha256').update(JSON.stringify(trezorParams)).digest().toString('hex')
const encryptedData = data.slice(dataIndex)
const encrypedDataSha256 = createHash('sha256').update(encryptedData).digest().toString('hex')
const validData = encrypedDataSha256 === checkHeaders.header.encrypedDataSha256
const validHeader = trezorParamsSha256 === checkHeaders.header.trezorParamsSha256
return {validData, validHeader}
}
function decrypt(session, data) {
if(!session || typeof session.cipherKeyValue !== 'function') {
throw new TypeError('session parameter is a required Trezor session')
}
if(typeof data !== 'function') {
throw new TypeError('data parameter should be a function')
}
return Promise.resolve(data()).then(data => {
if(!Buffer.isBuffer(data)) {
throw new TypeError('data function parameter should return a Buffer or Promise<Buffer>')
}
const {header, dataIndex} = dissect(data)
const {address, trezorMsg, encSecret, askOnEncrypt, askOnDecrypt} = header
const encryptedData = data.slice(dataIndex)
return session.cipherKeyValue(address, trezorMsg, Buffer.from(encSecret, 'hex'),
false/*encrypt*/, askOnEncrypt, askOnDecrypt)
.then(dec => {
const secret = Buffer.from(dec.message.value, 'hex')
if(asserts && secret.length !== 32) {
throw new Error('invalid secret length')
}
try {
return secretboxDecrypt(encryptedData, secret)
} catch(error) {
error.message = 'Decryption Failed ' + error.message
throw error
}
})
})
}
function toBinaryBuffer(data, typeError = 'expecting a hex string or buffer') {
try {
if(typeof data === 'string') {
data = Buffer.from(data, 'hex')
} else {
if(!Buffer.isBuffer(data)) {
throw 'unknown type'
}
}
} catch(error) {
throw new TypeError(typeError)
}
return data
}
/**
@arg {Buffer} buf
@return {Buffer}
*/
function secretboxEncrypt(buf, secret) {
const nonce = Buffer.from(sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES))
const ciphertext = sodium.crypto_secretbox_easy(buf, nonce, secret)
return Buffer.concat([nonce, Buffer.from(ciphertext)])
}
/**
@arg {Buffer} buf
@return Buffer
*/
function secretboxDecrypt(buf, secret) {
const nonce = buf.slice(0, sodium.crypto_box_NONCEBYTES);
const cypherbuf = buf.slice(sodium.crypto_box_NONCEBYTES);
return sodium.crypto_secretbox_open_easy(cypherbuf, nonce, secret, 'text');
}