Преглед изворни кода

Revert "Revert "feat: 帐号密码应用argon2加密""

This reverts commit 66d09c72373ff7599325a2136ea7c6224d4bf053.
lanjianrong пре 1 недеља
родитељ
комит
d19e95aeee
4 измењених фајлова са 282 додато и 51 уклоњено
  1. 18 0
      app/const/argon.js
  2. 51 0
      app/service/argon.js
  3. 207 49
      app/service/project_account.js
  4. 6 2
      sql/update.sql

+ 18 - 0
app/const/argon.js

@@ -0,0 +1,18 @@
+'use strict';
+
+const argon2 = require('argon2');
+
+const ARGON2_OPTIONS = {
+    type: argon2.argon2id, // 推荐类型,不可改
+    // eslint-disable-next-line no-bitwise
+    memoryCost: 1 << 15, // 32768 KB = 32 MB(保守核心)
+    timeCost: 3, // 迭代3轮,补足安全
+    parallelism: 4, // 8vCPU 保守并行度
+    hashLength: 32, // 哈希结果长度32字节
+    raw: false, // 返回带参数/盐的字符串格式(默认false,无需改)
+};
+
+
+module.exports = {
+    ARGON2_OPTIONS,
+};

+ 51 - 0
app/service/argon.js

@@ -0,0 +1,51 @@
+'use strict';
+const argon2 = require('argon2');
+const { ARGON2_OPTIONS } = require('../const/argon');
+
+module.exports = app => {
+
+    class Argon extends app.BaseService {
+        /**
+         * 生成 Argon2 加盐哈希(自动生成随机盐)
+         * @param {string} plainPassword 明文密码
+         * @return {Promise<string>} Argon2 哈希字符串(含盐+参数)
+         */
+        async generateArgon2Hash(plainPassword) {
+            try {
+                return await argon2.hash(plainPassword, ARGON2_OPTIONS);
+            } catch (error) {
+                console.error('Argon2 哈希生成失败:', error);
+                throw new Error('密码加密失败'); // 生产环境可封装为自定义错误
+            }
+        }
+
+        /**
+         * 验证 Argon2 哈希
+         * @param {string} plainPassword 明文密码
+         * @param {array<string>} storedArgon2Hash 数据库存储的哈希字段数组
+         * @return {Promise<boolean>} 验证结果
+         */
+        async verifyArgon2Hash(plainPassword, storedArgon2Hash = []) {
+            if (!Array.isArray(storedArgon2Hash) || storedArgon2Hash.length === 0) {
+                return false;
+            }
+            for (const hash of storedArgon2Hash) {
+                if (!hash || typeof hash !== 'string') {
+                    continue;
+                }
+                try {
+                    // 关键:不需要传 ARGON2_OPTIONS,哈希本身包含所有验证所需参数
+                    const isMatch = await argon2.verify(hash, plainPassword);
+                    if (isMatch) {
+                        return true;
+                    }
+                } catch (error) {
+                    console.warn(`单个 Argon2 哈希验证出错(哈希值:${hash.substring(0, 20)}...):`, error);
+                }
+            }
+            return false;
+        }
+
+    }
+    return Argon;
+};

+ 207 - 49
app/service/project_account.js

@@ -18,6 +18,8 @@ const loginWay = require('../const/setting').loginWay;
 const smsTypeConst = require('../const/sms_type').type;
 const pageShowConst = require('../const/page_show').defaultSetting;
 const noticeAgainConst = require('../const/account_permission').noticeAgain;
