Giter VIP home page Giter VIP logo

easychen / cookiecloud Goto Github PK

View Code? Open in Web Editor NEW
1.8K 1.8K 140.0 1.74 MB

CookieCloud是一个和自架服务器同步浏览器Cookie和LocalStorage的小工具,支持端对端加密,可设定同步时间间隔。本仓库包含了插件和服务器端源码。CookieCloud is a small tool for synchronizing browser cookies and LocalStorage with a self-hosted server. It supports end-to-end encryption and allows for setting the synchronization interval. This repository contains both the plugin and the server-side source code

License: GNU General Public License v3.0

PHP 3.57% JavaScript 51.93% TypeScript 43.14% HTML 0.42% SCSS 0.36% Dockerfile 0.58%

cookiecloud's Introduction

CookieCloud

中文 | English

CookieCloud is a small tool for syncing cookies with your self-hosted server, allowing you to synchronize browser cookies and local storage to your phone and cloud. It features built-in end-to-end encryption and allows you to set a synchronization interval.

The latest version now supports synchronization of local storage under the same domain name.

Telegram channel | Telegram group

⚠️ Breaking Change

Due to the high demand for local storage support, plugin version 0.1.5+ now also supports local storage in addition to cookies. This has resulted in a change to the encrypted text format (from a separate cookie object to { cookie_data, local_storage_data }).

Furthermore, to avoid conflicts in configuration synchronization, the configuration storage has been moved from remote to local. Users of previous versions will need to reconfigure their setup.

We apologize for any inconvenience this may cause 🙇🏻‍♂️

Official Tutorials

  1. Video: Bilibili | YouTube - Please follow and subscribe 🥺
  2. Tutorial: Juejin

FAQ

  1. Currently, synchronization is only one-way, meaning one browser can upload while another downloads.
  2. The browser extension officially supports Chrome and Edge. Other Chromium-based browsers might work but have not been tested. Use the source code cd extension && pnpm build --target=firefox-mv2 to compile a version for Firefox yourself. Be aware that Firefox's cookie format is different from Chrome's and they cannot be mixed.

Browser Plugin

  1. Installation from store: Edge Store | Chrome Store (Note: Versions in the store might be delayed due to review processes)
  2. Manual download and installation: See Release

Server Side

Third Party

Free server-side services provided by third parties are available for trial. Stability is determined by the third parties. We appreciate their sharing 👏

Some server-side versions might be outdated. If tests fail, try adding domain keywords before retrying.

Self-hosting

Option One: Deploy through Docker, simple, recommended method

Supports architectures: linux/amd64, linux/arm64, etc.

Start with Docker Command
docker run -p=8088:8088 easychen/cookiecloud:latest

Default port 8088, image address easychen/cookiecloud

Specify API Directory - Optional Step, Can Be Skipped

Add the environment variable -e API_ROOT=/subdirectory must start with a slash to specify a subdirectory:

docker run -e API_ROOT=/cookie -p=8088:8088 easychen/cookiecloud:latest
Start with Docker-compose
version: '3'
services:
  cookiecloud:
    image: easychen/cookiecloud:latest
    container_name: cookiecloud-app
    restart: always
    volumes:
      - ./data:/data/api/data
    ports:
      - 8088:8088

docker-compose.yml provided by aitixiong

Option Two: Deploy with Node

Suitable for environments without docker but supporting node, requires installing node in advance

cd api && yarn install && node app.js

Default port 8088, also supports the API_ROOT environment variable

Debugging and Log Viewing

Enter the browser plugin list, click on service worker, a panel will pop up where you can view the operation log

API Interface

Upload:

  • method: POST
  • url: /update
  • parameters
    • uuid
    • encrypted: the string encrypted locally

Download:

  • method: POST/GET
  • url: /get/:uuid
  • parameters:
    • password: optional, if not provided returns the encrypted string, if provided attempts to decrypt and send the content;

Cookie Encryption and Decryption Algorithm

Encryption

const data = JSON.stringify(cookies);

  1. md5(uuid+password) take the first 16 characters as the key
  2. AES.encrypt(data, the_key)

Decryption

  1. md5(uuid+password) take the first 16 characters as the key
  2. AES.decrypt(encrypted, the_key)

After decryption, get data, JSON.parse(data) to obtain the data object { cookie_data, local_storage_data };

Reference function

function cookie_decrypt( uuid, encrypted, password )
{
    const CryptoJS = require('crypto-js');
    const the_key = CryptoJS.MD5(uuid+'-'+password).toString().substring(0,16);
    const decrypted = CryptoJS.AES.decrypt(encrypted, the_key).toString(CryptoJS.enc.Utf8);
    const parsed = JSON.parse(decrypted);
    return parsed;
}

