{"id":15035653,"url":"https://github.com/easychen/cookiecloud","last_synced_at":"2025-05-14T05:11:16.468Z","repository":{"id":65340128,"uuid":"589830814","full_name":"easychen/CookieCloud","owner":"easychen","description":"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","archived":false,"fork":false,"pushed_at":"2025-05-07T02:49:18.000Z","size":1826,"stargazers_count":2258,"open_issues_count":45,"forks_count":198,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-05-07T03:36:37.506Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/easychen.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-17T03:11:37.000Z","updated_at":"2025-05-07T02:49:22.000Z","dependencies_parsed_at":"2023-12-13T06:22:59.361Z","dependency_job_id":"1675b498-948e-4c0c-b431-5b66e01a1f74","html_url":"https://github.com/easychen/CookieCloud","commit_stats":{"total_commits":59,"total_committers":13,"mean_commits":4.538461538461538,"dds":"0.23728813559322037","last_synced_commit":"fa51208f41fa12bdf40325631ac69055a4be4746"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easychen%2FCookieCloud","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easychen%2FCookieCloud/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easychen%2FCookieCloud/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easychen%2FCookieCloud/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/easychen","download_url":"https://codeload.github.com/easychen/CookieCloud/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254076850,"owners_count":22010611,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-09-24T20:29:08.162Z","updated_at":"2025-05-14T05:11:11.433Z","avatar_url":"https://github.com/easychen.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CookieCloud\n\n[中文](./README_cn.md) | [English](./README.md)\n\n![](extension/assets/icon.png)\n\nCookieCloud 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.\n\n\u003e The latest version now supports synchronization of local storage under the same domain name.\n\n[Telegram channel](https://t.me/CookieCloudTG) | [Telegram group](https://t.me/CookieCloudGroup)\n\n## ⚠️ Breaking Change\n\nDue 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 }`).\n\nFurthermore, 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.\n\nWe apologize for any inconvenience this may cause 🙇🏻‍♂️\n\n\n## Official Tutorials\n\n![](images/20230121141854.png)  \n\n1. Video: [Bilibili](https://www.bilibili.com/video/BV1fR4y1a7zb) | [YouTube](https://youtu.be/3oeSiGHXeQw) - Please follow and subscribe 🥺\n2. Tutorial: [Juejin](https://juejin.cn/post/7190963442017108027)\n\n## FAQ\n\n1. Currently, synchronization is only one-way, meaning one browser can upload while another downloads.\n2. 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 \u0026\u0026 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.\n\n![](images/20230121092535.png)  \n\n## Browser Plugin\n\n1. Installation from store: [Edge Store](https://microsoftedge.microsoft.com/addons/detail/cookiecloud/bffenpfpjikaeocaihdonmgnjjdpjkeo) | [Chrome Store](https://chrome.google.com/webstore/detail/cookiecloud/ffjiejobkoibkjlhjnlgmcnnigeelbdl) (Note: Versions in the store might be delayed due to review processes)\n2. Manual download and installation: See Release\n\n\n## Server Side\n\n### Third Party\n\n\u003e Free server-side services provided by third parties are available for trial. Stability is determined by the third parties. We appreciate their sharing 👏\n\n\u003e Some server-side versions might be outdated. If tests fail, try adding domain keywords before retrying.\n\n- \u003chttp://45.138.70.177:8088\u003e provided by [LSRNB](https://github.com/lsrnb)\n- \u003chttp://45.145.231.148:8088\u003e provided by [shellingford37](https://github.com/shellingford37)\n- \u003chttp://nastool.cn:8088\u003e provided by [nastools](https://github.com/jxxghp/nas-tools)\n- \u003chttps://cookies.xm.mk\u003e provided by [Xm798](https://github.com/Xm798)\n- \u003chttps://cookie.xy213.cn\u003e provided by [xuyan0213](https://github.com/xuyan0213)\n- \u003chttps://cookie-cloud.vantis-space.com\u003e provided by [vantis](https://github.com/vantis-zh)\n- \u003chttps://cookiecloud.25wz.cn\u003e provided by [wuquejs](https://github.com/wuquejs)\n- \u003chttps://cookiecloud.zhensnow.uk\u003e provided by [YeTianXingShi](https://github.com/YeTianXingShi)\n- \u003chttps://cookiecloud.ddsrem.com\u003e provided by [DDSRem](https://github.com/DDS-Derek)\n- \u003chttps://cookiecloud.d0zingcat.xyz\u003e provided by [d0zingcat](https://github.com/d0zingcat)\n\n### Self-hosting\n\n#### Option One: Deploy through Docker, simple, recommended method\n\nSupports architectures: linux/amd64, linux/arm64, etc.\n\n\n##### Start with Docker Command\n\n```bash\ndocker run -p=8088:8088 easychen/cookiecloud:latest\n```\nDefault port 8088, image address [easychen/cookiecloud](https://hub.docker.com/r/easychen/cookiecloud)\n\n###### Specify API Directory - Optional Step, Can Be Skipped\n\nAdd the environment variable -e API_ROOT=/`subdirectory must start with a slash` to specify a subdirectory:\n\n```bash\ndocker run -e API_ROOT=/cookie -p=8088:8088 easychen/cookiecloud:latest\n```\n\n##### Start with Docker-compose\n\n```yml\nversion: '3'\nservices:\n  cookiecloud:\n    image: easychen/cookiecloud:latest\n    container_name: cookiecloud-app\n    restart: always\n    volumes:\n      - ./data:/data/api/data\n    ports:\n      - 8088:8088\n```\n\n[docker-compose.yml provided by aitixiong](https://github.com/easychen/CookieCloud/issues/42)\n\n#### Option Two: Deploy with Node\n\n\u003e Suitable for environments without docker but supporting node, requires installing node in advance\n\n```bash\ncd api \u0026\u0026 yarn install \u0026\u0026 node app.js\n```\nDefault port 8088, also supports the API_ROOT environment variable\n\n## Debugging and Log Viewing\n\nEnter the browser plugin list, click on service worker, a panel will pop up where you can view the operation log\n\n![](images/20230121095327.png)  \n\n## API Interface\n\nUpload:\n\n- method: POST\n- url: /update\n- parameters\n  - uuid\n  - encrypted: the string encrypted locally\n\nDownload:\n\n- method: POST/GET\n- url: /get/:uuid\n- parameters:\n   - password: optional, if not provided returns the encrypted string, if provided attempts to decrypt and send the content;\n\n\n## Cookie Encryption and Decryption Algorithm\n\n### Encryption\n\nconst data = JSON.stringify(cookies);\n\n1. md5(uuid+password) take the first 16 characters as the key\n2. AES.encrypt(data, the_key)\n\n### Decryption\n\n1. md5(uuid+password) take the first 16 characters as the key\n2. AES.decrypt(encrypted, the_key)\n\nAfter decryption, get data, JSON.parse(data) to obtain the data object { cookie_data, local_storage_data };\n\nReference function\n\n```node\nfunction cookie_decrypt( uuid, encrypted, password )\n{\n    const CryptoJS = require('crypto-js');\n    const the_key = CryptoJS.MD5(uuid+'-'+password).toString().substring(0,16);\n    const decrypted = CryptoJS.AES.decrypt(encrypted, the_key).toString(CryptoJS.enc.Utf8);\n    const parsed = JSON.parse(decrypted);\n    return parsed;\n}\n```\n\nSee `extension/function.js` for more\n\n## Headless Browser Example Using CookieCloud\n\nRefer to `examples/playwright/tests/example.spec.js` \n\n```javascript\ntest('Access nexusphp using CookieCloud', async ({ page, browser }) =\u003e {\n  // Read and decrypt cloud cookie\n  const cookies = await cloud_cookie(COOKIE_CLOUD_HOST, COOKIE_CLOUD_UUID, COOKIE_CLOUD_PASSWORD);\n  // Add cookie to browser context\n  const context = await browser.newContext();\n  await context.addCookies(cookies);\n  page = await context.newPage();\n  // From this point on, the Cookie is already attached, proceed as normal\n  await page.goto('https://demo.nexusphp.org/index.php');\n  await expect(page.getByRole('link', { name: 'magik' })).toHaveText(\"magik\");\n  await context.close();\n});\n\n```\n\n### Functions\n\n```javascript\nasync function cloud_cookie( host, uuid, password )\n{\n  const fetch = require('cross-fetch');\n  const url = host+'/get/'+uuid;\n  const ret = await fetch(url);\n  const json = await ret.json();\n  let cookies = [];\n  if( json \u0026\u0026 json.encrypted )\n  {\n    const {cookie_data, local_storage_data} = cookie_decrypt(uuid, json.encrypted, password);\n    for( const key in cookie_data )\n    {\n      // merge cookie_data[key] to cookies\n      cookies = cookies.concat(cookie_data[key].map( item =\u003e {\n        if( item.sameSite == 'unspecified' ) item.sameSite = 'Lax';\n        return item;\n      } ));\n    }\n  }\n  return cookies;\n}\n\nfunction cookie_decrypt( uuid, encrypted, password )\n{\n    const CryptoJS = require('crypto-js');\n    const the_key = CryptoJS.MD5(uuid+'-'+password).toString().substring(0,16);\n    const decrypted = CryptoJS.AES.decrypt(encrypted, the_key).toString(CryptoJS.enc.Utf8);\n    const parsed = JSON.parse(decrypted);\n    return parsed;\n}\n```\n\n## Python Decryption\n\nRefer to the article [\"Implementation and Problem Handling of Crypto in Python for AES Encryption and Decryption in JS CryptoJS\"](https://blog.homurax.com/2022/08/12/python-crypto/) or use [PyCookieCloud](https://github.com/lupohan44/PyCookieCloud)\n\n[python2](https://github.com/easychen/CookieCloud/issues/76)\n\n[another example](examples/decrypt.py)\n\n## Go Decryption Algorithm\n\n[Thanks to sagan for sharing](https://github.com/easychen/CookieCloud/issues/49) \n\n```go\npackage main\n\nimport (\n\t\"bytes\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/md5\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n)\n\nconst (\n\tpkcs5SaltLen = 8\n\taes256KeyLen = 32\n)\n\ntype CookieCloudBody struct {\n\tUuid      string `json:\"uuid,omitempty\"`\n\tEncrypted string `json:\"encrypted,omitempty\"`\n}\n\nfunc main() {\n\tapiUrl := strings.TrimSuffix(os.Getenv(\"COOKIE_CLOUD_HOST\"), \"/\")\n\tuuid := os.Getenv(\"COOKIE_CLOUD_UUID\")\n\tpassword := os.Getenv(\"COOKIE_CLOUD_PASSWORD\")\n\n\tif apiUrl == \"\" || uuid == \"\" || password == \"\" {\n\t\tlog.Fatalf(\"COOKIE_CLOUD_HOST, COOKIE_CLOUD_UUID and COOKIE_CLOUD_PASSWORD env must be set\")\n\t}\n\tvar data *CookieCloudBody\n\tres, err := http.Get(apiUrl + \"/get/\" + uuid)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to request server: %v\", err)\n\t}\n\tif res.StatusCode != 200 {\n\t\tlog.Fatalf(\"Server return status %d\", res.StatusCode)\n\t}\n\tdefer res.Body.Close()\n\tbody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to read server response: %v\", err)\n\t}\n\terr = json.Unmarshal(body, \u0026data)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to parse server response as json: %v\", err)\n\t}\n\tkeyPassword := Md5String(uuid, \"-\", password)[:16]\n\tdecrypted, err := DecryptCryptoJsAesMsg(keyPassword, data.Encrypted)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to decrypt: %v\", err)\n\t}\n\tfmt.Printf(\"Decrypted: %s\\n\", decrypted)\n}\n\n// Decrypt a CryptoJS.AES.encrypt(msg, password) encrypted msg.\n// ciphertext is the result of CryptoJS.AES.encrypt(), which is the base64 string of\n// \"Salted__\" + [8 bytes random salt] + [actual ciphertext].\n// actual ciphertext is padded (make it's length align with block length) using Pkcs7.\n// CryptoJS use a OpenSSL-compatible EVP_BytesToKey to derive (key,iv) from (password,salt),\n// using md5 as hash type and 32 / 16 as length of key / block.\n// See: https://stackoverflow.com/questions/35472396/how-does-cryptojs-get-an-iv-when-none-is-specified ,\n// https://stackoverflow.com/questions/64797987/what-is-the-default-aes-config-in-crypto-js\nfunc DecryptCryptoJsAesMsg(password string, ciphertext string) ([]byte, error) {\n\tconst keylen = 32\n\tconst blocklen = 16\n\trawEncrypted, err := base64.StdEncoding.DecodeString(ciphertext)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to base64 decode Encrypted: %v\", err)\n\t}\n\tif len(rawEncrypted) \u003c 17 || len(rawEncrypted)%blocklen != 0 || string(rawEncrypted[:8]) != \"Salted__\" {\n\t\treturn nil, fmt.Errorf(\"invalid ciphertext\")\n\t}\n\tsalt := rawEncrypted[8:16]\n\tencrypted := rawEncrypted[16:]\n\tkey, iv := BytesToKey(salt, []byte(password), md5.New(), keylen, blocklen)\n\tnewCipher, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create aes cipher: %v\", err)\n\t}\n\tcfbdec := cipher.NewCBCDecrypter(newCipher, iv)\n\tdecrypted := make([]byte, len(encrypted))\n\tcfbdec.CryptBlocks(decrypted, encrypted)\n\tdecrypted, err = pkcs7strip(decrypted, blocklen)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to strip pkcs7 paddings (password may be incorrect): %v\", err)\n\t}\n\treturn decrypted, nil\n}\n\n// From https://github.com/walkert/go-evp .\n// BytesToKey implements the Openssl EVP_BytesToKey logic.\n// It takes the salt, data, a hash type and the key/block length used by that type.\n// As such it differs considerably from the openssl method in C.\nfunc BytesToKey(salt, data []byte, h hash.Hash, keyLen, blockLen int) (key, iv []byte) {\n\tsaltLen := len(salt)\n\tif saltLen \u003e 0 \u0026\u0026 saltLen != pkcs5SaltLen {\n\t\tpanic(fmt.Sprintf(\"Salt length is %d, expected %d\", saltLen, pkcs5SaltLen))\n\t}\n\tvar (\n\t\tconcat   []byte\n\t\tlastHash []byte\n\t\ttotalLen = keyLen + blockLen\n\t)\n\tfor ; len(concat) \u003c totalLen; h.Reset() {\n\t\t// concatenate lastHash, data and salt and write them to the hash\n\t\th.Write(append(lastHash, append(data, salt...)...))\n\t\t// passing nil to Sum() will return the current hash value\n\t\tlastHash = h.Sum(nil)\n\t\t// append lastHash to the running total bytes\n\t\tconcat = append(concat, lastHash...)\n\t}\n\treturn concat[:keyLen], concat[keyLen:totalLen]\n}\n\n// BytesToKeyAES256CBC implements the SHA256 version of EVP_BytesToKey using AES CBC\nfunc BytesToKeyAES256CBC(salt, data []byte) (key []byte, iv []byte) {\n\treturn BytesToKey(salt, data, sha256.New(), aes256KeyLen, aes.BlockSize)\n}\n\n// BytesToKeyAES256CBCMD5 implements the MD5 version of EVP_BytesToKey using AES CBC\nfunc BytesToKeyAES256CBCMD5(salt, data []byte) (key []byte, iv []byte) {\n\treturn BytesToKey(salt, data, md5.New(), aes256KeyLen, aes.BlockSize)\n}\n\n// return the MD5 hex hash string (lower-case) of input string(s)\nfunc Md5String(inputs ...string) string {\n\tkeyHash := md5.New()\n\tfor _, str := range inputs {\n\t\tio.WriteString(keyHash, str)\n\t}\n\treturn hex.EncodeToString(keyHash.Sum(nil))\n}\n\n// from https://gist.github.com/nanmu42/b838acc10d393bc51cb861128ce7f89c .\n// pkcs7strip remove pkcs7 padding\nfunc pkcs7strip(data []byte, blockSize int) ([]byte, error) {\n\tlength := len(data)\n\tif length == 0 {\n\t\treturn nil, errors.New(\"pkcs7: Data is empty\")\n\t}\n\tif length%blockSize != 0 {\n\t\treturn nil, errors.New(\"pkcs7: Data is not block-aligned\")\n\t}\n\tpadLen := int(data[length-1])\n\tref := bytes.Repeat([]byte{byte(padLen)}, padLen)\n\tif padLen \u003e blockSize || padLen == 0 || !bytes.HasSuffix(data, ref) {\n\t\treturn nil, errors.New(\"pkcs7: Invalid padding\")\n\t}\n\treturn data[:length-padLen], nil\n}\n\n```\n\n\n## Deno Reference\n\n[Thanks to JokerQyou for sharing](https://github.com/easychen/CookieCloud/issues/41)\n\n```ts\nimport {crypto, toHashString} from 'https://deno.land/std@0.200.0/crypto/mod.ts'\nimport {decode } from 'https://deno.land/std@0.200.0/encoding/base64.ts'\n\nconst evpkdf = async (\n  password: Uint8Array,\n  salt: Uint8Array,\n  iterations: number,\n): Promise\u003c{\n  key: Uint8Array,\n  iv: Uint8Array,\n}\u003e =\u003e {\n  const keySize = 32\n  const ivSize = 16\n  const derivedKey = new Uint8Array(keySize + ivSize)\n  let currentBlock = 1\n  let digest = new Uint8Array(0)\n  const hashLength = 16\n  while ((currentBlock - 1) * hashLength \u003c keySize + ivSize) {\n    const data = new Uint8Array(digest.length + password.length + salt.length)\n    data.set(digest)\n    data.set(password, digest.length)\n    data.set(salt, digest.length + password.length)\n    digest = await crypto.subtle.digest('MD5', data).then(buf =\u003e new Uint8Array(buf))\n\n    for (let i = 1; i \u003c iterations; i++) {\n      digest = await crypto.subtle.digest('MD5', digest).then(buf =\u003e new Uint8Array(buf))\n    }\n    derivedKey.set(digest, (currentBlock - 1) * hashLength)\n    currentBlock++\n  }\n  return {\n    key: derivedKey.slice(0, keySize),\n    iv: derivedKey.slice(keySize),\n  }\n}\n\nconst main = async (env: Record\u003cstring, string\u003e) =\u003e {\n  const {\n    COOKIE_CLOUD_HOST: CC_HOST,\n    COOKIE_CLOUD_UUID: CC_UUID,\n    COOKIE_CLOUD_PASSWORD: CC_PW,\n  } = env\n  const resp = await fetch(`${CC_HOST}/get/${CC_UUID}`).then(r =\u003e r.json())\n  console.log(resp)\n  let cookies = []\n  if (resp \u0026\u0026 resp.encrypted)  {\n    console.log(resp.encrypted)\n    console.log(new TextDecoder().decode(decode(resp.encrypted)).slice(0, 16))\n    const decoded = decode(resp.encrypted)\n    // Salted__ + 8 bytes salt, followed by cipher text\n    const salt = decoded.slice(8, 16)\n    const cipher_text = decoded.slice(16)\n\n    const password = await crypto.subtle.digest(\n      'MD5',\n      new TextEncoder().encode(`${CC_UUID}-${CC_PW}`),\n    ).then(\n      buf =\u003e toHashString(buf).substring(0, 16)\n    ).then(\n      p =\u003e new TextEncoder().encode(p)\n    )\n    const {key, iv} = await evpkdf(password, salt, 1)\n    const privete_key = await crypto.subtle.importKey(\n      'raw',\n      key,\n      'AES-CBC',\n      false,\n      ['decrypt'],\n    )\n\n    const d = await crypto.subtle.decrypt(\n      {name: 'AES-CBC', iv},\n      privete_key,\n      cipher_text,\n    )\n    console.log('decrypted:', new TextDecoder().decode(d))\n}\n```\n\nTranslated by GPT4\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feasychen%2Fcookiecloud","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feasychen%2Fcookiecloud","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feasychen%2Fcookiecloud/lists"}