@mybucks.online/core 2.1.0 → 2.2.1
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/README.md +13 -0
- package/index.js +16 -245
- package/package.json +2 -2
- package/src/credentials.js +137 -0
- package/src/random.js +69 -0
- package/src/token.js +120 -0
- package/test/random.test.js +84 -0
package/README.md
CHANGED
|
@@ -85,6 +85,19 @@ console.log("Network: ", network);
|
|
|
85
85
|
console.log("Legacy token: ", legacy); // true if token was in legacy format
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
### 4. Generate random credentials
|
|
89
|
+
|
|
90
|
+
`randomPassphrase` and `randomPIN` generate cryptographically random credentials that are guaranteed to pass the zxcvbn strength thresholds required by `generateHash` (`PASSPHRASE_MIN_ZXCVBN_SCORE = 3`, `PIN_MIN_ZXCVBN_SCORE = 1`).
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
import { randomPassphrase, randomPIN } from "@mybucks.online/core";
|
|
94
|
+
|
|
95
|
+
const passphrase = randomPassphrase(); // e.g. "Ax3!-bQ2#-mK7@-zP1$"
|
|
96
|
+
const pin = randomPIN(); // e.g. "k4r9w2"
|
|
97
|
+
|
|
98
|
+
const hash = await generateHash(passphrase, pin, showProgress);
|
|
99
|
+
```
|
|
100
|
+
|
|
88
101
|
## Changes (default vs legacy)
|
|
89
102
|
|
|
90
103
|
To make the wallet more secure and resilient against attacks, and to meet standards and follow best practices (e.g. NIST SP 800-132, OWASP, RFC 7914), we introduced a new version that is now the default. A **`legacy`** flag is available for backward compatibility with existing wallets and tokens.
|
package/index.js
CHANGED
|
@@ -1,245 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
r: 8, // block size parameter
|
|
18
|
-
p: 5, // parallelization parameter
|
|
19
|
-
keyLen: 64,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const HASH_OPTIONS = {
|
|
23
|
-
N: 131072, // CPU/memory cost parameter, 2^17, OWASP recommendation
|
|
24
|
-
r: 8, // block size parameter
|
|
25
|
-
p: 1, // parallelization parameter
|
|
26
|
-
keyLen: 64,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export const PASSPHRASE_MIN_LENGTH = 12;
|
|
30
|
-
export const PASSPHRASE_MAX_LENGTH = 128;
|
|
31
|
-
export const PIN_MIN_LENGTH = 6;
|
|
32
|
-
export const PIN_MAX_LENGTH = 16;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* This function computes the scrypt hash using provided passphrase and pin inputs.
|
|
36
|
-
* Passphrase and pin are validated by length (see PASSPHRASE_MIN/MAX_LENGTH, PIN_MIN/MAX_LENGTH) and zxcvbn; invalid or weak values are rejected (returns "").
|
|
37
|
-
*
|
|
38
|
-
* @param {string} passphrase - Length in [PASSPHRASE_MIN_LENGTH, PASSPHRASE_MAX_LENGTH], zxcvbn score >= 3
|
|
39
|
-
* @param {string} pin - Length in [PIN_MIN_LENGTH, PIN_MAX_LENGTH], zxcvbn score >= 1
|
|
40
|
-
* @param {*} cb a callback function designed to receive the progress updates during the scrypt hashing process.
|
|
41
|
-
* @param {boolean} legacy - when true, uses the legacy behavior (HASH_OPTIONS_LEGACY and the original salt generation using the last 4 characters of passphrase plus the full pin); when false, uses HASH_OPTIONS and a keccak256-based salt derived from ABI-encoding passphrase and pin
|
|
42
|
-
* @returns hash result as string format, or "" if passphrase/pin missing or too weak
|
|
43
|
-
*/
|
|
44
|
-
export async function generateHash(
|
|
45
|
-
passphrase,
|
|
46
|
-
pin,
|
|
47
|
-
cb = () => {},
|
|
48
|
-
legacy = false,
|
|
49
|
-
) {
|
|
50
|
-
if (!passphrase || !pin) {
|
|
51
|
-
return "";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const passphraseLen = passphrase.length;
|
|
55
|
-
if (
|
|
56
|
-
passphraseLen < PASSPHRASE_MIN_LENGTH ||
|
|
57
|
-
passphraseLen > PASSPHRASE_MAX_LENGTH
|
|
58
|
-
) {
|
|
59
|
-
return "";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const pinLen = pin.length;
|
|
63
|
-
if (pinLen < PIN_MIN_LENGTH || pinLen > PIN_MAX_LENGTH) {
|
|
64
|
-
return "";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const passphraseResult = zxcvbn(passphrase);
|
|
68
|
-
if (passphraseResult.score < 3) {
|
|
69
|
-
return "";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const pinResult = zxcvbn(pin);
|
|
73
|
-
if (pinResult.score < 1) {
|
|
74
|
-
return "";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const passwordBuffer = Buffer.from(passphrase);
|
|
78
|
-
let saltBuffer;
|
|
79
|
-
|
|
80
|
-
if (legacy) {
|
|
81
|
-
const legacySalt = `${passphrase.slice(-4)}${pin}`;
|
|
82
|
-
saltBuffer = Buffer.from(legacySalt);
|
|
83
|
-
} else {
|
|
84
|
-
const encoded = abi.encode(
|
|
85
|
-
["string", "string", "string"],
|
|
86
|
-
[KDF_DOMAIN_SEPARATOR, passphrase, pin],
|
|
87
|
-
);
|
|
88
|
-
const saltHash = ethers.keccak256(encoded);
|
|
89
|
-
saltBuffer = Buffer.from(saltHash.slice(2), "hex");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const options = legacy ? HASH_OPTIONS_LEGACY : HASH_OPTIONS;
|
|
93
|
-
|
|
94
|
-
const hashBuffer = await scrypt(
|
|
95
|
-
passwordBuffer,
|
|
96
|
-
saltBuffer,
|
|
97
|
-
options.N,
|
|
98
|
-
options.r,
|
|
99
|
-
options.p,
|
|
100
|
-
options.keyLen,
|
|
101
|
-
cb,
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
return Buffer.from(hashBuffer).toString("hex");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* This function derives the EVM private key from a result of the scrypt hash.
|
|
109
|
-
* @param {*} hash scrypt hash result
|
|
110
|
-
* @returns private key as string format
|
|
111
|
-
*/
|
|
112
|
-
export function getEvmPrivateKey(hash) {
|
|
113
|
-
return ethers.keccak256(abi.encode(["string"], [hash]));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* This function returns the EVM wallet address from a result of the scrypt hash.
|
|
118
|
-
* @param {*} hash scrypt hash result
|
|
119
|
-
* @returns address as string format
|
|
120
|
-
*/
|
|
121
|
-
export function getEvmWalletAddress(hash) {
|
|
122
|
-
const privateKey = getEvmPrivateKey(hash);
|
|
123
|
-
const wallet = new ethers.Wallet(privateKey);
|
|
124
|
-
return wallet.address;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* This function returns the TRON wallet address from a result of the scrypt hash.
|
|
129
|
-
* @param {*} hash scrypt hash result
|
|
130
|
-
* @returns address as string format
|
|
131
|
-
*/
|
|
132
|
-
export function getTronWalletAddress(hash) {
|
|
133
|
-
const privateKey = getEvmPrivateKey(hash);
|
|
134
|
-
return TronWeb.address.fromPrivateKey(privateKey.slice(2));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const URL_DELIMITER = "\u0002";
|
|
138
|
-
// Version byte for the default (non-legacy) token format.
|
|
139
|
-
// Uses a compact length-prefixed encoding for passphrase, pin and network.
|
|
140
|
-
const TOKEN_VERSION_COMPACT = 0x02;
|
|
141
|
-
|
|
142
|
-
const NETWORKS = [
|
|
143
|
-
"ethereum",
|
|
144
|
-
"polygon",
|
|
145
|
-
"arbitrum",
|
|
146
|
-
"optimism",
|
|
147
|
-
"bsc",
|
|
148
|
-
"avalanche",
|
|
149
|
-
"base",
|
|
150
|
-
"tron",
|
|
151
|
-
];
|
|
152
|
-
/**
|
|
153
|
-
* This function generates a transfer-link token by encoding passphrase, pin and network, and adding random padding.
|
|
154
|
-
* The transfer-link enables users to send their full ownership of a wallet account to another user for gifting or airdropping.
|
|
155
|
-
* Passphrase and PIN are validated by length (see PASSPHRASE_MIN/MAX_LENGTH, PIN_MIN/MAX_LENGTH) and zxcvbn; invalid or weak values are rejected (returns null).
|
|
156
|
-
* When legacy is false, payload is compact length-prefixed (version 0x02) to avoid concatenation ambiguity and keep the URL fragment short; when true, uses URL_DELIMITER concatenation.
|
|
157
|
-
*
|
|
158
|
-
* @param {string} passphrase - Length in [PASSPHRASE_MIN_LENGTH, PASSPHRASE_MAX_LENGTH], zxcvbn score >= 3
|
|
159
|
-
* @param {string} pin - Length in [PIN_MIN_LENGTH, PIN_MAX_LENGTH], zxcvbn score >= 1
|
|
160
|
-
* @param {string} network - ethereum | polygon | arbitrum | optimism | bsc | avalanche | base | tron
|
|
161
|
-
* @param {boolean} legacy - when true, use URL_DELIMITER concatenation; when false, use compact length-prefixed encoding for the payload
|
|
162
|
-
* @returns A string formatted as a transfer-link token, which can be appended to `https://app.mybucks.online#wallet=`, or null if invalid/weak
|
|
163
|
-
*/
|
|
164
|
-
export function generateToken(passphrase, pin, network, legacy = false) {
|
|
165
|
-
if (!passphrase || !pin || !network) {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
if (!NETWORKS.find((n) => n === network)) {
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const passphraseLen = passphrase.length;
|
|
173
|
-
if (
|
|
174
|
-
passphraseLen < PASSPHRASE_MIN_LENGTH ||
|
|
175
|
-
passphraseLen > PASSPHRASE_MAX_LENGTH
|
|
176
|
-
) {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const pinLen = pin.length;
|
|
181
|
-
if (pinLen < PIN_MIN_LENGTH || pinLen > PIN_MAX_LENGTH) {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (zxcvbn(passphrase).score < 3) {
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
if (zxcvbn(pin).score < 1) {
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
let payloadBuffer;
|
|
193
|
-
if (legacy) {
|
|
194
|
-
payloadBuffer = Buffer.from(
|
|
195
|
-
passphrase + URL_DELIMITER + pin + URL_DELIMITER + network,
|
|
196
|
-
"utf-8",
|
|
197
|
-
);
|
|
198
|
-
} else {
|
|
199
|
-
// Default format: compact length-prefixed encoding.
|
|
200
|
-
const passphraseBytes = Buffer.from(passphrase, "utf-8");
|
|
201
|
-
const pinBytes = Buffer.from(pin, "utf-8");
|
|
202
|
-
const networkBytes = Buffer.from(network, "utf-8");
|
|
203
|
-
|
|
204
|
-
payloadBuffer = Buffer.concat([
|
|
205
|
-
Buffer.from([TOKEN_VERSION_COMPACT]),
|
|
206
|
-
Buffer.from([passphraseBytes.length]),
|
|
207
|
-
passphraseBytes,
|
|
208
|
-
Buffer.from([pinBytes.length]),
|
|
209
|
-
pinBytes,
|
|
210
|
-
Buffer.from([networkBytes.length]),
|
|
211
|
-
networkBytes,
|
|
212
|
-
]);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const base64Encoded = payloadBuffer.toString("base64");
|
|
216
|
-
const padding = nanoid(12);
|
|
217
|
-
return padding.slice(0, 6) + base64Encoded + padding.slice(6);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* This function parses a transfer-link token generated by generateToken().
|
|
222
|
-
* Tokens whose payload starts with 0x02 are decoded as compact length-prefixed; otherwise payload is treated as legacy (UTF-8 + URL_DELIMITER).
|
|
223
|
-
* @param {string} token - token string returned by generateToken()
|
|
224
|
-
* @returns {[string, string, string, boolean]} [passphrase, pin, network, legacy] - legacy is true if token was in legacy format, false if compact-encoded
|
|
225
|
-
*/
|
|
226
|
-
export function parseToken(token) {
|
|
227
|
-
const payload = token.slice(6, token.length - 6);
|
|
228
|
-
const decoded = Buffer.from(payload, "base64");
|
|
229
|
-
|
|
230
|
-
if (decoded[0] === TOKEN_VERSION_COMPACT) {
|
|
231
|
-
let i = 1;
|
|
232
|
-
const lenP = decoded[i++];
|
|
233
|
-
const passphrase = decoded.subarray(i, i + lenP).toString("utf-8");
|
|
234
|
-
i += lenP;
|
|
235
|
-
const lenI = decoded[i++];
|
|
236
|
-
const pin = decoded.subarray(i, i + lenI).toString("utf-8");
|
|
237
|
-
i += lenI;
|
|
238
|
-
const lenN = decoded[i++];
|
|
239
|
-
const network = decoded.subarray(i, i + lenN).toString("utf-8");
|
|
240
|
-
return [passphrase, pin, network, false];
|
|
241
|
-
}
|
|
242
|
-
const str = decoded.toString("utf-8");
|
|
243
|
-
const [passphrase, pin, network] = str.split(URL_DELIMITER);
|
|
244
|
-
return [passphrase, pin, network, true];
|
|
245
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
PASSPHRASE_MIN_ZXCVBN_SCORE,
|
|
3
|
+
PIN_MIN_ZXCVBN_SCORE,
|
|
4
|
+
PASSPHRASE_MIN_LENGTH,
|
|
5
|
+
PASSPHRASE_MAX_LENGTH,
|
|
6
|
+
PIN_MIN_LENGTH,
|
|
7
|
+
PIN_MAX_LENGTH,
|
|
8
|
+
generateHash,
|
|
9
|
+
getEvmPrivateKey,
|
|
10
|
+
getEvmWalletAddress,
|
|
11
|
+
getTronWalletAddress,
|
|
12
|
+
} from "./src/credentials.js";
|
|
13
|
+
|
|
14
|
+
export { generateToken, parseToken } from "./src/token.js";
|
|
15
|
+
|
|
16
|
+
export { randomPassphrase, randomPIN } from "./src/random.js";
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mybucks.online/core",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Core module of Mybucks.online Crypto Wallet",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "node test/index.test.js && node test/legacy.test.js"
|
|
8
|
+
"test": "node test/index.test.js && node test/legacy.test.js && node test/random.test.js"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Buffer } from "buffer";
|
|
2
|
+
import { ethers } from "ethers";
|
|
3
|
+
import scryptJS from "scrypt-js";
|
|
4
|
+
import { TronWeb } from "tronweb";
|
|
5
|
+
import zxcvbn from "zxcvbn";
|
|
6
|
+
|
|
7
|
+
const { scrypt } = scryptJS;
|
|
8
|
+
const abi = new ethers.AbiCoder();
|
|
9
|
+
|
|
10
|
+
// Domain separator for the default (non-legacy) KDF path to prevent
|
|
11
|
+
// cross-protocol and cross-version hash reuse.
|
|
12
|
+
const KDF_DOMAIN_SEPARATOR = "mybucks.online-core.generateHash.v2";
|
|
13
|
+
|
|
14
|
+
const HASH_OPTIONS_LEGACY = {
|
|
15
|
+
N: 32768, // CPU/memory cost parameter, 2^15
|
|
16
|
+
r: 8, // block size parameter
|
|
17
|
+
p: 5, // parallelization parameter
|
|
18
|
+
keyLen: 64,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const HASH_OPTIONS = {
|
|
22
|
+
N: 131072, // CPU/memory cost parameter, 2^17, OWASP recommendation
|
|
23
|
+
r: 8, // block size parameter
|
|
24
|
+
p: 1, // parallelization parameter
|
|
25
|
+
keyLen: 64,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const PASSPHRASE_MIN_ZXCVBN_SCORE = 3;
|
|
29
|
+
export const PIN_MIN_ZXCVBN_SCORE = 1;
|
|
30
|
+
|
|
31
|
+
export const PASSPHRASE_MIN_LENGTH = 12;
|
|
32
|
+
export const PASSPHRASE_MAX_LENGTH = 128;
|
|
33
|
+
export const PIN_MIN_LENGTH = 6;
|
|
34
|
+
export const PIN_MAX_LENGTH = 16;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* This function computes the scrypt hash using provided passphrase and pin inputs.
|
|
38
|
+
* Passphrase and pin are validated by length (see PASSPHRASE_MIN/MAX_LENGTH, PIN_MIN/MAX_LENGTH) and zxcvbn; invalid or weak values are rejected (returns "").
|
|
39
|
+
*
|
|
40
|
+
* @param {string} passphrase - Length in [PASSPHRASE_MIN_LENGTH, PASSPHRASE_MAX_LENGTH], zxcvbn score >= 3
|
|
41
|
+
* @param {string} pin - Length in [PIN_MIN_LENGTH, PIN_MAX_LENGTH], zxcvbn score >= 1
|
|
42
|
+
* @param {*} cb a callback function designed to receive the progress updates during the scrypt hashing process.
|
|
43
|
+
* @param {boolean} legacy - when true, uses the legacy behavior (HASH_OPTIONS_LEGACY and the original salt generation using the last 4 characters of passphrase plus the full pin); when false, uses HASH_OPTIONS and a keccak256-based salt derived from ABI-encoding passphrase and pin
|
|
44
|
+
* @returns hash result as string format, or "" if passphrase/pin missing or too weak
|
|
45
|
+
*/
|
|
46
|
+
export async function generateHash(
|
|
47
|
+
passphrase,
|
|
48
|
+
pin,
|
|
49
|
+
cb = () => {},
|
|
50
|
+
legacy = false,
|
|
51
|
+
) {
|
|
52
|
+
if (!passphrase || !pin) {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const passphraseLen = passphrase.length;
|
|
57
|
+
if (
|
|
58
|
+
passphraseLen < PASSPHRASE_MIN_LENGTH ||
|
|
59
|
+
passphraseLen > PASSPHRASE_MAX_LENGTH
|
|
60
|
+
) {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const pinLen = pin.length;
|
|
65
|
+
if (pinLen < PIN_MIN_LENGTH || pinLen > PIN_MAX_LENGTH) {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const passphraseResult = zxcvbn(passphrase);
|
|
70
|
+
if (passphraseResult.score < PASSPHRASE_MIN_ZXCVBN_SCORE) {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const pinResult = zxcvbn(pin);
|
|
75
|
+
if (pinResult.score < PIN_MIN_ZXCVBN_SCORE) {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const passwordBuffer = Buffer.from(passphrase);
|
|
80
|
+
let saltBuffer;
|
|
81
|
+
|
|
82
|
+
if (legacy) {
|
|
83
|
+
const legacySalt = `${passphrase.slice(-4)}${pin}`;
|
|
84
|
+
saltBuffer = Buffer.from(legacySalt);
|
|
85
|
+
} else {
|
|
86
|
+
const encoded = abi.encode(
|
|
87
|
+
["string", "string", "string"],
|
|
88
|
+
[KDF_DOMAIN_SEPARATOR, passphrase, pin],
|
|
89
|
+
);
|
|
90
|
+
const saltHash = ethers.keccak256(encoded);
|
|
91
|
+
saltBuffer = Buffer.from(saltHash.slice(2), "hex");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const options = legacy ? HASH_OPTIONS_LEGACY : HASH_OPTIONS;
|
|
95
|
+
|
|
96
|
+
const hashBuffer = await scrypt(
|
|
97
|
+
passwordBuffer,
|
|
98
|
+
saltBuffer,
|
|
99
|
+
options.N,
|
|
100
|
+
options.r,
|
|
101
|
+
options.p,
|
|
102
|
+
options.keyLen,
|
|
103
|
+
cb,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return Buffer.from(hashBuffer).toString("hex");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* This function derives the EVM private key from a result of the scrypt hash.
|
|
111
|
+
* @param {*} hash scrypt hash result
|
|
112
|
+
* @returns private key as string format
|
|
113
|
+
*/
|
|
114
|
+
export function getEvmPrivateKey(hash) {
|
|
115
|
+
return ethers.keccak256(abi.encode(["string"], [hash]));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* This function returns the EVM wallet address from a result of the scrypt hash.
|
|
120
|
+
* @param {*} hash scrypt hash result
|
|
121
|
+
* @returns address as string format
|
|
122
|
+
*/
|
|
123
|
+
export function getEvmWalletAddress(hash) {
|
|
124
|
+
const privateKey = getEvmPrivateKey(hash);
|
|
125
|
+
const wallet = new ethers.Wallet(privateKey);
|
|
126
|
+
return wallet.address;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* This function returns the TRON wallet address from a result of the scrypt hash.
|
|
131
|
+
* @param {*} hash scrypt hash result
|
|
132
|
+
* @returns address as string format
|
|
133
|
+
*/
|
|
134
|
+
export function getTronWalletAddress(hash) {
|
|
135
|
+
const privateKey = getEvmPrivateKey(hash);
|
|
136
|
+
return TronWeb.address.fromPrivateKey(privateKey.slice(2));
|
|
137
|
+
}
|
package/src/random.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import zxcvbn from "zxcvbn";
|
|
2
|
+
import {
|
|
3
|
+
PASSPHRASE_MIN_ZXCVBN_SCORE,
|
|
4
|
+
PIN_MIN_ZXCVBN_SCORE,
|
|
5
|
+
} from "./credentials.js";
|
|
6
|
+
|
|
7
|
+
const UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
8
|
+
const LOWER = "abcdefghijklmnopqrstuvwxyz";
|
|
9
|
+
const DIGITS = "0123456789";
|
|
10
|
+
const SYMBOLS = "`~!@#$%^&*()-_+={}[]\\|:;\"'<>,.?/";
|
|
11
|
+
|
|
12
|
+
function randomInt(max) {
|
|
13
|
+
if (max > 255) {
|
|
14
|
+
throw new RangeError(`max must be <= 255, got ${max}`);
|
|
15
|
+
}
|
|
16
|
+
const arr = new Uint8Array(1);
|
|
17
|
+
const limit = 256 - (256 % max);
|
|
18
|
+
do {
|
|
19
|
+
globalThis.crypto.getRandomValues(arr);
|
|
20
|
+
} while (arr[0] >= limit);
|
|
21
|
+
return arr[0] % max;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function randomChar(charset) {
|
|
25
|
+
return charset[randomInt(charset.length)];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function generateSegment(length, charset) {
|
|
29
|
+
return Array.from({ length }, () => randomChar(charset)).join("");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generates a random passphrase in a UUID-inspired hyphen-separated block format (e.g. "xxxx-xxxx-xxxx-xxxx").
|
|
34
|
+
* Each block is composed of characters from uppercase, lowercase, digits, and symbols.
|
|
35
|
+
* Retries recursively until zxcvbn score >= 3 (required by generateHash).
|
|
36
|
+
*
|
|
37
|
+
* @param {number} blockLength - Number of characters per block (default: 4)
|
|
38
|
+
* @param {number} numBlocks - Number of blocks (default: 4)
|
|
39
|
+
* @returns {string} A randomly generated passphrase string
|
|
40
|
+
*/
|
|
41
|
+
export function randomPassphrase(blockLength = 4, numBlocks = 4) {
|
|
42
|
+
const segments = Array.from({ length: numBlocks }, () =>
|
|
43
|
+
generateSegment(blockLength, UPPER + LOWER + DIGITS + SYMBOLS),
|
|
44
|
+
);
|
|
45
|
+
const passphrase = segments.join("-");
|
|
46
|
+
|
|
47
|
+
if (zxcvbn(passphrase).score < PASSPHRASE_MIN_ZXCVBN_SCORE) {
|
|
48
|
+
return randomPassphrase(blockLength, numBlocks);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return passphrase;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generates a random PIN composed of digits and lowercase letters.
|
|
56
|
+
* Retries until the PIN passes zxcvbn score >= 1 (required by generateHash).
|
|
57
|
+
*
|
|
58
|
+
* @param {number} length - Number of characters in the PIN (default: 6)
|
|
59
|
+
* @returns {string} A randomly generated PIN string
|
|
60
|
+
*/
|
|
61
|
+
export function randomPIN(length = 6) {
|
|
62
|
+
const pin = generateSegment(length, DIGITS + LOWER);
|
|
63
|
+
|
|
64
|
+
if (zxcvbn(pin).score < PIN_MIN_ZXCVBN_SCORE) {
|
|
65
|
+
return randomPIN(length);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return pin;
|
|
69
|
+
}
|
package/src/token.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Buffer } from "buffer";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import zxcvbn from "zxcvbn";
|
|
4
|
+
import {
|
|
5
|
+
PASSPHRASE_MIN_LENGTH,
|
|
6
|
+
PASSPHRASE_MAX_LENGTH,
|
|
7
|
+
PIN_MIN_LENGTH,
|
|
8
|
+
PIN_MAX_LENGTH,
|
|
9
|
+
} from "./credentials.js";
|
|
10
|
+
|
|
11
|
+
const LEGACY_URL_DELIMITER = "\u0002";
|
|
12
|
+
// Version byte for the default (non-legacy) token format.
|
|
13
|
+
// Uses a compact length-prefixed encoding for passphrase, pin and network.
|
|
14
|
+
const TOKEN_VERSION_COMPACT = 0x02;
|
|
15
|
+
|
|
16
|
+
const NETWORKS = [
|
|
17
|
+
"ethereum",
|
|
18
|
+
"polygon",
|
|
19
|
+
"arbitrum",
|
|
20
|
+
"optimism",
|
|
21
|
+
"bsc",
|
|
22
|
+
"avalanche",
|
|
23
|
+
"base",
|
|
24
|
+
"tron",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* This function generates a transfer-link token by encoding passphrase, pin and network, and adding random padding.
|
|
29
|
+
* The transfer-link enables users to send their full ownership of a wallet account to another user for gifting or airdropping.
|
|
30
|
+
* Passphrase and PIN are validated by length (see PASSPHRASE_MIN/MAX_LENGTH, PIN_MIN/MAX_LENGTH) and zxcvbn; invalid or weak values are rejected (returns null).
|
|
31
|
+
* When legacy is false, payload is compact length-prefixed (version 0x02) to avoid concatenation ambiguity and keep the URL fragment short; when true, uses LEGACY_URL_DELIMITER concatenation.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} passphrase - Length in [PASSPHRASE_MIN_LENGTH, PASSPHRASE_MAX_LENGTH], zxcvbn score >= 3
|
|
34
|
+
* @param {string} pin - Length in [PIN_MIN_LENGTH, PIN_MAX_LENGTH], zxcvbn score >= 1
|
|
35
|
+
* @param {string} network - ethereum | polygon | arbitrum | optimism | bsc | avalanche | base | tron
|
|
36
|
+
* @param {boolean} legacy - when true, use LEGACY_URL_DELIMITER concatenation; when false, use compact length-prefixed encoding for the payload
|
|
37
|
+
* @returns A string formatted as a transfer-link token, which can be appended to `https://app.mybucks.online#wallet=`, or null if invalid/weak
|
|
38
|
+
*/
|
|
39
|
+
export function generateToken(passphrase, pin, network, legacy = false) {
|
|
40
|
+
if (!passphrase || !pin || !network) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (!NETWORKS.find((n) => n === network)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const passphraseLen = passphrase.length;
|
|
48
|
+
if (
|
|
49
|
+
passphraseLen < PASSPHRASE_MIN_LENGTH ||
|
|
50
|
+
passphraseLen > PASSPHRASE_MAX_LENGTH
|
|
51
|
+
) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const pinLen = pin.length;
|
|
56
|
+
if (pinLen < PIN_MIN_LENGTH || pinLen > PIN_MAX_LENGTH) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (zxcvbn(passphrase).score < 3) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (zxcvbn(pin).score < 1) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let payloadBuffer;
|
|
68
|
+
if (legacy) {
|
|
69
|
+
payloadBuffer = Buffer.from(
|
|
70
|
+
passphrase + LEGACY_URL_DELIMITER + pin + LEGACY_URL_DELIMITER + network,
|
|
71
|
+
"utf-8",
|
|
72
|
+
);
|
|
73
|
+
} else {
|
|
74
|
+
// Default format: compact length-prefixed encoding.
|
|
75
|
+
const passphraseBytes = Buffer.from(passphrase, "utf-8");
|
|
76
|
+
const pinBytes = Buffer.from(pin, "utf-8");
|
|
77
|
+
const networkBytes = Buffer.from(network, "utf-8");
|
|
78
|
+
|
|
79
|
+
payloadBuffer = Buffer.concat([
|
|
80
|
+
Buffer.from([TOKEN_VERSION_COMPACT]),
|
|
81
|
+
Buffer.from([passphraseBytes.length]),
|
|
82
|
+
passphraseBytes,
|
|
83
|
+
Buffer.from([pinBytes.length]),
|
|
84
|
+
pinBytes,
|
|
85
|
+
Buffer.from([networkBytes.length]),
|
|
86
|
+
networkBytes,
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const base64Encoded = payloadBuffer.toString("base64");
|
|
91
|
+
const padding = nanoid(12);
|
|
92
|
+
return padding.slice(0, 6) + base64Encoded + padding.slice(6);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* This function parses a transfer-link token generated by generateToken().
|
|
97
|
+
* Tokens whose payload starts with 0x02 are decoded as compact length-prefixed; otherwise payload is treated as legacy (UTF-8 + LEGACY_URL_DELIMITER).
|
|
98
|
+
* @param {string} token - token string returned by generateToken()
|
|
99
|
+
* @returns {[string, string, string, boolean]} [passphrase, pin, network, legacy] - legacy is true if token was in legacy format, false if compact-encoded
|
|
100
|
+
*/
|
|
101
|
+
export function parseToken(token) {
|
|
102
|
+
const payload = token.slice(6, token.length - 6);
|
|
103
|
+
const decoded = Buffer.from(payload, "base64");
|
|
104
|
+
|
|
105
|
+
if (decoded[0] === TOKEN_VERSION_COMPACT) {
|
|
106
|
+
let i = 1;
|
|
107
|
+
const lenP = decoded[i++];
|
|
108
|
+
const passphrase = decoded.subarray(i, i + lenP).toString("utf-8");
|
|
109
|
+
i += lenP;
|
|
110
|
+
const lenI = decoded[i++];
|
|
111
|
+
const pin = decoded.subarray(i, i + lenI).toString("utf-8");
|
|
112
|
+
i += lenI;
|
|
113
|
+
const lenN = decoded[i++];
|
|
114
|
+
const network = decoded.subarray(i, i + lenN).toString("utf-8");
|
|
115
|
+
return [passphrase, pin, network, false];
|
|
116
|
+
}
|
|
117
|
+
const str = decoded.toString("utf-8");
|
|
118
|
+
const [passphrase, pin, network] = str.split(LEGACY_URL_DELIMITER);
|
|
119
|
+
return [passphrase, pin, network, true];
|
|
120
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import zxcvbn from "zxcvbn";
|
|
4
|
+
import { randomPassphrase, randomPIN } from "../index.js";
|
|
5
|
+
import {
|
|
6
|
+
PASSPHRASE_MIN_ZXCVBN_SCORE,
|
|
7
|
+
PIN_MIN_ZXCVBN_SCORE,
|
|
8
|
+
} from "../src/credentials.js";
|
|
9
|
+
|
|
10
|
+
describe("randomPassphrase", () => {
|
|
11
|
+
test("should return a string", () => {
|
|
12
|
+
const passphrase = randomPassphrase();
|
|
13
|
+
assert.strictEqual(typeof passphrase, "string");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("should have correct total length", () => {
|
|
17
|
+
const passphrase = randomPassphrase(4, 4);
|
|
18
|
+
// blockLength * numBlocks + (numBlocks - 1) hyphens as separators
|
|
19
|
+
assert.strictEqual(passphrase.length, 4 * 4 + 3);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("should respect custom blockLength and numBlocks", () => {
|
|
23
|
+
const passphrase = randomPassphrase(6, 3);
|
|
24
|
+
assert.strictEqual(passphrase.length, 6 * 3 + 2);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("should have zxcvbn score >= 3", () => {
|
|
28
|
+
const passphrase = randomPassphrase();
|
|
29
|
+
assert.ok(
|
|
30
|
+
zxcvbn(passphrase).score >= PASSPHRASE_MIN_ZXCVBN_SCORE,
|
|
31
|
+
`passphrase zxcvbn score is below ${PASSPHRASE_MIN_ZXCVBN_SCORE}`,
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("should return different values on each call", () => {
|
|
36
|
+
const results = Array.from({ length: 10 }, () => randomPassphrase());
|
|
37
|
+
results.forEach((p, i) => console.log(` passphrase[${i}]: ${p}`));
|
|
38
|
+
assert.strictEqual(
|
|
39
|
+
new Set(results).size,
|
|
40
|
+
results.length,
|
|
41
|
+
"randomPassphrase returned duplicate values",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("randomPIN", () => {
|
|
47
|
+
test("should return a string", () => {
|
|
48
|
+
const pin = randomPIN();
|
|
49
|
+
assert.strictEqual(typeof pin, "string");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("should default to length 6", () => {
|
|
53
|
+
const pin = randomPIN();
|
|
54
|
+
assert.strictEqual(pin.length, 6);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("should respect custom length", () => {
|
|
58
|
+
const pin = randomPIN(10);
|
|
59
|
+
assert.strictEqual(pin.length, 10);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("should only contain digits and lowercase letters", () => {
|
|
63
|
+
const pin = randomPIN(32);
|
|
64
|
+
assert.ok(/^[0-9a-z]+$/.test(pin), "PIN contains invalid characters");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("should have zxcvbn score >= 1", () => {
|
|
68
|
+
const pin = randomPIN();
|
|
69
|
+
assert.ok(
|
|
70
|
+
zxcvbn(pin).score >= PIN_MIN_ZXCVBN_SCORE,
|
|
71
|
+
`PIN zxcvbn score is below ${PIN_MIN_ZXCVBN_SCORE}`,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("should return different values on each call", () => {
|
|
76
|
+
const results = Array.from({ length: 10 }, () => randomPIN());
|
|
77
|
+
results.forEach((p, i) => console.log(` pin[${i}]: ${p}`));
|
|
78
|
+
assert.strictEqual(
|
|
79
|
+
new Set(results).size,
|
|
80
|
+
results.length,
|
|
81
|
+
"randomPIN returned duplicate values",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
});
|