password.js 7.3 KB
Newer Older
DCloud_JSON's avatar
DCloud_JSON 已提交
1 2 3 4
const {
  getType
} = require('../../common/utils')
const crypto = require('crypto')
study夏羽's avatar
study夏羽 已提交
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
const createConfig = require('uni-config-center')
const shareConfig = createConfig({
  pluginId: 'uni-id'
})
let customPassword = {}
if (shareConfig.hasFile('custom-password.js')) {
  customPassword = shareConfig.requireFile('custom-password.js') || {}
}

const passwordAlgorithmMap = {
  UNI_ID_HMAC_SHA1: 'hmac-sha1',
  UNI_ID_HMAC_SHA256: 'hmac-sha256',
  UNI_ID_CUSTOM: 'custom'
}

const passwordAlgorithmKeyMap = Object.keys(passwordAlgorithmMap).reduce((res, item) => {
  res[passwordAlgorithmMap[item]] = item
  return res
}, {})

const passwordExtMethod = {
  [passwordAlgorithmMap.UNI_ID_HMAC_SHA1]: {
    verify ({ password }) {
      const { password_secret_version: passwordSecretVersion } = this.userRecord

      const passwordSecret = this._getSecretByVersion({
        version: passwordSecretVersion
      })

      const { passwordHash } = this.encrypt({
        password,
        passwordSecret
      })

      return passwordHash === this.userRecord.password
    },
    encrypt ({ password, passwordSecret }) {
      const { value: secret, version } = passwordSecret
      const hmac = crypto.createHmac('sha1', secret.toString('ascii'))

      hmac.update(password)

      return {
        passwordHash: hmac.digest('hex'),
        version
      }
    }
  },
  [passwordAlgorithmMap.UNI_ID_HMAC_SHA256]: {
    verify ({ password }) {
      const parse = this._parsePassword()
      const passwordHash = crypto.createHmac(parse.algorithm, parse.salt).update(password).digest('hex')

      return passwordHash === parse.hash
    },
    encrypt ({ password, passwordSecret }) {
      const { version } = passwordSecret

      // 默认使用 sha256 加密算法
      const salt = crypto.randomBytes(10).toString('hex')
      const sha256Hash = crypto.createHmac(passwordAlgorithmMap.UNI_ID_HMAC_SHA256.substring(5), salt).update(password).digest('hex')
      const algorithm = passwordAlgorithmKeyMap[passwordAlgorithmMap.UNI_ID_HMAC_SHA256]
      // B 为固定值,对应 PasswordMethodMaps 中的 sha256算法
      // hash 格式 $[PasswordMethodFlagMapsKey]$[salt size]$[salt][Hash]
      const passwordHash = `$${algorithm}$${salt.length}$${salt}${sha256Hash}`

      return {
        passwordHash,
        version
      }
    }
  },
  [passwordAlgorithmMap.UNI_ID_CUSTOM]: {
    verify ({ password, passwordSecret }) {
      if (!customPassword.verifyPassword) throw new Error('verifyPassword method not found in custom password file')

      // return true or false
      return customPassword.verifyPassword({
        password,
        passwordSecret,
        userRecord: this.userRecord,
        clientInfo: this.clientInfo
      })
    },
    encrypt ({ password, passwordSecret }) {
      if (!customPassword.encryptPassword) throw new Error('encryptPassword method not found in custom password file')
DCloud_JSON's avatar
DCloud_JSON 已提交
91

study夏羽's avatar
study夏羽 已提交
92 93 94 95 96 97 98
      // return object<{passwordHash: string, version: number}>
      return customPassword.encryptPassword({
        password,
        passwordSecret,
        clientInfo: this.clientInfo
      })
    }
DCloud_JSON's avatar
DCloud_JSON 已提交
99 100 101 102 103
  }
}

class PasswordUtils {
  constructor ({
study夏羽's avatar
study夏羽 已提交
104 105
    userRecord = {},
    clientInfo,
DCloud_JSON's avatar
DCloud_JSON 已提交
106 107
    passwordSecret
  } = {}) {
study夏羽's avatar
study夏羽 已提交
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
    if (!clientInfo) throw new Error('Invalid clientInfo')
    if (!passwordSecret) throw new Error('Invalid password secret')

    this.clientInfo = clientInfo
    this.userRecord = userRecord
    this.passwordSecret = this.prePasswordSecret(passwordSecret)
  }

  /**
   * passwordSecret 预处理
   * @param passwordSecret
   * @return {*[]}
   */
  prePasswordSecret (passwordSecret) {
    const newPasswordSecret = []
    if (getType(passwordSecret) === 'string') {
      newPasswordSecret.push({
        value: passwordSecret,
        type: passwordAlgorithmMap.UNI_ID_HMAC_SHA1
DCloud_JSON's avatar
DCloud_JSON 已提交
127
      })
study夏羽's avatar
study夏羽 已提交
128 129 130 131 132 133 134 135
    } else if (getType(passwordSecret) === 'array') {
      for (const secret of passwordSecret.sort((a, b) => a.version - b.version)) {
        newPasswordSecret.push({
          ...secret,
          // 没有 type 设置默认 type hmac-sha1
          type: secret.type || passwordAlgorithmMap.UNI_ID_HMAC_SHA1
        })
      }
DCloud_JSON's avatar
DCloud_JSON 已提交
136 137 138
    } else {
      throw new Error('Invalid password secret')
    }
study夏羽's avatar
study夏羽 已提交
139 140

    return newPasswordSecret
DCloud_JSON's avatar
DCloud_JSON 已提交
141 142
  }

study夏羽's avatar
study夏羽 已提交
143 144 145 146 147 148 149 150 151 152 153 154 155 156
  /**
   * 获取最新加密密钥
   * @return {*}
   * @private
   */
  _getLastestSecret () {
    return this.passwordSecret[this.passwordSecret.length - 1]
  }