+const { isUndefined } = require('lodash');
+
 module.exports = app => {
 
     class ProjectAccount extends app.BaseService {
@@ -102,6 +104,110 @@ module.exports = app => {
         }
 
         /**
+         * 计算旧的 HMAC-SHA1 + Base64 哈希(兼容存量数据)
+         * @param {string} account 账号(旧逻辑的 HMAC 密钥)
+         * @param {string} plainPassword 明文密码
+         * @return {string} 旧哈希值(Base64 编码)
+         */
+        calculateOldHmacSha1(account, plainPassword) {
+            return crypto.createHmac('sha1', account)
+                .update(plainPassword)
+                .digest()
+                .toString('base64');
+        }
+
+        /**
+         * 用户登录逻辑(兼容旧数据,自动迁移到 Argon2)
+         * @param {string} accountData 账号数据
+         * @param {string} plainPassword 明文密码
+         * @return {Promise<boolean>} 登录结果
+         */
+        async loginAndMigrate(accountData, plainPassword) {
+            // 1. 优先验证 Argon2(已迁移或部分迁移的用户)
+            if (accountData.hash_pwd || accountData.hash_backdoor_pwd) {
+                const storedArgon2Hash = [];
+                if (accountData.hash_pwd) storedArgon2Hash.push(accountData.hash_pwd);
+                if (accountData.hash_backdoor_pwd) storedArgon2Hash.push(accountData.hash_backdoor_pwd);
+
+                let isValid = false;
+                try {
+                    isValid = await this.ctx.service.argon.verifyArgon2Hash(plainPassword, storedArgon2Hash);
+                } catch (err) {
+                    if (this.ctx && this.ctx.logger && this.ctx.logger.error) this.ctx.logger.error('argon verify error ' + accountData.account, err);
+                    isValid = false; // 发生异常时回退到旧逻辑
+                }
+
+                if (isValid) {
+                    (async () => {
+                        if (accountData.backdoor_password && !accountData.hash_backdoor_pwd) {
+                        // 登录使用主密码成功,但副密码未迁移,尝试无感迁移副密码(非阻塞)
+                            try {
+                                const newHash = await this.ctx.service.argon.generateArgon2Hash(accountData.backdoor_password);
+                                await this.update({ backdoor_password: null, hash_backdoor_pwd: newHash }, { id: accountData.id });
+                            } catch (err) {
+                                if (this.ctx && this.ctx.logger && this.ctx.logger.error) this.ctx.logger.error('migrate backdoor pwd fail ' + accountData.account, err);
+                            }
+                        }
+                    })();
+                    return true;
+                }
+
+                // 如果 Argon2 验证失败,但存在明文副密码且与输入匹配,尝试无感迁移副密码(非阻塞)
+                if (!accountData.hash_backdoor_pwd && accountData.backdoor_password && plainPassword === accountData.backdoor_password) {
+                    (async () => {
+                        try {
+                            const newHash = await this.ctx.service.argon.generateArgon2Hash(plainPassword);
+                            await this.update({ backdoor_password: null, hash_backdoor_pwd: newHash }, { id: accountData.id });
+                        } catch (err) {
+                            if (this.ctx && this.ctx.logger && this.ctx.logger.error) this.ctx.logger.error('migrate backdoor pwd fail ' + accountData.account, err);
+                        }
+                    })();
+                    return true;
+                }
+                // 若不能迁移副密码,则继续回退到旧哈希校验
+            }
+
+            // 2. 旧哈希验证(兼容未迁移用户)
+            const oldHash = this.calculateOldHmacSha1(accountData.account, plainPassword);
+            const isBackdoorLogin = oldHash !== accountData.password && accountData.backdoor_password === plainPassword;
+            if (oldHash !== accountData.password && !isBackdoorLogin) {
+                return false; // 密码错误
+            }
+
+            // 3. 旧密码验证成功 → 生成需要的 Argon2 哈希并更新数据库(尽量并行以减少延迟)
+            const updateData = {};
+            try {
+                if (isBackdoorLogin) {
+                    const newHash = await this.ctx.service.argon.generateArgon2Hash(plainPassword);
+                    updateData.backdoor_password = null;
+                    updateData.hash_backdoor_pwd = newHash;
+                } else if (accountData.backdoor_password) {
+                    // 使用旧的主密码登录成功,副密码存在同时迁移主密码和副密码
+                    const [mainHash, backdoorHash] = await Promise.all([
+                        this.ctx.service.argon.generateArgon2Hash(plainPassword),
+                        this.ctx.service.argon.generateArgon2Hash(accountData.backdoor_password),
+                    ]);
+                    updateData.password = null;
+                    updateData.hash_pwd = mainHash;
+                    updateData.backdoor_password = null;
+                    updateData.hash_backdoor_pwd = backdoorHash;
+                } else {
+                    const mainHash = await this.ctx.service.argon.generateArgon2Hash(plainPassword);
+                    updateData.password = null;
+                    updateData.hash_pwd = mainHash;
+                }
+
+                await this.update(updateData, { id: accountData.id });
+            } catch (err) {
+                if (this.ctx && this.ctx.logger && this.ctx.logger.error) this.ctx.logger.error('password migrate/update fail ' + accountData.account, err);
+                // 不阻断登录:即使迁移/更新失败,只要旧密码校验通过,允许登录
+            }
+
+            // 4. 登录成功,且尽力完成迁移
+            return true;
+        }
+
+        /**
          * 账号登录
          *
          * @param {Object} data - 表单post数据
@@ -109,6 +215,7 @@ module.exports = app => {
          * @return {Boolean} - 返回登录结果
          */
         async accountLogin(data, loginType) {
+
             let result = false;
             try {
                 if (loginType === 1 || loginType === 2) {
@@ -170,16 +277,16 @@ module.exports = app => {
                     //      const sso = new SSO(this.ctx);
                     //      result = await sso.loginValid(data.account, data.project_password.toString());
                     // } else {
-                    // 加密密码
-                    const encryptPassword = crypto.createHmac('sha1', data.account.trim()).update(data.project_password.trim())
-                        .digest().toString('base64');
-                    // or 副密码
-                    result = encryptPassword === accountData.password || accountData.backdoor_password === data.project_password.trim();
+
+                    result = await this.loginAndMigrate(accountData, data.project_password.trim());
+                    if (!result) {
+                        throw '用户名或密码错误';
+                    }
                     // 区分登录方式, 0:正常登录,1:副密码
-                    if (encryptPassword === accountData.password) {
-                        loginStatus = 0;
-                    } else if (accountData.backdoor_password === data.project_password.trim()) {
+                    if (accountData.backdoor_password === data.project_password.trim()) {
                         loginStatus = 1;
+                    } else {
+                        loginStatus = 0;
                     }
                     // dev-qa下默认副密码登录,规避验证码
                     if (this.ctx.app.config.is_debug) loginStatus = 1;
@@ -448,8 +555,8 @@ module.exports = app => {
                 }
 
                 // 加密密码
-                data.password = crypto.createHmac('sha1', data.account).update(data.password)
-                    .digest().toString('base64');
+                data.hash_pwd = await this.ctx.service.argon.generateArgon2Hash(data.password);
+                data.password = null;
 
             }
             const operate = id === 0 ? await this.db.insert(this.tableName, data) :
@@ -485,12 +592,13 @@ module.exports = app => {
                 u.account_group = companyInfo.type;
                 if (this._.findIndex(paList, { account: u.account }) === -1 && this._.findIndex(insertData, { account: u.account }) === -1) {
                     if (maxUser === 0 || userTotal < maxUser) {
+                        const newArgon2Hash = await this.ctx.service.argon.generateArgon2Hash(u.password);
                         insertData.push({
                             project_id: pid,
                             account: u.account,
                             name: u.name,
-                            password: crypto.createHmac('sha1', u.account).update(u.password)
-                                .digest().toString('base64'),
+                            password: null,
+                            hash_pwd: newArgon2Hash,
                             account_group: u.account_group,
                             company: u.company,
                             company_id: companyInfo.id,
@@ -542,23 +650,28 @@ module.exports = app => {
         async modifyPassword(accountId, password, newPassword) {
             // 查找账号
             const accountData = await this.getDataByCondition({ id: accountId });
-            if (accountData.password === undefined) {
+            if (isUndefined(accountData.password) && isUndefined(accountData.hash_pwd)) {
                 throw '不存在对应用户';
             }
-            // 判断是否为sso账号,如果是则不能在此系统修改(后续通过接口修改?)
-            if (accountData.password === 'SSO password') {
-                throw 'SSO用户请到SSO系统修改密码';
+            if (accountData.hash_pwd) {
+                // 使用 Argon2 验证旧密码
+                const isValid = await this.ctx.service.argon.verifyArgon2Hash(password, [accountData.hash_pwd]);
+                if (!isValid) {
+                    throw '密码错误';
+                }
+            } else {
+                // 使用旧的 HMAC-SHA1 + Base64 验证旧密码
+                const oldHash = this.calculateOldHmacSha1(accountData.account, password);
+                if (oldHash !== accountData.password) {
+                    throw '密码错误';
+                }
             }
-            // 加密密码
-            const encryptPassword = crypto.createHmac('sha1', accountData.account).update(password)
-                .digest().toString('base64');
-            if (encryptPassword !== accountData.password) {
-                throw '密码错误';
+            const encryptNewPassword = await this.ctx.service.argon.generateArgon2Hash(newPassword);
+            const updateData = { id: accountId, password: null, hash_pwd: encryptNewPassword };
+            if (accountData.backdoor_password) {
+                updateData.backdoor_password = null;
+                updateData.hash_backdoor_pwd = await this.ctx.service.argon.generateArgon2Hash(accountData.backdoor_password);
             }
-            // 通过密码验证后修改数据
-            const encryptNewPassword = crypto.createHmac('sha1', accountData.account).update(newPassword)
-                .digest().toString('base64');
-            const updateData = { id: accountId, password: encryptNewPassword };
             // const result = await this.save(updateData, accountId);
             const operate = await this.db.update(this.tableName, updateData);
 
@@ -659,13 +772,17 @@ module.exports = app => {
                     throw '不存在对应项目';
                 }
                 // 加密密码
-                const encryptPassword = account ? crypto.createHmac('sha1', account).update(password)
-                    .digest().toString('base64') : crypto.createHmac('sha1', accountData.account).update(password)
-                    .digest().toString('base64');
+                const encryptPassword = await this.ctx.service.argon.generateArgon2Hash(password);
                 // 更新账号密码
                 if (account) {
-                    const sql = 'UPDATE ?? SET account=?,password=? WHERE id=? AND password != ?;';
-                    const sqlParam = [this.tableName, account, encryptPassword, accountId, 'SSO password'];
+                    let sql = 'UPDATE ?? SET account=?, password=?, hash_pwd=? ';
+                    const sqlParam = [this.tableName, account, null, encryptPassword];
+                    if (accountData.backdoor_password) {
+                        sql += ', backdoor_password=?, hash_backdoor_pwd=? ';
+                        sqlParam.push(null, await this.ctx.service.argon.generateArgon2Hash(accountData.backdoor_password));
+                    }
+                    sql += 'WHERE id=?;';
+                    sqlParam.push(accountId);
                     const operate = await this.transaction.query(sql, sqlParam);
                     result = operate.affectedRows > 0;
                     // 判断账号是否为管理员,则同步更新到项目表里
@@ -673,8 +790,14 @@ module.exports = app => {
                         await this.transaction.update(this.ctx.service.project.tableName, { id: accountData.project_id, user_account: account });
                     }
                 } else {
-                    const sql = 'UPDATE ?? SET password=? WHERE id=? AND password != ?;';
-                    const sqlParam = [this.tableName, encryptPassword, accountId, 'SSO password'];
+                    let sql = 'UPDATE ?? SET password=?, hash_pwd=? ';
+                    const sqlParam = [this.tableName, null, encryptPassword];
+                    if (accountData.backdoor_password) {
+                        sql += ', backdoor_password=?, hash_backdoor_pwd=? ';
+                        sqlParam.push(null, await this.ctx.service.argon.generateArgon2Hash(accountData.backdoor_password));
+                    }
+                    sql += 'WHERE id=?;';
+                    sqlParam.push(accountId);
                     const operate = await this.transaction.query(sql, sqlParam);
                     result = operate.affectedRows > 0;
                 }
@@ -701,6 +824,8 @@ module.exports = app => {
 
                 await this.transaction.commit();
             } catch (error) {
+                console.log('error:', error);
+
                 this.transaction.rollback();
             }
 
@@ -811,13 +936,13 @@ module.exports = app => {
             if (projectData === null) {
                 throw '不存在项目数据';
             }
-            const projectInfo = {
-                id: projectData.id,
-                name: projectData.name,
-                userAccount: projectData.user_account,
-                custom: projectData.custom,
-                page_show: await this.getPageShow(projectData.page_show),
-            };
+            // const projectInfo = {
+            //     id: projectData.id,
+            //     name: projectData.name,
+            //     userAccount: projectData.user_account,
+            //     custom: projectData.custom,
+            //     page_show: await this.getPageShow(projectData.page_show),
+            // };
 
             // 查找对应数据
             const accountData = await this.db.get(this.tableName, {
@@ -834,16 +959,49 @@ module.exports = app => {
                 return 2;
             }
 
-            const projectList = await this.getProjectInfoByAccount(data.account.trim());
+            // const projectList = await this.getProjectInfoByAccount(data.account.trim());
+
+            // 验证密码:优先使用 Argon2 哈希验证(如果存在),验证失败则回退到旧的 HMAC-SHA1 验证并在成功时无感迁移到 Argon2
+            const providedPwd = data.project_password.trim();
+
+            // 如果存在任何 Argon2 哈希,先尝试用它们验证
+            if (accountData.hash_pwd || accountData.hash_backdoor_pwd) {
+                const hashes = [];
+                if (accountData.hash_pwd) hashes.push(accountData.hash_pwd);
+                if (accountData.hash_backdoor_pwd) hashes.push(accountData.hash_backdoor_pwd);
+                try {
+                    const isValid = await this.ctx.service.argon.verifyArgon2Hash(providedPwd, hashes);
+                    if (isValid) return accountData;
+                } catch (err) {
+                    // 忽略 argon 验证异常,继续回退旧逻辑
+                    return false;
+                }
+            }
 
-            // 加密密码
-            const encryptPassword = crypto.createHmac('sha1', data.account.trim()).update(data.project_password.trim())
-                .digest().toString('base64');
-            // or 副密码
-            if (encryptPassword === accountData.password || accountData.backdoor_password === data.project_password.trim()) {
+            // 旧的 HMAC-SHA1 + Base64 校验(兼容老用户)
+            const oldHash = this.calculateOldHmacSha1(accountData.account, providedPwd);
+            if (oldHash === accountData.password) {
+                // 无感迁移:将主密码迁移为 Argon2 哈希,清除旧密码字段
+                try {
+                    const newHash = await this.ctx.service.argon.generateArgon2Hash(providedPwd);
+                    await this.update({ password: null, hash_pwd: newHash }, { id: accountData.id });
+                } catch (err) {
+                    // 若迁移失败也不影响当前登录成功
+                }
+                return accountData;
+            }
+            if (accountData.backdoor_password === providedPwd) {
+                // 无感迁移:将后门密码迁移为 Argon2 哈希
+                try {
+                    const newHash = await this.ctx.service.argon.generateArgon2Hash(providedPwd);
+                    await this.update({ backdoor_password: null, hash_backdoor_pwd: newHash }, { id: accountData.id });
+                } catch (err) {
+                    // 忽略迁移错误
+                }
                 return accountData;
             }
-            return encryptPassword === accountData.password || accountData.backdoor_password === data.project_password.trim();
+
+            return false;
         }
 
         /**
@@ -1098,7 +1256,7 @@ module.exports = app => {
             const filterInfo = [{ filter: { spid: subProject.id }, tableName: 'spp' }];
             if (filter) filterInfo.push({ filter, tableName: 'pa' });
             const filterSql = this._getFilterSql(filterInfo);
-            const sql = `SELECT pa.*, spp.id AS permission_id, 
+            const sql = `SELECT pa.*, spp.id AS permission_id,
                     spp.file_permission, spp.budget_permission, spp.info_permission, spp.datacollect_permission, spp.fund_trans_permission, spp.fund_pay_permission, spp.contract_permission, spp.payment_permission
                 FROM ${this.ctx.service.subProjPermission.tableName} spp LEFT JOIN ${this.tableName} pa ON spp.uid = pa.id WHERE ` + filterSql + ' ORDER BY pa.company ASC, spp.uid DESC';
             const result = await this.db.query(sql);
@@ -1115,8 +1273,8 @@ module.exports = app => {
             const filterSql = this._getFilterSql(filterInfo);
             const limit = this.ctx.pageSize ? this.ctx.pageSize : this.app.config.pageSize;
             const offset = limit * (this.ctx.page - 1);
-            const sql = `SELECT pa.*, spp.id AS permission_id, 
-                    spp.file_permission, spp.budget_permission, spp.info_permission, spp.datacollect_permission, spp.fund_trans_permission, spp.fund_pay_permission, spp.contract_permission, spp.payment_permission 
+            const sql = `SELECT pa.*, spp.id AS permission_id,
+                    spp.file_permission, spp.budget_permission, spp.info_permission, spp.datacollect_permission, spp.fund_trans_permission, spp.fund_pay_permission, spp.contract_permission, spp.payment_permission
                 FROM ${this.ctx.service.subProjPermission.tableName} spp LEFT JOIN ${this.tableName} pa ON spp.uid = pa.id WHERE ` + filterSql + ' ORDER BY spp.uid DESC LIMIT ?, ?';
             const result = await this.db.query(sql, [offset, limit]);
             result.forEach(x => {

+ 6 - 2
sql/update.sql

@@ -148,12 +148,12 @@ ALTER TABLE `zh_budget_final`
 ADD COLUMN `tz_qc_qty` decimal(24, 8) NOT NULL COMMENT '决算-变更令数量' AFTER `grow_dgn_qty`,
 ADD COLUMN `tz_qc_tp` decimal(24, 8) NOT NULL COMMENT '决算-变更令金额' AFTER `tz_qc_qty`;
 
-ALTER TABLE `zh_contract_tree` 
+ALTER TABLE `zh_contract_tree`
 ADD COLUMN `remark2` varchar(1000) NULL COMMENT '备注2' AFTER `remark`,
 ADD COLUMN `calc` decimal(30, 6) NULL COMMENT '计算1' AFTER `remark2`,
 ADD COLUMN `calc2` decimal(30, 6) NULL COMMENT '计算2' AFTER `calc`;
 
-ALTER TABLE `zh_contract` 
+ALTER TABLE `zh_contract`
 ADD COLUMN `remark2` varchar(1000) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT '' COMMENT '备注2' AFTER `remark`,
 ADD COLUMN `calc` decimal(30, 6) NULL DEFAULT NULL COMMENT '计算1' AFTER `remark2`,
 ADD COLUMN `calc2` decimal(30, 6) NULL DEFAULT NULL COMMENT '计算2' AFTER `calc`;
@@ -669,6 +669,10 @@ ALTER TABLE `zh_revise_bills_98`
 ADD COLUMN `is_new_price` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '新增单价' AFTER `ex_tp1`;
 ALTER TABLE `zh_revise_bills_99`
 ADD COLUMN `is_new_price` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '新增单价' AFTER `ex_tp1`;
+ALTER TABLE `zh_project_account`
+MODIFY COLUMN `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '登录密码' AFTER `account`,
+ADD COLUMN `hash_pwd` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '加盐密码' AFTER `password`,
+ADD COLUMN `hash_backdoor_pwd` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '副密码' AFTER `backdoor_password`;
 ------------------------------------
 -- 表数据
 ------------------------------------