See extension/function.js for more

Headless Browser Example Using CookieCloud

Refer to examples/playwright/tests/example.spec.js

test('Access nexusphp using CookieCloud', async ({ page, browser }) => {
  // Read and decrypt cloud cookie
  const cookies = await cloud_cookie(COOKIE_CLOUD_HOST, COOKIE_CLOUD_UUID, COOKIE_CLOUD_PASSWORD);
  // Add cookie to browser context
  const context = await browser.newContext();
  await context.addCookies(cookies);
  page = await context.newPage();
  // From this point on, the Cookie is already attached, proceed as normal
  await page.goto('https://demo.nexusphp.org/index.php');
  await expect(page.getByRole('link', { name: 'magik' })).toHaveText("magik");
  await context.close();
});

Functions

async function cloud_cookie( host, uuid, password )
{
  const fetch = require('cross-fetch');
  const url = host+'/get/'+uuid;
  const ret = await fetch(url);
  const json = await ret.json();
  let cookies = [];
  if( json && json.encrypted )
  {
    const {cookie_data, local_storage_data} = cookie_decrypt(uuid, json.encrypted, password);
    for( const key in cookie_data )
    {
      // merge cookie_data[key] to cookies
      cookies = cookies.concat(cookie_data[key].map( item => {
        if( item.sameSite == 'unspecified' ) item.sameSite = 'Lax';
        return item;
      } ));
    }
  }
  return cookies;
}

function cookie_decrypt( uuid, encrypted, password )
{
    const CryptoJS = require('crypto-js');
    const the_key = CryptoJS.MD5(uuid+'-'+password).toString().substring(0,16);
    const decrypted = CryptoJS.AES.decrypt(encrypted, the_key).toString(CryptoJS.enc.Utf8);
    const parsed = JSON.parse(decrypted);
    return parsed;
}

Python Decryption

Refer to the article "Implementation and Problem Handling of Crypto in Python for AES Encryption and Decryption in JS CryptoJS" or use PyCookieCloud

Go Decryption Algorithm

Thanks to sagan for sharing

package main

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/md5"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"hash"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
)

const (
	pkcs5SaltLen = 8
	aes256KeyLen = 32
)

type CookieCloudBody struct {
	Uuid      string `json:"uuid,omitempty"`
	Encrypted string `json:"encrypted,omitempty"`
}

func main() {
	apiUrl := strings.TrimSuffix(os.Getenv("COOKIE_CLOUD_HOST"), "/")
	uuid := os.Getenv("COOKIE_CLOUD_UUID")
	password := os.Getenv("COOKIE_CLOUD_PASSWORD")

	if apiUrl == "" || uuid == "" || password == "" {
		log.Fatalf("COOKIE_CLOUD_HOST, COOKIE_CLOUD_UUID and COOKIE_CLOUD_PASSWORD env must be set")
	}
	var data *CookieCloudBody
	res, err := http.Get(apiUrl + "/get/" + uuid)
	if err != nil {
		log.Fatalf("Failed to request server: %v", err)
	}
	if res.StatusCode != 200 {
		log.Fatalf("Server return status %d", res.StatusCode)
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		log.Fatalf("Failed to read server response: %v", err)
	}
	err = json.Unmarshal(body, &data)
	if err != nil {
		log.Fatalf("Failed to parse server response as json: %v", err)
	}
	keyPassword := Md5String(uuid, "-", password)[:16]
	decrypted, err := DecryptCryptoJsAesMsg(keyPassword, data.Encrypted)
	if err != nil {
		log.Fatalf("Failed to decrypt: %v", err)
	}
	fmt.Printf("Decrypted: %s\n", decrypted)
}

