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.
- package/.eslintrc.json +14 -0
- package/LICENSE +21 -0
- package/README.md +25 -3
- package/clean-pkg.sh +4 -0
- package/npm-readme.md +4 -0
- package/package.json +10 -1
- package/remove-versions.sh +10 -0
- package/src/clients/base-evernode-client.js +609 -0
- package/src/clients/host-client.js +560 -0
- package/src/clients/registry-client.js +54 -0
- package/src/clients/tenant-client.js +276 -0
- package/src/defaults.js +21 -0
- package/src/eccrypto.js +258 -0
- package/src/encryption-helper.js +96 -0
- package/src/event-emitter.js +45 -0
- package/src/evernode-common.js +113 -0
- package/src/evernode-helpers.js +45 -0
- package/src/firestore/firestore-handler.js +309 -0
- package/src/index.js +37 -0
- package/src/state-helpers.js +396 -0
- package/src/transaction-helper.js +62 -0
- package/src/util-helpers.js +50 -0
- package/src/xfl-helpers.js +130 -0
- package/src/xrpl-account.js +515 -0
- package/src/xrpl-api.js +301 -0
- package/src/xrpl-common.js +17 -0
- package/test/package-lock.json +884 -0
- package/test/package.json +9 -0
- package/test/test.js +409 -0
- package/index.js +0 -15876
@@ -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
|
+
}
|
package/src/defaults.js
ADDED
@@ -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
|
+
}
|
package/src/eccrypto.js
ADDED
@@ -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
|
+
}
|