Skip to content

Commit 18d0364

Browse files
committed
feat(cloud-sync): enhance keyless sync utilities and improve documentation
- Add a guideline to avoid using JSON.stringify() for cryptographic operations, promoting the use of stringUtils.stableStringify() for deterministic serialization. - Refactor keylessCloudSyncUtils to export functions directly, improving usability and clarity. - Update BIP32 key documentation to clarify encryption states and usage. - Clean up ServiceMasterPassword and CloudSyncItemBuilder by removing unnecessary checks and improving error handling. - Adjust keyless sync constants to reflect updated derivation paths for better clarity and consistency.
1 parent 90ebcd9 commit 18d0364

File tree

7 files changed

+74
-80
lines changed

7 files changed

+74
-80
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ OneKey is an open-source multi-chain crypto wallet with a monorepo architecture
6969
-**NEVER** modify auto-generated files (`translations.ts`, locale JSON files)
7070
-**NEVER** bypass TypeScript types with `any` or `@ts-ignore` without documented justification
7171
-**NEVER** commit code that fails linting or TypeScript compilation
72+
-**NEVER** use `JSON.stringify()` for cryptographic operations → ALWAYS use `stringUtils.stableStringify()` for deterministic serialization when computing hashes or signatures
7273

7374
## Git Basics
7475

