Skip to content

Commit 51b2074

Browse files
committed
feat(cloud-sync): enhance keyless sync with proper encryption handling and unified pwdHash
- Add comprehensive BIP32 key encryption documentation - Fix keyless credential derivation to use deterministic decrypted keys - Remove keyless wallet filtering from sync flow managers - Unify pwdHash retrieval from sync credential across all modes - Clean up duplicate API calls in mock server - Add debug tools and UI improvements for keyless sync testing - Remove unused import in Button component
1 parent 414e264 commit 51b2074

File tree

14 files changed

+190
-71
lines changed

14 files changed

+190
-71
lines changed

packages/components/src/primitives/Button/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { Spinner } from '../Spinner';
1919
import { useSharedPress } from './useEvent';
2020

2121
import type { IIconProps, IKeyOfIcons } from '../Icon';
22-
import type { GestureResponderEvent } from 'react-native';
2322

2423
export interface IButtonProps extends ThemeableStackProps {
2524
type?: ButtonHTMLAttributes<HTMLButtonElement>['type'];

packages/core/src/secret/bip32.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,36 @@ export type IBip32ExtendedKeySerialized = {
1111
key: string;
1212
chainCode: string;
1313
};
14+
15+
/**
16+
* BIP32 Extended Key
17+
*
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
43+
*/
1444
export type IBip32ExtendedKey = {
1545
key: Buffer;
1646
chainCode: Buffer;

packages/kit-bg/src/dbs/local/LocalDbBase.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -884,9 +884,6 @@ export abstract class LocalDbBase extends LocalDbBaseContainer {
884884
// TODO performance
885885
const { wallets } = await this.getWallets();
886886
const walletsByXfp = wallets.filter((w) => {
887-
if (w.isKeyless && !includingKeylessWallets) {
888-
return false;
889-
}
890887
return w.xfp === xfp;
891888
});
892889
return walletsByXfp;
@@ -914,9 +911,6 @@ export abstract class LocalDbBase extends LocalDbBaseContainer {
914911
const { wallets } = await this.getWallets();
915912
if (walletType === WALLET_TYPE_HD) {
916913
const wallet = wallets.find((w) => {
917-
if (w.isKeyless) {
918-
return false;
919-
}
920914
const r = w.type === walletType && w.hash === walletHash;
921915
return r;
922916
});

packages/kit-bg/src/services/ServicePrimeCloudSync/CloudSyncFlowManager/CloudSyncFlowManagerAccount.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,6 @@ export class CloudSyncFlowManagerAccount extends CloudSyncFlowManagerBase<
2626
return false;
2727
}
2828

29-
// Keyless wallet accounts should not be synced
30-
const walletId = accountUtils.getWalletIdFromAccountId({
31-
accountId: account.id,
32-
});
33-
if (accountUtils.isKeylessWallet({ walletId })) {
34-
return false;
35-
}
36-
3729
return (
3830
accountUtils.isWatchingAccount({ accountId: account.id }) ||
3931
accountUtils.isImportedAccount({ accountId: account.id })

packages/kit-bg/src/services/ServicePrimeCloudSync/CloudSyncFlowManager/CloudSyncFlowManagerIndexedAccount.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,6 @@ export class CloudSyncFlowManagerIndexedAccount extends CloudSyncFlowManagerBase
4040
): Promise<boolean> {
4141
const { indexedAccount, wallet } = target;
4242

43-
// Keyless wallet should not be synced
44-
if (accountUtils.isKeylessWallet({ walletId: wallet?.id })) {
45-
return false;
46-
}
47-
4843
if (wallet?.xfp && accountUtils.isValidWalletXfp({ xfp: wallet.xfp })) {
4944
return true;
5045
}

packages/kit-bg/src/services/ServicePrimeCloudSync/CloudSyncFlowManager/CloudSyncFlowManagerWallet.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,6 @@ export class CloudSyncFlowManagerWallet extends CloudSyncFlowManagerBase<
2424
): Promise<boolean> {
2525
const { wallet } = target;
2626

27-
// Keyless wallet should not be synced
28-
if (accountUtils.isKeylessWallet({ walletId: wallet.id })) {
29-
return false;
30-
}
31-
3227
console.log('isSupportSync', wallet.id);
3328

3429
return (

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

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -859,15 +859,13 @@ class ServicePrimeCloudSync extends ServiceBase {
859859
lockItem = undefined;
860860
// pwdHash = RESET_CLOUD_SYNC_MASTER_PASSWORD_UUID; // TODO server should clear pwdHash
861861
} else {
862-
if (activeMode === ECloudSyncMode.Keyless) {
863-
pwdHash = '';
864-
} else {
865-
pwdHash =
866-
await this.backgroundApi.serviceMasterPassword.getLocalMasterPasswordUUID();
867-
}
862+
// eslint-disable-next-line no-param-reassign
863+
syncCredential = syncCredential || (await this.getSyncCredentialSafe());
864+
pwdHash =
865+
syncCredential?.keylessCredential?.pwdHash ||
866+
syncCredential?.masterPasswordUUID ||
867+
'';
868868
if (isFlush) {
869-
// eslint-disable-next-line no-param-reassign
870-
syncCredential = syncCredential || (await this.getSyncCredentialSafe());
871869
const syncCredentialForLock = syncCredential
872870
? this.syncManagers.lock.getLockStaticSyncCredential(syncCredential)
873871
: undefined;
@@ -1499,8 +1497,13 @@ class ServicePrimeCloudSync extends ServiceBase {
14991497
const allLocalItems = localItems;
15001498
const totalItemsCount = allLocalItems.length;
15011499

1500+
const credential = await this.getSyncCredentialSafe();
1501+
// const pwdHash =
1502+
// await this.backgroundApi.serviceMasterPassword.getLocalMasterPasswordUUIDSafe();
15021503
const pwdHash =
1503-
await this.backgroundApi.serviceMasterPassword.getLocalMasterPasswordUUIDSafe();
1504+
credential?.keylessCredential?.pwdHash ||
1505+
credential?.masterPasswordUUID ||
1506+
'';
15041507
if (pwdHash) {
15051508
localItems = allLocalItems.filter((item) => item.pwdHash === pwdHash);
15061509
const availableItemsCount = localItems.length;
@@ -1671,6 +1674,7 @@ class ServicePrimeCloudSync extends ServiceBase {
16711674
}
16721675
}
16731676

1677+
@backgroundMethod()
16741678
async getSyncCredentialSafe(): Promise<ICloudSyncCredential | undefined> {
16751679
try {
16761680
return await this.getSyncCredentialWithCache();
@@ -1704,15 +1708,17 @@ class ServicePrimeCloudSync extends ServiceBase {
17041708
};
17051709
}
17061710

1707-
const { masterPasswordUUID, encryptedSecurityPasswordR1 } =
1708-
await primeMasterPasswordPersistAtom.get();
1709-
if (!masterPasswordUUID || !encryptedSecurityPasswordR1) {
1710-
void this.showAlertDialogIfLocalPasswordNotSet();
1711-
throw new OneKeyError(
1712-
'No masterPasswordUUID or encryptedSecurityPasswordR1 in atom',
1713-
);
1714-
}
1715-
1711+
const {
1712+
masterPasswordUUID,
1713+
// encryptedSecurityPasswordR1
1714+
} = await primeMasterPasswordPersistAtom.get();
1715+
// if (!masterPasswordUUID || !encryptedSecurityPasswordR1) {
1716+
// void this.showAlertDialogIfLocalPasswordNotSet();
1717+
// throw new OneKeyError(
1718+
// 'No masterPasswordUUID or encryptedSecurityPasswordR1 in atom',
1719+
// );
1720+
// }
1721+
//
17161722
const securityPasswordR1Info =
17171723
await this.backgroundApi.serviceMasterPassword.getSecurityPasswordR1InfoSafe(
17181724
{

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

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,6 @@ class KeylessCloudSyncMockApi {
7272
signatureHeader: string;
7373
postData: ICloudSyncUploadPostData;
7474
}): Promise<ICloudSyncUploadResult | undefined> {
75-
void params.client.post<IApiClientResponse<ICloudSyncUploadResult>>(
76-
'/prime/v1/sync/upload-keyless',
77-
params.postData,
78-
);
79-
8075
return this.postToMockServer<ICloudSyncUploadResult>({
8176
client: params.client,
8277
url: '/prime/v1/sync/upload-keyless',
@@ -95,12 +90,6 @@ class KeylessCloudSyncMockApi {
9590
result: ICloudSyncCheckServerStatusResult;
9691
serverTime: string;
9792
}> {
98-
void params.client.post<
99-
IApiClientResponse<ICloudSyncCheckServerStatusResult>
100-
>('/prime/v1/sync/check-keyless', {
101-
...params.postData,
102-
});
103-
10493
return this.postToMockServer<{
10594
result: ICloudSyncCheckServerStatusResult;
10695
serverTime: string;
@@ -119,11 +108,6 @@ class KeylessCloudSyncMockApi {
119108
signatureHeader?: string;
120109
postData: ICloudSyncDownloadPostData;
121110
}): Promise<ICloudSyncDownloadResult> {
122-
void params.client.post<IApiClientResponse<ICloudSyncDownloadResult>>(
123-
'/prime/v1/sync/download-keyless',
124-
params.postData,
125-
);
126-
127111
if (params.publicKey && params.signatureHeader) {
128112
return this.postToMockServer<ICloudSyncDownloadResult>({
129113
client: params.client,

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

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
publicFromPrivate,
1616
sign,
1717
} from '@onekeyhq/core/src/secret';
18+
import {
19+
decryptAsync,
20+
encryptAsync,
21+
} from '@onekeyhq/core/src/secret/encryptors/aes256';
1822
import type { ICoreHdCredentialEncryptHex } from '@onekeyhq/core/src/types';
1923
import {
2024
KEYLESS_PWDHASH_CONTEXT,
@@ -97,13 +101,24 @@ export async function deriveKeylessCredential({
97101
password,
98102
);
99103

100-
const encryptionKeyHex = bufferUtils.bytesToHex(
101-
encryptionKeyInfo.extendedKey.key,
102-
);
104+
// Decrypt private keys to get deterministic hex values
105+
// batchGetPrivateKeys returns encrypted keys with random salt/IV,
106+
// so we decrypt them to ensure consistent values across sessions
107+
const decryptedSigningPrivateKey = await decryptAsync({
108+
password,
109+
data: signingKey.extendedKey.key,
110+
});
111+
112+
const decryptedEncryptionKey = await decryptAsync({
113+
password,
114+
data: encryptionKeyInfo.extendedKey.key,
115+
});
116+
117+
const encryptionKeyHex = bufferUtils.bytesToHex(decryptedEncryptionKey);
103118

104119
return {
105120
keylessWalletId,
106-
signingPrivateKey: bufferUtils.bytesToHex(signingKey.extendedKey.key),
121+
signingPrivateKey: bufferUtils.bytesToHex(decryptedSigningPrivateKey),
107122
signingPublicKey: bufferUtils.bytesToHex(signingPublicKey),
108123
encryptionKey: encryptionKeyHex,
109124
pwdHash: computeKeylessPwdHash(encryptionKeyHex),
@@ -177,9 +192,9 @@ function generateNonce(): string {
177192
/**
178193
* Sign message and build Header content
179194
*
180-
* @param signingPrivateKey - Signing private key (hex)
195+
* @param signingPrivateKey - Signing private key (decrypted, hex format)
181196
* @param signingPublicKey - Signing public key (hex)
182-
* @param password - Wallet password (for decrypting private key)
197+
* @param password - Wallet password (for encrypting private key before signing)
183198
* @param dataHash - Data hash to include when uploading (optional)
184199
* @returns Base64 encoded signature Header value
185200
*/
@@ -208,10 +223,16 @@ export async function buildKeylessSignatureHeader({
208223
const messageString = JSON.stringify(signMessage);
209224
const messageHash = sha256(bufferUtils.toBuffer(messageString, 'utf8'));
210225

211-
// Sign (private key is encrypted, needs password to decrypt)
226+
// Encrypt private key before signing (sign function expects encrypted key)
227+
const encryptedPrivateKey = await encryptAsync({
228+
password,
229+
data: bufferUtils.toBuffer(signingPrivateKey, 'hex'),
230+
});
231+
232+
// Sign using encrypted private key
212233
const signature = await sign(
213234
'secp256k1',
214-
bufferUtils.toBuffer(signingPrivateKey, 'hex'),
235+
encryptedPrivateKey,
215236
Buffer.from(messageHash),
216237
password,
217238
);

packages/kit/src/components/MultipleClickStack/MultipleClickStack.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { GestureResponderEvent } from 'react-native';
1010
export function MultipleClickStack({
1111
children,
1212
onPress,
13-
showDevBgColor: _showDevBgColor = false,
13+
showDevBgColor = false,
1414
triggerAt = platformEnv.isDev ? 3 : 10,
1515
debugComponent,
1616
...others
@@ -28,8 +28,8 @@ export function MultipleClickStack({
2828
return (
2929
<>
3030
<Stack
31-
bg={undefined}
32-
// bg={showDevBgColor && platformEnv.isDev ? '$bgCritical' : undefined}
31+
// bg={undefined}
32+
bg={showDevBgColor && platformEnv.isDev ? '$bgCritical' : undefined}
3333
{...others}
3434
onPress={(event) => {
3535
if (clickCount > triggerAt) {

0 commit comments

Comments
 (0)