@mybucks.online/core 1.1.2 → 2.0.0
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 +21 -1
- package/index.js +105 -31
- package/package.json +2 -2
- package/test/index.test.js +185 -58
- package/test/legacy.test.js +91 -0
package/README.md
CHANGED
|
@@ -79,11 +79,31 @@ console.log("https://app.mybucks.online/#wallet=" + token);
|
|
|
79
79
|
|
|
80
80
|
```javascript
|
|
81
81
|
import { parseToken } from "@mybucks.online/core";
|
|
82
|
-
const [passphrase, pin, network] = parseToken(token);
|
|
82
|
+
const [passphrase, pin, network, legacy] = parseToken(token);
|
|
83
83
|
console.log("Account credentials are: ", passphrase, pin);
|
|
84
84
|
console.log("Network: ", network);
|
|
85
|
+
console.log("Legacy token: ", legacy); // true if token was in legacy format
|
|
85
86
|
```
|
|
86
87
|
|
|
88
|
+
## Changes (default vs legacy)
|
|
89
|
+
|
|
90
|
+
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.
|
|
91
|
+
|
|
92
|
+
**Scrypt parameters (default)**
|
|
93
|
+
- **N** is increased from 2^15 to **2^17** to raise the memory cost and make GPU/ASIC brute-force attacks much harder.
|
|
94
|
+
- **p** is reduced from 5 to **1** so hashing time stays the same or lower for users while resistance to brute-force is improved.
|
|
95
|
+
|
|
96
|
+
**Salt generation (default)**
|
|
97
|
+
- Legacy used only the **last 4 characters of the passphrase** plus the pin, which discarded most of the passphrase entropy.
|
|
98
|
+
- The default now derives the salt from the **full passphrase and pin** via a structured encoding and adds a **domain separator** so hashes are bound to this KDF and not reusable in other protocols or versions.
|
|
99
|
+
|
|
100
|
+
**Token generation (default)**
|
|
101
|
+
- Legacy encoded the transfer-link token by **concatenating** passphrase, pin and network with a delimiter, which is ambiguous for some inputs.
|
|
102
|
+
- The default uses **ABI encoding** for the payload so there is no concatenation ambiguity.
|
|
103
|
+
- `parseToken` accepts both legacy and default token formats automatically and returns `[passphrase, pin, network, legacy]`, where `legacy` is `true` if the token was in legacy format.
|
|
104
|
+
|
|
105
|
+
Use `generateHash(passphrase, pin, cb, true)` or `generateToken(passphrase, pin, network, true)` only when you need to match existing legacy wallets or tokens.
|
|
106
|
+
|
|
87
107
|
## Test
|
|
88
108
|
```bash
|
|
89
109
|
npm run test
|
package/index.js
CHANGED
|
@@ -8,13 +8,24 @@ import zxcvbn from "zxcvbn";
|
|
|
8
8
|
const { scrypt } = scryptJS;
|
|
9
9
|
const abi = new ethers.AbiCoder();
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// Domain separator for the default (non-legacy) KDF path to prevent
|
|
12
|
+
// cross-protocol and cross-version hash reuse.
|
|
13
|
+
const KDF_DOMAIN_SEPARATOR = "mybucks.online-core.generateHash.v2";
|
|
14
|
+
|
|
15
|
+
const HASH_OPTIONS_LEGACY = {
|
|
12
16
|
N: 32768, // CPU/memory cost parameter, 2^15
|
|
13
17
|
r: 8, // block size parameter
|
|
14
18
|
p: 5, // parallelization parameter
|
|
15
19
|
keyLen: 64,
|
|
16
20
|
};
|
|
17
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
|
+
|
|
18
29
|
export const PASSPHRASE_MIN_LENGTH = 12;
|
|
19
30
|
export const PASSPHRASE_MAX_LENGTH = 128;
|
|
20
31
|
export const PIN_MIN_LENGTH = 6;
|
|
@@ -24,22 +35,31 @@ export const PIN_MAX_LENGTH = 16;
|
|
|
24
35
|
* This function computes the scrypt hash using provided passphrase and pin inputs.
|
|
25
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 "").
|
|
26
37
|
*
|
|
27
|
-
* @param {
|
|
28
|
-
* @param {
|
|
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
|
|
29
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
|
|
30
42
|
* @returns hash result as string format, or "" if passphrase/pin missing or too weak
|
|
31
43
|
*/
|
|
32
|
-
export async function generateHash(
|
|
44
|
+
export async function generateHash(
|
|
45
|
+
passphrase,
|
|
46
|
+
pin,
|
|
47
|
+
cb = () => {},
|
|
48
|
+
legacy = false,
|
|
49
|
+
) {
|
|
33
50
|
if (!passphrase || !pin) {
|
|
34
51
|
return "";
|
|
35
52
|
}
|
|
36
53
|
|
|
37
|
-
const passphraseLen =
|
|
38
|
-
if (
|
|
54
|
+
const passphraseLen = passphrase.length;
|
|
55
|
+
if (
|
|
56
|
+
passphraseLen < PASSPHRASE_MIN_LENGTH ||
|
|
57
|
+
passphraseLen > PASSPHRASE_MAX_LENGTH
|
|
58
|
+
) {
|
|
39
59
|
return "";
|
|
40
60
|
}
|
|
41
61
|
|
|
42
|
-
const pinLen =
|
|
62
|
+
const pinLen = pin.length;
|
|
43
63
|
if (pinLen < PIN_MIN_LENGTH || pinLen > PIN_MAX_LENGTH) {
|
|
44
64
|
return "";
|
|
45
65
|
}
|
|
@@ -54,19 +74,31 @@ export async function generateHash(passphrase, pin, cb = () => {}) {
|
|
|
54
74
|
return "";
|
|
55
75
|
}
|
|
56
76
|
|
|
57
|
-
const salt = `${passphrase.slice(-4)}${pin}`;
|
|
58
|
-
|
|
59
77
|
const passwordBuffer = Buffer.from(passphrase);
|
|
60
|
-
|
|
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;
|
|
61
93
|
|
|
62
94
|
const hashBuffer = await scrypt(
|
|
63
95
|
passwordBuffer,
|
|
64
96
|
saltBuffer,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
cb
|
|
97
|
+
options.N,
|
|
98
|
+
options.r,
|
|
99
|
+
options.p,
|
|
100
|
+
options.keyLen,
|
|
101
|
+
cb,
|
|
70
102
|
);
|
|
71
103
|
|
|
72
104
|
return Buffer.from(hashBuffer).toString("hex");
|
|
@@ -103,6 +135,8 @@ export function getTronWalletAddress(hash) {
|
|
|
103
135
|
}
|
|
104
136
|
|
|
105
137
|
const URL_DELIMITER = "\u0002";
|
|
138
|
+
const TOKEN_VERSION_ABI = 0x02;
|
|
139
|
+
|
|
106
140
|
const NETWORKS = [
|
|
107
141
|
"ethereum",
|
|
108
142
|
"polygon",
|
|
@@ -114,16 +148,18 @@ const NETWORKS = [
|
|
|
114
148
|
"tron",
|
|
115
149
|
];
|
|
116
150
|
/**
|
|
117
|
-
* This function generates a transfer-link token by encoding passphrase and
|
|
151
|
+
* This function generates a transfer-link token by encoding passphrase, pin and network, and adding random padding.
|
|
118
152
|
* The transfer-link enables users to send their full ownership of a wallet account to another user for gifting or airdropping.
|
|
119
|
-
* Passphrase and PIN are validated
|
|
153
|
+
* 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).
|
|
154
|
+
* When legacy is false, payload is ABI-encoded to avoid concatenation ambiguity; when true, uses URL_DELIMITER concatenation.
|
|
120
155
|
*
|
|
121
|
-
* @param {
|
|
122
|
-
* @param {
|
|
123
|
-
* @param {
|
|
156
|
+
* @param {string} passphrase - Length in [PASSPHRASE_MIN_LENGTH, PASSPHRASE_MAX_LENGTH], zxcvbn score >= 3
|
|
157
|
+
* @param {string} pin - Length in [PIN_MIN_LENGTH, PIN_MAX_LENGTH], zxcvbn score >= 1
|
|
158
|
+
* @param {string} network - ethereum | polygon | arbitrum | optimism | bsc | avalanche | base | tron
|
|
159
|
+
* @param {boolean} legacy - when true, use URL_DELIMITER concatenation; when false, use ABI encoding for the payload
|
|
124
160
|
* @returns A string formatted as a transfer-link token, which can be appended to `https://app.mybucks.online#wallet=`, or null if invalid/weak
|
|
125
161
|
*/
|
|
126
|
-
export function generateToken(passphrase, pin, network) {
|
|
162
|
+
export function generateToken(passphrase, pin, network, legacy = false) {
|
|
127
163
|
if (!passphrase || !pin || !network) {
|
|
128
164
|
return null;
|
|
129
165
|
}
|
|
@@ -131,6 +167,19 @@ export function generateToken(passphrase, pin, network) {
|
|
|
131
167
|
return null;
|
|
132
168
|
}
|
|
133
169
|
|
|
170
|
+
const passphraseLen = passphrase.length;
|
|
171
|
+
if (
|
|
172
|
+
passphraseLen < PASSPHRASE_MIN_LENGTH ||
|
|
173
|
+
passphraseLen > PASSPHRASE_MAX_LENGTH
|
|
174
|
+
) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const pinLen = pin.length;
|
|
179
|
+
if (pinLen < PIN_MIN_LENGTH || pinLen > PIN_MAX_LENGTH) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
134
183
|
if (zxcvbn(passphrase).score < 3) {
|
|
135
184
|
return null;
|
|
136
185
|
}
|
|
@@ -138,23 +187,48 @@ export function generateToken(passphrase, pin, network) {
|
|
|
138
187
|
return null;
|
|
139
188
|
}
|
|
140
189
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
190
|
+
let payloadBuffer;
|
|
191
|
+
if (legacy) {
|
|
192
|
+
payloadBuffer = Buffer.from(
|
|
193
|
+
passphrase + URL_DELIMITER + pin + URL_DELIMITER + network,
|
|
194
|
+
"utf-8",
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
const encoded = abi.encode(
|
|
198
|
+
["string", "string", "string"],
|
|
199
|
+
[passphrase, pin, network],
|
|
200
|
+
);
|
|
201
|
+
const encodedBuffer = Buffer.from(encoded.slice(2), "hex");
|
|
202
|
+
payloadBuffer = Buffer.concat([
|
|
203
|
+
Buffer.from([TOKEN_VERSION_ABI]),
|
|
204
|
+
encodedBuffer,
|
|
205
|
+
]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const base64Encoded = payloadBuffer.toString("base64");
|
|
146
209
|
const padding = nanoid(12);
|
|
147
210
|
return padding.slice(0, 6) + base64Encoded + padding.slice(6);
|
|
148
211
|
}
|
|
149
212
|
|
|
150
213
|
/**
|
|
151
214
|
* This function parses a transfer-link token generated by generateToken().
|
|
152
|
-
*
|
|
153
|
-
* @
|
|
215
|
+
* Tokens with version byte 0x02 are ABI-decoded; otherwise payload is treated as legacy (UTF-8 + URL_DELIMITER).
|
|
216
|
+
* @param {string} token - token string returned by generateToken()
|
|
217
|
+
* @returns {[string, string, string, boolean]} [passphrase, pin, network, legacy] - legacy is true if token was in legacy format, false if ABI-encoded
|
|
154
218
|
*/
|
|
155
219
|
export function parseToken(token) {
|
|
156
220
|
const payload = token.slice(6, token.length - 6);
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
221
|
+
const decoded = Buffer.from(payload, "base64");
|
|
222
|
+
|
|
223
|
+
if (decoded[0] === TOKEN_VERSION_ABI) {
|
|
224
|
+
const hex = "0x" + decoded.subarray(1).toString("hex");
|
|
225
|
+
const [passphrase, pin, network] = abi.decode(
|
|
226
|
+
["string", "string", "string"],
|
|
227
|
+
hex,
|
|
228
|
+
);
|
|
229
|
+
return [passphrase, pin, network, false];
|
|
230
|
+
}
|
|
231
|
+
const str = decoded.toString("utf-8");
|
|
232
|
+
const [passphrase, pin, network] = str.split(URL_DELIMITER);
|
|
233
|
+
return [passphrase, pin, network, true];
|
|
160
234
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mybucks.online/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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"
|
|
8
|
+
"test": "node test/index.test.js && node test/legacy.test.js"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
package/test/index.test.js
CHANGED
|
@@ -14,16 +14,15 @@ const DEMO_PIN = "112324";
|
|
|
14
14
|
const DEMO_NETWORK = "optimism";
|
|
15
15
|
|
|
16
16
|
const DEMO_HASH =
|
|
17
|
-
"
|
|
17
|
+
"70198936dedf67b784a0a7271fca9e562467754eb012a8c9f3a97aaf4e2be725b0995ebfc9112a18395214b3357368f7e2812a23d013c2b42dec0701bc44dd68";
|
|
18
18
|
const DEMO_PRIVATE_KEY =
|
|
19
|
-
"
|
|
20
|
-
const DEMO_WALLET_EVM_ADDRESS = "
|
|
21
|
-
const DEMO_WALLET_TRON_ADDRESS = "
|
|
19
|
+
"0x942d4f1a0e4bc3b525db81eacba59131ceef498e401142b821613e20539b89e7";
|
|
20
|
+
const DEMO_WALLET_EVM_ADDRESS = "0xC3Bb18Ed137577e5482fA7C6AEaca8b3F68Dafba";
|
|
21
|
+
const DEMO_WALLET_TRON_ADDRESS = "TTp8uDeig42XdefAoTGWTj61uNMYvEVnXR";
|
|
22
22
|
|
|
23
|
-
const
|
|
24
|
-
"VWnsSGRGVtb0FjY291bnQ1JgIxMTIzMjQCb3B0aW1pc20=_wNovT";
|
|
23
|
+
const DEMO_LEGACY_TOKEN = "VWnsSGRGVtb0FjY291bnQ1JgIxMTIzMjQCb3B0aW1pc20=_wNovT";
|
|
25
24
|
|
|
26
|
-
describe("generateHash", () => {
|
|
25
|
+
describe("generateHash (default)", () => {
|
|
27
26
|
test("should return empty string if passphrase or pin is blank", async () => {
|
|
28
27
|
const hash = await generateHash("", "");
|
|
29
28
|
assert.strictEqual(hash, "");
|
|
@@ -63,35 +62,80 @@ describe("generateHash", () => {
|
|
|
63
62
|
}
|
|
64
63
|
});
|
|
65
64
|
|
|
66
|
-
test("should return
|
|
65
|
+
test("should return empty string if passphrase length is out of range", async () => {
|
|
66
|
+
const tooShort = "Abc!23xy"; // strong-ish but below min length
|
|
67
|
+
const tooLong = "Str0ng-and-L0ng-Passphrase-!".repeat(6); // strong but above max length
|
|
68
|
+
|
|
69
|
+
const hashShort = await generateHash(tooShort, DEMO_PIN);
|
|
70
|
+
const hashLong = await generateHash(tooLong, DEMO_PIN);
|
|
71
|
+
|
|
72
|
+
assert.strictEqual(hashShort, "");
|
|
73
|
+
assert.strictEqual(hashLong, "");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("should return empty string if pin length is out of range", async () => {
|
|
77
|
+
const tooShort = "12aB!"; // strong-ish but below min length
|
|
78
|
+
const tooLong = "9aB!9aB!9aB!9aB!9"; // strong but above max length (> 16 chars)
|
|
79
|
+
|
|
80
|
+
const hashShort = await generateHash(DEMO_PASSPHRASE, tooShort);
|
|
81
|
+
const hashLong = await generateHash(DEMO_PASSPHRASE, tooLong);
|
|
82
|
+
|
|
83
|
+
assert.strictEqual(hashShort, "");
|
|
84
|
+
assert.strictEqual(hashLong, "");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("should return valid scrypt hash result", async () => {
|
|
67
88
|
const hash = await generateHash(DEMO_PASSPHRASE, DEMO_PIN);
|
|
68
89
|
assert.strictEqual(hash, DEMO_HASH);
|
|
69
90
|
});
|
|
91
|
+
|
|
92
|
+
test("should return same hash for same passphrase and pin (deterministic)", async () => {
|
|
93
|
+
const hash1 = await generateHash(DEMO_PASSPHRASE, DEMO_PIN);
|
|
94
|
+
const hash2 = await generateHash(DEMO_PASSPHRASE, DEMO_PIN);
|
|
95
|
+
assert.strictEqual(hash1, hash2);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("should generate different results for same naive concatenation", async () => {
|
|
99
|
+
// These two pairs share the same concatenated string
|
|
100
|
+
// passphrase1 + pin1 === passphrase2 + pin2
|
|
101
|
+
const passphrase1 = "My-1st-car-was-a-red-Ford-2005!";
|
|
102
|
+
const pin1 = "909011";
|
|
103
|
+
const passphrase2 = "My-1st-car-was-a-red-Ford-";
|
|
104
|
+
const pin2 = "2005!909011";
|
|
105
|
+
|
|
106
|
+
const hash1 = await generateHash(passphrase1, pin1);
|
|
107
|
+
const hash2 = await generateHash(passphrase2, pin2);
|
|
108
|
+
|
|
109
|
+
assert.notStrictEqual(hash1, "");
|
|
110
|
+
assert.notStrictEqual(hash2, "");
|
|
111
|
+
assert.notStrictEqual(
|
|
112
|
+
hash1,
|
|
113
|
+
hash2,
|
|
114
|
+
"hashes must differ even when naive concatenation matches"
|
|
115
|
+
);
|
|
116
|
+
});
|
|
70
117
|
});
|
|
71
118
|
|
|
72
|
-
describe("getEvmPrivateKey", () => {
|
|
73
|
-
test("should return 256bit private key", async () => {
|
|
119
|
+
describe("getEvmPrivateKey (default)", () => {
|
|
120
|
+
test("should return 256bit private key from default hash", async () => {
|
|
74
121
|
const hash = await generateHash(DEMO_PASSPHRASE, DEMO_PIN);
|
|
75
122
|
const privateKey = getEvmPrivateKey(hash);
|
|
76
|
-
|
|
77
123
|
assert.strictEqual(privateKey, DEMO_PRIVATE_KEY);
|
|
78
124
|
});
|
|
79
125
|
});
|
|
80
126
|
|
|
81
|
-
describe("getEvmWalletAddress", () => {
|
|
82
|
-
test("should return a valid wallet address", async () => {
|
|
127
|
+
describe("getEvmWalletAddress (default)", () => {
|
|
128
|
+
test("should return a valid wallet address from default hash", async () => {
|
|
83
129
|
const hash = await generateHash(DEMO_PASSPHRASE, DEMO_PIN);
|
|
84
130
|
const address = getEvmWalletAddress(hash);
|
|
85
|
-
|
|
86
131
|
assert.strictEqual(address, DEMO_WALLET_EVM_ADDRESS);
|
|
87
132
|
});
|
|
88
133
|
});
|
|
89
134
|
|
|
90
|
-
describe("getTronWalletAddress", () => {
|
|
91
|
-
test("should return a valid wallet address", async () => {
|
|
135
|
+
describe("getTronWalletAddress (default)", () => {
|
|
136
|
+
test("should return a valid TRON wallet address from default hash", async () => {
|
|
92
137
|
const hash = await generateHash(DEMO_PASSPHRASE, DEMO_PIN);
|
|
93
138
|
const address = getTronWalletAddress(hash);
|
|
94
|
-
|
|
95
139
|
assert.strictEqual(address, DEMO_WALLET_TRON_ADDRESS);
|
|
96
140
|
});
|
|
97
141
|
});
|
|
@@ -105,6 +149,20 @@ describe("generateToken", () => {
|
|
|
105
149
|
assert.strictEqual(generateToken("passphrase", "123456", "invalid"), null);
|
|
106
150
|
});
|
|
107
151
|
|
|
152
|
+
test("should return null if passphrase length is out of range", () => {
|
|
153
|
+
const tooShort = "Abc!23xy"; // strong-ish but below min length
|
|
154
|
+
const tooLong = "Str0ng-and-L0ng-Passphrase-!".repeat(6); // strong but above max length
|
|
155
|
+
assert.strictEqual(generateToken(tooShort, DEMO_PIN, DEMO_NETWORK), null);
|
|
156
|
+
assert.strictEqual(generateToken(tooLong, DEMO_PIN, DEMO_NETWORK), null);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("should return null if pin length is out of range", () => {
|
|
160
|
+
const tooShort = "12aB!"; // strong-ish but below min length
|
|
161
|
+
const tooLong = "9aB!9aB!9aB!9aB!9"; // strong but above max length (> 16 chars)
|
|
162
|
+
assert.strictEqual(generateToken(DEMO_PASSPHRASE, tooShort, DEMO_NETWORK), null);
|
|
163
|
+
assert.strictEqual(generateToken(DEMO_PASSPHRASE, tooLong, DEMO_NETWORK), null);
|
|
164
|
+
});
|
|
165
|
+
|
|
108
166
|
test("should return null for weak passphrase (zxcvbn score < 3)", () => {
|
|
109
167
|
const weakPassphrases = [
|
|
110
168
|
"password",
|
|
@@ -141,68 +199,137 @@ describe("generateToken", () => {
|
|
|
141
199
|
}
|
|
142
200
|
});
|
|
143
201
|
|
|
144
|
-
test("should return valid token",
|
|
145
|
-
const token = generateToken(DEMO_PASSPHRASE, DEMO_PIN, DEMO_NETWORK);
|
|
202
|
+
test("should return valid token with padding (legacy=false)", () => {
|
|
203
|
+
const token = generateToken(DEMO_PASSPHRASE, DEMO_PIN, DEMO_NETWORK, false);
|
|
204
|
+
assert.ok(token !== null);
|
|
205
|
+
assert.ok(token.length >= 6 + 6, "token has 6-char prefix and suffix padding");
|
|
206
|
+
});
|
|
146
207
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
);
|
|
208
|
+
test("should return valid token with padding (legacy=true)", () => {
|
|
209
|
+
const token = generateToken(DEMO_PASSPHRASE, DEMO_PIN, DEMO_NETWORK, true);
|
|
210
|
+
assert.ok(token !== null);
|
|
211
|
+
assert.ok(token.length >= 6 + 6, "token has 6-char prefix and suffix padding");
|
|
152
212
|
});
|
|
153
213
|
|
|
154
|
-
test("should return
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
);
|
|
159
|
-
assert.notStrictEqual(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
generateToken(DEMO_PASSPHRASE, DEMO_PIN, "bsc"),
|
|
173
|
-
null
|
|
174
|
-
);
|
|
175
|
-
assert.notStrictEqual(
|
|
176
|
-
generateToken(DEMO_PASSPHRASE, DEMO_PIN, "avalanche"),
|
|
177
|
-
null
|
|
178
|
-
);
|
|
179
|
-
assert.notStrictEqual(
|
|
180
|
-
generateToken(DEMO_PASSPHRASE, DEMO_PIN, "base"),
|
|
181
|
-
null
|
|
214
|
+
test("should return different token for same inputs when legacy true vs false", () => {
|
|
215
|
+
const tokenLegacy = generateToken(DEMO_PASSPHRASE, DEMO_PIN, DEMO_NETWORK, true);
|
|
216
|
+
const tokenAbi = generateToken(DEMO_PASSPHRASE, DEMO_PIN, DEMO_NETWORK, false);
|
|
217
|
+
const payloadLegacy = tokenLegacy.slice(6, tokenLegacy.length - 6);
|
|
218
|
+
const payloadAbi = tokenAbi.slice(6, tokenAbi.length - 6);
|
|
219
|
+
assert.notStrictEqual(payloadLegacy, payloadAbi, "payloads must differ (legacy vs ABI encoding)");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("should generate different results for same naive concatenation (legacy=false)", () => {
|
|
223
|
+
const network = "polygon";
|
|
224
|
+
const passphrase1 = "My-1st-car-was-a-red-Ford-2005!";
|
|
225
|
+
const pin1 = "909011";
|
|
226
|
+
const passphrase2 = "My-1st-car-was-a-red-Ford-";
|
|
227
|
+
const pin2 = "2005!909011";
|
|
228
|
+
assert.strictEqual(
|
|
229
|
+
passphrase1 + pin1 + network,
|
|
230
|
+
passphrase2 + pin2 + network,
|
|
231
|
+
"same naive concatenation"
|
|
182
232
|
);
|
|
233
|
+
|
|
234
|
+
const token1 = generateToken(passphrase1, pin1, network, false);
|
|
235
|
+
const token2 = generateToken(passphrase2, pin2, network, false);
|
|
236
|
+
|
|
237
|
+
assert.ok(token1 !== null);
|
|
238
|
+
assert.ok(token2 !== null);
|
|
239
|
+
const payload1 = token1.slice(6, token1.length - 6);
|
|
240
|
+
const payload2 = token2.slice(6, token2.length - 6);
|
|
183
241
|
assert.notStrictEqual(
|
|
184
|
-
|
|
185
|
-
|
|
242
|
+
payload1,
|
|
243
|
+
payload2,
|
|
244
|
+
"payloads must differ even when naive concatenation matches"
|
|
186
245
|
);
|
|
187
246
|
});
|
|
247
|
+
|
|
248
|
+
test("should return valid token for all networks (legacy=false)", () => {
|
|
249
|
+
const networks = [
|
|
250
|
+
"ethereum",
|
|
251
|
+
"polygon",
|
|
252
|
+
"arbitrum",
|
|
253
|
+
"optimism",
|
|
254
|
+
"bsc",
|
|
255
|
+
"avalanche",
|
|
256
|
+
"base",
|
|
257
|
+
"tron",
|
|
258
|
+
];
|
|
259
|
+
for (const network of networks) {
|
|
260
|
+
assert.notStrictEqual(
|
|
261
|
+
generateToken(DEMO_PASSPHRASE, DEMO_PIN, network, false),
|
|
262
|
+
null
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("should return valid token for all networks (legacy=true)", () => {
|
|
268
|
+
const networks = [
|
|
269
|
+
"ethereum",
|
|
270
|
+
"polygon",
|
|
271
|
+
"arbitrum",
|
|
272
|
+
"optimism",
|
|
273
|
+
"bsc",
|
|
274
|
+
"avalanche",
|
|
275
|
+
"base",
|
|
276
|
+
"tron",
|
|
277
|
+
];
|
|
278
|
+
for (const network of networks) {
|
|
279
|
+
assert.notStrictEqual(
|
|
280
|
+
generateToken(DEMO_PASSPHRASE, DEMO_PIN, network, true),
|
|
281
|
+
null
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
188
285
|
});
|
|
189
286
|
|
|
190
287
|
describe("parseToken", () => {
|
|
191
|
-
test("should return
|
|
192
|
-
const
|
|
288
|
+
test("should return [passphrase, pin, network, legacy] for token generated with legacy=false", () => {
|
|
289
|
+
const token = generateToken(DEMO_PASSPHRASE, DEMO_PIN, DEMO_NETWORK, false);
|
|
290
|
+
const [passphrase, pin, network, legacy] = parseToken(token);
|
|
291
|
+
assert.strictEqual(passphrase, DEMO_PASSPHRASE);
|
|
292
|
+
assert.strictEqual(pin, DEMO_PIN);
|
|
293
|
+
assert.strictEqual(network, DEMO_NETWORK);
|
|
294
|
+
assert.strictEqual(legacy, false);
|
|
295
|
+
});
|
|
193
296
|
|
|
297
|
+
test("should return [passphrase, pin, network, legacy] for token generated with legacy=true", () => {
|
|
298
|
+
const token = generateToken(DEMO_PASSPHRASE, DEMO_PIN, DEMO_NETWORK, true);
|
|
299
|
+
const [passphrase, pin, network, legacy] = parseToken(token);
|
|
194
300
|
assert.strictEqual(passphrase, DEMO_PASSPHRASE);
|
|
195
301
|
assert.strictEqual(pin, DEMO_PIN);
|
|
196
302
|
assert.strictEqual(network, DEMO_NETWORK);
|
|
303
|
+
assert.strictEqual(legacy, true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("should parse DEMO_LEGACY_TOKEN and return DEMO_PASSPHRASE, DEMO_PIN, DEMO_NETWORK and legacy=true", () => {
|
|
307
|
+
const [passphrase, pin, network, legacy] = parseToken(DEMO_LEGACY_TOKEN);
|
|
308
|
+
assert.strictEqual(passphrase, DEMO_PASSPHRASE);
|
|
309
|
+
assert.strictEqual(pin, DEMO_PIN);
|
|
310
|
+
assert.strictEqual(network, DEMO_NETWORK);
|
|
311
|
+
assert.strictEqual(legacy, true);
|
|
197
312
|
});
|
|
198
313
|
});
|
|
199
314
|
|
|
200
315
|
describe("generateToken and parseToken", () => {
|
|
201
|
-
test("should
|
|
316
|
+
test("should round-trip when legacy=false", () => {
|
|
317
|
+
const testPassphrase = "My-1st-car-was-a-red-Ford-2005!";
|
|
318
|
+
const testPin = "909011";
|
|
319
|
+
const testNetwork = "polygon";
|
|
320
|
+
const token = generateToken(testPassphrase, testPin, testNetwork, false);
|
|
321
|
+
|
|
322
|
+
const [passphrase, pin, network] = parseToken(token);
|
|
323
|
+
assert.strictEqual(passphrase, testPassphrase);
|
|
324
|
+
assert.strictEqual(pin, testPin);
|
|
325
|
+
assert.strictEqual(network, testNetwork);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("should round-trip when legacy=true", () => {
|
|
202
329
|
const testPassphrase = "My-1st-car-was-a-red-Ford-2005!";
|
|
203
330
|
const testPin = "909011";
|
|
204
331
|
const testNetwork = "polygon";
|
|
205
|
-
const token = generateToken(testPassphrase, testPin, testNetwork);
|
|
332
|
+
const token = generateToken(testPassphrase, testPin, testNetwork, true);
|
|
206
333
|
|
|
207
334
|
const [passphrase, pin, network] = parseToken(token);
|
|
208
335
|
assert.strictEqual(passphrase, testPassphrase);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
generateHash,
|
|
5
|
+
getEvmPrivateKey,
|
|
6
|
+
getEvmWalletAddress,
|
|
7
|
+
getTronWalletAddress,
|
|
8
|
+
} from "../index.js";
|
|
9
|
+
|
|
10
|
+
const DEMO_PASSPHRASE = "DemoAccount5&";
|
|
11
|
+
const DEMO_PIN = "112324";
|
|
12
|
+
|
|
13
|
+
const DEMO_HASH =
|
|
14
|
+
"af9a22d75f8f69d33fe8fc294e8f413219d9c75374dec07fda2e4a66868599609887a10e04981e17356d2c07432fc89c11089172fdf91c0015b9a4beef11e447";
|
|
15
|
+
const DEMO_PRIVATE_KEY =
|
|
16
|
+
"0x71743de900c63ed741263a2a4513c1b1829e80bd9f18d5d3a593e651b914cb3b";
|
|
17
|
+
const DEMO_WALLET_EVM_ADDRESS = "0x347CEB6Bf002Ee1819009bA07d8dCAA95Efe6465";
|
|
18
|
+
const DEMO_WALLET_TRON_ADDRESS = "TEkjnbpr2cTgRFgmrbv2Gb7GdgupZ5Sh3A";
|
|
19
|
+
|
|
20
|
+
describe("generateHash (legacy)", () => {
|
|
21
|
+
test("should return empty string if passphrase or pin is blank", async () => {
|
|
22
|
+
const hash = await generateHash("", "", () => {}, true);
|
|
23
|
+
assert.strictEqual(hash, "");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("should return empty string for weak passphrase (zxcvbn score < 3)", async () => {
|
|
27
|
+
const weakPassphrases = [
|
|
28
|
+
"password",
|
|
29
|
+
"asdfasdf",
|
|
30
|
+
"123123123",
|
|
31
|
+
"P@ssw0rd",
|
|
32
|
+
"London123",
|
|
33
|
+
"oxford2024",
|
|
34
|
+
"John19821012",
|
|
35
|
+
"asdfASDFaSdf",
|
|
36
|
+
"qwerqwerqwer",
|
|
37
|
+
"1234567890",
|
|
38
|
+
"Julia18921012",
|
|
39
|
+
];
|
|
40
|
+
for (const passphrase of weakPassphrases) {
|
|
41
|
+
const hash = await generateHash(passphrase, DEMO_PIN, () => {}, true);
|
|
42
|
+
assert.strictEqual(hash, "");
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("should return empty string for weak pin (zxcvbn score < 1)", async () => {
|
|
47
|
+
const weakPins = [
|
|
48
|
+
"111111",
|
|
49
|
+
"111111111111111",
|
|
50
|
+
"12341234",
|
|
51
|
+
"aaaaaaaaaa",
|
|
52
|
+
"asdfasdf",
|
|
53
|
+
];
|
|
54
|
+
for (const pin of weakPins) {
|
|
55
|
+
const hash = await generateHash(DEMO_PASSPHRASE, pin, () => {}, true);
|
|
56
|
+
assert.strictEqual(hash, "", `Expected "" for weak pin "${pin}"`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("should return scrypt hash result", async () => {
|
|
61
|
+
const hash = await generateHash(DEMO_PASSPHRASE, DEMO_PIN, () => {}, true);
|
|
62
|
+
assert.strictEqual(hash, DEMO_HASH);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("getEvmPrivateKey (legacy)", () => {
|
|
67
|
+
test("should return 256bit private key", async () => {
|
|
68
|
+
const hash = await generateHash(DEMO_PASSPHRASE, DEMO_PIN, () => {}, true);
|
|
69
|
+
const privateKey = getEvmPrivateKey(hash);
|
|
70
|
+
|
|
71
|
+
assert.strictEqual(privateKey, DEMO_PRIVATE_KEY);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("getEvmWalletAddress (legacy)", () => {
|
|
76
|
+
test("should return a valid wallet address", async () => {
|
|
77
|
+
const hash = await generateHash(DEMO_PASSPHRASE, DEMO_PIN, () => {}, true);
|
|
78
|
+
const address = getEvmWalletAddress(hash);
|
|
79
|
+
|
|
80
|
+
assert.strictEqual(address, DEMO_WALLET_EVM_ADDRESS);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("getTronWalletAddress (legacy)", () => {
|
|
85
|
+
test("should return a valid wallet address", async () => {
|
|
86
|
+
const hash = await generateHash(DEMO_PASSPHRASE, DEMO_PIN, () => {}, true);
|
|
87
|
+
const address = getTronWalletAddress(hash);
|
|
88
|
+
|
|
89
|
+
assert.strictEqual(address, DEMO_WALLET_TRON_ADDRESS);
|
|
90
|
+
});
|
|
91
|
+
});
|