packages/core/src/secret/bip32.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,8 @@ export type IBip32ExtendedKeySerialized = {
1515
/**
1616
* BIP32 Extended Key
1717
*
18-
* Represents a BIP32 hierarchical deterministic key pair with chain code.
19-
*
20-
* **Key Encryption State:**
21-
* The `key` field can be in two states depending on context:
22-
*
23-
* 1. **Unencrypted (Raw Private Key):**
24-
* - During BIP32 derivation calculations (CKDPriv, CKDPub)
25-
* - Internal processing within secret module
26-
* - Should NEVER be exposed outside the secret module
27-
*
28-
* 2. **Encrypted (AES-256 Encrypted Private Key):**
29-
* - When returned by `batchGetPrivateKeys()` or similar functions
30-
* - When stored or passed between modules
31-
* - Encryption method: AES-256-CBC with PBKDF2
32-
* - Random salt (32 bytes) + Random IV (16 bytes)
33-
* - Format: `[salt(32) + iv(16) + encrypted_data]`
34-
* - Must be decrypted with `decryptAsync({ password, data: key })` before use
35-
*
36-
* **For Public Keys:**
37-
* When this type represents a public key (via `deriver.N()`):
38-
* - `key` contains the raw public key (unencrypted, as public keys are meant to be public)
39-
* - No encryption is applied
40-
*
41-
* @see batchGetKeys() in secret/index.ts for encryption logic
42-
* @see encryptAsync() in secret/encryptors/aes256.ts for encryption implementation
18+
* key 字段:模块内推导为明文私钥;跨模块返回/存储为 AES-256 加密。
19+
* 公钥场景不加密。
4320
*/
4421
export type IBip32ExtendedKey = {
4522
key: Buffer;

packages/kit-bg/src/services/ServiceMasterPassword/ServiceMasterPassword.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,9 +1021,6 @@ class ServiceMasterPassword extends ServiceBase {
10211021
serverPwdHash: pwdHash,
10221022
},
10231023
);
1024-
if (!oldLocalItem) {
1025-
return;
1026-
}
10271024
if (!oldLocalItem.rawDataJson) {
10281025
throw new OneKeyLocalError('No raw data json');
10291026
}

packages/kit-bg/src/services/ServicePrimeCloudSync/ServicePrimeCloudSync.tsx

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,7 @@ import { CloudSyncFlowManagerWallet } from './CloudSyncFlowManager/CloudSyncFlow
9393
import cloudSyncItemBuilder from './cloudSyncItemBuilder';
9494
// Keyless backend API is not available yet; use mock storage for Keyless mode.
9595
import { keylessMockApi } from './keylessCloudSyncMockApi';
96-
import {
97-
buildKeylessSignatureHeader,
98-
computeDataHash,
99-
deriveKeylessCredential,
100-
isKeylessPwdHash,
101-
} from './keylessCloudSyncUtils';
96+
import keylessCloudSyncUtils from './keylessCloudSyncUtils';
10297

10398
import type { RealmSchemaCloudSyncItem } from '../../dbs/local/realm/schemas/RealmSchemaCloudSyncItem';
10499
import type { IPrimeCloudSyncPersistAtomData } from '../../states/jotai/atoms';
@@ -185,11 +180,12 @@ class ServicePrimeCloudSync extends ServiceBase {
185180
}
186181

187182
try {
188-
const keylessCredential = await deriveKeylessCredential({
189-
hdCredential: credential.credential,
190-
password,
191-
keylessWalletId: keylessWallet.id,
192-
});
183+
const keylessCredential =
184+
await keylessCloudSyncUtils.deriveKeylessCredential({
185+
hdCredential: credential.credential,
186+
password,
187+
keylessWalletId: keylessWallet.id,
188+
});
193189

194190
this.keylessCredentialCache = keylessCredential;
195191
return keylessCredential;
@@ -258,12 +254,15 @@ class ServicePrimeCloudSync extends ServiceBase {
258254
return null;
259255
}
260256

261-
const signatureHeader = await buildKeylessSignatureHeader({
262-
signingPrivateKey: keylessCredential.signingPrivateKey,
263-
signingPublicKey: keylessCredential.signingPublicKey,
264-
password,
265-
dataHash: computeDataHash(stringUtils.stableStringify(postData)),
266-
});
257+
const signatureHeader =
258+
await keylessCloudSyncUtils.buildKeylessSignatureHeader({
259+
signingPrivateKey: keylessCredential.signingPrivateKey,
260+
signingPublicKey: keylessCredential.signingPublicKey,
261+
password,
262+
dataHash: keylessCloudSyncUtils.computeDataHash(
263+
stringUtils.stableStringify(postData),
264+
),
265+
});
267266
return {
268267
publicKey: keylessCredential.signingPublicKey,
269268
signatureHeader,
@@ -2336,7 +2335,7 @@ class ServicePrimeCloudSync extends ServiceBase {
23362335
// Skip Lock items with keyless pwdHash
23372336
if (
23382337
localItem.dataType === EPrimeCloudSyncDataType.Lock &&
2339-
isKeylessPwdHash(localItem.pwdHash)
2338+
keylessCloudSyncUtils.isKeylessPwdHash(localItem.pwdHash)
23402339
) {
23412340
return null;
23422341
}
@@ -2361,13 +2360,13 @@ class ServicePrimeCloudSync extends ServiceBase {
23612360
shouldDecrypt?: boolean; // decrypt the data to rawDataJson
23622361
syncCredential: ICloudSyncCredential | undefined;
23632362
serverPwdHash: string;
2364-
}): Promise<IDBCloudSyncItem | null> {
2363+
}): Promise<IDBCloudSyncItem> {
23652364
// Skip Lock items with keyless pwdHash
23662365
if (
23672366
serverItem.dataType === EPrimeCloudSyncDataType.Lock &&
2368-
isKeylessPwdHash(serverItem.pwdHash)
2367+
keylessCloudSyncUtils.isKeylessPwdHash(serverItem.pwdHash)
23692368
) {
2370-
return null;
2369+
throw new OneKeyError('Lock item not support for keyless mode');
23712370
}
23722371
const localItem: IDBCloudSyncItem = {
23732372
id: serverItem.key,

packages/kit-bg/src/services/ServicePrimeCloudSync/cloudSyncItemBuilder.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,7 @@ import type {
2424
ICloudSyncRawDataJson,
2525
} from '@onekeyhq/shared/types/prime/primeCloudSyncTypes';
2626

27-
import {
28-
decryptWithKeylessKey,
29-
encryptWithKeylessKey,
30-
isKeylessPwdHash,
31-
} from './keylessCloudSyncUtils';
27+
import keylessCloudSyncUtils from './keylessCloudSyncUtils';
3228

3329
import type {
3430
IDBCloudSyncItem,
@@ -50,7 +46,7 @@ class CloudSyncItemBuilder {
5046
const { keylessCredential } = syncCredential;
5147
if (keylessCredential) {
5248
// pwdHash is precomputed in deriveKeylessCredential
53-
return keylessCredential.pwdHash;
49+
return keylessCredential.pwdHash || '';
5450
}
5551

5652
// Fallback to OneKey ID mode
@@ -213,7 +209,7 @@ class CloudSyncItemBuilder {
213209
const { keylessCredential } = syncCredential;
214210
// Use keyless encryption if keylessCredential is available
215211
if (keylessCredential) {
216-
encryptedData = await encryptWithKeylessKey({
212+
encryptedData = await keylessCloudSyncUtils.encryptWithKeylessKey({
217213
rawData,
218214
encryptionKey: keylessCredential.encryptionKey,
219215
});
@@ -251,20 +247,25 @@ class CloudSyncItemBuilder {
251247

252248
if (syncCredential && item.data) {
253249
let decryptedData: string | undefined;
250+
const credentialPwdHash: string | undefined =
251+
this.getPwdHash(syncCredential);
254252

255253
// Determine decryption method based on pwdHash prefix
256-
if (isKeylessPwdHash(item.pwdHash) && syncCredential.keylessCredential) {
254+
if (
255+
keylessCloudSyncUtils.isKeylessPwdHash(item.pwdHash) &&
256+
syncCredential.keylessCredential
257+
) {
257258
// Keyless decryption
258259
try {
259-
decryptedData = await decryptWithKeylessKey({
260+
decryptedData = await keylessCloudSyncUtils.decryptWithKeylessKey({
260261
encryptedData: item.data,
261262
encryptionKey: syncCredential.keylessCredential.encryptionKey,
262263
});
263264
} catch (error) {
264265
console.error('decryptSyncItem keyless decrypt error', error, item);
265266
throw new IncorrectMasterPassword();
266267
}
267-
} else if (!isKeylessPwdHash(item.pwdHash)) {
268+
} else if (!keylessCloudSyncUtils.isKeylessPwdHash(item.pwdHash)) {
268269
// OneKey ID decryption
269270
let credentialToUse = syncCredential;
270271
if (item.dataType === EPrimeCloudSyncDataType.Lock) {
@@ -296,7 +297,9 @@ class CloudSyncItemBuilder {
296297

297298
try {
298299
if (decryptedData) {
300+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
299301
rawDataJson = JSON.parse(decryptedData) as ICloudSyncRawDataJson;
302+
item.pwdHash = credentialPwdHash || '';
300303
}
301304
} catch (error) {
302305
console.error('decryptSyncItem jsonParse error', error, item);

packages/kit-bg/src/services/ServicePrimeCloudSync/keylessCloudSyncUtils.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
* Uses the unique Keyless wallet mnemonic to derive keys for operations.
66
*/
77

8-
import { sha256 } from '@noble/hashes/sha256';
9-
import { sha512 } from '@noble/hashes/sha512';
10-
118
import {
129
batchGetPrivateKeys,
1310
decryptStringAsync,
@@ -27,7 +24,9 @@ import {
2724
KEYLESS_SYNC_ENCRYPTION_CONTEXT,
2825
} from '@onekeyhq/shared/src/consts/keylessCloudSyncConsts';
2926
import { OneKeyLocalError } from '@onekeyhq/shared/src/errors';
27+
import appCrypto from '@onekeyhq/shared/src/appCrypto';
3028
import bufferUtils from '@onekeyhq/shared/src/utils/bufferUtils';
29+
import stringUtils from '@onekeyhq/shared/src/utils/stringUtils';
3130
import type {
3231
IKeylessCloudSyncCredential,
3332
IKeylessCloudSyncSignMessage,
@@ -42,10 +41,12 @@ import type {
4241
* @param encryptionKey - Keyless encryption key (hex string)
4342
* @returns pwdHash string with 'keyless-' prefix
4443
*/
45-
export function computeKeylessPwdHash(encryptionKey: string): string {
44+
function computeKeylessPwdHash(encryptionKey: string): string {
4645
const context: string = KEYLESS_PWDHASH_CONTEXT;
4746
const hashInput = `${context}:${encryptionKey}`;
48-
const hash = sha512(bufferUtils.toBuffer(hashInput, 'utf8'));
47+
const hash = appCrypto.hash.sha512Sync(
48+
bufferUtils.toBuffer(hashInput, 'utf8'),
49+
);
4950
const prefix: string = KEYLESS_PWDHASH_PREFIX;
5051
return `${prefix}${bufferUtils.bytesToHex(hash)}`;
5152
}
@@ -56,7 +57,7 @@ export function computeKeylessPwdHash(encryptionKey: string): string {
5657
* @param pwdHash - pwdHash string to check
5758
* @returns true if pwdHash starts with 'keyless-' prefix
5859
*/
59-
export function isKeylessPwdHash(pwdHash: string): boolean {
60+
function isKeylessPwdHash(pwdHash: string): boolean {
6061
return pwdHash.startsWith(KEYLESS_PWDHASH_PREFIX);
6162
}
6263

@@ -68,7 +69,7 @@ export function isKeylessPwdHash(pwdHash: string): boolean {
6869
* @param keylessWalletId - Keyless wallet ID
6970
* @returns Keyless sync credentials containing signing and encryption keys
7071
*/
71-
export async function deriveKeylessCredential({
72+
async function deriveKeylessCredential({
7273
hdCredential,
7374
password,
7475
keylessWalletId,
@@ -83,7 +84,7 @@ export async function deriveKeylessCredential({
8384
'secp256k1',
8485
hdCredential,
8586
password,
86-
KEYLESS_SYNC_DERIVATION_PATH_PREFIX, // "m/44'/1919'/0'"
87+
KEYLESS_SYNC_DERIVATION_PATH_PREFIX, // "m/44'/38716591'/98351420'"
8788
['0/0', '0/1'],
8889
);
8990

@@ -132,7 +133,7 @@ export async function deriveKeylessCredential({
132133
* @param encryptionKey - Encryption key (hex)
133134
* @returns Encrypted data (hex string)
134135
*/
135-
export async function encryptWithKeylessKey({
136+
async function encryptWithKeylessKey({
136137
rawData,
137138
encryptionKey,
138139
}: {
@@ -156,7 +157,7 @@ export async function encryptWithKeylessKey({
156157
* @param encryptionKey - Encryption key (hex)
157158
* @returns Decrypted raw data (UTF8 string)
158159
*/
159-
export async function decryptWithKeylessKey({
160+
async function decryptWithKeylessKey({
160161
encryptedData,
161162
encryptionKey,
162163
}: {
@@ -198,7 +199,7 @@ function generateNonce(): string {
198199
* @param dataHash - Data hash to include when uploading (optional)
199200
* @returns Base64 encoded signature Header value
200201
*/
201-
export async function buildKeylessSignatureHeader({
202+
async function buildKeylessSignatureHeader({
202203
signingPrivateKey,
203204
signingPublicKey,
204205
password,
@@ -220,8 +221,11 @@ export async function buildKeylessSignatureHeader({
220221
};
221222

222223
// Compute message hash
223-
const messageString = JSON.stringify(signMessage);
224-
const messageHash = sha256(bufferUtils.toBuffer(messageString, 'utf8'));
224+
// Use stableStringify to ensure consistent serialization regardless of property order
225+
const messageString = stringUtils.stableStringify(signMessage);
226+
const messageHash = appCrypto.hash.sha256Sync(
227+
bufferUtils.toBuffer(messageString, 'utf8'),
228+
);
225229

226230
// Encrypt private key before signing (sign function expects encrypted key)
227231
const encryptedPrivateKey = await encryptAsync({
@@ -247,7 +251,7 @@ export async function buildKeylessSignatureHeader({
247251

248252
// Base64 encode
249253
return bufferUtils.bytesToBase64(
250-
bufferUtils.toBuffer(JSON.stringify(headerPayload), 'utf8'),
254+
bufferUtils.toBuffer(stringUtils.stableStringify(headerPayload), 'utf8'),
251255
);
252256
}
253257

@@ -257,8 +261,8 @@ export async function buildKeylessSignatureHeader({
257261
* @param data - Data to hash
258262
* @returns Hash value (hex string)
259263
*/
260-
export function computeDataHash(data: string): string {
261-
const hash = sha256(bufferUtils.toBuffer(data, 'utf8'));
264+
function computeDataHash(data: string): string {
265+
const hash = appCrypto.hash.sha256Sync(bufferUtils.toBuffer(data, 'utf8'));
262266
return bufferUtils.bytesToHex(hash);
263267
}
264268

@@ -268,7 +272,7 @@ export function computeDataHash(data: string): string {
268272
* @param signatureHeader - Base64 encoded signature Header
269273
* @returns Parsed signature payload
270274
*/
271-
export function parseSignatureHeader(
275+
function parseSignatureHeader(
272276
signatureHeader: string,
273277
): IKeylessCloudSyncSignaturePayload | null {
274278
try {
@@ -280,3 +284,14 @@ export function parseSignatureHeader(
280284
return null;
281285
}
282286
}
287+
288+
export default {
289+
computeKeylessPwdHash,
290+
isKeylessPwdHash,
291+
deriveKeylessCredential,
292+
encryptWithKeylessKey,
293+
decryptWithKeylessKey,
294+
buildKeylessSignatureHeader,
295+
computeDataHash,
296+
parseSignatureHeader,
297+
};

packages/shared/src/consts/keylessCloudSyncConsts.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
/**
22
* Keyless Cloud Sync Constants
33
*
4-
* Keyless-specific derivation paths (BIP44 coin type 1919 = OneKey custom)
4+
* Keyless-specific derivation paths
55
*/
66

77
/** Keyless sync derivation path prefix */
8-
export const KEYLESS_SYNC_DERIVATION_PATH_PREFIX = "m/44'/1919'/0'";
8+
export const KEYLESS_SYNC_DERIVATION_PATH_PREFIX = "m/44'/38716591'/98351420'";
99

1010
/** Keyless signing key derivation path */
11-
export const KEYLESS_SYNC_DERIVATION_PATH_SIGNING = "m/44'/1919'/0'/0/0";
11+
export const KEYLESS_SYNC_DERIVATION_PATH_SIGNING =
12+
"m/44'/38716591'/98351420'/0/0";
1213

1314
/** Keyless encryption key derivation path */
14-
export const KEYLESS_SYNC_DERIVATION_PATH_ENCRYPTION = "m/44'/1919'/0'/0/1";
15+
export const KEYLESS_SYNC_DERIVATION_PATH_ENCRYPTION =
16+
"m/44'/38716591'/98351420'/0/1";
1517

1618
/** HTTP Header name for Keyless signature */
1719
export const KEYLESS_SYNC_SIGNATURE_HEADER = 'X-Keyless-Sync-Signature';

0 commit comments

Comments
 (0)