// Decrypt a CryptoJS.AES.encrypt(msg, password) encrypted msg.
// ciphertext is the result of CryptoJS.AES.encrypt(), which is the base64 string of
// "Salted__" + [8 bytes random salt] + [actual ciphertext].
// actual ciphertext is padded (make it's length align with block length) using Pkcs7.
// CryptoJS use a OpenSSL-compatible EVP_BytesToKey to derive (key,iv) from (password,salt),
// using md5 as hash type and 32 / 16 as length of key / block.
// See: https://stackoverflow.com/questions/35472396/how-does-cryptojs-get-an-iv-when-none-is-specified ,
// https://stackoverflow.com/questions/64797987/what-is-the-default-aes-config-in-crypto-js
func DecryptCryptoJsAesMsg(password string, ciphertext string) ([]byte, error) {
	const keylen = 32
	const blocklen = 16
	rawEncrypted, err := base64.StdEncoding.DecodeString(ciphertext)
	if err != nil {
		return nil, fmt.Errorf("failed to base64 decode Encrypted: %v", err)
	}
	if len(rawEncrypted) < 17 || len(rawEncrypted)%blocklen != 0 || string(rawEncrypted[:8]) != "Salted__" {
		return nil, fmt.Errorf("invalid ciphertext")
	}
	salt := rawEncrypted[8:16]
	encrypted := rawEncrypted[16:]
	key, iv := BytesToKey(salt, []byte(password), md5.New(), keylen, blocklen)
	newCipher, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("failed to create aes cipher: %v", err)
	}
	cfbdec := cipher.NewCBCDecrypter(newCipher, iv)
	decrypted := make([]byte, len(encrypted))
	cfbdec.CryptBlocks(decrypted, encrypted)
	decrypted, err = pkcs7strip(decrypted, blocklen)
	if err != nil {
		return nil, fmt.Errorf("failed to strip pkcs7 paddings (password may be incorrect): %v", err)
	}
	return decrypted, nil
}

// From https://github.com/walkert/go-evp .
// BytesToKey implements the Openssl EVP_BytesToKey logic.
// It takes the salt, data, a hash type and the key/block length used by that type.
// As such it differs considerably from the openssl method in C.
func BytesToKey(salt, data []byte, h hash.Hash, keyLen, blockLen int) (key, iv []byte) {
	saltLen := len(salt)
	if saltLen > 0 && saltLen != pkcs5SaltLen {
		panic(fmt.Sprintf("Salt length is %d, expected %d", saltLen, pkcs5SaltLen))
	}
	var (
		concat   []byte
		lastHash []byte
		totalLen = keyLen + blockLen
	)
	for ; len(concat) < totalLen; h.Reset() {
		// concatenate lastHash, data and salt and write them to the hash
		h.Write(append(lastHash, append(data, salt...)...))
		// passing nil to Sum() will return the current hash value
		lastHash = h.Sum(nil)
		// append lastHash to the running total bytes
		concat = append(concat, lastHash...)
	}
	return concat[:keyLen], concat[keyLen:totalLen]
}

// BytesToKeyAES256CBC implements the SHA256 version of EVP_BytesToKey using AES CBC
func BytesToKeyAES256CBC(salt, data []byte) (key []byte, iv []byte) {
	return BytesToKey(salt, data, sha256.New(), aes256KeyLen, aes.BlockSize)
}

// BytesToKeyAES256CBCMD5 implements the MD5 version of EVP_BytesToKey using AES CBC
func BytesToKeyAES256CBCMD5(salt, data []byte) (key []byte, iv []byte) {
	return BytesToKey(salt, data, md5.New(), aes256KeyLen, aes.BlockSize)
}

// return the MD5 hex hash string (lower-case) of input string(s)
func Md5String(inputs ...string) string {
	keyHash := md5.New()
	for _, str := range inputs {
		io.WriteString(keyHash, str)
	}
	return hex.EncodeToString(keyHash.Sum(nil))
}

// from https://gist.github.com/nanmu42/b838acc10d393bc51cb861128ce7f89c .
// pkcs7strip remove pkcs7 padding
func pkcs7strip(data []byte, blockSize int) ([]byte, error) {
	length := len(data)
	if length == 0 {
		return nil, errors.New("pkcs7: Data is empty")
	}
	if length%blockSize != 0 {
		return nil, errors.New("pkcs7: Data is not block-aligned")
	}
	padLen := int(data[length-1])
	ref := bytes.Repeat([]byte{byte(padLen)}, padLen)
	if padLen > blockSize || padLen == 0 || !bytes.HasSuffix(data, ref) {
		return nil, errors.New("pkcs7: Invalid padding")
	}
	return data[:length-padLen], nil
}

Deno Reference

Thanks to JokerQyou for sharing

import {crypto, toHashString} from 'https://deno.land/[email protected]/crypto/mod.ts'
import {decode } from 'https://deno.land/[email protected]/encoding/base64.ts'

