Loading repository data…
Loading repository data…
easychen / repository
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
A transparent discovery signal based on current public GitHub metadata.
This score does not audit code, security, maintainers, documentation quality, or suitability. Verify the repository and its current documentation before adoption.
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.
Since version 0.3.0, the project has been rewritten using wxt. It now supports encryption algorithms with a fixed IV and supports more standard libraries for decryption. See the wxt branch for details.
The latest version now supports synchronization of local storage under the same domain name.
Telegram channel | Telegram group
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 🙇🏻♂️
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.For testing purposes only. Stability is not guaranteed. It is recommended to set up your own server to further enhance data security.
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.
Supports architectures: linux/amd64, linux/arm64, etc.
docker run -p=8088:8088 easychen/cookiecloud:latest
Default port 8088, image address easychen/cookiecloud
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
version: '3'
services:
cookiecloud:
image: easychen/cookiecloud:latest
container_name: cookiecloud-app
restart: always
volumes:
- ./data:/data/api/data
ports:
- 8088:8088
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
Enter the browser plugin list, click on service worker, a panel will pop up where you can view the operation log
Upload:
Download:
const data = JSON.stringify(cookies);
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
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();
});
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;
}
Refer to the article "Implementation and Problem Handling of Crypto in Python for AES Encryption and Decryption in JS CryptoJS" or use PyCookieCloud
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 != p