From 6b85401d5c6b5ad449489f36867a9868b2a90227 Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 8 Dec 2025 09:02:24 -0800 Subject: [PATCH] fix(mnemonic): fix recovery-by-mnemonic using hash verification instead of address matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem MPC wallet addresses have no cryptographic relationship with recovery mnemonics, so address-based verification always failed for account recovery. ## Solution Changed mnemonic verification from address matching to hash-based verification: - Mnemonic acts as identity credential, verified by hash stored in blockchain-service - Uses accountSequence to lookup stored mnemonic hash for verification ## Changes ### blockchain-service - recovery-mnemonic.adapter.ts: - generateMnemonic() now async, uses bcrypt (rounds=12) for secure hashing - verifyMnemonic() now async, supports both bcrypt and legacy SHA256 hashes - Added backward compatibility for existing SHA256 hashed mnemonics - mnemonic-verification.service.ts: - await verifyMnemonic() for async bcrypt comparison - address-derivation.service.ts: - await generateMnemonic() for async bcrypt hashing - package.json: added bcrypt dependency ### identity-service - user-application.service.ts: - Changed recoverByMnemonic() to use verifyMnemonicByAccount (hash verification) - Added rate limiting: 5 failed attempts per hour per accountSequence - Uses Redis to track failed verification attempts - redis.service.ts: - Added incr() and expire() methods for rate limiting - Added updateKeygenStatusAtomic() with Lua script for atomic state transitions - mpc-keygen-completed.handler.ts: - Uses atomic Redis update to prevent race conditions - blockchain-wallet.handler.ts: - Uses atomic Redis update for completed status ## Security Improvements - bcrypt with 12 rounds for mnemonic hashing (anti-brute-force) - Rate limiting prevents brute force attacks on mnemonic recovery - Atomic Redis operations prevent race conditions in wallet creation flow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../blockchain-service/package-lock.json | 83 +++++++++++++++++-- .../services/blockchain-service/package.json | 2 + .../services/address-derivation.service.ts | 28 +++++-- .../services/mnemonic-verification.service.ts | 4 +- .../blockchain/recovery-mnemonic.adapter.ts | 40 ++++++--- .../blockchain-wallet.handler.ts | 10 ++- .../mpc-keygen-completed.handler.ts | 18 +++- .../services/user-application.service.ts | 28 ++++--- .../src/infrastructure/redis/redis.service.ts | 62 ++++++++++++++ 9 files changed, 228 insertions(+), 47 deletions(-) diff --git a/backend/services/blockchain-service/package-lock.json b/backend/services/blockchain-service/package-lock.json index 43aca0c0..ec29cf38 100644 --- a/backend/services/blockchain-service/package-lock.json +++ b/backend/services/blockchain-service/package-lock.json @@ -20,6 +20,7 @@ "@prisma/client": "^5.7.0", "@scure/bip32": "^1.3.2", "@scure/bip39": "^1.6.0", + "bcrypt": "^6.0.0", "bech32": "^2.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -34,6 +35,7 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", + "@types/bcrypt": "^6.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", @@ -253,6 +255,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1756,6 +1759,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.20.tgz", "integrity": "sha512-hxJxZF7jcKGuUzM9EYbuES80Z/36piJbiqmPy86mk8qOn5gglFebBTvcx7PWVbRNSb4gngASYnefBj/Y2HAzpQ==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "20.4.1", "iterare": "1.2.1", @@ -1802,6 +1806,7 @@ "integrity": "sha512-kRdtyKA3+Tu70N3RQ4JgmO1E3LzAMs/eppj7SfjabC7TgqNWoS4RLhWl4BqmsNVmjj6D5jgfPVtHtgYkU3AfpQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -1859,6 +1864,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-10.4.20.tgz", "integrity": "sha512-zu/o84Z0uTUClNnGIGfIjcrO3z6T60h/pZPSJK50o4mehbEvJ76fijj6R/WTW0VP+1N16qOv/NsiYLKJA5Cc3w==", "license": "MIT", + "peer": true, "dependencies": { "iterare": "1.2.1", "tslib": "2.8.1" @@ -1917,6 +1923,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.20.tgz", "integrity": "sha512-rh97mX3rimyf4xLMLHuTOBKe6UD8LOJ14VlJ1F/PTd6C6ZK9Ak6EHuJvdaGcSFQhd3ZMBh3I6CuujKGW9pNdIg==", "license": "MIT", + "peer": true, "dependencies": { "body-parser": "1.20.3", "cors": "2.8.5", @@ -2390,6 +2397,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -2424,6 +2441,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2561,6 +2579,7 @@ "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2722,6 +2741,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -3079,6 +3099,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3092,7 +3113,6 @@ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -3135,6 +3155,7 @@ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -3499,6 +3520,20 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/bech32": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", @@ -3612,6 +3647,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3884,13 +3920,15 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/class-validator": { "version": "0.14.3", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", "license": "MIT", + "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -4645,6 +4683,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4701,6 +4740,7 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5369,7 +5409,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=4.0" }, @@ -6027,6 +6066,7 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", "license": "MIT", + "peer": true, "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", @@ -6309,6 +6349,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -7110,6 +7151,7 @@ "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", "license": "MIT", + "peer": true, "engines": { "node": ">=14.0.0" } @@ -7559,6 +7601,15 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -7589,6 +7640,17 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8029,6 +8091,7 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -8087,6 +8150,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -8131,8 +8195,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", @@ -8303,7 +8366,8 @@ "version": "0.1.14", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/repeat-string": { "version": "1.6.1", @@ -8533,6 +8597,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -8588,6 +8653,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -9556,6 +9622,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9702,6 +9769,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9999,7 +10067,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -10014,7 +10081,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -10025,7 +10091,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/backend/services/blockchain-service/package.json b/backend/services/blockchain-service/package.json index 1a7efd04..ce7c29f7 100644 --- a/backend/services/blockchain-service/package.json +++ b/backend/services/blockchain-service/package.json @@ -39,6 +39,7 @@ "@prisma/client": "^5.7.0", "@scure/bip32": "^1.3.2", "@scure/bip39": "^1.6.0", + "bcrypt": "^6.0.0", "bech32": "^2.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -53,6 +54,7 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", + "@types/bcrypt": "^6.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", diff --git a/backend/services/blockchain-service/src/application/services/address-derivation.service.ts b/backend/services/blockchain-service/src/application/services/address-derivation.service.ts index 3a318742..61d46a8b 100644 --- a/backend/services/blockchain-service/src/application/services/address-derivation.service.ts +++ b/backend/services/blockchain-service/src/application/services/address-derivation.service.ts @@ -82,24 +82,36 @@ export class AddressDerivationService { // 3. 生成恢复助记词 (与账户序列号关联) this.logger.log(`[MNEMONIC] Generating recovery mnemonic for account ${accountSequence}`); - const mnemonicResult = this.recoveryMnemonic.generateMnemonic({ + const mnemonicResult = await this.recoveryMnemonic.generateMnemonic({ userId: userId.toString(), publicKey, }); this.logger.log(`[MNEMONIC] Recovery mnemonic generated, hash: ${mnemonicResult.mnemonicHash.slice(0, 16)}...`); // 4. 存储恢复助记词到 blockchain-service 数据库 (使用 accountSequence 关联) - await this.prisma.recoveryMnemonic.create({ - data: { + // 检查是否已存在,避免重复创建 + const existingMnemonic = await this.prisma.recoveryMnemonic.findFirst({ + where: { accountSequence, - publicKey, - encryptedMnemonic: mnemonicResult.encryptedMnemonic, - mnemonicHash: mnemonicResult.mnemonicHash, status: 'ACTIVE', - isBackedUp: false, }, }); - this.logger.log(`[MNEMONIC] Recovery mnemonic saved for account ${accountSequence}`); + + if (existingMnemonic) { + this.logger.warn(`[MNEMONIC] Recovery mnemonic already exists for account ${accountSequence}, skipping creation`); + } else { + await this.prisma.recoveryMnemonic.create({ + data: { + accountSequence, + publicKey, + encryptedMnemonic: mnemonicResult.encryptedMnemonic, + mnemonicHash: mnemonicResult.mnemonicHash, + status: 'ACTIVE', + isBackedUp: false, + }, + }); + this.logger.log(`[MNEMONIC] Recovery mnemonic saved for account ${accountSequence}`); + } // 5. 发布钱包地址创建事件 (包含所有链的地址和助记词) const event = new WalletAddressCreatedEvent({ diff --git a/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts b/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts index 717b85c8..8965da00 100644 --- a/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts +++ b/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts @@ -48,8 +48,8 @@ export class MnemonicVerificationService { }; } - // 2. 使用 RecoveryMnemonicAdapter 验证哈希 - const result = this.recoveryMnemonic.verifyMnemonic(mnemonic, recoveryRecord.mnemonicHash); + // 2. 使用 RecoveryMnemonicAdapter 验证哈希 (bcrypt 是异步的) + const result = await this.recoveryMnemonic.verifyMnemonic(mnemonic, recoveryRecord.mnemonicHash); if (result.valid) { this.logger.log(`Mnemonic verified successfully for account ${accountSequence}`); diff --git a/backend/services/blockchain-service/src/infrastructure/blockchain/recovery-mnemonic.adapter.ts b/backend/services/blockchain-service/src/infrastructure/blockchain/recovery-mnemonic.adapter.ts index 1c6fd159..49580269 100644 --- a/backend/services/blockchain-service/src/infrastructure/blockchain/recovery-mnemonic.adapter.ts +++ b/backend/services/blockchain-service/src/infrastructure/blockchain/recovery-mnemonic.adapter.ts @@ -10,6 +10,7 @@ import { validateMnemonic, entropyToMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { createHash, createCipheriv, createDecipheriv, randomBytes } from 'crypto'; import { ConfigService } from '@nestjs/config'; +import * as bcrypt from 'bcrypt'; export interface GenerateMnemonicParams { userId: string; @@ -52,7 +53,7 @@ export class RecoveryMnemonicAdapter { * 2. 从熵生成 12 词 BIP39 助记词 * 3. 加密存储 */ - generateMnemonic(params: GenerateMnemonicParams): GenerateMnemonicResult { + async generateMnemonic(params: GenerateMnemonicParams): Promise { const { userId, publicKey } = params; this.logger.log(`Generating recovery mnemonic for user=${userId}, publicKey=${publicKey.slice(0, 16)}...`); @@ -83,8 +84,8 @@ export class RecoveryMnemonicAdapter { // 加密助记词 const encryptedMnemonic = this.encryptMnemonic(mnemonic); - // 计算助记词哈希 (用于验证,不可逆) - const mnemonicHash = this.hashMnemonic(mnemonic); + // 计算助记词哈希 (用于验证,不可逆,使用 bcrypt) + const mnemonicHash = await this.hashMnemonic(mnemonic); this.logger.log(`Recovery mnemonic generated: hash=${mnemonicHash.slice(0, 16)}...`); @@ -97,16 +98,26 @@ export class RecoveryMnemonicAdapter { } /** - * 验证助记词是否正确 + * 验证助记词是否正确 (异步,使用 bcrypt) */ - verifyMnemonic(mnemonic: string, expectedHash: string): VerifyMnemonicResult { + async verifyMnemonic(mnemonic: string, expectedHash: string): Promise { if (!validateMnemonic(mnemonic, wordlist)) { return { valid: false, message: 'Invalid mnemonic format' }; } - const hash = this.hashMnemonic(mnemonic); - if (hash !== expectedHash) { - return { valid: false, message: 'Mnemonic does not match' }; + // 兼容旧的 SHA256 hash 和新的 bcrypt hash + if (expectedHash.startsWith('$2')) { + // bcrypt hash + const isValid = await bcrypt.compare(mnemonic, expectedHash); + if (!isValid) { + return { valid: false, message: 'Mnemonic does not match' }; + } + } else { + // 旧的 SHA256 hash (兼容性) + const hash = this.hashMnemonicSha256(mnemonic); + if (hash !== expectedHash) { + return { valid: false, message: 'Mnemonic does not match' }; + } } return { valid: true }; @@ -146,9 +157,18 @@ export class RecoveryMnemonicAdapter { } /** - * 计算助记词哈希 (不可逆) + * 计算助记词哈希 (使用 bcrypt,抗暴力破解) */ - private hashMnemonic(mnemonic: string): string { + private async hashMnemonic(mnemonic: string): Promise { + // bcrypt rounds = 12, 足够安全且不会太慢 + const hash = await bcrypt.hash(mnemonic, 12); + return hash; + } + + /** + * SHA256 哈希 (兼容旧数据) + */ + private hashMnemonicSha256(mnemonic: string): string { const hash1 = createHash('sha256').update(mnemonic).digest(); const hash2 = createHash('sha256').update(hash1).digest(); return hash2.toString('hex'); diff --git a/backend/services/identity-service/src/application/event-handlers/blockchain-wallet.handler.ts b/backend/services/identity-service/src/application/event-handlers/blockchain-wallet.handler.ts index 82b62906..ffaa79f1 100644 --- a/backend/services/identity-service/src/application/event-handlers/blockchain-wallet.handler.ts +++ b/backend/services/identity-service/src/application/event-handlers/blockchain-wallet.handler.ts @@ -110,6 +110,7 @@ export class BlockchainWalletHandler implements OnModuleInit { } // 5. Update Redis status to completed (include mnemonic for first-time retrieval) + // Uses atomic operation to ensure proper state transition const statusData: WalletCompletedStatusData = { status: 'completed', userId, @@ -119,13 +120,18 @@ export class BlockchainWalletHandler implements OnModuleInit { updatedAt: new Date().toISOString(), }; - await this.redisService.set( + const updated = await this.redisService.updateKeygenStatusAtomic( `${KEYGEN_STATUS_PREFIX}${userId}`, JSON.stringify(statusData), + 'completed', KEYGEN_STATUS_TTL, ); - this.logger.log(`[STATUS] Keygen status updated to 'completed' for user: ${userId}`); + if (updated) { + this.logger.log(`[STATUS] Keygen status updated to 'completed' for user: ${userId}`); + } else { + this.logger.log(`[STATUS] Status not updated for user: ${userId} (unexpected - completed should always succeed)`); + } // Log all addresses addresses.forEach((addr) => { diff --git a/backend/services/identity-service/src/application/event-handlers/mpc-keygen-completed.handler.ts b/backend/services/identity-service/src/application/event-handlers/mpc-keygen-completed.handler.ts index 3e4bfc02..8e5ed9e1 100644 --- a/backend/services/identity-service/src/application/event-handlers/mpc-keygen-completed.handler.ts +++ b/backend/services/identity-service/src/application/event-handlers/mpc-keygen-completed.handler.ts @@ -88,6 +88,9 @@ export class MpcKeygenCompletedHandler implements OnModuleInit { * From mpc-service, keygen is complete with public key. * Update status to "deriving" - blockchain-service will now derive addresses * and send WalletAddressCreated event which BlockchainWalletHandler will process. + * + * Uses atomic Redis update to ensure status only advances forward: + * pending -> generating -> deriving -> completed */ private async handleKeygenCompleted(payload: KeygenCompletedPayload): Promise { const { publicKey, extraPayload } = payload; @@ -100,10 +103,12 @@ export class MpcKeygenCompletedHandler implements OnModuleInit { const { userId, username } = extraPayload; this.logger.log(`[STATUS] Keygen completed: userId=${userId}, username=${username}`); this.logger.log(`[STATUS] Public key: ${publicKey?.substring(0, 30)}...`); - this.logger.log(`[STATUS] Waiting for blockchain-service to derive addresses...`); try { + this.logger.log(`[STATUS] Waiting for blockchain-service to derive addresses...`); + // Update status to "deriving" - waiting for blockchain-service + // Uses atomic operation to ensure we don't overwrite higher-priority status const statusData: KeygenStatusData = { status: 'deriving', userId, @@ -111,14 +116,19 @@ export class MpcKeygenCompletedHandler implements OnModuleInit { updatedAt: new Date().toISOString(), }; - await this.redisService.set( + const updated = await this.redisService.updateKeygenStatusAtomic( `${KEYGEN_STATUS_PREFIX}${userId}`, JSON.stringify(statusData), + 'deriving', KEYGEN_STATUS_TTL, ); - this.logger.log(`[STATUS] Keygen status updated to 'deriving' for user: ${userId}`); - this.logger.log(`[STATUS] blockchain-service will derive addresses and send WalletAddressCreated event`); + if (updated) { + this.logger.log(`[STATUS] Keygen status updated to 'deriving' for user: ${userId}`); + this.logger.log(`[STATUS] blockchain-service will derive addresses and send WalletAddressCreated event`); + } else { + this.logger.log(`[STATUS] Status not updated for user: ${userId} (current status has higher priority)`); + } } catch (error) { this.logger.error(`[ERROR] Failed to update keygen status: ${error}`, error); } diff --git a/backend/services/identity-service/src/application/services/user-application.service.ts b/backend/services/identity-service/src/application/services/user-application.service.ts index f6333fdb..9d3f7410 100644 --- a/backend/services/identity-service/src/application/services/user-application.service.ts +++ b/backend/services/identity-service/src/application/services/user-application.service.ts @@ -156,27 +156,31 @@ export class UserApplicationService { if (!account) throw new ApplicationError('账户序列号不存在'); if (!account.isActive) throw new ApplicationError('账户已冻结或注销'); - // 获取账户的钱包地址用于验证 - const expectedAddresses: Array<{ chainType: string; address: string }> = []; - const kavaWallet = account.getWalletAddress(ChainType.KAVA); - if (kavaWallet) { - expectedAddresses.push({ chainType: 'KAVA', address: kavaWallet.address }); + // 检查验证失败次数限制 (防止暴力破解) + const failKey = `mnemonic:fail:${command.accountSequence}`; + const failCountStr = await this.redisService.get(failKey); + const failCount = failCountStr ? parseInt(failCountStr, 10) : 0; + if (failCount >= 5) { + throw new ApplicationError('验证次数过多,请1小时后重试'); } - if (expectedAddresses.length === 0) { - throw new ApplicationError('账户没有关联的钱包地址'); - } - - // 调用 blockchain-service 验证助记词 - const verifyResult = await this.blockchainClient.verifyMnemonic({ + // 调用 blockchain-service 验证助记词 hash + // 助记词作为身份凭证,通过 hash 匹配验证,不需要与钱包地址关联 + const verifyResult = await this.blockchainClient.verifyMnemonicByAccount({ + accountSequence: command.accountSequence, mnemonic: command.mnemonic, - expectedAddresses, }); if (!verifyResult.valid) { + // 记录失败次数 + await this.redisService.incr(failKey); + await this.redisService.expire(failKey, 3600); // 1小时过期 throw new ApplicationError('助记词错误'); } + // 验证成功,清除失败计数 + await this.redisService.delete(failKey); + account.addDevice(command.newDeviceId, command.deviceName); account.recordLogin(); await this.userRepository.save(account); diff --git a/backend/services/identity-service/src/infrastructure/redis/redis.service.ts b/backend/services/identity-service/src/infrastructure/redis/redis.service.ts index d4626787..8628096c 100644 --- a/backend/services/identity-service/src/infrastructure/redis/redis.service.ts +++ b/backend/services/identity-service/src/infrastructure/redis/redis.service.ts @@ -44,6 +44,68 @@ export class RedisService implements OnModuleDestroy { await this.client.expire(key, seconds); } + /** + * 原子更新 keygen 状态 + * 使用 Lua 脚本确保状态只能向前推进: pending < generating < deriving < completed + * failed 状态只有在当前不是 completed 时才能设置 + * + * @returns true 如果更新成功, false 如果当前状态优先级更高 + */ + async updateKeygenStatusAtomic( + key: string, + newStatusData: string, + newStatus: string, + ttlSeconds: number, + ): Promise { + // 状态优先级:completed > failed > deriving > generating > pending + // 数字越大优先级越高 + const luaScript = ` + local key = KEYS[1] + local newData = ARGV[1] + local newStatus = ARGV[2] + local ttl = tonumber(ARGV[3]) + + -- 定义状态优先级 + local priority = { + pending = 1, + generating = 2, + deriving = 3, + failed = 4, + completed = 5 + } + + local newPriority = priority[newStatus] or 0 + + -- 获取当前状态 + local currentData = redis.call('GET', key) + if currentData then + local current = cjson.decode(currentData) + local currentStatus = current.status or 'pending' + local currentPriority = priority[currentStatus] or 0 + + -- 只有新状态优先级更高时才更新 + if newPriority <= currentPriority then + return 0 -- 不更新 + end + end + + -- 更新状态 + redis.call('SET', key, newData, 'EX', ttl) + return 1 -- 更新成功 + `; + + const result = await this.client.eval( + luaScript, + 1, + key, + newStatusData, + newStatus, + ttlSeconds.toString(), + ); + + return result === 1; + } + onModuleDestroy() { this.client.disconnect(); }