Skip to content

数据加密

Cookie Plus 在上传和下载 Cookie / LocalStorage 数据时,要求对数据进行 AES-256-GCM 加密。加密密码为用户在插件设置中配置的同步密码,API 调用方需使用相同的密码进行加解密。

加密算法

项目说明
算法AES-256-GCM
密钥派生PBKDF2(SHA-256,100000 次迭代)
盐值(Salt)cookie-plus-salt-v1(固定字符串)
IV 长度12 字节(随机生成)
密文格式Base64(IV + Ciphertext + AuthTag)

加密流程

  1. 使用 PBKDF2 从用户密码派生 AES-256 密钥(盐值固定为 cookie-plus-salt-v1,迭代 100000 次,哈希算法 SHA-256)
  2. 生成 12 字节随机 IV
  3. 使用 AES-256-GCM 对明文进行加密
  4. 将 IV(12 字节)和密文(含 GCM AuthTag)拼接后进行 Base64 编码
  5. 将 Base64 字符串作为 data 字段传入上传接口

解密流程

  1. data 字段的 Base64 字符串进行解码
  2. 提取前 12 字节作为 IV,剩余部分为密文(含 GCM AuthTag)
  3. 使用相同密码通过 PBKDF2 派生密钥
  4. 使用 AES-256-GCM 解密,得到原始 JSON 明文

代码示例

JavaScript / Node.js(Web Crypto API)

适用于浏览器和 Node.js 18+ 环境。

javascript
const SALT = 'cookie-plus-salt-v1';
const IV_LENGTH = 12;

async function deriveKey(password) {
  const enc = new TextEncoder();
  const keyMaterial = await crypto.subtle.importKey(
    'raw',
    enc.encode(password),
    'PBKDF2',
    false,
    ['deriveKey']
  );
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt: enc.encode(SALT), iterations: 100000, hash: 'SHA-256' },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
}

function arrayBufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = '';
  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}

function base64ToArrayBuffer(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
}

/**
 * 加密
 * @param {string} plainText - 待加密的明文(JSON 字符串)
 * @param {string} password  - 用户同步密码
 * @returns {Promise<string>} Base64 编码的密文
 */
async function encrypt(plainText, password) {
  const key = await deriveKey(password);
  const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
  const enc = new TextEncoder();
  const cipherBuffer = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    enc.encode(plainText)
  );
  const combined = new Uint8Array(IV_LENGTH + cipherBuffer.byteLength);
  combined.set(iv, 0);
  combined.set(new Uint8Array(cipherBuffer), IV_LENGTH);
  return arrayBufferToBase64(combined.buffer);
}

/**
 * 解密
 * @param {string} cipherBase64 - Base64 编码的密文
 * @param {string} password     - 用户同步密码
 * @returns {Promise<string>} 解密后的明文
 */
async function decrypt(cipherBase64, password) {
  const key = await deriveKey(password);
  const combined = new Uint8Array(base64ToArrayBuffer(cipherBase64));
  const iv = combined.slice(0, IV_LENGTH);
  const cipherBytes = combined.slice(IV_LENGTH);
  const decBuffer = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv },
    key,
    cipherBytes
  );
  return new TextDecoder().decode(decBuffer);
}

// ── 使用示例 ──

const password = 'your-sync-password';

// 加密 Cookie 数据
const cookieData = JSON.stringify([
  { name: 'session', value: 'abc123', domain: '.example.com', path: '/' }
]);
const encrypted = await encrypt(cookieData, password);
console.log('加密结果:', encrypted);

// 解密 Cookie 数据
const decrypted = await decrypt(encrypted, password);
console.log('解密结果:', JSON.parse(decrypted));

Python

python
import os
import base64
import hashlib
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes

SALT = b'cookie-plus-salt-v1'
IV_LENGTH = 12
ITERATIONS = 100000
KEY_LENGTH = 32  # 256 bits

def derive_key(password: str) -> bytes:
    """通过 PBKDF2 从密码派生 AES-256 密钥"""
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=KEY_LENGTH,
        salt=SALT,
        iterations=ITERATIONS,
    )
    return kdf.derive(password.encode('utf-8'))

def encrypt(plain_text: str, password: str) -> str:
    """加密明文,返回 Base64 编码的密文"""
    key = derive_key(password)
    iv = os.urandom(IV_LENGTH)
    aesgcm = AESGCM(key)
    cipher_bytes = aesgcm.encrypt(iv, plain_text.encode('utf-8'), None)
    combined = iv + cipher_bytes
    return base64.b64encode(combined).decode('utf-8')

def decrypt(cipher_base64: str, password: str) -> str:
    """解密 Base64 编码的密文,返回明文"""
    key = derive_key(password)
    combined = base64.b64decode(cipher_base64)
    iv = combined[:IV_LENGTH]
    cipher_bytes = combined[IV_LENGTH:]
    aesgcm = AESGCM(key)
    plain_bytes = aesgcm.decrypt(iv, cipher_bytes, None)
    return plain_bytes.decode('utf-8')

