You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从账户凭据派生密钥密码生成加密密钥时403签名无效问题排查

账户凭据派生密钥密码导致请求签名无效(403错误)排查

问题背景

从账户凭据(用户名+密码+盐)派生密钥密码,用于在服务器端生成加密密钥时,始终返回403错误:请求签名无效。其中Key Signature需用密钥密码计算,Request Signature需用账户密码计算,密钥密码要求客户端唯一、服务器不可知,已尝试服务器文档中的所有方法均未解决。

输入参数

LocalName   Local Name of the algorithm to use for the new key.
Namespace   Namespace, defining the algorithm.
Id          ID of the key. The ID must be unique, for the current account. Different accounts can have keys with the same ID.
Nonce       A unique random string, at least 32 characters long, with sufficient entropy to not be reused again. If reused, an error will be returned.
KeySignature    Cryptographic signature of the key ID, using the key password. (Password is not stored with the key, instead this signature will be the base for the cryptographic key used to encrypt the key on the server.
RequestSignature    Cryptographic signature of the request, using the account password.

签名计算规则

Key Signature计算步骤

1. 拼接字符串 Username ":" Host ":" LocalName ":" Namespace ":" Id,记为s1;其中Host为服务器域名,必须与HTTP请求头中的Host一致,Username为账户注册/登录时使用的用户名。
2. 将密钥对应的密码进行UTF-8编码,记为Key1。
3. 将字符串s1进行UTF-8编码,记为Data1。
4. 使用Key1和Data1计算HMAC-SHA256签名,记为H1。
5. 对H1进行Base64编码,结果即为Key Signature。

Request Signature计算步骤

1. 拼接字符串 s1 ":" KeySignature ":" Nonce,记为s2;s1为Key Signature计算过程中的中间结果。
2. 将账户对应的密码进行UTF-8编码,记为Key2。
3. 将字符串s2进行UTF-8编码,记为Data2。
4. 使用Key2和Data2计算HMAC-SHA256签名,记为H2。
5. 对H2进行Base64编码,结果即为Request Signature。

尝试过的代码

方法一:HMAC-SHA256派生密钥密码

Digest createOrGetKeyPassword(String value) {
  final salt = _generateSalt();

  final secretBytes = utf8.encode(value);

  final hmac = Hmac(sha256, salt);

  final digest = hmac.convert(secretBytes);

  print('---- KEY PASSWORD DERIVED ---- => $digest');

  return digest;
} 

方法二:使用cryptography_plus包的PBKDF2派生

Future<SecretKey> _deriveSecret(String value) async {
  final salt = _generateSalt();

  final saltString = base64Encode(salt);

  final combined = value + saltString;

  print("---- VALUE TO DERIVE ----- => $combined");

  List<int> accountBytes = utf8.encode(combined);

  final pbkdf2 = Pbkdf2(
    macAlgorithm: Hmac.sha256(),
    iterations: 100000,
    bits: 256,
  );

  final secretKey = await pbkdf2.deriveKey(
    secretKey: SecretKey(accountBytes),
    nonce: salt,
  );

  return secretKey;
}

盐生成代码

List<int> _generateSalt() {
  final random = Random.secure();
  return List.generate(32, (_) => random.nextInt(256));
}

签名计算代码

static String sign(String keyData, String dataS) {
  List<int> key = utf8.encode(keyData);

  List<int> data = utf8.encode(dataS);

  Hmac hmacSha256 = Hmac(sha256, key);
  List<int> h = hmacSha256.convert(data).bytes;

  String signature = base64Encode(h);
  return signature;
}

创建密钥的请求代码

Future<void> createKey() async {
  final host = 'api-host';
  final nonce = generateRandomString(32);
  final localName = "localName";
  final namespace = "namespace";
  final username = "userName";
  final password = "passoword";

  const id = 'key1';

  final s1 = "$username:$host:$localName:$namespace:$id";

  final deriveKeyPassowrd = username + password + id;

  final keyPassword = createOrGetKeyPassword(deriveKeyPassword);

  final keySig = AgentApiSignature.sign(keyPassword, s1);

  final s2 = "$s1:$keySig:$nonce";

  final reqSig = AgentApiSignature.sign(password, s2);

  final jsonObject = <String, dynamic>{
    'localName': localName,
    'namespace': namespace,
    'id': id,
    'nonce': nonce,
    'keySignature': keySig,
    'requestSignature': reqSig,
  };

  try {
    final response = await dio.post(
      ApiEndpoints.createKey,
      data: jsonObject,
    );

    if (response.statusCode == 200) {
     print('Success');
    }
  } on DioException catch (e) {
    ErrorHandler.handleError(e);
  }
}

错误原因分析

  1. 密钥密码派生逻辑错误:

    • 每次调用createOrGetKeyPassword都会生成新的随机盐,导致每次派生的密钥密码不一致,服务器无法基于固定规则验证签名。
    • 派生密钥密码的输入参数错误:代码中使用username + password + id作为输入,而非要求的用户名+密码+盐。
  2. 签名函数参数传递错误:

    • createOrGetKeyPassword返回Digest对象,直接传入sign函数时,会使用Digest的默认toString()结果(格式如HmacSha256(...))作为密钥,而非密钥密码的原始字节编码,导致Key1完全错误。
  3. PBKDF2使用逻辑错误:

    • 方法二中将密码与盐字符串拼接后作为输入,不符合PBKDF2的标准用法(应将密码和盐作为独立参数传入),导致派生的密钥不符合预期。
  4. Host参数可能不匹配:

    • 代码中host设置为'api-host',若与实际请求的HTTP Host头不一致,会导致s1错误,进而影响两个签名的计算。
  5. 笔误问题:

    • 请求代码中password拼写为passoword,若为实际代码中的错误,会直接导致Request Signature计算错误。

可行解决方案

1. 固定密钥密码的盐

生成一次盐后持久化存储(如SharedPreferences),后续所有请求复用该盐,确保密钥密码派生结果一致:

// 示例:从本地存储获取盐,不存在则生成并存储
Future<List<int>> getOrGenerateSalt() async {
  final prefs = await SharedPreferences.getInstance();
  final saltBase64 = prefs.getString('key_password_salt');
  if (saltBase64 != null) {
    return base64Decode(saltBase64);
  } else {
    final salt = _generateSalt();
    await prefs.setString('key_password_salt', base64Encode(salt));
    return salt;
  }
}

2. 修正密钥密码派生逻辑

按照要求使用用户名+密码+盐作为输入,返回正确的密钥密码字符串(如Base64编码的字节):

Future<String> createOrGetKeyPassword(String username, String password) async {
  final salt = await getOrGenerateSalt();
  final saltString = base64Encode(salt);
  // 使用用户名+密码+盐作为派生输入
  final input = '$username$password$saltString';
  final inputBytes = utf8.encode(input);
  
  final hmac = Hmac(sha256, salt);
  final digest = hmac.convert(inputBytes);
  
  // 返回Base64编码的密钥密码
  return base64Encode(digest.bytes);
}

3. 修正签名函数的参数传递

确保传入sign函数的是正确的密钥字符串:

// 在createKey中修改调用逻辑
final keyPassword = await createOrGetKeyPassword(username, password);
final keySig = AgentApiSignature.sign(keyPassword, s1);

4. 修正PBKDF2派生逻辑

遵循标准用法,将密码和盐作为独立参数传入:

Future<String> _deriveSecret(String username, String password, List<int> salt) async {
  final passwordBytes = utf8.encode('$username$password');
  final pbkdf2 = Pbkdf2(
    macAlgorithm: Hmac.sha256(),
    iterations: 100000,
    bits: 256,
  );
  
  final secretKey = await pbkdf2.deriveKey(
    secretKey: SecretKey(passwordBytes),
    nonce: salt,
  );
  
  final keyBytes = await secretKey.extractBytes();
  return base64Encode(keyBytes);
}

5. 验证Host参数一致性

确保s1中的host与实际请求的HTTP Host头完全一致,例如请求URL为https://api.example.com/v1/create-key时,host应设置为'api.example.com'。

6. 检查请求细节

  • 确保Nonce是长度≥32的随机字符串,且不重复。
  • 确认请求的Content-Type为application/json(Dio默认配置通常满足,但需避免手动修改)。
  • 修正账户密码的拼写错误(若代码中passoword为笔误)。

内容的提问来源于stack exchange,提问作者Mmaduegbunam Elochukwu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 09:04:52