const evpkdf = async (
  password: Uint8Array,
  salt: Uint8Array,
  iterations: number,
): Promise<{
  key: Uint8Array,
  iv: Uint8Array,
}> => {
  const keySize = 32
  const ivSize = 16
  const derivedKey = new Uint8Array(keySize + ivSize)
  let currentBlock = 1
  let digest = new Uint8Array(0)
  const hashLength = 16
  while ((currentBlock - 1) * hashLength < keySize + ivSize) {
    const data = new Uint8Array(digest.length + password.length + salt.length)
    data.set(digest)
    data.set(password, digest.length)
    data.set(salt, digest.length + password.length)
    digest = await crypto.subtle.digest('MD5', data).then(buf => new Uint8Array(buf))

    for (let i = 1; i < iterations; i++) {
      digest = await crypto.subtle.digest('MD5', digest).then(buf => new Uint8Array(buf))
    }
    derivedKey.set(digest, (currentBlock - 1) * hashLength)
    currentBlock++
  }
  return {
    key: derivedKey.slice(0, keySize),
    iv: derivedKey.slice(keySize),
  }
}

const main = async (env: Record<string, string>) => {
  const {
    COOKIE_CLOUD_HOST: CC_HOST,
    COOKIE_CLOUD_UUID: CC_UUID,
    COOKIE_CLOUD_PASSWORD: CC_PW,
  } = env
  const resp = await fetch(`${CC_HOST}/get/${CC_UUID}`).then(r => r.json())
  console.log(resp)
  let cookies = []
  if (resp && resp.encrypted)  {
    console.log(resp.encrypted)
    console.log(new TextDecoder().decode(decode(resp.encrypted)).slice(0, 16))
    const decoded = decode(resp.encrypted)
    // Salted__ + 8 bytes salt, followed by cipher text
    const salt = decoded.slice(8, 16)
    const cipher_text = decoded.slice(16)

    const password = await crypto.subtle.digest(
      'MD5',
      new TextEncoder().encode(`${CC_UUID}-${CC_PW}`),
    ).then(
      buf => toHashString(buf).substring(0, 16)
    ).then(
      p => new TextEncoder().encode(p)
    )
    const {key, iv} = await evpkdf(password, salt, 1)
    const privete_key = await crypto.subtle.importKey(
      'raw',
      key,
      'AES-CBC',
      false,
      ['decrypt'],
    )

    const d = await crypto.subtle.decrypt(
      {name: 'AES-CBC', iv},
      privete_key,
      cipher_text,
    )
    console.log('decrypted:', new TextDecoder().decode(d))
}

Translated by GPT4

cookiecloud's People

Contributors

d0zingcat avatar ddsrem avatar easychen avatar helloworlde avatar j20cc avatar lupohan44 avatar shellingford37 avatar vantis-zh avatar wuquejs avatar xuyan0213 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cookiecloud's Issues

PayloadTooLargeError: request entity too large

浏览器插件提示:测试失败,请检查填写的信息是否正确;
服务端提示:
Server start on http://localhost:8088
PayloadTooLargeError: request entity too large
at Gunzip.onData (/data/api/node_modules/raw-body/index.js:253:12)
at Gunzip.emit (node:events:513:28)
at addChunk (node:internal/streams/readable:315:12)
at readableAddChunk (node:internal/streams/readable:289:9)
at Gunzip.Readable.push (node:internal/streams/readable:228:10)
at Zlib.processCallback (node:zlib:566:10) {
limit: 102400,
received: 114688,
type: 'entity.too.large'
}

Go 版本的解密代码

写了一个 Go 版本的解密代码,供参考。

package main

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/md5"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"hash"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
)

const (
	pkcs5SaltLen = 8
	aes256KeyLen = 32
)

type CookieCloudBody struct {
	Uuid      string `json:"uuid,omitempty"`
	Encrypted string `json:"encrypted,omitempty"`
}

func main() {
	apiUrl := strings.TrimSuffix(os.Getenv("COOKIE_CLOUD_HOST"), "/")
	uuid := os.Getenv("COOKIE_CLOUD_UUID")
	password := os.Getenv("COOKIE_CLOUD_PASSWORD")

	if apiUrl == "" || uuid == "" || password == "" {
		log.Fatalf("COOKIE_CLOUD_HOST, COOKIE_CLOUD_UUID and COOKIE_CLOUD_PASSWORD env must be set")
	}
	var data *CookieCloudBody
	res, err := http.Get(apiUrl + "/get/" + uuid)
	if err != nil {
		log.Fatalf("Failed to request server: %v", err)
	}
	if res.StatusCode != 200 {
		log.Fatalf("Server return status %d", res.StatusCode)
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		log.Fatalf("Failed to read server response: %v", err)
	}
	err = json.Unmarshal(body, &data)
	if err != nil {
		log.Fatalf("Failed to parse server response as json: %v", err)
	}
	keyPassword := Md5String(uuid, "-", password)[:16]
	decrypted, err := DecryptCryptoJsAesMsg(keyPassword, data.Encrypted)
	if err != nil {
		log.Fatalf("Failed to decrypt: %v", err)
	}
	fmt.Printf("Decrypted: %s\n", decrypted)
}