# ── 使用示例 ──

import json

password = 'your-sync-password'

# 加密 Cookie 数据
cookie_data = json.dumps([
    {'name': 'session', 'value': 'abc123', 'domain': '.example.com', 'path': '/'}
])
encrypted = encrypt(cookie_data, password)
print('加密结果:', encrypted)

# 解密 Cookie 数据
decrypted = decrypt(encrypted, password)
print('解密结果:', json.loads(decrypted))

依赖安装

Python 示例需要安装 cryptography 库:

bash
pip install cryptography

Java

java
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Base64;

public class CookiePlusCrypto {

    private static final String SALT = "cookie-plus-salt-v1";
    private static final int IV_LENGTH = 12;
    private static final int ITERATIONS = 100000;
    private static final int KEY_LENGTH = 256;
    private static final int TAG_LENGTH = 128;

    private static SecretKey deriveKey(String password) throws Exception {
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        KeySpec spec = new PBEKeySpec(
            password.toCharArray(),
            SALT.getBytes(StandardCharsets.UTF_8),
            ITERATIONS,
            KEY_LENGTH
        );
        byte[] keyBytes = factory.generateSecret(spec).getEncoded();
        return new SecretKeySpec(keyBytes, "AES");
    }

    public static String encrypt(String plainText, String password) throws Exception {
        SecretKey key = deriveKey(password);
        byte[] iv = new byte[IV_LENGTH];
        new SecureRandom().nextBytes(iv);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(TAG_LENGTH, iv));
        byte[] cipherBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

        byte[] combined = new byte[IV_LENGTH + cipherBytes.length];
        System.arraycopy(iv, 0, combined, 0, IV_LENGTH);
        System.arraycopy(cipherBytes, 0, combined, IV_LENGTH, cipherBytes.length);

        return Base64.getEncoder().encodeToString(combined);
    }

    public static String decrypt(String cipherBase64, String password) throws Exception {
        SecretKey key = deriveKey(password);
        byte[] combined = Base64.getDecoder().decode(cipherBase64);

        byte[] iv = new byte[IV_LENGTH];
        System.arraycopy(combined, 0, iv, 0, IV_LENGTH);
        byte[] cipherBytes = new byte[combined.length - IV_LENGTH];
        System.arraycopy(combined, IV_LENGTH, cipherBytes, 0, cipherBytes.length);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_LENGTH, iv));
        byte[] plainBytes = cipher.doFinal(cipherBytes);

        return new String(plainBytes, StandardCharsets.UTF_8);
    }
}

Go

go
package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"io"

	"golang.org/x/crypto/pbkdf2"
)

const (
	salt       = "cookie-plus-salt-v1"
	ivLength   = 12
	iterations = 100000
	keyLength  = 32
)

func deriveKey(password string) []byte {
	return pbkdf2.Key(
		[]byte(password),
		[]byte(salt),
		iterations,
		keyLength,
		sha256.New,
	)
}

func Encrypt(plainText, password string) (string, error) {
	key := deriveKey(password)
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}
	aesGCM, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	iv := make([]byte, ivLength)
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		return "", err
	}

	cipherBytes := aesGCM.Seal(nil, iv, []byte(plainText), nil)
	combined := append(iv, cipherBytes...)
	return base64.StdEncoding.EncodeToString(combined), nil
}

func Decrypt(cipherBase64, password string) (string, error) {
	key := deriveKey(password)
	combined, err := base64.StdEncoding.DecodeString(cipherBase64)
	if err != nil {
		return "", err
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}
	aesGCM, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	iv := combined[:ivLength]
	cipherBytes := combined[ivLength:]
	plainBytes, err := aesGCM.Open(nil, iv, cipherBytes, nil)
	if err != nil {
		return "", err
	}
	return string(plainBytes), nil
}

依赖安装

Go 示例需要安装 golang.org/x/crypto 模块:

bash
go get golang.org/x/crypto

注意事项

密码一致性

API 调用方使用的加密密码必须与 Cookie Plus 插件中设置的同步密码一致,否则将无法正确解密数据。

  • 密码来源: 加密密码为用户在 Cookie Plus 插件设置页面中配置的「端到端密钥」
  • 数据格式: 加密前的明文是 JSON 字符串(Cookie 为 JSON 数组,LocalStorage 为 JSON 对象)
  • 传输安全: 加密后的 Base64 字符串通过 HTTPS 传输,确保双重安全
  • 跨语言兼容: 只要遵循相同的加密参数(PBKDF2 + AES-256-GCM),任何语言实现的加解密均可互通

Cookie Plus - 浏览器 Cookie 云同步插件