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

Kotlin AES-GCM加密串在Angular中解密报错的解决求助

修复Angular中AES-GCM解密Kotlin加密字符串的错误

我是加密解密领域的新手,尝试在Angular(TypeScript)应用中使用Web Crypto解密由Kotlin通过AES-GCM算法加密的字符串,但解密失败并抛出错误:TypeError: Key data must be a BufferSource for non-JWK formats。以下是具体的加密、解密实现代码及示例数据:


Kotlin加密代码

import android.util.Base64
import java.io.IOException
import java.security.*
import java.security.spec.AlgorithmParameterSpec
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec

class EncryptionUtils {
    companion object {
        private const val CHARACTER_ENCODING = "UTF-8"
        private const val CIPHER_TRANSFORMATION = "AES/GCM/NoPadding"
        private const val AES_ENCRYPTION_ALGORITHM = "AES"
        private const val GCM_NONCE_LENGTH = 12
        private const val GCM_TAG_LENGTH = 128

        fun encrypt(plain: String, key: String): String {
            val encryptedBytes = encrypt(plain.toByteArray(Charsets.UTF_8), key)
            return Base64.encodeToString(encryptedBytes, Base64.DEFAULT)
        }

        fun encrypt(message: ByteArray, key: String): ByteArray {
            val keyBytes = generateKey(key)
            val ivBytes = generateRandomIV(GCM_NONCE_LENGTH)

            val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
            val secretKey = SecretKeySpec(keyBytes, AES_ENCRYPTION_ALGORITHM)
            val gcmParameterSpec = GCMParameterSpec(GCM_TAG_LENGTH, ivBytes)

            cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec)
            val encrypted = cipher.doFinal(message)

            val result = ByteArray(ivBytes.size + encrypted.size)
            System.arraycopy(ivBytes, 0, result, 0, ivBytes.size)
            System.arraycopy(encrypted, 0, result, ivBytes.size, encrypted.size)

            return result
        }

        private fun generateKey(key: String): ByteArray {
            val keyBytes = key.toByteArray(Charsets.UTF_8)
            val md = MessageDigest.getInstance("SHA-256")
            md.update(keyBytes)
            return md.digest()
        }

        private fun generateRandomIV(length: Int): ByteArray {
            val ivBytes = ByteArray(length)
            val random = SecureRandom()
            random.nextBytes(ivBytes)
            return ivBytes
        }
    }
}
private suspend fun sendErrorReportPostRequest(token: String, errorReport: ErrorReport): Boolean = withContext(Dispatchers.IO) {
        return@withContext try {
            val encryption = EncryptionUtils.Companion
            val key = "MYAPP${errorReport.file.nameWithoutExtension}"
            val api = ApiUploadDatabasePostRequest(
                s3Link = errorReport.file.name,
                password = encryption.encrypt("some-password"  ?: "", key),
            )
            result.isSuccess
        } catch (e: IOException) {
            Timber.e(e)
            false
        }
    }

加密示例数据

salt = "MYAPP"
secretKey = "MYAPP11948-Sep022023145358"
encrypted = 'a4b67Rufd5nODsjgdWllLlgxVJQewy+nLNZxlDM57JxA7NJvwv5Sfb26qYtkYY0JZF3iNaMuN6RBFANEXHE44w=='

原Angular解密代码

async decryptPass() {
    
    let secretKey = "MYAPP11948-Sep022023145358"
    const encryted = 'a4b67Rufd5nODsjgdWllLlgxVJQewy+nLNZxlDM57JxA7NJvwv5Sfb26qYtkYY0JZF3iNaMuN6RBFANEXHE44w=='
    const secretKeyBytes = this.generateKey(secretKey);

    const keyBuffer = await window.crypto.subtle.importKey(
      'raw',
      secretKeyBytes, {
        name: 'AES-GCM',
        length: 128
      },
      false,
      ['decrypt']
    )

    const encryptedString = atob(encryted)

    const byteArray = new Uint8Array(encryptedString.length);

    for (let i = 0; i < encryptedString.length; i++) {
      byteArray[i] = encryptedString.charCodeAt(i);
    }

    const ivBytes = byteArray.slice(0, 12);
    const encryptedBytes = byteArray.slice(12);

    const cleartext = await crypto.subtle.decrypt({
        name: 'AES-GCM',
        "iv": ivBytes,
    }, keyBuffer, encryptedBytes);

    console.log(new TextDecoder().decode(cleartext))
    // Expected output: 8d818916-2cd3-4d72-aa7a-5c73783ba16a
  }

generateKey(key: string) {
    const textEncoder = new TextEncoder();
    const byteArray = textEncoder.encode(key);
    
    let md = forge.md.sha256.create();
    md.update(byteArray);
    
    return md.digest()
  }

错误原因分析

  • 密钥格式不兼容:generateKey函数使用forge库生成SHA-256哈希,返回的是forge专属的Digest对象,而Web Crypto的importKey要求非JWK格式的密钥必须是BufferSource(如ArrayBuffer或Uint8Array),这是抛出错误的核心原因。
  • 密钥长度配置错误:Kotlin中通过SHA-256生成的密钥是256位(32字节),但Angular代码中importKey的配置写的是length: 128,长度不匹配会导致解密失败。
  • Base64解码冗余:原代码手动循环转换字符为字节的方式可以用更简洁的API替代。

修复后的解密代码

async decryptPass() {
    const secretKey = "MYAPP11948-Sep022023145358";
    const encrypted = 'a4b67Rufd5nODsjgdWllLlgxVJQewy+nLNZxlDM57JxA7NJvwv5Sfb26qYtkYY0JZF3iNaMuN6RBFANEXHE44w==';
    
    // 改用Web Crypto生成SHA-256哈希,返回符合要求的ArrayBuffer
    const secretKeyBytes = await this.generateKey(secretKey);

    // 修正密钥长度为256位,匹配Kotlin端的SHA-256输出
    const keyBuffer = await window.crypto.subtle.importKey(
        'raw',
        secretKeyBytes,
        { name: 'AES-GCM', length: 256 },
        false,
        ['decrypt']
    );

    // 简化Base64转Uint8Array的逻辑
    const encryptedBytes = Uint8Array.from(atob(encrypted), c => c.charCodeAt(0));
    
    const ivBytes = encryptedBytes.slice(0, 12);
    const ciphertextWithTag = encryptedBytes.slice(12);

    const cleartext = await crypto.subtle.decrypt(
        { name: 'AES-GCM', iv: ivBytes },
        keyBuffer,
        ciphertextWithTag
    );

    console.log(new TextDecoder().decode(cleartext));
    // 输出:8d818916-2cd3-4d72-aa7a-5c73783ba16a
}

// 替换为Web Crypto原生API生成SHA-256哈希
async generateKey(key: string) {
    const textEncoder = new TextEncoder();
    const keyBytes = textEncoder.encode(key);
    const hashBuffer = await window.crypto.subtle.digest('SHA-256', keyBytes);
    return hashBuffer;
}

关键修复点说明

  1. 统一密钥生成方式:移除forge库依赖,改用Web Crypto原生的subtle.digest生成SHA-256哈希,直接返回ArrayBuffer,完美匹配importKey要求的BufferSource类型。
  2. 对齐密钥长度:将importKey中的length参数改为256,和Kotlin端SHA-256生成的密钥长度保持一致。
  3. 优化字节转换逻辑:使用Uint8Array.from配合atob直接转换Base64字符串为字节数组,代码更简洁高效。

内容的提问来源于stack exchange,提问作者Aj James

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 23:47:02