// Decrypt a CryptoJS.AES.encrypt(msg, password) encrypted msg.
// ciphertext is the result of CryptoJS.AES.encrypt(), which is the base64 string of
// "Salted__" + [8 bytes random salt] + [actual ciphertext].
// actual ciphertext is padded (make it's length align with block length) using Pkcs7.
// CryptoJS use a OpenSSL-compatible EVP_BytesToKey to derive (key,iv) from (password,salt),
// using md5 as hash type and 32 / 16 as length of key / block.
// See: https://stackoverflow.com/questions/35472396/how-does-cryptojs-get-an-iv-when-none-is-specified ,
// https://stackoverflow.com/questions/64797987/what-is-the-default-aes-config-in-crypto-js
func DecryptCryptoJsAesMsg(password string, ciphertext string) ([]byte, error) {
	const keylen = 32
	const blocklen = 16
	rawEncrypted, err := base64.StdEncoding.DecodeString(ciphertext)
	if err != nil {
		return nil, fmt.Errorf("failed to base64 decode Encrypted: %v", err)
	}
	if len(rawEncrypted) < 17 || len(rawEncrypted)%blocklen != 0 || string(rawEncrypted[:8]) != "Salted__" {
		return nil, fmt.Errorf("invalid ciphertext")
	}
	salt := rawEncrypted[8:16]
	encrypted := rawEncrypted[16:]
	key, iv := BytesToKey(salt, []byte(password), md5.New(), keylen, blocklen)
	newCipher, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("failed to create aes cipher: %v", err)
	}
	cfbdec := cipher.NewCBCDecrypter(newCipher, iv)
	decrypted := make([]byte, len(encrypted))
	cfbdec.CryptBlocks(decrypted, encrypted)
	decrypted, err = pkcs7strip(decrypted, blocklen)
	if err != nil {
		return nil, fmt.Errorf("failed to strip pkcs7 paddings (password may be incorrect): %v", err)
	}
	return decrypted, nil
}

// From https://github.com/walkert/go-evp .
// BytesToKey implements the Openssl EVP_BytesToKey logic.
// It takes the salt, data, a hash type and the key/block length used by that type.
// As such it differs considerably from the openssl method in C.
func BytesToKey(salt, data []byte, h hash.Hash, keyLen, blockLen int) (key, iv []byte) {
	saltLen := len(salt)
	if saltLen > 0 && saltLen != pkcs5SaltLen {
		panic(fmt.Sprintf("Salt length is %d, expected %d", saltLen, pkcs5SaltLen))
	}
	var (
		concat   []byte
		lastHash []byte
		totalLen = keyLen + blockLen
	)
	for ; len(concat) < totalLen; h.Reset() {
		// concatenate lastHash, data and salt and write them to the hash
		h.Write(append(lastHash, append(data, salt...)...))
		// passing nil to Sum() will return the current hash value
		lastHash = h.Sum(nil)
		// append lastHash to the running total bytes
		concat = append(concat, lastHash...)
	}
	return concat[:keyLen], concat[keyLen:totalLen]
}

// BytesToKeyAES256CBC implements the SHA256 version of EVP_BytesToKey using AES CBC
func BytesToKeyAES256CBC(salt, data []byte) (key []byte, iv []byte) {
	return BytesToKey(salt, data, sha256.New(), aes256KeyLen, aes.BlockSize)
}

// BytesToKeyAES256CBCMD5 implements the MD5 version of EVP_BytesToKey using AES CBC
func BytesToKeyAES256CBCMD5(salt, data []byte) (key []byte, iv []byte) {
	return BytesToKey(salt, data, md5.New(), aes256KeyLen, aes.BlockSize)
}

// return the MD5 hex hash string (lower-case) of input string(s)
func Md5String(inputs ...string) string {
	keyHash := md5.New()
	for _, str := range inputs {
		io.WriteString(keyHash, str)
	}
	return hex.EncodeToString(keyHash.Sum(nil))
}

// from https://gist.github.com/nanmu42/b838acc10d393bc51cb861128ce7f89c .
// pkcs7strip remove pkcs7 padding
func pkcs7strip(data []byte, blockSize int) ([]byte, error) {
	length := len(data)
	if length == 0 {
		return nil, errors.New("pkcs7: Data is empty")
	}
	if length%blockSize != 0 {
		return nil, errors.New("pkcs7: Data is not block-aligned")
	}
	padLen := int(data[length-1])
	ref := bytes.Repeat([]byte{byte(padLen)}, padLen)
	if padLen > blockSize || padLen == 0 || !bytes.HasSuffix(data, ref) {
		return nil, errors.New("pkcs7: Invalid padding")
	}
	return data[:length-padLen], nil
}

