@keetanetwork/anchor 0.0.37 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/encrypted-container.d.ts +53 -3
- package/lib/encrypted-container.d.ts.map +1 -1
- package/lib/encrypted-container.js +549 -93
- package/lib/encrypted-container.js.map +1 -1
- package/lib/http-server/index.d.ts.map +1 -1
- package/lib/http-server/index.js +58 -5
- package/lib/http-server/index.js.map +1 -1
- package/lib/queue/drivers/queue_firestore.d.ts +29 -0
- package/lib/queue/drivers/queue_firestore.d.ts.map +1 -0
- package/lib/queue/drivers/queue_firestore.js +279 -0
- package/lib/queue/drivers/queue_firestore.js.map +1 -0
- package/lib/queue/index.d.ts +57 -0
- package/lib/queue/index.d.ts.map +1 -1
- package/lib/queue/index.js +127 -21
- package/lib/queue/index.js.map +1 -1
- package/lib/resolver.d.ts +4 -15
- package/lib/resolver.d.ts.map +1 -1
- package/lib/resolver.js +468 -636
- package/lib/resolver.js.map +1 -1
- package/lib/utils/signing.d.ts +12 -3
- package/lib/utils/signing.d.ts.map +1 -1
- package/lib/utils/signing.js +7 -13
- package/lib/utils/signing.js.map +1 -1
- package/lib/utils/types.d.ts +14 -2
- package/lib/utils/types.d.ts.map +1 -1
- package/lib/utils/types.js.map +1 -1
- package/npm-shrinkwrap.json +7 -7
- package/package.json +3 -2
- package/services/asset-movement/client.d.ts +2 -2
- package/services/asset-movement/client.d.ts.map +1 -1
- package/services/asset-movement/client.js +2 -2
- package/services/asset-movement/client.js.map +1 -1
- package/services/asset-movement/common.d.ts +201 -24
- package/services/asset-movement/common.d.ts.map +1 -1
- package/services/asset-movement/common.js +305 -80
- package/services/asset-movement/common.js.map +1 -1
- package/services/fx/client.d.ts +38 -11
- package/services/fx/client.d.ts.map +1 -1
- package/services/fx/client.js +187 -42
- package/services/fx/client.js.map +1 -1
- package/services/fx/common.d.ts +55 -6
- package/services/fx/common.d.ts.map +1 -1
- package/services/fx/common.js +142 -16
- package/services/fx/common.js.map +1 -1
- package/services/fx/server.d.ts +51 -7
- package/services/fx/server.d.ts.map +1 -1
- package/services/fx/server.js +333 -109
- package/services/fx/server.js.map +1 -1
- package/services/fx/util.d.ts +31 -0
- package/services/fx/util.d.ts.map +1 -0
- package/services/fx/util.js +132 -0
- package/services/fx/util.js.map +1 -0
|
@@ -1,37 +1,275 @@
|
|
|
1
1
|
import crypto from './utils/crypto.js';
|
|
2
2
|
import { lib as KeetaNetLib } from '@keetanetwork/keetanet-client';
|
|
3
|
-
import { Buffer, arrayBufferToBuffer, bufferToArrayBuffer } from './utils/buffer.js';
|
|
3
|
+
import { Buffer, arrayBufferToBuffer, arrayBufferLikeToBuffer, bufferToArrayBuffer } from './utils/buffer.js';
|
|
4
4
|
import { isArray } from './utils/array.js';
|
|
5
|
+
import { KeetaAnchorError } from './error.js';
|
|
6
|
+
// #region Error Handling
|
|
7
|
+
/**
|
|
8
|
+
* Error codes for EncryptedContainer operations
|
|
9
|
+
*/
|
|
10
|
+
export const EncryptedContainerErrorCodes = [
|
|
11
|
+
// Parsing/Malformed data
|
|
12
|
+
'MALFORMED_BASE_FORMAT',
|
|
13
|
+
'MALFORMED_VERSION',
|
|
14
|
+
'MALFORMED_DATA_STRUCTURE',
|
|
15
|
+
'MALFORMED_KEY_INFO',
|
|
16
|
+
'MALFORMED_SIGNER_INFO',
|
|
17
|
+
// Algorithm issues
|
|
18
|
+
'UNSUPPORTED_VERSION',
|
|
19
|
+
'UNSUPPORTED_CIPHER_ALGORITHM',
|
|
20
|
+
'UNSUPPORTED_DIGEST_ALGORITHM',
|
|
21
|
+
'UNSUPPORTED_SIGNATURE_ALGORITHM',
|
|
22
|
+
'UNSUPPORTED_KEY_TYPE',
|
|
23
|
+
// Key/Decryption issues
|
|
24
|
+
'NO_KEYS_PROVIDED',
|
|
25
|
+
'NO_MATCHING_KEY',
|
|
26
|
+
'DECRYPTION_FAILED',
|
|
27
|
+
'DECOMPRESSION_FAILED',
|
|
28
|
+
// Signing issues
|
|
29
|
+
'SIGNER_REQUIRES_PRIVATE_KEY',
|
|
30
|
+
'NOT_SIGNED',
|
|
31
|
+
'SIGNATURE_VERIFICATION_FAILED',
|
|
32
|
+
// State issues
|
|
33
|
+
'NO_PLAINTEXT_AVAILABLE',
|
|
34
|
+
'NO_ENCODED_DATA_AVAILABLE',
|
|
35
|
+
'PLAINTEXT_DISABLED',
|
|
36
|
+
// Access management
|
|
37
|
+
'ENCRYPTION_REQUIRED',
|
|
38
|
+
'INVALID_PRINCIPALS',
|
|
39
|
+
'ACCESS_MANAGEMENT_NOT_ALLOWED',
|
|
40
|
+
// Internal errors
|
|
41
|
+
'INTERNAL_ERROR'
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* Error class for EncryptedContainer operations
|
|
45
|
+
*/
|
|
46
|
+
export class EncryptedContainerError extends KeetaAnchorError {
|
|
47
|
+
static name = 'EncryptedContainerError';
|
|
48
|
+
encryptedContainerErrorObjectTypeID;
|
|
49
|
+
static encryptedContainerErrorObjectTypeID = 'f4a8c2e1-7b3d-4f9a-8c5e-2d1f0a9b8c7e';
|
|
50
|
+
code;
|
|
51
|
+
/**
|
|
52
|
+
* Check if a string is a valid EncryptedContainerErrorCode
|
|
53
|
+
*/
|
|
54
|
+
static isValidCode(code) {
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
56
|
+
return (EncryptedContainerErrorCodes.includes(code));
|
|
57
|
+
}
|
|
58
|
+
constructor(code, message) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.code = code;
|
|
61
|
+
// EncryptedContainerError is not a user error
|
|
62
|
+
this.userError = false;
|
|
63
|
+
Object.defineProperty(this, 'encryptedContainerErrorObjectTypeID', {
|
|
64
|
+
value: EncryptedContainerError.encryptedContainerErrorObjectTypeID,
|
|
65
|
+
enumerable: false
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
static isInstance(input) {
|
|
69
|
+
return (this.hasPropWithValue(input, 'encryptedContainerErrorObjectTypeID', EncryptedContainerError.encryptedContainerErrorObjectTypeID));
|
|
70
|
+
}
|
|
71
|
+
toJSON() {
|
|
72
|
+
return ({
|
|
73
|
+
...super.toJSON(),
|
|
74
|
+
code: this.code
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
static async fromJSON(input) {
|
|
78
|
+
const { message, other } = this.extractErrorProperties(input, this);
|
|
79
|
+
// Extract and validate code
|
|
80
|
+
if (!('code' in other) || typeof other.code !== 'string') {
|
|
81
|
+
throw (new Error('Invalid EncryptedContainerError JSON: missing code property'));
|
|
82
|
+
}
|
|
83
|
+
// Validate code is a valid EncryptedContainerErrorCode
|
|
84
|
+
const code = other.code;
|
|
85
|
+
if (!this.isValidCode(code)) {
|
|
86
|
+
throw (new Error(`Invalid EncryptedContainerError JSON: unknown code ${code}`));
|
|
87
|
+
}
|
|
88
|
+
const error = new this(code, message);
|
|
89
|
+
error.restoreFromJSON(other);
|
|
90
|
+
return (error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// #endregion
|
|
5
94
|
const zlibDeflateAsync = KeetaNetLib.Utils.Buffer.ZlibDeflateAsync;
|
|
6
95
|
const zlibInflateAsync = KeetaNetLib.Utils.Buffer.ZlibInflateAsync;
|
|
7
96
|
const ASN1toJS = KeetaNetLib.Utils.ASN1.ASN1toJS;
|
|
8
97
|
const JStoASN1 = KeetaNetLib.Utils.ASN1.JStoASN1;
|
|
9
98
|
const Account = KeetaNetLib.Account;
|
|
99
|
+
/*
|
|
100
|
+
* ASN.1 Schema
|
|
101
|
+
*
|
|
102
|
+
* EncryptedContainer DEFINITIONS ::=
|
|
103
|
+
* BEGIN
|
|
104
|
+
* Version ::= INTEGER { v2(1) }
|
|
105
|
+
*
|
|
106
|
+
* KeyStore ::= SEQUENCE {
|
|
107
|
+
* publicKey OCTET STRING,
|
|
108
|
+
* encryptedSymmetricKey OCTET STRING,
|
|
109
|
+
* ...
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* EncryptedContainerBox ::= SEQUENCE {
|
|
113
|
+
* keys SEQUENCE OF KeyStore,
|
|
114
|
+
* encryptionAlgorithm OBJECT IDENTIFIER,
|
|
115
|
+
* initializationVector OCTET STRING,
|
|
116
|
+
* encryptedValue OCTET STRING,
|
|
117
|
+
* ...
|
|
118
|
+
* }
|
|
119
|
+
*
|
|
120
|
+
* PlaintextContainerBox ::= SEQUENCE {
|
|
121
|
+
* plainValue OCTET STRING,
|
|
122
|
+
* ...
|
|
123
|
+
* }
|
|
124
|
+
*
|
|
125
|
+
* -- RFC 5652 Section 5.3 SignerInfo (adapted for KeetaNet)
|
|
126
|
+
* SignerInfo ::= SEQUENCE {
|
|
127
|
+
* version CMSVersion, -- INTEGER (3 for subjectKeyIdentifier)
|
|
128
|
+
* sid [0] SubjectKeyIdentifier, -- OCTET STRING (publicKeyAndType)
|
|
129
|
+
* digestAlgorithm OBJECT IDENTIFIER, -- SHA3-256: 2.16.840.1.101.3.4.2.8
|
|
130
|
+
* signatureAlgorithm OBJECT IDENTIFIER, -- Derived from account type
|
|
131
|
+
* signature OCTET STRING,
|
|
132
|
+
* ...
|
|
133
|
+
* }
|
|
134
|
+
*
|
|
135
|
+
* ContainerPackage ::= SEQUENCE {
|
|
136
|
+
* version Version,
|
|
137
|
+
* encryptedContainer [0] EXPLICIT EncryptedContainerBox OPTIONAL,
|
|
138
|
+
* plaintextContainer [1] EXPLICIT PlaintextContainerBox OPTIONAL,
|
|
139
|
+
* signerInfo [2] EXPLICIT SignerInfo OPTIONAL,
|
|
140
|
+
* ...
|
|
141
|
+
* } (WITH COMPONENTS {
|
|
142
|
+
* encryptedContainer PRESENT,
|
|
143
|
+
* plaintextContainer ABSENT
|
|
144
|
+
* } |
|
|
145
|
+
* WITH COMPONENTS {
|
|
146
|
+
* encryptedContainer ABSENT,
|
|
147
|
+
* plaintextContainer PRESENT
|
|
148
|
+
* })
|
|
149
|
+
* END
|
|
150
|
+
*
|
|
151
|
+
*/
|
|
152
|
+
/**
|
|
153
|
+
* OID constants for cryptographic algorithms
|
|
154
|
+
* // XXX:TODO: We should standardize this somewhere centralized
|
|
155
|
+
*/
|
|
10
156
|
const oidDB = {
|
|
157
|
+
// Digest algorithms
|
|
158
|
+
'sha3-256': '2.16.840.1.101.3.4.2.8',
|
|
159
|
+
// Signature algorithms
|
|
160
|
+
'ed25519': '1.3.101.112',
|
|
161
|
+
'secp256k1': '1.3.132.0.10',
|
|
162
|
+
'secp256r1': '1.2.840.10045.3.1.7',
|
|
163
|
+
// Encryption algorithms
|
|
11
164
|
'aes-256-cbc': '2.16.840.1.101.3.4.1.42'
|
|
12
165
|
};
|
|
166
|
+
/**
|
|
167
|
+
* Supported algorithms
|
|
168
|
+
* These are the canonical names as defined in oidDB
|
|
169
|
+
*/
|
|
170
|
+
const SUPPORTED_DIGEST_ALGORITHMS = ['sha3-256'];
|
|
171
|
+
const SUPPORTED_SIGNATURE_ALGORITHMS = ['ed25519', 'secp256k1', 'secp256r1'];
|
|
172
|
+
/**
|
|
173
|
+
* Known OID aliases that ASN1 parsers may return instead of our canonical names
|
|
174
|
+
*/
|
|
175
|
+
const OID_ALIASES = {
|
|
176
|
+
'prime256v1': 'secp256r1'
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Build reverse lookup: name/alias -> numeric OID
|
|
180
|
+
*/
|
|
181
|
+
const nameToNumericOID = {};
|
|
182
|
+
for (const [name, numericOID] of Object.entries(oidDB)) {
|
|
183
|
+
nameToNumericOID[name] = numericOID;
|
|
184
|
+
}
|
|
185
|
+
for (const [alias, canonicalName] of Object.entries(OID_ALIASES)) {
|
|
186
|
+
nameToNumericOID[alias] = oidDB[canonicalName];
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Normalize an OID (name, alias, or already numeric) to its numeric form.
|
|
190
|
+
* Returns undefined if the OID is not recognized.
|
|
191
|
+
*/
|
|
192
|
+
function normalizeToNumericOID(oid) {
|
|
193
|
+
if (/^\d+(\.\d+)*$/.test(oid)) {
|
|
194
|
+
return (oid);
|
|
195
|
+
}
|
|
196
|
+
// Otherwise look up the name/alias
|
|
197
|
+
return (nameToNumericOID[oid]);
|
|
198
|
+
}
|
|
199
|
+
// Pre-compute supported numeric OIDs for validation
|
|
200
|
+
const supportedDigestOIDs = new Set(SUPPORTED_DIGEST_ALGORITHMS.map(name => oidDB[name]));
|
|
201
|
+
const supportedSignatureOIDs = new Set(SUPPORTED_SIGNATURE_ALGORITHMS.map(name => oidDB[name]));
|
|
202
|
+
/**
|
|
203
|
+
* Map account key algorithm to signature algorithm OID
|
|
204
|
+
*/
|
|
205
|
+
function getSignatureAlgorithmOID(account) {
|
|
206
|
+
const keyType = account.keyType;
|
|
207
|
+
const KeyAlgo = Account.AccountKeyAlgorithm;
|
|
208
|
+
if (keyType === KeyAlgo.ECDSA_SECP256K1) {
|
|
209
|
+
return (oidDB['secp256k1']);
|
|
210
|
+
}
|
|
211
|
+
else if (keyType === KeyAlgo.ED25519) {
|
|
212
|
+
return (oidDB['ed25519']);
|
|
213
|
+
}
|
|
214
|
+
else if (keyType === KeyAlgo.ECDSA_SECP256R1) {
|
|
215
|
+
return (oidDB['secp256r1']);
|
|
216
|
+
}
|
|
217
|
+
throw (new EncryptedContainerError('UNSUPPORTED_KEY_TYPE', `Unsupported key type for signing: ${keyType}`));
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Build a typed encrypted container box with context tag [0].
|
|
221
|
+
*/
|
|
222
|
+
function buildEncryptedBox(keys, algorithmOID, iv, encryptedData) {
|
|
223
|
+
return ({
|
|
224
|
+
type: 'context',
|
|
225
|
+
value: 0,
|
|
226
|
+
kind: 'explicit',
|
|
227
|
+
contains: [keys, { type: 'oid', oid: algorithmOID }, iv, encryptedData]
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Build a typed plaintext container box with context tag [1].
|
|
232
|
+
*/
|
|
233
|
+
function buildPlaintextBox(data) {
|
|
234
|
+
return ({
|
|
235
|
+
type: 'context',
|
|
236
|
+
value: 1,
|
|
237
|
+
kind: 'explicit',
|
|
238
|
+
contains: [data]
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Build a typed signer info box with context tag [2].
|
|
243
|
+
*/
|
|
244
|
+
function buildSignerInfoBox(signerInfo) {
|
|
245
|
+
return ({
|
|
246
|
+
type: 'context',
|
|
247
|
+
value: 2,
|
|
248
|
+
kind: 'explicit',
|
|
249
|
+
contains: signerInfo
|
|
250
|
+
});
|
|
251
|
+
}
|
|
13
252
|
/**
|
|
14
253
|
* Compiles the ASN.1 for the container
|
|
15
254
|
*
|
|
255
|
+
* @param plaintext The plaintext data to encode
|
|
256
|
+
* @param encryptionOptions Optional encryption options
|
|
257
|
+
* @param signingOptions Optional signing options (will include SignerInfo)
|
|
16
258
|
* @returns The ASN.1 DER data
|
|
17
259
|
*/
|
|
18
|
-
async function buildASN1(plaintext, encryptionOptions) {
|
|
19
|
-
const compressedPlaintext = Buffer.from(await zlibDeflateAsync(plaintext));
|
|
20
|
-
const sequence = [];
|
|
21
|
-
/*
|
|
22
|
-
* Version v2 (1)
|
|
23
|
-
*/
|
|
24
|
-
sequence[0] = 1;
|
|
260
|
+
async function buildASN1(plaintext, encryptionOptions, signingOptions) {
|
|
261
|
+
const compressedPlaintext = Buffer.from(await zlibDeflateAsync(bufferToArrayBuffer(plaintext)));
|
|
25
262
|
/*
|
|
26
|
-
*
|
|
263
|
+
* Build the container box (encrypted or plaintext)
|
|
27
264
|
*/
|
|
265
|
+
let containerBox;
|
|
28
266
|
if (encryptionOptions) {
|
|
29
267
|
const { keys, cipherKey, cipherIV, cipherAlgo } = encryptionOptions;
|
|
30
268
|
if (keys === undefined || keys.length === 0 || cipherKey === undefined || cipherIV === undefined || cipherAlgo === undefined) {
|
|
31
|
-
throw (new
|
|
269
|
+
throw (new EncryptedContainerError('INTERNAL_ERROR', 'Unsupported method invocation'));
|
|
32
270
|
}
|
|
33
271
|
if (!(cipherAlgo in oidDB)) {
|
|
34
|
-
throw (new
|
|
272
|
+
throw (new EncryptedContainerError('UNSUPPORTED_CIPHER_ALGORITHM', `Unsupported algorithm: ${cipherAlgo}`));
|
|
35
273
|
}
|
|
36
274
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
37
275
|
const algorithmOID = oidDB[cipherAlgo];
|
|
@@ -44,68 +282,97 @@ async function buildASN1(plaintext, encryptionOptions) {
|
|
|
44
282
|
const encryptionKeysSequence = await Promise.all(keys.map(async function (key) {
|
|
45
283
|
const encryptedSymmetricKey = Buffer.from(await key.encrypt(cipherKeyArrayBuffer));
|
|
46
284
|
const retval = [
|
|
47
|
-
key.publicKeyAndType,
|
|
48
|
-
encryptedSymmetricKey
|
|
285
|
+
Buffer.from(key.publicKeyAndType),
|
|
286
|
+
Buffer.from(encryptedSymmetricKey)
|
|
49
287
|
];
|
|
50
288
|
return (retval);
|
|
51
289
|
}));
|
|
52
|
-
|
|
53
|
-
type: 'context',
|
|
54
|
-
value: 0,
|
|
55
|
-
kind: 'explicit',
|
|
56
|
-
contains: [
|
|
57
|
-
encryptionKeysSequence,
|
|
58
|
-
{ type: 'oid', oid: algorithmOID },
|
|
59
|
-
cipherIV,
|
|
60
|
-
encryptedData
|
|
61
|
-
]
|
|
62
|
-
};
|
|
290
|
+
containerBox = buildEncryptedBox(encryptionKeysSequence, algorithmOID, cipherIV, encryptedData);
|
|
63
291
|
}
|
|
64
292
|
else {
|
|
65
293
|
/*
|
|
66
294
|
* Otherwise we simply pass in the compressed data
|
|
67
295
|
*/
|
|
68
|
-
|
|
69
|
-
type: 'context',
|
|
70
|
-
value: 1,
|
|
71
|
-
kind: 'explicit',
|
|
72
|
-
contains: [compressedPlaintext]
|
|
73
|
-
};
|
|
296
|
+
containerBox = buildPlaintextBox(Buffer.from(compressedPlaintext));
|
|
74
297
|
}
|
|
75
|
-
|
|
76
|
-
|
|
298
|
+
/*
|
|
299
|
+
* Build the typed container package
|
|
300
|
+
*/
|
|
301
|
+
const container = [1, containerBox];
|
|
302
|
+
if (signingOptions) {
|
|
303
|
+
/*
|
|
304
|
+
* Sign the compressed plaintext (before encryption) so signature is verifiable after decryption
|
|
305
|
+
*/
|
|
306
|
+
const { signer } = signingOptions;
|
|
307
|
+
if (!signer.hasPrivateKey) {
|
|
308
|
+
throw (new EncryptedContainerError('SIGNER_REQUIRES_PRIVATE_KEY', 'Signer account must have a private key'));
|
|
309
|
+
}
|
|
310
|
+
// Hash the compressed plaintext with SHA3-256
|
|
311
|
+
const digestHash = crypto.createHash('sha3-256');
|
|
312
|
+
digestHash.update(compressedPlaintext);
|
|
313
|
+
const digest = digestHash.digest();
|
|
314
|
+
// Sign the digest
|
|
315
|
+
const signatureBuffer = await signer.sign(bufferToArrayBuffer(digest), { raw: true });
|
|
316
|
+
// Get signature as Buffer
|
|
317
|
+
let signature;
|
|
318
|
+
if (Buffer.isBuffer(signatureBuffer)) {
|
|
319
|
+
signature = Buffer.from(signatureBuffer);
|
|
320
|
+
}
|
|
321
|
+
else if ('get' in signatureBuffer && typeof signatureBuffer.get === 'function') {
|
|
322
|
+
signature = arrayBufferToBuffer(signatureBuffer.get());
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
326
|
+
signature = arrayBufferToBuffer(signatureBuffer);
|
|
327
|
+
}
|
|
328
|
+
const signerInfoASN1 = [
|
|
329
|
+
3, // CMSVersion 3 for subjectKeyIdentifier
|
|
330
|
+
{
|
|
331
|
+
type: 'context',
|
|
332
|
+
value: 0,
|
|
333
|
+
kind: 'implicit',
|
|
334
|
+
contains: Buffer.from(signer.publicKeyAndType)
|
|
335
|
+
},
|
|
336
|
+
{ type: 'oid', oid: oidDB['sha3-256'] },
|
|
337
|
+
{ type: 'oid', oid: getSignatureAlgorithmOID(signer) },
|
|
338
|
+
signature
|
|
339
|
+
];
|
|
340
|
+
container.push(buildSignerInfoBox(signerInfoASN1));
|
|
341
|
+
}
|
|
342
|
+
const outputASN1 = JStoASN1(container);
|
|
343
|
+
const outputDER = arrayBufferToBuffer(outputASN1.toBER(false));
|
|
77
344
|
return (outputDER);
|
|
78
345
|
}
|
|
79
346
|
function parseASN1Bare(input, acceptableEncryptionAlgorithms = ['aes-256-cbc', 'null']) {
|
|
80
347
|
const inputSequence = ASN1toJS(bufferToArrayBuffer(input));
|
|
81
|
-
if (!isArray(inputSequence
|
|
82
|
-
throw (new
|
|
348
|
+
if (!isArray(inputSequence) || inputSequence.length < 2) {
|
|
349
|
+
throw (new EncryptedContainerError('MALFORMED_BASE_FORMAT', 'Malformed data detected (incorrect base format)'));
|
|
83
350
|
}
|
|
84
351
|
const version = inputSequence[0];
|
|
85
352
|
if (typeof version !== 'bigint') {
|
|
86
|
-
throw (new
|
|
353
|
+
throw (new EncryptedContainerError('MALFORMED_VERSION', 'Malformed data detected (version expected at position 0)'));
|
|
87
354
|
}
|
|
88
355
|
if (version !== 1n) {
|
|
89
|
-
throw (new
|
|
356
|
+
throw (new EncryptedContainerError('UNSUPPORTED_VERSION', 'Malformed data detected (unsupported version)'));
|
|
90
357
|
}
|
|
91
358
|
const valueBox = inputSequence[1];
|
|
92
359
|
if (typeof valueBox !== 'object' || valueBox === null) {
|
|
93
|
-
throw (new
|
|
360
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Malformed data detected (data expected at position 1)'));
|
|
94
361
|
}
|
|
95
362
|
if (!('type' in valueBox) || typeof valueBox.type !== 'string') {
|
|
96
|
-
throw (new
|
|
363
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Malformed data detected (expected type at position 1)'));
|
|
97
364
|
}
|
|
98
365
|
if (valueBox.type !== 'context') {
|
|
99
|
-
throw (new
|
|
366
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Malformed data detected (expected context at position 1)'));
|
|
100
367
|
}
|
|
101
368
|
if (!('value' in valueBox) || typeof valueBox.value !== 'number') {
|
|
102
|
-
throw (new
|
|
369
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Malformed data detected (expected context value at position 1)'));
|
|
103
370
|
}
|
|
104
371
|
if (valueBox.value !== 0 && valueBox.value !== 1) {
|
|
105
|
-
throw (new
|
|
372
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Malformed data detected (expected context value of 0 or 1)'));
|
|
106
373
|
}
|
|
107
374
|
if (!('contains' in valueBox) || typeof valueBox.contains !== 'object' || valueBox.contains === null) {
|
|
108
|
-
throw (new
|
|
375
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Malformed data detected (expected contents at position 1)'));
|
|
109
376
|
}
|
|
110
377
|
let isEncrypted;
|
|
111
378
|
if (valueBox.value === 0) {
|
|
@@ -119,24 +386,24 @@ function parseASN1Bare(input, acceptableEncryptionAlgorithms = ['aes-256-cbc', '
|
|
|
119
386
|
let cipherInfo;
|
|
120
387
|
if (isEncrypted) {
|
|
121
388
|
if (!isArray(value, 4)) {
|
|
122
|
-
throw (new
|
|
389
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Malformed data (incorrect number of elements within position 1 -- expected 4)'));
|
|
123
390
|
}
|
|
124
391
|
const keyInfoUnchecked = value[0];
|
|
125
392
|
if (!isArray(keyInfoUnchecked)) {
|
|
126
|
-
throw (new
|
|
393
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Malformed data (expected sequence at position 2.0)'));
|
|
127
394
|
}
|
|
128
395
|
const keyInfo = keyInfoUnchecked.map(function (checkKeyInfo) {
|
|
129
396
|
if (!isArray(checkKeyInfo, 2)) {
|
|
130
|
-
throw (new
|
|
397
|
+
throw (new EncryptedContainerError('MALFORMED_KEY_INFO', 'Malformed key information (expected sequence of 2 at position 1.0.x)'));
|
|
131
398
|
}
|
|
132
399
|
const publicKeyBuffer = checkKeyInfo[0];
|
|
133
400
|
if (!Buffer.isBuffer(publicKeyBuffer)) {
|
|
134
|
-
throw (new
|
|
401
|
+
throw (new EncryptedContainerError('MALFORMED_KEY_INFO', 'Malformed key information (expected octet string for public key at position 1.0.x)'));
|
|
135
402
|
}
|
|
136
403
|
const publicKey = Account.fromPublicKeyAndType(publicKeyBuffer);
|
|
137
404
|
const encryptedSymmetricKey = checkKeyInfo[1];
|
|
138
405
|
if (!Buffer.isBuffer(encryptedSymmetricKey)) {
|
|
139
|
-
throw (new
|
|
406
|
+
throw (new EncryptedContainerError('MALFORMED_KEY_INFO', 'Malformed key information (expected octet string for cipher key at position 1.0.x)'));
|
|
140
407
|
}
|
|
141
408
|
return ({
|
|
142
409
|
publicKey,
|
|
@@ -149,11 +416,11 @@ function parseASN1Bare(input, acceptableEncryptionAlgorithms = ['aes-256-cbc', '
|
|
|
149
416
|
const encryptionAlgorithm = 'aes-256-cbc';
|
|
150
417
|
const cipherIV = value[2];
|
|
151
418
|
if (!Buffer.isBuffer(cipherIV)) {
|
|
152
|
-
throw (new
|
|
419
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Malformed data (cipher IV expected at position 1.2)'));
|
|
153
420
|
}
|
|
154
421
|
const encryptedCompressedValue = value[3];
|
|
155
422
|
if (!Buffer.isBuffer(encryptedCompressedValue)) {
|
|
156
|
-
throw (new
|
|
423
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Malformed data (encrypted compressed buffer expected at position 1.3)'));
|
|
157
424
|
}
|
|
158
425
|
cipherInfo = {
|
|
159
426
|
keys: keyInfo,
|
|
@@ -161,25 +428,100 @@ function parseASN1Bare(input, acceptableEncryptionAlgorithms = ['aes-256-cbc', '
|
|
|
161
428
|
encryptedData: encryptedCompressedValue,
|
|
162
429
|
encryptionAlgorithm: encryptionAlgorithm
|
|
163
430
|
};
|
|
164
|
-
containedCompressed = encryptedCompressedValue;
|
|
431
|
+
containedCompressed = Buffer.from(encryptedCompressedValue);
|
|
165
432
|
}
|
|
166
433
|
else {
|
|
167
434
|
if (!isArray(value, 1)) {
|
|
168
|
-
throw (new
|
|
435
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Malformed data (incorrect number of elements within position 1 -- expected 1)'));
|
|
169
436
|
}
|
|
170
437
|
const containedCompressedUnchecked = value[0];
|
|
171
438
|
if (!Buffer.isBuffer(containedCompressedUnchecked)) {
|
|
172
|
-
throw (new
|
|
439
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Malformed data (compressed buffer expected at position 1.0)'));
|
|
173
440
|
}
|
|
174
441
|
if (!acceptableEncryptionAlgorithms.includes('null')) {
|
|
175
|
-
throw (new
|
|
442
|
+
throw (new EncryptedContainerError('ENCRYPTION_REQUIRED', 'Malformed data (plaintext found but the null encryption algorithm is not acceptable)'));
|
|
443
|
+
}
|
|
444
|
+
containedCompressed = Buffer.from(containedCompressedUnchecked);
|
|
445
|
+
}
|
|
446
|
+
// Parse SignerInfo if present
|
|
447
|
+
let signerInfo;
|
|
448
|
+
if (inputSequence.length >= 3) {
|
|
449
|
+
const signerInfoBox = inputSequence[2];
|
|
450
|
+
if (typeof signerInfoBox === 'object' && signerInfoBox !== null &&
|
|
451
|
+
'type' in signerInfoBox && signerInfoBox.type === 'context' &&
|
|
452
|
+
'value' in signerInfoBox && signerInfoBox.value === 2 &&
|
|
453
|
+
'contains' in signerInfoBox && isArray(signerInfoBox.contains, 5)) {
|
|
454
|
+
const signerInfoData = signerInfoBox.contains;
|
|
455
|
+
// Parse version
|
|
456
|
+
const signerVersion = signerInfoData[0];
|
|
457
|
+
if (typeof signerVersion !== 'bigint') {
|
|
458
|
+
throw (new EncryptedContainerError('MALFORMED_SIGNER_INFO', 'Malformed SignerInfo (version expected at position 0)'));
|
|
459
|
+
}
|
|
460
|
+
// Parse sid (subjectKeyIdentifier in context tag [0])
|
|
461
|
+
const sidBox = signerInfoData[1];
|
|
462
|
+
let signerPublicKeyAndType;
|
|
463
|
+
if (typeof sidBox === 'object' && sidBox !== null &&
|
|
464
|
+
'type' in sidBox && sidBox.type === 'context' &&
|
|
465
|
+
'value' in sidBox && sidBox.value === 0 &&
|
|
466
|
+
'contains' in sidBox && (Buffer.isBuffer(sidBox.contains) || sidBox.contains instanceof ArrayBuffer)) {
|
|
467
|
+
signerPublicKeyAndType = arrayBufferLikeToBuffer(sidBox.contains);
|
|
468
|
+
}
|
|
469
|
+
else if (Buffer.isBuffer(sidBox)) {
|
|
470
|
+
// Handle case where ASN.1 parser unwraps the context tag
|
|
471
|
+
signerPublicKeyAndType = Buffer.from(sidBox);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
throw (new EncryptedContainerError('MALFORMED_SIGNER_INFO', 'Malformed SignerInfo (sid expected at position 1)'));
|
|
475
|
+
}
|
|
476
|
+
// Parse digestAlgorithm
|
|
477
|
+
const digestAlgoRaw = signerInfoData[2];
|
|
478
|
+
let digestAlgorithmOID;
|
|
479
|
+
if (typeof digestAlgoRaw === 'object' && digestAlgoRaw !== null &&
|
|
480
|
+
'type' in digestAlgoRaw && digestAlgoRaw.type === 'oid' &&
|
|
481
|
+
'oid' in digestAlgoRaw && typeof digestAlgoRaw.oid === 'string') {
|
|
482
|
+
const normalized = normalizeToNumericOID(digestAlgoRaw.oid);
|
|
483
|
+
if (!normalized) {
|
|
484
|
+
throw (new EncryptedContainerError('MALFORMED_SIGNER_INFO', `Unknown digest algorithm: ${digestAlgoRaw.oid}`));
|
|
485
|
+
}
|
|
486
|
+
digestAlgorithmOID = normalized;
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
throw (new EncryptedContainerError('MALFORMED_SIGNER_INFO', 'Malformed SignerInfo (digestAlgorithm expected at position 2)'));
|
|
490
|
+
}
|
|
491
|
+
// Parse signatureAlgorithm
|
|
492
|
+
const sigAlgoRaw = signerInfoData[3];
|
|
493
|
+
let signatureAlgorithmOID;
|
|
494
|
+
if (typeof sigAlgoRaw === 'object' && sigAlgoRaw !== null &&
|
|
495
|
+
'type' in sigAlgoRaw && sigAlgoRaw.type === 'oid' &&
|
|
496
|
+
'oid' in sigAlgoRaw && typeof sigAlgoRaw.oid === 'string') {
|
|
497
|
+
const normalized = normalizeToNumericOID(sigAlgoRaw.oid);
|
|
498
|
+
if (!normalized) {
|
|
499
|
+
throw (new EncryptedContainerError('MALFORMED_SIGNER_INFO', `Unknown signature algorithm: ${sigAlgoRaw.oid}`));
|
|
500
|
+
}
|
|
501
|
+
signatureAlgorithmOID = normalized;
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
throw (new EncryptedContainerError('MALFORMED_SIGNER_INFO', 'Malformed SignerInfo (signatureAlgorithm expected at position 3)'));
|
|
505
|
+
}
|
|
506
|
+
// Parse signature
|
|
507
|
+
const signatureRaw = signerInfoData[4];
|
|
508
|
+
if (!Buffer.isBuffer(signatureRaw)) {
|
|
509
|
+
throw (new EncryptedContainerError('MALFORMED_SIGNER_INFO', 'Malformed SignerInfo (signature expected at position 4)'));
|
|
510
|
+
}
|
|
511
|
+
signerInfo = {
|
|
512
|
+
version: Number(signerVersion),
|
|
513
|
+
signerPublicKeyAndType,
|
|
514
|
+
digestAlgorithmOID,
|
|
515
|
+
signatureAlgorithmOID,
|
|
516
|
+
signature: Buffer.from(signatureRaw)
|
|
517
|
+
};
|
|
176
518
|
}
|
|
177
|
-
containedCompressed = containedCompressedUnchecked;
|
|
178
519
|
}
|
|
179
520
|
return ({
|
|
180
521
|
version: version,
|
|
181
522
|
isEncrypted: isEncrypted,
|
|
182
523
|
innerValue: containedCompressed,
|
|
524
|
+
signerInfo,
|
|
183
525
|
...cipherInfo
|
|
184
526
|
});
|
|
185
527
|
}
|
|
@@ -188,15 +530,15 @@ async function parseASN1Decrypt(inputInfo, keys) {
|
|
|
188
530
|
let cipherInfo;
|
|
189
531
|
if (inputInfo.isEncrypted) {
|
|
190
532
|
if (keys === undefined || keys.length === 0) {
|
|
191
|
-
throw (new
|
|
533
|
+
throw (new EncryptedContainerError('NO_KEYS_PROVIDED', 'Encrypted Container found with encryption but no keys for decryption supplied'));
|
|
192
534
|
}
|
|
193
535
|
const algorithm = inputInfo.encryptionAlgorithm;
|
|
194
536
|
if (algorithm === undefined) {
|
|
195
|
-
throw (new
|
|
537
|
+
throw (new EncryptedContainerError('MALFORMED_DATA_STRUCTURE', 'Encrypted Container found with encryption but no algorithm supplied'));
|
|
196
538
|
}
|
|
197
539
|
const keyInfo = inputInfo.keys;
|
|
198
540
|
if (keyInfo === undefined) {
|
|
199
|
-
throw (new
|
|
541
|
+
throw (new EncryptedContainerError('INTERNAL_ERROR', 'Encrypted container found with missing keys'));
|
|
200
542
|
}
|
|
201
543
|
let decryptionKeyInfo;
|
|
202
544
|
for (const checkKeyInfo of keyInfo) {
|
|
@@ -214,19 +556,31 @@ async function parseASN1Decrypt(inputInfo, keys) {
|
|
|
214
556
|
}
|
|
215
557
|
}
|
|
216
558
|
if (decryptionKeyInfo === undefined) {
|
|
217
|
-
throw (new
|
|
559
|
+
throw (new EncryptedContainerError('NO_MATCHING_KEY', 'No keys found which can perform decryption on the supplied encryption box'));
|
|
560
|
+
}
|
|
561
|
+
let cipherKey;
|
|
562
|
+
try {
|
|
563
|
+
const dataToDecrypt = bufferToArrayBuffer(decryptionKeyInfo.encryptedSymmetricKey);
|
|
564
|
+
cipherKey = arrayBufferLikeToBuffer(await decryptionKeyInfo.privateKey.decrypt(dataToDecrypt));
|
|
565
|
+
}
|
|
566
|
+
catch (err) {
|
|
567
|
+
throw (new EncryptedContainerError('DECRYPTION_FAILED', `Key decryption failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
218
568
|
}
|
|
219
|
-
const cipherKey = Buffer.from(await decryptionKeyInfo.privateKey.decrypt(bufferToArrayBuffer(decryptionKeyInfo.encryptedSymmetricKey)));
|
|
220
569
|
const cipherIV = inputInfo.cipherIV;
|
|
221
570
|
if (cipherIV === undefined) {
|
|
222
|
-
throw (new
|
|
571
|
+
throw (new EncryptedContainerError('INTERNAL_ERROR', 'No Cipher IV found'));
|
|
223
572
|
}
|
|
224
573
|
const encryptedCompressedValue = inputInfo.innerValue;
|
|
225
574
|
const decipher = crypto.createDecipheriv(algorithm, cipherKey, cipherIV);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
575
|
+
try {
|
|
576
|
+
containedCompressed = Buffer.concat([
|
|
577
|
+
decipher.update(encryptedCompressedValue),
|
|
578
|
+
decipher.final()
|
|
579
|
+
]);
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
throw (new EncryptedContainerError('DECRYPTION_FAILED', `Cipher decryption failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
583
|
+
}
|
|
230
584
|
cipherInfo = {
|
|
231
585
|
isEncrypted: true,
|
|
232
586
|
keys: keyInfo,
|
|
@@ -241,7 +595,14 @@ async function parseASN1Decrypt(inputInfo, keys) {
|
|
|
241
595
|
isEncrypted: false
|
|
242
596
|
};
|
|
243
597
|
}
|
|
244
|
-
|
|
598
|
+
let plaintext;
|
|
599
|
+
try {
|
|
600
|
+
const inflated = await zlibInflateAsync(bufferToArrayBuffer(containedCompressed));
|
|
601
|
+
plaintext = arrayBufferLikeToBuffer(inflated);
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
throw (new EncryptedContainerError('DECOMPRESSION_FAILED', `Inflate failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
605
|
+
}
|
|
245
606
|
return ({
|
|
246
607
|
version: inputInfo.version,
|
|
247
608
|
plaintext: plaintext,
|
|
@@ -274,11 +635,15 @@ export class EncryptedContainer {
|
|
|
274
635
|
* Encryption details
|
|
275
636
|
*/
|
|
276
637
|
_internalState;
|
|
638
|
+
/**
|
|
639
|
+
* Signing information
|
|
640
|
+
*/
|
|
641
|
+
#signingInfo = {};
|
|
277
642
|
/**
|
|
278
643
|
* The plaintext or encoded (and possibly encrypted) data
|
|
279
644
|
*/
|
|
280
645
|
#data;
|
|
281
|
-
constructor(principals) {
|
|
646
|
+
constructor(principals, signer) {
|
|
282
647
|
if (principals === null) {
|
|
283
648
|
this._internalState = {
|
|
284
649
|
principals: null
|
|
@@ -292,6 +657,9 @@ export class EncryptedContainer {
|
|
|
292
657
|
}
|
|
293
658
|
;
|
|
294
659
|
this.#data = { plaintext: Buffer.alloc(0) };
|
|
660
|
+
if (signer) {
|
|
661
|
+
this.#signingInfo.signer = signer;
|
|
662
|
+
}
|
|
295
663
|
}
|
|
296
664
|
get encrypted() {
|
|
297
665
|
return (this._internalState.principals !== null);
|
|
@@ -326,11 +694,22 @@ export class EncryptedContainer {
|
|
|
326
694
|
*
|
|
327
695
|
* @param data The plaintext data to encrypt or encode
|
|
328
696
|
* @param principals The list of principals who can access the data if it is null then the data is not encrypted
|
|
329
|
-
* @param
|
|
697
|
+
* @param options Options including locked (plaintext accessibility) and signer (for RFC 5652 signing). For backward compatibility, can also be a boolean for locked.
|
|
330
698
|
* @returns The EncryptedContainer instance with the plaintext data and principals set
|
|
331
699
|
*/
|
|
332
|
-
static fromPlaintext(data, principals,
|
|
333
|
-
|
|
700
|
+
static fromPlaintext(data, principals, options) {
|
|
701
|
+
// Handle backward compatibility - if options is a boolean, treat it as the locked flag
|
|
702
|
+
let lockedOpt;
|
|
703
|
+
let signer;
|
|
704
|
+
if (typeof options === 'boolean') {
|
|
705
|
+
lockedOpt = options;
|
|
706
|
+
}
|
|
707
|
+
else if (options !== undefined) {
|
|
708
|
+
lockedOpt = options.locked;
|
|
709
|
+
signer = options.signer;
|
|
710
|
+
}
|
|
711
|
+
const retval = new EncryptedContainer(principals, signer);
|
|
712
|
+
let locked = lockedOpt;
|
|
334
713
|
if (locked === undefined) {
|
|
335
714
|
locked = true;
|
|
336
715
|
if (principals === null) {
|
|
@@ -373,16 +752,16 @@ export class EncryptedContainer {
|
|
|
373
752
|
*/
|
|
374
753
|
#computeAndSetKeyInfo(mustBeEncrypted) {
|
|
375
754
|
if (this._encoded === undefined) {
|
|
376
|
-
throw (new
|
|
755
|
+
throw (new EncryptedContainerError('NO_ENCODED_DATA_AVAILABLE', 'No encoded data available'));
|
|
377
756
|
}
|
|
378
757
|
const plaintextWrapper = parseASN1Bare(this._encoded);
|
|
379
758
|
if (mustBeEncrypted && !plaintextWrapper.isEncrypted) {
|
|
380
|
-
throw (new
|
|
759
|
+
throw (new EncryptedContainerError('ENCRYPTION_REQUIRED', 'Unable to set key information from plaintext -- it is not encrypted but that was required'));
|
|
381
760
|
}
|
|
382
761
|
if (plaintextWrapper.isEncrypted) {
|
|
383
762
|
const principals = this._internalState.principals;
|
|
384
763
|
if (principals === null) {
|
|
385
|
-
throw (new
|
|
764
|
+
throw (new EncryptedContainerError('INVALID_PRINCIPALS', 'May not encrypt data with a null set of principals'));
|
|
386
765
|
}
|
|
387
766
|
/*
|
|
388
767
|
* Compute the new accounts by merging the input from the
|
|
@@ -395,7 +774,7 @@ export class EncryptedContainer {
|
|
|
395
774
|
const blobPrincipals = (plaintextWrapper.keys ?? []).map((keyInfo) => {
|
|
396
775
|
const currentPublicKey = keyInfo.publicKey;
|
|
397
776
|
if (!currentPublicKey.isAccount()) {
|
|
398
|
-
throw (new
|
|
777
|
+
throw (new EncryptedContainerError('INTERNAL_ERROR', 'Non-account found within the encryption key list'));
|
|
399
778
|
}
|
|
400
779
|
for (const checkExistingKey of principals) {
|
|
401
780
|
if (checkExistingKey.comparePublicKey(currentPublicKey)) {
|
|
@@ -407,15 +786,19 @@ export class EncryptedContainer {
|
|
|
407
786
|
this._internalState.principals = blobPrincipals;
|
|
408
787
|
// Confirm updated principals are populated correctly which sets container to encrypted
|
|
409
788
|
if (!this.encrypted) {
|
|
410
|
-
throw (new
|
|
789
|
+
throw (new EncryptedContainerError('INTERNAL_ERROR', 'Encrypted data found but not marked as encrypted'));
|
|
411
790
|
}
|
|
412
791
|
}
|
|
413
792
|
else {
|
|
414
793
|
this._internalState.principals = null;
|
|
415
794
|
if (this.encrypted) {
|
|
416
|
-
throw (new
|
|
795
|
+
throw (new EncryptedContainerError('INTERNAL_ERROR', 'Plaintext data found but marked as encrypted'));
|
|
417
796
|
}
|
|
418
797
|
}
|
|
798
|
+
// Store parsed signer info if present
|
|
799
|
+
if (plaintextWrapper.signerInfo) {
|
|
800
|
+
this.#signingInfo.parsedSignerInfo = plaintextWrapper.signerInfo;
|
|
801
|
+
}
|
|
419
802
|
return (plaintextWrapper);
|
|
420
803
|
}
|
|
421
804
|
/**
|
|
@@ -427,20 +810,20 @@ export class EncryptedContainer {
|
|
|
427
810
|
return (this._plaintext);
|
|
428
811
|
}
|
|
429
812
|
if (this._encoded === undefined) {
|
|
430
|
-
throw (new
|
|
813
|
+
throw (new EncryptedContainerError('NO_ENCODED_DATA_AVAILABLE', 'No plaintext or encoded data available'));
|
|
431
814
|
}
|
|
432
815
|
const info = this.#computeAndSetKeyInfo(this.encrypted);
|
|
433
816
|
let principals = this._internalState.principals;
|
|
434
817
|
if (info.isEncrypted) {
|
|
435
818
|
if (principals === null) {
|
|
436
|
-
throw (new
|
|
819
|
+
throw (new EncryptedContainerError('INVALID_PRINCIPALS', 'May not decrypt data with a null set of principals'));
|
|
437
820
|
}
|
|
438
821
|
}
|
|
439
822
|
else {
|
|
440
823
|
principals = [];
|
|
441
824
|
}
|
|
442
825
|
const plaintextWrapper = await parseASN1Decrypt(info, principals);
|
|
443
|
-
const plaintext = plaintextWrapper.plaintext;
|
|
826
|
+
const plaintext = Buffer.from(plaintextWrapper.plaintext);
|
|
444
827
|
this.#data = { ...this.#data, plaintext };
|
|
445
828
|
return (plaintext);
|
|
446
829
|
}
|
|
@@ -449,9 +832,12 @@ export class EncryptedContainer {
|
|
|
449
832
|
*/
|
|
450
833
|
async #computePlaintextEncoded() {
|
|
451
834
|
if (this._plaintext === undefined) {
|
|
452
|
-
throw (new
|
|
835
|
+
throw (new EncryptedContainerError('NO_PLAINTEXT_AVAILABLE', 'No plaintext data available'));
|
|
453
836
|
}
|
|
454
|
-
const
|
|
837
|
+
const signingOptions = this.#signingInfo.signer
|
|
838
|
+
? { signer: this.#signingInfo.signer }
|
|
839
|
+
: undefined;
|
|
840
|
+
const structuredData = await buildASN1(this._plaintext, undefined, signingOptions);
|
|
455
841
|
return (structuredData);
|
|
456
842
|
}
|
|
457
843
|
/**
|
|
@@ -461,20 +847,23 @@ export class EncryptedContainer {
|
|
|
461
847
|
*/
|
|
462
848
|
async #computeEncryptedEncoded() {
|
|
463
849
|
if (this._plaintext === undefined) {
|
|
464
|
-
throw (new
|
|
850
|
+
throw (new EncryptedContainerError('NO_PLAINTEXT_AVAILABLE', 'No encrypted nor plaintext data available'));
|
|
465
851
|
}
|
|
466
852
|
if (!this.#isEncrypted()) {
|
|
467
|
-
throw (new
|
|
853
|
+
throw (new EncryptedContainerError('INTERNAL_ERROR', 'Asked to encrypt a plaintext buffer'));
|
|
468
854
|
}
|
|
855
|
+
const signingOptions = this.#signingInfo.signer
|
|
856
|
+
? { signer: this.#signingInfo.signer }
|
|
857
|
+
: undefined;
|
|
469
858
|
/**
|
|
470
859
|
* structured data is the ASN.1 encoded structure
|
|
471
860
|
*/
|
|
472
861
|
const structuredData = await buildASN1(this._plaintext, {
|
|
473
862
|
keys: this._internalState.principals,
|
|
474
|
-
cipherKey: crypto.randomBytes(32),
|
|
475
|
-
cipherIV: crypto.randomBytes(16),
|
|
863
|
+
cipherKey: Buffer.from(crypto.randomBytes(32)),
|
|
864
|
+
cipherIV: Buffer.from(crypto.randomBytes(16)),
|
|
476
865
|
cipherAlgo: this._internalState.cipherAlgo
|
|
477
|
-
});
|
|
866
|
+
}, signingOptions);
|
|
478
867
|
return (structuredData);
|
|
479
868
|
}
|
|
480
869
|
async #computeEncoded() {
|
|
@@ -498,10 +887,10 @@ export class EncryptedContainer {
|
|
|
498
887
|
*/
|
|
499
888
|
grantAccessSync(accounts) {
|
|
500
889
|
if (this._plaintext === undefined) {
|
|
501
|
-
throw (new
|
|
890
|
+
throw (new EncryptedContainerError('NO_PLAINTEXT_AVAILABLE', 'Unable to grant access, plaintext not available'));
|
|
502
891
|
}
|
|
503
892
|
if (!this.#isEncrypted()) {
|
|
504
|
-
throw (new
|
|
893
|
+
throw (new EncryptedContainerError('ACCESS_MANAGEMENT_NOT_ALLOWED', 'May not manage access to a plaintext container'));
|
|
505
894
|
}
|
|
506
895
|
if (!Array.isArray(accounts)) {
|
|
507
896
|
accounts = [accounts];
|
|
@@ -526,10 +915,10 @@ export class EncryptedContainer {
|
|
|
526
915
|
*/
|
|
527
916
|
revokeAccessSync(account) {
|
|
528
917
|
if (this._plaintext === undefined) {
|
|
529
|
-
throw (new
|
|
918
|
+
throw (new EncryptedContainerError('NO_PLAINTEXT_AVAILABLE', 'Unable to revoke access, plaintext not available'));
|
|
530
919
|
}
|
|
531
920
|
if (!this.#isEncrypted()) {
|
|
532
|
-
throw (new
|
|
921
|
+
throw (new EncryptedContainerError('ACCESS_MANAGEMENT_NOT_ALLOWED', 'May not manage access to a plaintext container'));
|
|
533
922
|
}
|
|
534
923
|
// Encoded data is invalidated with the new permissions so set only the plaintext data
|
|
535
924
|
this.setPlaintext(this._plaintext);
|
|
@@ -558,11 +947,11 @@ export class EncryptedContainer {
|
|
|
558
947
|
*/
|
|
559
948
|
async getPlaintext() {
|
|
560
949
|
if (!this.#mayAccessPlaintext) {
|
|
561
|
-
throw (new
|
|
950
|
+
throw (new EncryptedContainerError('PLAINTEXT_DISABLED', 'May not access plaintext'));
|
|
562
951
|
}
|
|
563
952
|
const plaintext = await this.#computePlaintext();
|
|
564
953
|
if (plaintext === undefined) {
|
|
565
|
-
throw (new
|
|
954
|
+
throw (new EncryptedContainerError('INTERNAL_ERROR', 'Plaintext could not be decoded'));
|
|
566
955
|
}
|
|
567
956
|
/*
|
|
568
957
|
* Make a copy of our internal buffer so that any changes made
|
|
@@ -577,7 +966,7 @@ export class EncryptedContainer {
|
|
|
577
966
|
async getEncodedBuffer() {
|
|
578
967
|
const serialized = await this.#computeEncoded();
|
|
579
968
|
if (serialized === undefined) {
|
|
580
|
-
throw (new
|
|
969
|
+
throw (new EncryptedContainerError('INTERNAL_ERROR', 'Could not encode data'));
|
|
581
970
|
}
|
|
582
971
|
/*
|
|
583
972
|
* Make a copy of our internal buffer so that any changes made
|
|
@@ -592,10 +981,77 @@ export class EncryptedContainer {
|
|
|
592
981
|
*/
|
|
593
982
|
get principals() {
|
|
594
983
|
if (!this.#isEncrypted()) {
|
|
595
|
-
throw (new
|
|
984
|
+
throw (new EncryptedContainerError('ACCESS_MANAGEMENT_NOT_ALLOWED', 'May not manage access to a plaintext container'));
|
|
596
985
|
}
|
|
597
986
|
return (this._internalState.principals);
|
|
598
987
|
}
|
|
988
|
+
/**
|
|
989
|
+
* Check if this container is signed
|
|
990
|
+
*/
|
|
991
|
+
get isSigned() {
|
|
992
|
+
return (this.#signingInfo.signer !== undefined || this.#signingInfo.parsedSignerInfo !== undefined);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Get the signing account of this container.
|
|
996
|
+
*/
|
|
997
|
+
getSigningAccount() {
|
|
998
|
+
// If we have a signer account set (for new containers), return it
|
|
999
|
+
if (this.#signingInfo.signer) {
|
|
1000
|
+
return (this.#signingInfo.signer);
|
|
1001
|
+
}
|
|
1002
|
+
// If we have parsed signer info (from encoded container), construct account from it
|
|
1003
|
+
if (this.#signingInfo.parsedSignerInfo) {
|
|
1004
|
+
const { signerPublicKeyAndType } = this.#signingInfo.parsedSignerInfo;
|
|
1005
|
+
return (Account.fromPublicKeyAndType(signerPublicKeyAndType));
|
|
1006
|
+
}
|
|
1007
|
+
return (undefined);
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Verify the signature on this container.
|
|
1011
|
+
* This requires decrypting the container first to access the compressed plaintext.
|
|
1012
|
+
*
|
|
1013
|
+
* @returns true if signature is valid, false if invalid, or throws if not signed or plaintext unavailable
|
|
1014
|
+
*/
|
|
1015
|
+
async verifySignature() {
|
|
1016
|
+
// If we have a signer but no parsed info yet, encode first to produce the signed DER
|
|
1017
|
+
if (!this.#signingInfo.parsedSignerInfo && this.#signingInfo.signer) {
|
|
1018
|
+
const encoded = await this.#computeEncoded();
|
|
1019
|
+
if (encoded) {
|
|
1020
|
+
const parsed = parseASN1Bare(encoded);
|
|
1021
|
+
if (parsed.signerInfo) {
|
|
1022
|
+
this.#signingInfo.parsedSignerInfo = parsed.signerInfo;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (!this.#signingInfo.parsedSignerInfo) {
|
|
1027
|
+
throw (new EncryptedContainerError('NOT_SIGNED', 'Container is not signed'));
|
|
1028
|
+
}
|
|
1029
|
+
const signerInfo = this.#signingInfo.parsedSignerInfo;
|
|
1030
|
+
// Validate digest algorithm OID
|
|
1031
|
+
if (!supportedDigestOIDs.has(signerInfo.digestAlgorithmOID)) {
|
|
1032
|
+
throw (new EncryptedContainerError('UNSUPPORTED_DIGEST_ALGORITHM', `Unsupported digest algorithm OID: ${signerInfo.digestAlgorithmOID}`));
|
|
1033
|
+
}
|
|
1034
|
+
// Validate signature algorithm OID
|
|
1035
|
+
if (!supportedSignatureOIDs.has(signerInfo.signatureAlgorithmOID)) {
|
|
1036
|
+
throw (new EncryptedContainerError('UNSUPPORTED_SIGNATURE_ALGORITHM', `Unsupported signature algorithm OID: ${signerInfo.signatureAlgorithmOID}`));
|
|
1037
|
+
}
|
|
1038
|
+
// We need the plaintext to verify the signature
|
|
1039
|
+
const plaintext = await this.#computePlaintext();
|
|
1040
|
+
if (!plaintext) {
|
|
1041
|
+
throw (new EncryptedContainerError('NO_PLAINTEXT_AVAILABLE', 'Unable to compute plaintext for signature verification'));
|
|
1042
|
+
}
|
|
1043
|
+
// Recompute the digest of the compressed plaintext
|
|
1044
|
+
const compressedPlaintext = Buffer.from(await zlibDeflateAsync(bufferToArrayBuffer(plaintext)));
|
|
1045
|
+
const digestHash = crypto.createHash('sha3-256');
|
|
1046
|
+
digestHash.update(compressedPlaintext);
|
|
1047
|
+
const digest = digestHash.digest();
|
|
1048
|
+
// Get the signer's account (public key only)
|
|
1049
|
+
const signerAccount = Account.fromPublicKeyAndType(signerInfo.signerPublicKeyAndType);
|
|
1050
|
+
// Verify the signature
|
|
1051
|
+
const signature = signerInfo.signature;
|
|
1052
|
+
const isValid = signerAccount.verify(bufferToArrayBuffer(digest), bufferToArrayBuffer(signature), { raw: true });
|
|
1053
|
+
return (isValid);
|
|
1054
|
+
}
|
|
599
1055
|
}
|
|
600
1056
|
/** @internal */
|
|
601
1057
|
export const _Testing = {
|