evernode-js-client 0.5.10 → 0.5.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,276 @@
1
+ const { BaseEvernodeClient } = require('./base-evernode-client');
2
+ const { EvernodeEvents, MemoFormats, MemoTypes, ErrorCodes, ErrorReasons, EvernodeConstants } = require('../evernode-common');
3
+ const { EncryptionHelper } = require('../encryption-helper');
4
+ const { XrplAccount } = require('../xrpl-account');
5
+ const { UtilHelpers } = require('../util-helpers');
6
+ const { Buffer } = require('buffer');
7
+ const codec = require('ripple-address-codec');
8
+ const { EvernodeHelpers } = require('../evernode-helpers');
9
+ const { TransactionHelper } = require('../transaction-helper');
10
+
11
+ const DEFAULT_WAIT_TIMEOUT = 60000;
12
+
13
+ const TenantEvents = {
14
+ AcquireSuccess: EvernodeEvents.AcquireSuccess,
15
+ AcquireError: EvernodeEvents.AcquireError,
16
+ ExtendSuccess: EvernodeEvents.ExtendSuccess,
17
+ ExtendError: EvernodeEvents.ExtendError,
18
+ }
19
+
20
+ class TenantClient extends BaseEvernodeClient {
21
+
22
+ /**
23
+ * Constructs a tenant client instance.
24
+ * @param {string} xrpAddress XRPL address of the tenant.
25
+ * @param {string} XRPL secret of the tenant.
26
+ * @param {object} options [Optional] An object with 'rippledServer' URL and 'registryAddress'.
27
+ */
28
+ constructor(xrpAddress, xrpSecret, options = {}) {
29
+ super(xrpAddress, xrpSecret, Object.values(TenantEvents), false, options);
30
+ }
31
+
32
+ async prepareAccount() {
33
+ try {
34
+ if (!await this.xrplAcc.getMessageKey())
35
+ await this.xrplAcc.setAccountFields({ MessageKey: this.accKeyPair.publicKey });
36
+ }
37
+ catch (err) {
38
+ console.log("Error in preparing user xrpl account for Evernode.", err);
39
+ }
40
+ }
41
+
42
+ async getLeaseHost(hostAddress) {
43
+ const host = new XrplAccount(hostAddress, null, { xrplApi: this.xrplApi });
44
+ // Find an owned NFT with matching Evernode host NFT prefix.
45
+ const nft = (await host.getNfts()).find(n => n.URI.startsWith(EvernodeConstants.NFT_PREFIX_HEX));
46
+ if (!nft)
47
+ throw { reason: ErrorReasons.HOST_INVALID, error: "Host is not registered." };
48
+
49
+ // Check whether the token was actually issued from Evernode registry contract.
50
+ const issuerHex = nft.NFTokenID.substr(8, 40);
51
+ const issuerAddr = codec.encodeAccountID(Buffer.from(issuerHex, 'hex'));
52
+ if (issuerAddr != this.registryAddress)
53
+ throw { reason: ErrorReasons.HOST_INVALID, error: "Host is not registered." };
54
+
55
+ // Check whether active.
56
+ const hostInfo = await this.getHostInfo(host.address);
57
+ if (!hostInfo)
58
+ throw { reason: ErrorReasons.HOST_INVALID, error: "Host is not registered." };
59
+ else if (!hostInfo.active)
60
+ throw { reason: ErrorReasons.HOST_INACTIVE, error: "Host is not active." };
61
+
62
+ return host;
63
+ }
64
+
65
+ /**
66
+ *
67
+ * @param {string} hostAddress XRPL address of the host to acquire the lease.
68
+ * @param {object} requirement The instance requirements and configuration.
69
+ * @param {object} options [Optional] Options for the XRPL transaction.
70
+ * @returns The transaction result.
71
+ */
72
+ async acquireLeaseSubmit(hostAddress, requirement, options = {}) {
73
+
74
+ const hostAcc = await this.getLeaseHost(hostAddress);
75
+ let selectedOfferIndex = options.leaseOfferIndex;
76
+
77
+ // Attempt to get first available offer, if offer is not specified in options.
78
+ if (!selectedOfferIndex) {
79
+ const nftOffers = await EvernodeHelpers.getLeaseOffers(hostAcc);
80
+ selectedOfferIndex = nftOffers && nftOffers[0] && nftOffers[0].index;
81
+
82
+ if (!selectedOfferIndex)
83
+ throw { reason: ErrorReasons.NO_OFFER, error: "No offers available." };
84
+ }
85
+
86
+ // Encrypt the requirements with the host's encryption key (Specified in MessageKey field of the host account).
87
+ const encKey = await hostAcc.getMessageKey();
88
+ if (!encKey)
89
+ throw { reason: ErrorReasons.INTERNAL_ERR, error: "Host encryption key not set." };
90
+
91
+ const ecrypted = await EncryptionHelper.encrypt(encKey, requirement, {
92
+ iv: options.iv, // Must be null or 16 bytes.
93
+ ephemPrivateKey: options.ephemPrivateKey // Must be null or 32 bytes.
94
+ });
95
+
96
+ return this.xrplAcc.buyNft(selectedOfferIndex, [{ type: MemoTypes.ACQUIRE_LEASE, format: MemoFormats.BASE64, data: ecrypted }], options.transactionOptions);
97
+ }
98
+
99
+ /**
100
+ * Watch for the acquire-success response after the acquire request is made.
101
+ * @param {object} tx The transaction returned by the acquireLeaseSubmit function.
102
+ * @param {object} options [Optional] Options for the XRPL transaction.
103
+ * @returns An object including transaction details,instance info, and acquireReference Id.
104
+ */
105
+ async watchAcquireResponse(tx, options = {}) {
106
+ console.log(`Waiting for acquire response... (txHash: ${tx.id})`);
107
+
108
+ return new Promise(async (resolve, reject) => {
109
+ let rejected = false;
110
+ const failTimeout = setTimeout(() => {
111
+ rejected = true;
112
+ reject({ error: ErrorCodes.ACQUIRE_ERR, reason: ErrorReasons.TIMEOUT });
113
+ }, options.timeout || DEFAULT_WAIT_TIMEOUT);
114
+
115
+ let relevantTx = null;
116
+ while (!rejected && !relevantTx) {
117
+ const txList = await this.xrplAcc.getAccountTrx(tx.details.ledger_index);
118
+ for (let t of txList) {
119
+ t.tx.Memos = TransactionHelper.deserializeMemos(t.tx?.Memos);
120
+ const res = await this.extractEvernodeEvent(t.tx);
121
+ if ((res?.name === EvernodeEvents.AcquireSuccess || res?.name === EvernodeEvents.AcquireError) && res?.data?.acquireRefId === tx.id) {
122
+ clearTimeout(failTimeout);
123
+ relevantTx = res;
124
+ break;
125
+ }
126
+ }
127
+ await new Promise(resolveSleep => setTimeout(resolveSleep, 2000));
128
+ }
129
+
130
+ if (!rejected) {
131
+ if (relevantTx?.name === TenantEvents.AcquireSuccess) {
132
+ resolve({
133
+ transaction: relevantTx?.data.transaction,
134
+ instance: relevantTx?.data.payload.content,
135
+ acquireRefId: relevantTx?.data.acquireRefId
136
+ });
137
+ } else if (relevantTx?.name === TenantEvents.AcquireError) {
138
+ reject({
139
+ error: ErrorCodes.ACQUIRE_ERR,
140
+ transaction: relevantTx?.data.transaction,
141
+ reason: relevantTx?.data.reason,
142
+ acquireRefId: relevantTx?.data.acquireRefId
143
+ });
144
+ }
145
+ }
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Acquire an instance from a host
151
+ * @param {string} hostAddress XRPL address of the host to acquire the lease.
152
+ * @param {object} requirement The instance requirements and configuration.
153
+ * @param {object} options [Optional] Options for the XRPL transaction.
154
+ * @returns An object including transaction details,instance info, and acquireReference Id.
155
+ */
156
+ acquireLease(hostAddress, requirement, options = {}) {
157
+ return new Promise(async (resolve, reject) => {
158
+ const tx = await this.acquireLeaseSubmit(hostAddress, requirement, options).catch(error => {
159
+ reject({ error: ErrorCodes.ACQUIRE_ERR, reason: error.reason || ErrorReasons.TRANSACTION_FAILURE, content: error.error || error });
160
+ });
161
+ if (tx) {
162
+ try {
163
+ const response = await this.watchAcquireResponse(tx, options);
164
+ resolve(response);
165
+ } catch (error) {
166
+ reject(error);
167
+ }
168
+ }
169
+ });
170
+ }
171
+
172
+ /**
173
+ * This function is called by a tenant client to submit the extend lease transaction in certain host. This function will be called inside extendLease function. This function can take four parameters as follows.
174
+ * @param {string} hostAddress XRPL account address of the host.
175
+ * @param {number} amount Cost for the extended moments , in EVRs.
176
+ * @param {string} tokenID Tenant received instance name. this name can be retrieve by performing acquire Lease.
177
+ * @param {object} options This is an optional field and contains necessary details for the transactions.
178
+ * @returns The transaction result.
179
+ */
180
+ async extendLeaseSubmit(hostAddress, amount, tokenID, options = {}) {
181
+ const host = await this.getLeaseHost(hostAddress);
182
+ return this.xrplAcc.makePayment(host.address, amount.toString(), EvernodeConstants.EVR, this.config.evrIssuerAddress,
183
+ [{ type: MemoTypes.EXTEND_LEASE, format: MemoFormats.HEX, data: tokenID }], options.transactionOptions);
184
+ }
185
+
186
+ /**
187
+ * This function watches for an extendlease-success response(transaction) and returns the response or throws the error response on extendlease-error response from the host XRPL account. This function is called within the extendLease function.
188
+ * @param {object} tx Response of extendLeaseSubmit.
189
+ * @param {object} options This is an optional field and contains necessary details for the transactions.
190
+ * @returns An object including transaction details.
191
+ */
192
+ async watchExtendResponse(tx, options = {}) {
193
+ console.log(`Waiting for extend lease response... (txHash: ${tx.id})`);
194
+
195
+ return new Promise(async (resolve, reject) => {
196
+ let rejected = false;
197
+ const failTimeout = setTimeout(() => {
198
+ rejected = true;
199
+ reject({ error: ErrorCodes.EXTEND_ERR, reason: ErrorReasons.TIMEOUT });
200
+ }, options.timeout || DEFAULT_WAIT_TIMEOUT);
201
+
202
+ let relevantTx = null;
203
+ while (!rejected && !relevantTx) {
204
+ const txList = await this.xrplAcc.getAccountTrx(tx.details.ledger_index);
205
+ for (let t of txList) {
206
+ t.tx.Memos = TransactionHelper.deserializeMemos(t.tx.Memos);
207
+ const res = await this.extractEvernodeEvent(t.tx);
208
+ if ((res?.name === TenantEvents.ExtendSuccess || res?.name === TenantEvents.ExtendError) && res?.data?.extendRefId === tx.id) {
209
+ clearTimeout(failTimeout);
210
+ relevantTx = res;
211
+ break;
212
+ }
213
+ }
214
+ await new Promise(resolveSleep => setTimeout(resolveSleep, 1000));
215
+ }
216
+
217
+ if (!rejected) {
218
+ if (relevantTx?.name === TenantEvents.ExtendSuccess) {
219
+ resolve({
220
+ transaction: relevantTx?.data.transaction,
221
+ expiryMoment: relevantTx?.data.expiryMoment,
222
+ extendeRefId: relevantTx?.data.extendRefId
223
+ });
224
+ } else if (relevantTx?.name === TenantEvents.ExtendError) {
225
+ reject({
226
+ error: ErrorCodes.EXTEND_ERR,
227
+ transaction: relevantTx?.data.transaction,
228
+ reason: relevantTx?.data.reason
229
+ });
230
+ }
231
+ }
232
+ });
233
+ }
234
+
235
+ /**
236
+ * This function is called by a tenant client to extend an available instance in certain host. This function can take four parameters as follows.
237
+ * @param {string} hostAddress XRPL account address of the host.
238
+ * @param {number} moments 1190 ledgers (est. 1 hour).
239
+ * @param {string} instanceName Tenant received instance name. this name can be retrieve by performing acquire Lease.
240
+ * @param {object} options This is an optional field and contains necessary details for the transactions.
241
+ * @returns An object including transaction details.
242
+ */
243
+ extendLease(hostAddress, moments, instanceName, options = {}) {
244
+ return new Promise(async (resolve, reject) => {
245
+ const tokenID = instanceName;
246
+ const nft = (await this.xrplAcc.getNfts())?.find(n => n.NFTokenID == tokenID);
247
+
248
+ if (!nft) {
249
+ reject({ error: ErrorCodes.EXTEND_ERR, reason: ErrorReasons.NO_NFT, content: 'Could not find the nft for lease extend request.' });
250
+ return;
251
+ }
252
+
253
+ let minLedgerIndex = this.xrplApi.ledgerIndex;
254
+
255
+ // Get the agreement lease amount from the nft and calculate EVR amount to be sent.
256
+ const uriInfo = UtilHelpers.decodeLeaseNftUri(nft.URI);
257
+ const tx = await this.extendLeaseSubmit(hostAddress, moments * uriInfo.leaseAmount, tokenID, options).catch(error => {
258
+ reject({ error: ErrorCodes.EXTEND_ERR, reason: error.reason || ErrorReasons.TRANSACTION_FAILURE, content: error.error || error });
259
+ });
260
+
261
+ if (tx) {
262
+ try {
263
+ const response = await this.watchExtendResponse(tx, minLedgerIndex, options)
264
+ resolve(response);
265
+ } catch (error) {
266
+ reject(error);
267
+ }
268
+ }
269
+ });
270
+ }
271
+ }
272
+
273
+ module.exports = {
274
+ TenantEvents,
275
+ TenantClient
276
+ }
@@ -0,0 +1,21 @@
1
+ const DefaultValues = {
2
+ registryAddress: 'raaFre81618XegCrzTzVotAmarBcqNSAvK',
3
+ rippledServer: 'wss://hooks-testnet-v2.xrpl-labs.com',
4
+ xrplApi: null,
5
+ stateIndexId: 'evernodeindex'
6
+ }
7
+
8
+ class Defaults {
9
+ static set(newDefaults) {
10
+ Object.assign(DefaultValues, newDefaults)
11
+ }
12
+
13
+ static get() {
14
+ return { ...DefaultValues };
15
+ }
16
+ }
17
+
18
+ module.exports = {
19
+ DefaultValues,
20
+ Defaults
21
+ }
@@ -0,0 +1,258 @@
1
+ // Code taken from https://github.com/bitchan/eccrypto/blob/master/browser.js
2
+ // We are using this code file directly because the full eccrypto library causes a conflict with
3
+ // tiny-secp256k1 used by xrpl libs during ncc/webpack build.
4
+
5
+ var EC = require("elliptic").ec;
6
+ var ec = new EC("secp256k1");
7
+ var browserCrypto = global.crypto || global.msCrypto || {};
8
+ var subtle = browserCrypto.subtle || browserCrypto.webkitSubtle;
9
+
10
+ var nodeCrypto = require('crypto');
11
+
12
+ const EC_GROUP_ORDER = Buffer.from('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', 'hex');
13
+ const ZERO32 = Buffer.alloc(32, 0);
14
+
15
+ function assert(condition, message) {
16
+ if (!condition) {
17
+ throw new Error(message || "Assertion failed");
18
+ }
19
+ }
20
+
21
+ function isScalar(x) {
22
+ return Buffer.isBuffer(x) && x.length === 32;
23
+ }
24
+
25
+ function isValidPrivateKey(privateKey) {
26
+ if (!isScalar(privateKey)) {
27
+ return false;
28
+ }
29
+ return privateKey.compare(ZERO32) > 0 && // > 0
30
+ privateKey.compare(EC_GROUP_ORDER) < 0; // < G
31
+ }
32
+
33
+ // Compare two buffers in constant time to prevent timing attacks.
34
+ function equalConstTime(b1, b2) {
35
+ if (b1.length !== b2.length) {
36
+ return false;
37
+ }
38
+ var res = 0;
39
+ for (var i = 0; i < b1.length; i++) {
40
+ res |= b1[i] ^ b2[i]; // jshint ignore:line
41
+ }
42
+ return res === 0;
43
+ }
44
+
45
+ /* This must check if we're in the browser or
46
+ not, since the functions are different and does
47
+ not convert using browserify */
48
+ function randomBytes(size) {
49
+ var arr = new Uint8Array(size);
50
+ if (typeof browserCrypto.getRandomValues === 'undefined') {
51
+ return Buffer.from(nodeCrypto.randomBytes(size));
52
+ } else {
53
+ browserCrypto.getRandomValues(arr);
54
+ }
55
+ return Buffer.from(arr);
56
+ }
57
+
58
+ function sha512(msg) {
59
+ return new Promise(function (resolve) {
60
+ var hash = nodeCrypto.createHash('sha512');
61
+ var result = hash.update(msg).digest();
62
+ resolve(new Uint8Array(result));
63
+ });
64
+ }
65
+
66
+ function getAes(op) {
67
+ return function (iv, key, data) {
68
+ return new Promise(function (resolve) {
69
+ if (subtle) {
70
+ var importAlgorithm = { name: "AES-CBC" };
71
+ var keyp = subtle.importKey("raw", key, importAlgorithm, false, [op]);
72
+ return keyp.then(function (cryptoKey) {
73
+ var encAlgorithm = { name: "AES-CBC", iv: iv };
74
+ return subtle[op](encAlgorithm, cryptoKey, data);
75
+ }).then(function (result) {
76
+ resolve(Buffer.from(new Uint8Array(result)));
77
+ });
78
+ } else {
79
+ if (op === 'encrypt') {
80
+ var cipher = nodeCrypto.createCipheriv('aes-256-cbc', key, iv);
81
+ let firstChunk = cipher.update(data);
82
+ let secondChunk = cipher.final();
83
+ resolve(Buffer.concat([firstChunk, secondChunk]));
84
+ }
85
+ else if (op === 'decrypt') {
86
+ var decipher = nodeCrypto.createDecipheriv('aes-256-cbc', key, iv);
87
+ let firstChunk = decipher.update(data);
88
+ let secondChunk = decipher.final();
89
+ resolve(Buffer.concat([firstChunk, secondChunk]));
90
+ }
91
+ }
92
+ });
93
+ };
94
+ }
95
+
96
+ var aesCbcEncrypt = getAes("encrypt");
97
+ var aesCbcDecrypt = getAes("decrypt");
98
+
99
+ function hmacSha256Sign(key, msg) {
100
+ return new Promise(function (resolve) {
101
+ var hmac = nodeCrypto.createHmac('sha256', Buffer.from(key));
102
+ hmac.update(msg);
103
+ var result = hmac.digest();
104
+ resolve(result);
105
+ });
106
+ }
107
+
108
+ function hmacSha256Verify(key, msg, sig) {
109
+ return new Promise(function (resolve) {
110
+ var hmac = nodeCrypto.createHmac('sha256', Buffer.from(key));
111
+ hmac.update(msg);
112
+ var expectedSig = hmac.digest();
113
+ resolve(equalConstTime(expectedSig, sig));
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Generate a new valid private key. Will use the window.crypto or window.msCrypto as source
119
+ * depending on your browser.
120
+ * @return {Buffer} A 32-byte private key.
121
+ * @function
122
+ */
123
+ exports.generatePrivate = function () {
124
+ var privateKey = randomBytes(32);
125
+ while (!isValidPrivateKey(privateKey)) {
126
+ privateKey = randomBytes(32);
127
+ }
128
+ return privateKey;
129
+ };
130
+
131
+ var getPublic = exports.getPublic = function (privateKey) {
132
+ // This function has sync API so we throw an error immediately.
133
+ assert(privateKey.length === 32, "Bad private key");
134
+ assert(isValidPrivateKey(privateKey), "Bad private key");
135
+ // XXX(Kagami): `elliptic.utils.encode` returns array for every
136
+ // encoding except `hex`.
137
+ return Buffer.from(ec.keyFromPrivate(privateKey).getPublic("arr"));
138
+ };
139
+
140
+ /**
141
+ * Get compressed version of public key.
142
+ */
143
+ var getPublicCompressed = exports.getPublicCompressed = function (privateKey) { // jshint ignore:line
144
+ assert(privateKey.length === 32, "Bad private key");
145
+ assert(isValidPrivateKey(privateKey), "Bad private key");
146
+ // See https://github.com/wanderer/secp256k1-node/issues/46
147
+ let compressed = true;
148
+ return Buffer.from(ec.keyFromPrivate(privateKey).getPublic(compressed, "arr"));
149
+ };
150
+
151
+ // NOTE(Kagami): We don't use promise shim in Browser implementation
152
+ // because it's supported natively in new browsers (see
153
+ // <http://caniuse.com/#feat=promises>) and we can use only new browsers
154
+ // because of the WebCryptoAPI (see
155
+ // <http://caniuse.com/#feat=cryptography>).
156
+ exports.sign = function (privateKey, msg) {
157
+ return new Promise(function (resolve) {
158
+ assert(privateKey.length === 32, "Bad private key");
159
+ assert(isValidPrivateKey(privateKey), "Bad private key");
160
+ assert(msg.length > 0, "Message should not be empty");
161
+ assert(msg.length <= 32, "Message is too long");
162
+ resolve(Buffer.from(ec.sign(msg, privateKey, { canonical: true }).toDER()));
163
+ });
164
+ };
165
+
166
+ exports.verify = function (publicKey, msg, sig) {
167
+ return new Promise(function (resolve, reject) {
168
+ assert(publicKey.length === 65 || publicKey.length === 33, "Bad public key");
169
+ if (publicKey.length === 65) {
170
+ assert(publicKey[0] === 4, "Bad public key");
171
+ }
172
+ if (publicKey.length === 33) {
173
+ assert(publicKey[0] === 2 || publicKey[0] === 3, "Bad public key");
174
+ }
175
+ assert(msg.length > 0, "Message should not be empty");
176
+ assert(msg.length <= 32, "Message is too long");
177
+ if (ec.verify(msg, sig, publicKey)) {
178
+ resolve(null);
179
+ } else {
180
+ reject(new Error("Bad signature"));
181
+ }
182
+ });
183
+ };
184
+
185
+ var derive = exports.derive = function (privateKeyA, publicKeyB) {
186
+ return new Promise(function (resolve) {
187
+ assert(Buffer.isBuffer(privateKeyA), "Bad private key");
188
+ assert(Buffer.isBuffer(publicKeyB), "Bad public key");
189
+ assert(privateKeyA.length === 32, "Bad private key");
190
+ assert(isValidPrivateKey(privateKeyA), "Bad private key");
191
+ assert(publicKeyB.length === 65 || publicKeyB.length === 33, "Bad public key");
192
+ if (publicKeyB.length === 65) {
193
+ assert(publicKeyB[0] === 4, "Bad public key");
194
+ }
195
+ if (publicKeyB.length === 33) {
196
+ assert(publicKeyB[0] === 2 || publicKeyB[0] === 3, "Bad public key");
197
+ }
198
+ var keyA = ec.keyFromPrivate(privateKeyA);
199
+ var keyB = ec.keyFromPublic(publicKeyB);
200
+ var Px = keyA.derive(keyB.getPublic()); // BN instance
201
+ resolve(Buffer.from(Px.toArray()));
202
+ });
203
+ };
204
+
205
+ exports.encrypt = function (publicKeyTo, msg, opts) {
206
+ opts = opts || {};
207
+ // Tmp variables to save context from flat promises;
208
+ var iv, ephemPublicKey, ciphertext, macKey;
209
+ return new Promise(function (resolve) {
210
+ var ephemPrivateKey = opts.ephemPrivateKey || randomBytes(32);
211
+ // There is a very unlikely possibility that it is not a valid key
212
+ while (!isValidPrivateKey(ephemPrivateKey)) {
213
+ ephemPrivateKey = opts.ephemPrivateKey || randomBytes(32);
214
+ }
215
+ ephemPublicKey = getPublic(ephemPrivateKey);
216
+ resolve(derive(ephemPrivateKey, publicKeyTo));
217
+ }).then(function (Px) {
218
+ return sha512(Px);
219
+ }).then(function (hash) {
220
+ iv = opts.iv || randomBytes(16);
221
+ var encryptionKey = hash.slice(0, 32);
222
+ macKey = hash.slice(32);
223
+ return aesCbcEncrypt(iv, encryptionKey, msg);
224
+ }).then(function (data) {
225
+ ciphertext = data;
226
+ var dataToMac = Buffer.concat([iv, ephemPublicKey, ciphertext]);
227
+ return hmacSha256Sign(macKey, dataToMac);
228
+ }).then(function (mac) {
229
+ return {
230
+ iv: iv,
231
+ ephemPublicKey: ephemPublicKey,
232
+ ciphertext: ciphertext,
233
+ mac: mac,
234
+ };
235
+ });
236
+ };
237
+
238
+ exports.decrypt = function (privateKey, opts) {
239
+ // Tmp variable to save context from flat promises;
240
+ var encryptionKey;
241
+ return derive(privateKey, opts.ephemPublicKey).then(function (Px) {
242
+ return sha512(Px);
243
+ }).then(function (hash) {
244
+ encryptionKey = hash.slice(0, 32);
245
+ var macKey = hash.slice(32);
246
+ var dataToMac = Buffer.concat([
247
+ opts.iv,
248
+ opts.ephemPublicKey,
249
+ opts.ciphertext
250
+ ]);
251
+ return hmacSha256Verify(macKey, dataToMac, opts.mac);
252
+ }).then(function (macGood) {
253
+ assert(macGood, "Bad MAC");
254
+ return aesCbcDecrypt(opts.iv, encryptionKey, opts.ciphertext);
255
+ }).then(function (msg) {
256
+ return Buffer.from(new Uint8Array(msg));
257
+ });
258
+ };
@@ -0,0 +1,96 @@
1
+ class ed25519 {
2
+ static async #getLibrary() {
3
+ const _sodium = require('libsodium-wrappers');
4
+ await _sodium.ready;
5
+ return _sodium;
6
+ }
7
+
8
+ static async encrypt(publicKeyBuf, messageBuf) {
9
+ const sodium = await this.#getLibrary();
10
+ const curve25519PublicKey = sodium.crypto_sign_ed25519_pk_to_curve25519(publicKeyBuf.slice(1));
11
+ return Buffer.from(sodium.crypto_box_seal(messageBuf, curve25519PublicKey));
12
+ }
13
+
14
+ static async decrypt(privateKeyBuf, encryptedBuf) {
15
+ const sodium = await this.#getLibrary();
16
+ const keyPair = sodium.crypto_sign_seed_keypair(privateKeyBuf.slice(1));
17
+ const curve25519PublicKey_ = sodium.crypto_sign_ed25519_pk_to_curve25519(keyPair.publicKey);
18
+ const curve25519PrivateKey = sodium.crypto_sign_ed25519_sk_to_curve25519(keyPair.privateKey);
19
+ return Buffer.from(sodium.crypto_box_seal_open(encryptedBuf, curve25519PublicKey_, curve25519PrivateKey));
20
+ }
21
+ }
22
+
23
+ class secp256k1 {
24
+ // Offsets of the properties in the encrypted buffer.
25
+ static ivOffset = 65;
26
+ static macOffset = this.ivOffset + 16;
27
+ static ciphertextOffset = this.macOffset + 32;
28
+
29
+ static #getLibrary() {
30
+ const eccrypto = require('./eccrypto') // Using local copy of the eccrypto code file.
31
+ return eccrypto;
32
+ }
33
+
34
+ static async encrypt(publicKeyBuf, messageBuf, options = {}) {
35
+ const eccrypto = this.#getLibrary();
36
+ // For the encryption library, both keys and data should be buffers.
37
+ const encrypted = await eccrypto.encrypt(publicKeyBuf, messageBuf, options);
38
+ // Concat all the properties of the encrypted object to a single buffer.
39
+ return Buffer.concat([encrypted.ephemPublicKey, encrypted.iv, encrypted.mac, encrypted.ciphertext]);
40
+ }
41
+
42
+ static async decrypt(privateKeyBuf, encryptedBuf) {
43
+ const eccrypto = this.#getLibrary();
44
+ // Extract the buffer from the string and prepare encrypt object from buffer offsets for decryption.
45
+ const encryptedObj = {
46
+ ephemPublicKey: encryptedBuf.slice(0, this.ivOffset),
47
+ iv: encryptedBuf.slice(this.ivOffset, this.macOffset),
48
+ mac: encryptedBuf.slice(this.macOffset, this.ciphertextOffset),
49
+ ciphertext: encryptedBuf.slice(this.ciphertextOffset)
50
+ }
51
+
52
+ const decrypted = await eccrypto.decrypt(privateKeyBuf.slice(1), encryptedObj)
53
+ .catch(err => console.log(err));
54
+
55
+ return decrypted;
56
+ }
57
+ }
58
+
59
+ class EncryptionHelper {
60
+ static contentFormat = 'base64';
61
+ static keyFormat = 'hex';
62
+ static ed25519KeyType = 'ed25519';
63
+ static secp256k1KeyType = 'ecdsa-secp256k1';
64
+
65
+ static #getAlgorithmFromKey(key) {
66
+ const bytes = Buffer.from(key, this.keyFormat);
67
+ return bytes.length === 33 && bytes.at(0) === 0xed
68
+ ? this.ed25519KeyType
69
+ : this.secp256k1KeyType;
70
+ }
71
+
72
+ static #getEncryptor(key) {
73
+ const format = this.#getAlgorithmFromKey(key);
74
+ return format === this.secp256k1KeyType ? secp256k1 : ed25519;
75
+ }
76
+
77
+ static async encrypt(publicKey, message, options = {}) {
78
+ const publicKeyBuf = Buffer.from(publicKey, this.keyFormat);
79
+ const messageBuf = Buffer.from(JSON.stringify(message));
80
+ const encryptor = this.#getEncryptor(publicKey);
81
+ const result = await encryptor.encrypt(publicKeyBuf, messageBuf, options);
82
+ return result ? result.toString(this.contentFormat) : null;
83
+ }
84
+
85
+ static async decrypt(privateKey, encrypted) {
86
+ const privateKeyBuf = Buffer.from(privateKey, this.keyFormat);
87
+ const encryptedBuf = Buffer.from(encrypted, this.contentFormat);
88
+ const encryptor = this.#getEncryptor(privateKey);
89
+ const decrypted = await encryptor.decrypt(privateKeyBuf, encryptedBuf);
90
+ return decrypted ? JSON.parse(decrypted.toString()) : null;
91
+ }
92
+ }
93
+
94
+ module.exports = {
95
+ EncryptionHelper
96
+ }