24小时不重复上传这个逻辑不会生效。

if( ( !payload['no_cache'] || parseInt(payload['no_cache']) < 1 ) && last_uploaded_info && last_uploaded_info.sha256 === sha256 && new Date().getTime() - last_uploaded_info.timestamp < 1000*60*60*24 )

SHA256算法,在相同明文的情况下,每次生成的密文都不一样。这就导致这一行代码last_uploaded_info.sha256 === sha256永远是false。

cookie失效&谷歌检测到cookie异常

使用的浏览器

上传数据的浏览器为edge
接收数据的浏览器为chrome

部署方式

docker

问题描述

每次重启计算机打开浏览器就会发现cookie失效(chrome的谷歌账号也被退出了,即右上角头像显示已暂停),需要手动同步一次才行,并且使用谷歌登录的时候提示“谷歌检测到您的cookie存在异常”。

【代码参考】适用于deno的解密代码

仅使用 std,供参考。要点是模仿 CryptoJS 使用字符串类型 key(实际作为 password 来使用)时,使用 EvpKDF 来生成 key 和 IV。

import {crypto, toHashString} from 'https://deno.land/[email protected]/crypto/mod.ts'
import {decode } from 'https://deno.land/[email protected]/encoding/base64.ts'

const evpkdf = async (
  password: Uint8Array,
  salt: Uint8Array,
  iterations: number,
): Promise<{
  key: Uint8Array,
  iv: Uint8Array,
}> => {
  const keySize = 32
  const ivSize = 16
  const derivedKey = new Uint8Array(keySize + ivSize)
  let currentBlock = 1
  let digest = new Uint8Array(0)
  const hashLength = 16
  while ((currentBlock - 1) * hashLength < keySize + ivSize) {
    const data = new Uint8Array(digest.length + password.length + salt.length)
    data.set(digest)
    data.set(password, digest.length)
    data.set(salt, digest.length + password.length)
    digest = await crypto.subtle.digest('MD5', data).then(buf => new Uint8Array(buf))

    for (let i = 1; i < iterations; i++) {
      digest = await crypto.subtle.digest('MD5', digest).then(buf => new Uint8Array(buf))
    }
    derivedKey.set(digest, (currentBlock - 1) * hashLength)
    currentBlock++
  }
  return {
    key: derivedKey.slice(0, keySize),
    iv: derivedKey.slice(keySize),
  }
}

