Appearance
数据加密
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) |
加密流程
- 使用 PBKDF2 从用户密码派生 AES-256 密钥(盐值固定为
cookie-plus-salt-v1,迭代 100000 次,哈希算法 SHA-256) - 生成 12 字节随机 IV
- 使用 AES-256-GCM 对明文进行加密
- 将 IV(12 字节)和密文(含 GCM AuthTag)拼接后进行 Base64 编码
- 将 Base64 字符串作为
data字段传入上传接口
解密流程
- 对
data字段的 Base64 字符串进行解码 - 提取前 12 字节作为 IV,剩余部分为密文(含 GCM AuthTag)
- 使用相同密码通过 PBKDF2 派生密钥
- 使用 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 cryptographyJava
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),任何语言实现的加解密均可互通