  _getOldestSecret () {
    return this.passwordSecret[0]
  }

  _getSecretByVersion ({ version } = {}) {
DCloud_JSON's avatar
DCloud_JSON 已提交
157
    if (!version && version !== 0) {
study夏羽's avatar
study夏羽 已提交
158
      return this._getOldestSecret()
DCloud_JSON's avatar
DCloud_JSON 已提交
159 160 161 162 163 164 165
    }
    if (this.passwordSecret.length === 1) {
      return this.passwordSecret[0]
    }
    return this.passwordSecret.find(item => item.version === version)
  }

study夏羽's avatar
study夏羽 已提交
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
  /**
   * 获取密码的验证/加密方法
   * @param passwordSecret
   * @return {*[]}
   * @private
   */
  _getPasswordExt (passwordSecret) {
    const ext = passwordExtMethod[passwordSecret.type]
    if (!ext) {
      throw new Error(`暂不支持 ${passwordSecret.type} 类型的加密算法`)
    }

    const passwordExt = Object.create(null)

    for (const key in ext) {
      passwordExt[key] = ext[key].bind(Object.assign(this, Object.keys(ext).reduce((res, item) => {
        if (item !== key) {
          res[item] = ext[item].bind(this)
        }
        return res
      }, {})))
    }

    return passwordExt
DCloud_JSON's avatar
DCloud_JSON 已提交
190 191
  }

study夏羽's avatar
study夏羽 已提交
192 193 194 195 196 197 198 199 200 201 202
  _parsePassword () {
    const [algorithmKey = '', cost = 0, hashStr = ''] = this.userRecord.password.split('$').filter(key => key)
    const algorithm = passwordAlgorithmMap[algorithmKey] ? passwordAlgorithmMap[algorithmKey].substring(5) : null
    const salt = hashStr.substring(0, Number(cost))
    const hash = hashStr.substring(Number(cost))

    return {
      algorithm,
      salt,
      hash
    }
DCloud_JSON's avatar
DCloud_JSON 已提交
203 204
  }

study夏羽's avatar
study夏羽 已提交
205 206 207 208 209 210 211 212 213 214 215
  /**
   * 生成加密后的密码
   * @param {String} password 密码
   */
  generatePasswordHash ({ password }) {
    if (!password) throw new Error('Invalid password')

    const passwordSecret = this._getLastestSecret()
    const ext = this._getPasswordExt(passwordSecret)

    const { passwordHash, version } = ext.encrypt({
DCloud_JSON's avatar
DCloud_JSON 已提交
216
      password,
study夏羽's avatar
study夏羽 已提交
217
      passwordSecret
DCloud_JSON's avatar
DCloud_JSON 已提交
218
    })
study夏羽's avatar
study夏羽 已提交
219 220 221 222

    return {
      passwordHash,
      version
DCloud_JSON's avatar
DCloud_JSON 已提交
223
    }
study夏羽's avatar
study夏羽 已提交
224 225 226 227 228 229 230 231 232 233 234 235 236 237
  }

  /**
   * 密码校验
   * @param {String} password
   * @param {Boolean} autoRefresh
   * @return {{refreshPasswordInfo: {version: *, passwordHash: *}, success: boolean}|{success: boolean}}
   */
  checkUserPassword ({ password, autoRefresh = true }) {
    if (!password) throw new Error('Invalid password')

    const { password_secret_version: passwordSecretVersion } = this.userRecord
    const passwordSecret = this._getSecretByVersion({
      version: passwordSecretVersion
DCloud_JSON's avatar
DCloud_JSON 已提交
238
    })
study夏羽's avatar
study夏羽 已提交
239 240 241 242 243
    const ext = this._getPasswordExt(passwordSecret)

    const success = ext.verify({ password, passwordSecret })

    if (!success) {
DCloud_JSON's avatar
DCloud_JSON 已提交
244 245 246 247
      return {
        success: false
      }
    }
study夏羽's avatar
study夏羽 已提交
248

DCloud_JSON's avatar
DCloud_JSON 已提交
249
    let refreshPasswordInfo
study夏羽's avatar
study夏羽 已提交
250 251
    if (autoRefresh && passwordSecretVersion !== this._getLastestSecret().version) {
      refreshPasswordInfo = this.generatePasswordHash({ password })
DCloud_JSON's avatar
DCloud_JSON 已提交
252
    }
study夏羽's avatar
study夏羽 已提交
253

DCloud_JSON's avatar
DCloud_JSON 已提交
254 255 256 257 258 259 260 261
    return {
      success: true,
      refreshPasswordInfo
    }
  }
}

module.exports = PasswordUtils