const main = async (env: Record<string, string>) => {
  const {
    COOKIE_CLOUD_HOST: CC_HOST,
    COOKIE_CLOUD_UUID: CC_UUID,
    COOKIE_CLOUD_PASSWORD: CC_PW,
  } = env
  const resp = await fetch(`${CC_HOST}/get/${CC_UUID}`).then(r => r.json())
  console.log(resp)
  let cookies = []
  if (resp && resp.encrypted)  {
    console.log(resp.encrypted)
    console.log(new TextDecoder().decode(decode(resp.encrypted)).slice(0, 16))
    const decoded = decode(resp.encrypted)
    // Salted__ + 8 bytes salt, followed by cipher text
    const salt = decoded.slice(8, 16)
    const cipher_text = decoded.slice(16)

    const password = await crypto.subtle.digest(
      'MD5',
      new TextEncoder().encode(`${CC_UUID}-${CC_PW}`),
    ).then(
      buf => toHashString(buf).substring(0, 16)
    ).then(
      p => new TextEncoder().encode(p)
    )
    const {key, iv} = await evpkdf(password, salt, 1)
    const privete_key = await crypto.subtle.importKey(
      'raw',
      key,
      'AES-CBC',
      false,
      ['decrypt'],
    )

    const d = await crypto.subtle.decrypt(
      {name: 'AES-CBC', iv},
      privete_key,
      cipher_text,
    )
    console.log('decrypted:', new TextDecoder().decode(d))
}

插件的黑白名单是否能支持正则?

想要实现的功能:

只同步baidu.com,不同步tieba.baidu.com
使用白名单无法实现此功能,只能用黑名单来屏蔽tieba.baidu.com,

但是使用黑名单时,白名单又无效...
把浏览器里所有的cookie都同步了

if( cookie.domain?.includes(domain) )
{
ret_cookies[domain].push( cookie );
}

关于密钥生成算法

首先我没有密码学的背景,所以如果我的理解有错还请谅解和指正。

根据项目的README,目前AES key是通过一轮md5生成的,这样可能会导致可以获取uuid的中间人(如同步服务器)可以相对容易地通过穷举进行解密

假设一个12位的大小写+数字的密码,每个字符有 $62\approx2^6$ 种可能,则这个密码相当于 $(2^6)^{12} = 2^{72}$ 位的密钥。密钥生成函数的作用是将密码文本映射到一个 $2^{128}$ 的AES key的空间,而穷举AES key是很困难的。但这里有一个前提,就是密钥生成应该相对费时,否则已知salt(uuid)的中间人就可以直接对密码空间进行穷举,从而导致加密变弱。

以著名的开源密码管理器KeePass为例,其使用的密钥生成函数为Argon2,并且可以设置转换次数来从控制密码文本到密钥所需要的最小计算量/计算时间。目前这个扩展使用的是一轮md5进行密钥生成,而这个密钥生成函数计算量太小了。这样一来,可以穷举的对象就从AES key空间变为密码文本空间,从而使得穷举变得容易。

【功能建议】指定 IV 使用标准的 aes-128-cbc 加密方式

如题,在使用 crypto-js 实现 AES 加密时,若不指定 iv,则会根据其内置策略生成,同样的内容每次加密结果都不一样。
但是对于非 NodeJS 应用来说,除非去适配其同样的 iv 处理逻辑,否则很难解密,只能通过非正常手段去调用 crypto-js
对于 NodeJS 应用来说也只能去调用 crypto-js 库,假若已经使用了其他同类库则有些多此一举。

建议显式的写死 IV 参数,使得加密结果与通用标准 aes-128-cbc 保持一致的输出。
示例:

import CryptoJS from 'crypto-js';
import { type BinaryLike,  createDecipheriv,  createHash,  scryptSync } from 'node:crypto';

function cryptoJSAesEncrypt(str: string, key: string, iv = '0000000000000000') {
  const options = {
    iv: CryptoJS.enc.Utf8.parse(iv),
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  };

  return CryptoJS.AES.encrypt(str, CryptoJS.enc.Utf8.parse(key), options).toString();
}

// nodeJS crypto aes 解密
export function aesDecrypt(data: unknown, key: BinaryLike, algorithm = 'aes-128-ecb', iv: BinaryLike | null = null, salt = '') {
  if (!(data instanceof Buffer)) {
    if (typeof data !== 'string') data = JSON.stringify(data);
    data = Buffer.from(data as string, 'utf8');
  }
  if (salt) key = scryptSync(key, salt, 16);

  const decipher = createDecipheriv(algorithm, key, iv);
  decipher.setAutoPadding(true);
  return Buffer.concat([decipher.update(data as Buffer), decipher.final()]);
}

// test
const str = '123456';
const key = '0123456789abcdef'; // AES-128: 16 字符, AES-256: 32字符
const iv = '0000000000000000';
const encrypted = cryptoJSAesEncrypt(str, key, iv);
const algorithm = 'aes-128-cbc';
const decrypted = aesDecrypt(Buffer.from(encrypted, 'base64'), key, algorithm, iv).toString();
console.log(str === decrypted);

是否可以增加双向同步功能

例如:在A电脑上登陆了aaa.com和bbb.com网站,在B电脑上登陆了ccc.com与ddd.com,经过双向同步后,两台电脑都拥有了aaa.com、bbb.com、ccc.com和ddd.com的cookie

kiwi 浏览器无法同步 ChatGPT-Next-Web localstorage 的部分字段

打算同步 ChatGPT-Next-Web 的对话,在 PC 上同步没问题,在手机 kiwi 浏览器上部分字段未同步。
在扩展的调试工具 console 里显示覆盖成功,但实际上只有部分字段成功覆盖。

kiwi 浏览器用 Storage Area Explorer 扩展手动导入是没问题的。

最新服务端Docker镜像当上传cookie大小超过102400时报错

使用的是latest镜像sha256:0bf28f7aa4e9415d9bf0a1d4518d788eb3d745ee6b1eab237e03901e35a349d5
因为登录网站比较多,就没写白名单,全部上传时就超限了
下面是日志:

stdout: Server start on http://localhost:8088
stderr: PayloadTooLargeError: request entity too large
stderr:     at Gunzip.onData (/data/api/node_modules/raw-body/index.js:253:12)
stderr:     at Gunzip.emit (node:events:513:28)
stderr:     at addChunk (node:internal/streams/readable:315:12)
stderr:     at readableAddChunk (node:internal/streams/readable:289:9)
stderr:     at Gunzip.Readable.push (node:internal/streams/readable:228:10)
stderr:     at Zlib.processCallback (node:zlib:566:10) {
stderr:   limit: 102400,
stderr:   received: 114688,
stderr:   type: 'entity.too.large'
stderr: }

希望能提供一个选项,更改这个limit,谢谢作者

建议插件开启一键导入导出配置设置

首先非常感谢作者设计了这么好用的工具,配合nastool也解决了繁琐的cookies更新问题。

因为个人设置的非同步全部网站,所以同步域名关键词、Cookie保活等数据较多。加上服务器地址、同步key、端对端加密密码。需要复制粘贴的数据也很多。能否开启一键导入导出数据,方便不同设备迁移设置。
仅需要手动修改是上传、还是下载cookie就行了。

如何在油猴脚本中让插件立即同步Cookie呢

我的使用场景是这样子的,我想使用油猴脚本中自动登录某个网站,登录成功之后想cookie自动同步,找了一圈没找到好的方案,是不是只能在油猴脚本里再参考插件的function.js里的代码写一个上传功能呢,希望得到大佬的解惑

能否添加 WebDAV 协议支持

一直使用 floccus+坚果云 同步浏览器书签,通过 floccus 将书签同步到坚果云,而且坚果云免费上传流量1G/月,下载流量3G/月,完全够用,这样就不用自己配置服务器了

请问出现错误提示是为啥?

请问,之前我在本台电脑上传到了服务器(但是,没在其他电脑覆盖到浏览器过),保存了服务器地址、用户KEY和密码。

刚才我在此电脑输入服务器地址、用户KEY和密码,点测试或手动同步都是失败。如图:https://s1.ax1x.com/2023/02/24/pSzL6zt.png

请问错在哪里?不是很理解工作的原理。。

firefox编译后无法安装

编译后,我是用web-ext build 打包成扩展包,导入firefox时提示

16:15:00.030 1689236100030	addons.xpi	WARN	Invalid XPI: Error: Cannot find id for addon /home/h/cookiecloud-0.2.1.zip.(resource://gre/modules/addons/XPIInstall.jsm:1564:11) JS Stack trace: [email protected]:1564:11

同步完成,但未更新任何站点的Cookie!

有个问题想请教一下,我是想用来同步PT站点的cookie导入nastool的,服务器端是920+ docker搭建的,同步的浏览器均为Chrome,在公司用Windows,手动安装插件,由于没有公网,用的是kooldns的映射服务,在家中用Mac,应用商店内安装,访问映射地址和http://localhost:8088 均返回Hello World!API ROOT =,测试和手动同步均显示成功,nastool里却提示 同步完成,但未更新任何站点的Cookie! 我看主页提供的第三方服务器端也是返回Hello World!API ROOT =,实在想不到是哪里出了问题,还请大佬指教!

[Question] 是否可以提供一下开发环境信息?

本人的环境

macOS 13.1
node v18.13.0
pnpm v7.25.0
yarn 1.22.19

使用pnpm

➜  extension git:(master) ✗ pnpm build         

> [email protected] build /Users/user/dev/CookieCloud/extension
> plasmo build

🟣 Plasmo v0.62.2
🔴 The Browser Extension Framework
🔵 INFO   | Prepare to bundle the extension...
🔴 ERROR  | node\_modules/.pnpm/[email protected]\_biqbaboplfbrettd7655fr4n2y/node\_modules/antd/es/config-provider/style/index.js does not export 'resetIcon'
          | undefined
🔴 EXIT   | 👋 Good bye and have a great day!
 ELIFECYCLE  Command failed with exit code 1.

使用yarn

➜  extension git:(master) ✗ yarn build
yarn run v1.22.19
warning package.json: License should be a valid SPDX license expression
$ plasmo build
🟣 Plasmo v0.62.2
🔴 The Browser Extension Framework
🔵 INFO   | Prepare to bundle the extension...
🔴 ERROR  | node\_modules/antd/es/config-provider/style/index.js does not export 'resetIcon'
          | undefined
🔴 EXIT   | 👋 Good bye and have a great day!
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

相关

ant-design/ant-design#39170

问题

能否列出一下具体的开发环境?

建议加上docker-compose部署yml

测试好的yml文件

version: '2'
services:
  cookiecloud:
    image: easychen/cookiecloud:latest
    container_name: cookiecloud-app
    restart: always
    volumes:
      - ./data:/data/api/data
    ports:
      - 8088:8088

请问可以关闭webui吗

家宽docker布置的,为了方便同步单位电脑,把8088端口映射出去了。
上海这边不允许家宽搭建web网站,有封宽带风险。
想问一下有什么办法关闭容器的webui吗

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.