封面/截图加密对接教程

开启图片加密后,基本上EFV生成的封面、截图全部会被加密乱码,完全看不到任何实质内容,需前端解密调用。

对接之前请先了解下封面截图加密的原理及设置 ⇒ 传送门,这里只提供前端使用示例,然后自行对接。

加密模式说明

EFV提供两种图片加密模式,可在后台自由切换:

模式 适用场景 安全强度 是否需要配置密钥
二进制加密(XOR) 网页端 基础 否,无需任何配置
AES加密 App端 是,需配置 Key/IV

AES模式下支持四种加密类型,可在后台自由选择,推荐使用AES-256-CTR,性能最高。

XOR加密

适用于网页端,无需任何密钥配置,开启后直接使用以下代码解密显示。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>EFV XOR加密图片解密展示</title>
</head>
<body>
<img id="decodedImage" src="" alt="解密后的图片将显示在这里">

<script>
function fetchEncryptedImage() {
    return fetch('http://127.0.0.1:3000/videos/202403/08/65eb06d5561d9e3d0b990740/2.jpg')
        .then(response => response.arrayBuffer());
}

// 解密函数,对图片数据的前9个字节进行XOR操作
function decryptImage(data) {
    let view = new Uint8Array(data);
    for (let i = 0; i < Math.min(9, view.length); i++) {
        view[i] = view[i] ^ 0x12;
    }
    return data;
}

// 将解密后的图片数据设置为img元素的src属性
function displayDecryptedImage(data) {
    let blob = new Blob([data]);
    let url = URL.createObjectURL(blob);
    document.getElementById('decodedImage').src = url;
}

// 加载并解密图片
fetchEncryptedImage().then(encryptedData => {
    let decryptedData = decryptImage(encryptedData);
    displayDecryptedImage(decryptedData);
}).catch(error => console.error('加载或解密图片时出错:', error));
</script>
</body>
</html>

AES加密

推荐App端使用,需在后台配置KeyIV后使用,Key/IV请妥善保管,切勿泄露。

HTML示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>EFV AES加密图片解密展示</title>
</head>
<body>
<img id="decodedImage" src="" alt="解密后的图片将显示在这里">

<script>
// 与后台配置的Key/IV/加密类型保持一致
const AES_KEY_HEX = '你的hex字符串';  // 后台AES Key
const AES_IV_HEX = '你的32位hex字符串';  // 后台AES IV
const AES_CIPHER = 'aes-256-ctr';  // 后台选择的加密类型

function hexToBytes(hex) {
    const bytes = new Uint8Array(hex.length / 2);
    for (let i = 0; i < hex.length; i += 2) {
        bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
    }
    return bytes;
}

async function fetchEncryptedImage() {
    const response = await fetch('http://127.0.0.1:3000/videos/202403/08/65eb06d5561d9e3d0b990740/2.jpg');
    return response.arrayBuffer();
}

// 使用Web Crypto API进行AES解密,自动适配CTR/CBC模式
async function decryptImage(encryptedData) {
    const keyBytes = hexToBytes(AES_KEY_HEX);
    const ivBytes = hexToBytes(AES_IV_HEX);

    let algorithmName, decryptParams;
    if (AES_CIPHER.includes('cbc')) {
        algorithmName = 'AES-CBC';
        decryptParams = { name: 'AES-CBC', iv: ivBytes };
    } else {
        algorithmName = 'AES-CTR';
        decryptParams = { name: 'AES-CTR', counter: ivBytes, length: 64 };
    }

    const cryptoKey = await crypto.subtle.importKey(
        'raw',
        keyBytes,
        { name: algorithmName },
        false,
        ['decrypt']
    );

    return crypto.subtle.decrypt(decryptParams, cryptoKey, encryptedData);
}

function displayDecryptedImage(data) {
    const blob = new Blob([data]);
    const url = URL.createObjectURL(blob);
    document.getElementById('decodedImage').src = url;
}

// 加载并解密图片
fetchEncryptedImage()
    .then(encryptedData => decryptImage(encryptedData))
    .then(decryptedData => displayDecryptedImage(decryptedData))
    .catch(error => console.error('加载或解密图片时出错:', error));
</script>
</body>
</html>

Android(Kotlin)示例

import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec

object ImageDecryptor {
    private val KEY_HEX = "你的hex字符串"
    private val IV_HEX = "你的32位hex字符串"
    private val AES_CIPHER = "AES/CTR/NoPadding"  // CTR: AES/CTR/NoPadding  CBC: AES/CBC/PKCS5Padding

    fun decrypt(encryptedBytes: ByteArray): ByteArray {
        val key = SecretKeySpec(KEY_HEX.hexToBytes(), "AES")
        val ivSpec = IvParameterSpec(IV_HEX.hexToBytes())
        val cipher = Cipher.getInstance(AES_CIPHER)
        cipher.init(Cipher.DECRYPT_MODE, key, ivSpec)
        return cipher.doFinal(encryptedBytes)
    }

    private fun String.hexToBytes() =
        chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}

iOS(Swift)示例

import CryptoKit

struct ImageDecryptor {
    static let keyHex = "你的hex字符串"
    static let ivHex = "你的32位hex字符串"
    static let aesCipher = "ctr"  // 后台选择的模式:ctr或cbc

    static func decrypt(_ data: Data) throws -> Data {
        let keyData = Data(hexString: keyHex)!
        let key = SymmetricKey(data: keyData)
        if aesCipher == "cbc" {
            let iv = Data(hexString: ivHex)!
            let sealedBox = try AES.CBC.SealedBox(combined: iv + data)
            return try AES.CBC.open(sealedBox, using: key)
        } else {
            let nonce = try AES.CTR.Nonce(data: Data(hexString: ivHex)!)
            return try AES.CTR.decrypt(data, using: key, nonce: nonce)
        }
    }
}

Flutter(Dart)示例

import 'package:pointycastle/export.dart';

Uint8List decryptImage(Uint8List encrypted, String keyHex, String ivHex, String cipher) {
    final key = Uint8List.fromList(HEX.decode(keyHex));
    final iv = Uint8List.fromList(HEX.decode(ivHex));

    if (cipher.contains('cbc')) {
        final cbcCipher = CBCBlockCipher(AESEngine())
            ..init(false, ParametersWithIV(KeyParameter(key), iv));
        final output = Uint8List(encrypted.length);
        for (var i = 0; i < encrypted.length; i += 16) {
            cbcCipher.processBlock(encrypted, i, output, i);
        }
        return output;
    } else {
        final ctrCipher = CTRStreamCipher(AESEngine())
            ..init(false, ParametersWithIV(KeyParameter(key), iv));
        return ctrCipher.process(encrypted);
    }
}

注意事项

  • XOR模式和AES模式不可同时开启,后台切换后立即生效
  • AES模式下Key/IV不可泄露,请勿将其硬编码在网页端JS
  • 后台更换Key/IV或切换加密类型后,所有客户端需同步更新,否则解密失败
  • AES-256系列Key64hex字符串,AES-128系列Key32hex字符串,IV均为32hex字符串
  • 后端统一按后台当前配置的加密模式返回,响应头x-encode-mode标识本次加密类型,客户端据此选择对应解密方式
  • CTR模式密文长度与原文一致;CBC模式密文长度略大于原文,传输时Content-Length会有差异