From 56003b7f698aa72177938128fa2db888611c3558 Mon Sep 17 00:00:00 2001 From: Knowit Date: Mon, 11 May 2026 11:24:48 +0800 Subject: [PATCH] secrets: ship encrypted SA key, switch install to git-clone + decrypt - secrets/bookkeeping-sa.json.enc: team service-account key, encrypted with AES-256-CBC + PBKDF2(100k iter) using a 48-char random passphrase. Safe to commit to a public repo; the passphrase lives in the team password manager. - scripts/decrypt-key.sh: one-liner that decrypts to ~/.config/gcp/ (mode 600) and prints the service-account email so users know which address to share their Sheet with. - secrets/README.md: explains the crypto, decrypt flow, and rotation procedures (passphrase rotation vs underlying GCP key rotation). - README + DEPLOY.md + setup.md: install flow updated. Users no longer wait for the admin to send a JSON; they git clone, run decrypt-key.sh with the passphrase from the team password manager, and continue. Cuts one out-of-band file transfer from the user experience. Co-Authored-By: Claude Opus 4.7 (1M context) --- DEPLOY.md | 82 +++++++++++++++++-------------- README.md | 33 +++++++------ README.zh-CN.md | 35 ++++++++------ scripts/decrypt-key.sh | 35 ++++++++++++++ scripts/setup.md | 83 +++++++++++++++++++++----------- secrets/README.md | 81 +++++++++++++++++++++++++++++++ secrets/bookkeeping-sa.json.enc | Bin 0 -> 2368 bytes 7 files changed, 254 insertions(+), 95 deletions(-) create mode 100755 scripts/decrypt-key.sh create mode 100644 secrets/README.md create mode 100644 secrets/bookkeeping-sa.json.enc diff --git a/DEPLOY.md b/DEPLOY.md index c81749a..9b851a0 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -16,15 +16,14 @@ You will keep your own Google Sheet (only you can read it). A small "service acc ## Before you start: a 30-second checklist -Make sure you have all five of these. If any is missing, stop and get it before continuing. +Make sure you have all four of these. If any is missing, stop and get it before continuing. | You need | How to check | |---|---| | **A Google account** | You can sign in to gmail.com | | **A Mac or Linux computer** | This guide uses Mac. Linux is identical. **Windows users**: use WSL (Windows Subsystem for Linux), or ask your AI agent for the Windows-equivalent commands | | **OpenClaw (or another AI client) installed** | You already use it to chat with an AI | -| **A JSON file your admin sent you** | A file usually named `autoacct-sa.json` — typically delivered through a password manager, encrypted email, or Slack DM. **If you don't have this, message your admin first.** | -| **A service-account email your admin gave you** | Looks like `autoacct@something.iam.gserviceaccount.com`. Your admin sent this together with the JSON. | +| **A passphrase from your admin** | A ~48-character random string, typically delivered through a password manager (1Password / Bitwarden). **If you don't have it, message your admin first.** | | **About 5 minutes** | Don't start during a phone call. | --- @@ -82,48 +81,57 @@ pip install google-api-python-client google-auth --- -# Part 2 — Put your admin's key file in the right place (1 minute) +# Part 2 — Unlock the bundled service-account key (1 minute) -## Step 3. Find the JSON file your admin sent you +The repo ships with the team's service-account key already encrypted (AES-256). You don't need any extra file — you just need the passphrase from your admin to unlock it. -Locate the file (e.g. `autoacct-sa.json`). It is typically in: -- Your **Downloads** folder (if downloaded from email or a link) -- A folder you exported from a password manager (1Password / Bitwarden) -- An attachment on your desktop +## Step 3. Have the passphrase ready -**Treat this file like a password.** Anyone who has it can write to any Google Sheet you've shared with your admin's service account. Don't email it to yourself, don't post it anywhere, don't put it on a USB stick. +The passphrase is a ~48-character random string. It looks something like: -## Step 4. Move the JSON file to its permanent home - -Paste this into the Terminal (assuming the file is in your Downloads folder and named `autoacct-sa.json`): - -```bash -mkdir -p ~/.config/gcp -mv ~/Downloads/autoacct-sa.json ~/.config/gcp/autoacct-sa.json -chmod 600 ~/.config/gcp/autoacct-sa.json +``` +d03wb3gAnXyo2N8e6FYGIUTNUd3-rFu-UxuEYbVWgOOZxZnG ``` -**What you should see:** nothing visible. The Terminal returns silently — that means it worked. +Your admin will have stored it in the team password manager (1Password / Bitwarden). Open that, find the entry called something like "AutoACCT decrypt passphrase", and **copy it now**. You'll paste it in the next step. -**If you see "No such file or directory":** the file isn't in `~/Downloads`. Find where it actually is, and adjust the `mv` command. For example, if it's on your Desktop: +If you can't find it, message your admin before continuing. + +## Step 4. Run the decrypt script + +Paste this into the Terminal: ```bash -mv ~/Desktop/autoacct-sa.json ~/.config/gcp/autoacct-sa.json +bash ~/.openclaw/workspace/skills/AutoACCT/scripts/decrypt-key.sh ``` -**If the file has a different name** (e.g. `autoacct-project-12345.json`): use that name in the `mv` command, but **rename it to `autoacct-sa.json` while moving**: +The Terminal prompts: -```bash -mv ~/Downloads/autoacct-project-12345.json ~/.config/gcp/autoacct-sa.json +``` +enter AES-256-CBC decryption password: ``` -**Verify:** paste this into the Terminal: +**Paste the passphrase from Step 3.** (Right-click → Paste, or Cmd+V. The characters won't appear on screen — that's normal; it's hiding your password.) Press **Enter**. + +**What success looks like:** +``` +Decrypted to /Users/you/.config/gcp/bookkeeping-sa.json + +Service-account email: bookkeeping@autoacct.iam.gserviceaccount.com +Next step: share your Google Sheet with this email (Editor). +``` + +**Write down the service-account email** — you'll paste it in Step 8. + +**If you see `bad decrypt`:** the passphrase is wrong. Either you mistyped it, or your admin sent a different one. Try again with a fresh paste. + +**Verify:** ```bash ls -la ~/.config/gcp/ ``` -You should see one line containing `autoacct-sa.json` with permissions `-rw-------`. +You should see one line containing `bookkeeping-sa.json` with permissions `-rw-------`. --- @@ -156,14 +164,14 @@ You'll paste this exact name into `config.json` in Step 10. **Tab name and confi **Verify:** Column A says `Date`, Column N (scroll right if needed) says `Logged At`. -## Step 8. Share the sheet with your admin's service account +## Step 8. Share the sheet with the service account This is the step that lets the skill write into your sheet. 1. Click the green **Share** button (top right of the sheet). -2. In the "Add people, groups, and calendar events" box, **paste the service-account email your admin gave you**. It looks something like: +2. In the "Add people, groups, and calendar events" box, **paste the service-account email that `decrypt-key.sh` printed back in Step 4**. It looks like: ``` - autoacct@your-project-12345.iam.gserviceaccount.com + bookkeeping@autoacct.iam.gserviceaccount.com ``` 3. Make sure the role on the right says **Editor** (not Viewer / Commenter). 4. **Uncheck "Notify people"** — there's no real person on the other end of that email. @@ -212,7 +220,7 @@ Nothing visible happens — that's expected. The Terminal just made a copy of th { "sheet_id": "PASTE_YOUR_GOOGLE_SHEET_URL_OR_ID_HERE", "worksheet": "Sheet1", - "service_account_path": "~/.config/gcp/autoacct-sa.json", + "service_account_path": "~/.config/gcp/bookkeeping-sa.json", "hkd_fx_provider": "frankfurter" } ``` @@ -229,7 +237,7 @@ Nothing visible happens — that's expected. The Terminal just made a copy of th { "sheet_id": "https://docs.google.com/spreadsheets/d/1abcDEF...xyz123/edit#gid=0", "worksheet": "Sheet1", - "service_account_path": "~/.config/gcp/autoacct-sa.json", + "service_account_path": "~/.config/gcp/bookkeeping-sa.json", "hkd_fx_provider": "frankfurter" } ``` @@ -266,10 +274,10 @@ Switch to your Google Sheet — there's a new row with `TEST` as the merchant. * | You see | What it means | How to fix | |---|---|---| -| `HTTP 403` or `The caller does not have permission` | You forgot Step 8 (sharing the sheet with the service-account email), or the email was typed incorrectly | Re-share the sheet with the exact email your admin gave you, role Editor. | +| `HTTP 403` or `The caller does not have permission` | You forgot Step 8 (sharing the sheet with the service-account email), or the email was typed incorrectly | Re-run `bash scripts/decrypt-key.sh` to print the email again, then re-share the sheet with that exact address, role Editor. | | `HTTP 400: Unable to parse range` | The `worksheet` in `config.json` doesn't match the actual tab name | Open `config.json`, fix it to exactly match the tab name at the bottom-left of your sheet (`Sheet1` or `工作表1`). | | `HTTP 404` or `Requested entity was not found` | The `sheet_id` in `config.json` is wrong | Open your sheet in the browser, copy the **full URL** from the address bar, paste it into `sheet_id` (replacing whatever's there). | -| `FileNotFoundError ... autoacct-sa.json` | The JSON file isn't where `config.json` says it is | Run `ls -la ~/.config/gcp/` — confirm the file is there with the exact name `autoacct-sa.json`. If it has a different name, either rename it or update `service_account_path` in `config.json`. | +| `FileNotFoundError ... bookkeeping-sa.json` | The JSON file isn't where `config.json` says it is | Run `ls -la ~/.config/gcp/` — confirm the file is there with the exact name `bookkeeping-sa.json`. If it has a different name, either rename it or update `service_account_path` in `config.json`. | | `ImportError: No module named 'googleapiclient'` | You skipped or partially failed the `pip install` step | Re-run `pip install google-api-python-client google-auth`. If that fails, try `pip3` or `python3 -m pip install ...`. | | `config.json not found` | You skipped Step 10 | Run `cp config.example.json config.json` from inside the skill folder. | | `Expecting value: line 1 column 1` (JSON error) | Your `config.json` has smart quotes or is otherwise malformed | Re-open with TextEdit in plain-text mode (Format → Make Plain Text), or ask your AI agent to repair it. | @@ -284,16 +292,16 @@ If your problem is not in this table: copy the **exact error text** and paste it |---|---| | Use a different sheet | Repeat Steps 5–9 on the new sheet (including the Share-with-service-account step), then update `sheet_id` and `worksheet` in `config.json`. | | Stop the skill writing to a sheet | Open the sheet → Share → find the service-account email → click trash icon → Save. The skill will get HTTP 403 on the next attempt. | -| Move to a new computer | Repeat Parts 1, 2, and 4 on the new computer (re-clone, copy the JSON, copy `config.json`). Your sheet and its sharing don't change. | -| You think your JSON file leaked | Message your admin immediately. They can revoke the key on the GCP side; nothing they revoke can come back to bite you afterwards. | +| Move to a new computer | Repeat Parts 1, 2, and 4 on the new computer (re-clone, re-run `decrypt-key.sh`, copy `config.json`). Your sheet and its sharing don't change. | +| You think the decrypted JSON or passphrase leaked | Message your admin immediately. They will rotate both the passphrase **and** the underlying GCP key; you'll then `git pull` + re-run `decrypt-key.sh` with the new passphrase. | --- # Summary of what just happened (so you understand what you set up) 1. **Your computer** has the AutoACCT skill in `~/.openclaw/workspace/skills/AutoACCT/`. When you drop a receipt photo into your AI chat, the AI runs `scripts/append_row.py`, which is a small Python program. -2. **The JSON file** at `~/.config/gcp/autoacct-sa.json` is the key that authenticates the Python program as a "service account" your admin created. +2. **The repo ships an encrypted JSON file** (`secrets/bookkeeping-sa.json.enc`) — the service-account key locked with AES-256 + your team's passphrase. `decrypt-key.sh` unlocks it into `~/.config/gcp/bookkeeping-sa.json` (mode 600). The unlocked JSON authenticates the Python program as the service account your admin created in Google Cloud. 3. **The service account** is allowed to edit Google Sheets only when those sheets have been shared with its email. You shared *your* sheet with it in Step 8 — so it can only write to your sheet (and any other sheet you might share with it later). -4. **Other users on your team** have the **same JSON file** but their own sheets. The service account can write to each of their sheets only because each user has shared *their own* sheet with it. They cannot see your sheet, and you cannot see theirs. +4. **Other users on your team** decrypt the **same JSON file** with the **same passphrase** but write to **their own sheets**. The service account can write to each of their sheets only because each user has shared *their own* sheet with it. They cannot see your sheet, and you cannot see theirs — unless someone explicitly shares. That's it. Welcome to AutoACCT. diff --git a/README.md b/README.md index c59c68c..ab3699b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Intended to be invoked manually inside OpenClaw today, and wired up to a WhatsAp ## Install (end users) -Your admin will have given you **two things**: a service-account JSON key file (e.g. `autoacct-sa.json`) and a service-account email (e.g. `autoacct@your-project.iam.gserviceaccount.com`). If you don't have them, ask your admin first. +The repo bundles the team's Google service-account key, encrypted with AES-256. Ask your admin for **one thing**: the passphrase (it's in your team's password manager). Follow the 4 steps below. Takes ~5 minutes. @@ -26,18 +26,17 @@ Follow the 4 steps below. Takes ~5 minutes. ```bash git clone https://github.com/CharlesZhang2023/AutoACCT.git ~/.openclaw/workspace/skills/AutoACCT +cd ~/.openclaw/workspace/skills/AutoACCT pip install google-api-python-client google-auth ``` -### Step 2 — Drop the admin's JSON key into `~/.config/gcp/` +### Step 2 — Decrypt the bundled service-account key ```bash -mkdir -p ~/.config/gcp -mv ~/Downloads/autoacct-sa.json ~/.config/gcp/autoacct-sa.json -chmod 600 ~/.config/gcp/autoacct-sa.json +bash scripts/decrypt-key.sh ``` -(Replace `~/Downloads/autoacct-sa.json` with wherever you saved the file your admin sent.) +You'll be prompted for the passphrase. On success the script writes the JSON to `~/.config/gcp/bookkeeping-sa.json` (mode 600) and prints the **service-account email** — copy it; you'll paste it into Step 3. ### Step 3 — Create your Google Sheet and share it with the service account @@ -48,7 +47,7 @@ chmod 600 ~/.config/gcp/autoacct-sa.json ``` Date Merchant Category Amount Currency Amount (HKD) FX Rate FX Date Payment Method Line Items Raw OCR Note Receipt Logged At ``` -5. Click **Share** (top right) → paste the **service-account email** your admin gave you → role **Editor** → **Send** (you can uncheck "Notify people"). +5. Click **Share** (top right) → paste the **service-account email** that `decrypt-key.sh` printed in Step 2 → role **Editor** → **Send** (you can uncheck "Notify people"). 6. **Copy the full URL from your browser's address bar.** Something like: `https://docs.google.com/spreadsheets/d/1abc...xyz/edit#gid=0` (The script extracts the sheet ID for you — either the full URL or just the bare ID works.) @@ -66,7 +65,7 @@ Open `config.json` and fill in **sheet_id** (paste the URL from Step 3.6) and ** { "sheet_id": "https://docs.google.com/spreadsheets/d/1abc...xyz/edit", "worksheet": "Sheet1", - "service_account_path": "~/.config/gcp/autoacct-sa.json", + "service_account_path": "~/.config/gcp/bookkeeping-sa.json", "hkd_fx_provider": "frankfurter" } ``` @@ -85,11 +84,12 @@ If you hit an error, see [`scripts/setup.md`](scripts/setup.md) for troubleshoot ## Admin setup (one time, done by you before distributing) -Before users can run the steps above, **you** (the admin) create one shared service account and distribute the JSON to users. See [`scripts/setup.md`](scripts/setup.md) for the full admin guide — short version: +See [`scripts/setup.md`](scripts/setup.md) for the full admin guide and [`secrets/README.md`](secrets/README.md) for the encryption mechanics. Short version: 1. Create a GCP project, enable Sheets API, create a service account, download the JSON key. -2. Distribute the JSON file + the service-account email to your users via a secure channel (1Password / Bitwarden / encrypted email — **never commit to git**). -3. Tell users to follow the 4 steps above. +2. Encrypt the JSON with a strong random passphrase and commit `secrets/bookkeeping-sa.json.enc` to the repo (see `secrets/README.md` for the openssl one-liner). +3. Store the passphrase in your team password manager. Tell users to follow the 4 install steps above. +4. Rotate the passphrase when team members leave; rotate the underlying GCP key when the passphrase or any decrypted JSON might have leaked. ## Use @@ -105,10 +105,13 @@ Caption is optional; use it to add context (payment method, split, category hint | `categories.md` | Fixed category list (14 categories) | | `schema.md` | Google Sheet column order (A–N) | | `config.example.json` | Template → copy to `config.json` (gitignored) | -| `scripts/fx_convert.py` | Currency → HKD via frankfurter.app | -| `scripts/append_row.py` | Writes one row to Google Sheets | -| `scripts/setup.md` | Admin setup guide + troubleshooting | -| `DEPLOY.md` | Step-by-step install guide for non-technical users | +| `scripts/fx_convert.py` | Currency → HKD via frankfurter.app | +| `scripts/append_row.py` | Writes one row to Google Sheets | +| `scripts/decrypt-key.sh` | Decrypts bundled SA key to `~/.config/gcp/` | +| `scripts/setup.md` | Admin setup guide + troubleshooting | +| `secrets/bookkeeping-sa.json.enc` | Team SA key, AES-256 encrypted (safe to commit) | +| `secrets/README.md` | How the encryption works + rotation procedures | +| `DEPLOY.md` | Step-by-step install guide for non-technical users | ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index eae0c27..1353711 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -16,7 +16,7 @@ ## 安装(用户端) -管理员(admin)会给你**两样东西**:一个 service-account JSON 密钥文件(如 `autoacct-sa.json`),以及一个 service-account 邮箱(形如 `autoacct@your-project.iam.gserviceaccount.com`)。没拿到先找管理员要。 +仓库里**自带了团队的 Google service-account 密钥**,已用 AES-256 加密。管理员只需给你**一样东西**:解密 passphrase(一般在团队密码管理器里)。 按下面 4 步操作,约 5 分钟。 @@ -26,18 +26,17 @@ ```bash git clone https://github.com/CharlesZhang2023/AutoACCT.git ~/.openclaw/workspace/skills/AutoACCT +cd ~/.openclaw/workspace/skills/AutoACCT pip install google-api-python-client google-auth ``` -### Step 2 — 把管理员发的 JSON 放到 `~/.config/gcp/` +### Step 2 — 解密内置的 service-account 密钥 ```bash -mkdir -p ~/.config/gcp -mv ~/Downloads/autoacct-sa.json ~/.config/gcp/autoacct-sa.json -chmod 600 ~/.config/gcp/autoacct-sa.json +bash scripts/decrypt-key.sh ``` -(`~/Downloads/autoacct-sa.json` 改成你实际保存文件的路径。) +会提示你输入 passphrase。成功后脚本会把解出的 JSON 写到 `~/.config/gcp/bookkeeping-sa.json`(权限 600),并打印 **service-account 邮箱** —— 复制下来,Step 3 要用。 ### Step 3 — 建你自己的 Google Sheet 并把它 share 给 service account @@ -48,7 +47,7 @@ chmod 600 ~/.config/gcp/autoacct-sa.json ``` Date Merchant Category Amount Currency Amount (HKD) FX Rate FX Date Payment Method Line Items Raw OCR Note Receipt Logged At ``` -5. 右上角 **Share** → 粘贴管理员给你的 **service-account 邮箱** → 权限选 **Editor** → **Send**("Notify people" 可以不勾) +5. 右上角 **Share** → 粘贴 Step 2 `decrypt-key.sh` 打印出来的 **service-account 邮箱** → 权限选 **Editor** → **Send**("Notify people" 可以不勾) 6. **从浏览器地址栏直接复制 sheet 的完整 URL**,类似: `https://docs.google.com/spreadsheets/d/1abc...xyz/edit#gid=0` (脚本会自动从 URL 里抽出 sheet ID,所以完整链接或裸 ID 都行。) @@ -66,7 +65,7 @@ cp config.example.json config.json { "sheet_id": "https://docs.google.com/spreadsheets/d/1abc...xyz/edit", "worksheet": "Sheet1", - "service_account_path": "~/.config/gcp/autoacct-sa.json", + "service_account_path": "~/.config/gcp/bookkeeping-sa.json", "hkd_fx_provider": "frankfurter" } ``` @@ -83,13 +82,14 @@ echo '{"date":"2026-04-20","merchant":"TEST","category":"Other","amount":1,"curr 遇到报错可以参考 [`scripts/setup.md`](scripts/setup.md) 的故障排查。 -## 管理员一次性配置(你做一遍,再把 JSON 发给用户) +## 管理员一次性配置 -用户能跑上面 4 步之前,**你(管理员)**先建好一个共享的 service account,把 JSON 发给用户。完整管理员指南见 [`scripts/setup.md`](scripts/setup.md),简版流程: +完整管理员指南见 [`scripts/setup.md`](scripts/setup.md),加密机制说明见 [`secrets/README.md`](secrets/README.md)。简版: 1. 建 GCP 项目 → 启用 Sheets API → 建 service account → 下载 JSON key -2. 通过安全渠道把 JSON 文件 + service-account 邮箱发给每个用户(1Password / Bitwarden / 加密邮件 ——**绝不能 commit 到 git**) -3. 让用户按上面 4 步装 +2. 用强随机 passphrase 加密 JSON,把 `secrets/bookkeeping-sa.json.enc` commit 进仓库(openssl 一行命令见 `secrets/README.md`) +3. 把 passphrase 存到团队密码管理器,告诉用户按上面 4 步装 +4. 成员离职时轮换 passphrase;passphrase 或解密后的 JSON 有泄露风险时,轮换底层 GCP key ## 使用 @@ -105,10 +105,13 @@ echo '{"date":"2026-04-20","merchant":"TEST","category":"Other","amount":1,"curr | `categories.md` | 固定的 14 个分类列表 | | `schema.md` | Google Sheet 列顺序(A–N) | | `config.example.json` | 配置模板 → 复制为 `config.json`(已 gitignore) | -| `scripts/fx_convert.py` | 原币种 → HKD 换算(frankfurter.app) | -| `scripts/append_row.py` | 向 Google Sheet 写入一行 | -| `scripts/setup.md` | 管理员配置指南 + 故障排查 | -| `DEPLOY.md` | 面向非技术用户的逐步安装指南(英文) | +| `scripts/fx_convert.py` | 原币种 → HKD 换算(frankfurter.app) | +| `scripts/append_row.py` | 向 Google Sheet 写入一行 | +| `scripts/decrypt-key.sh` | 解密内置 SA key 到 `~/.config/gcp/` | +| `scripts/setup.md` | 管理员配置指南 + 故障排查 | +| `secrets/bookkeeping-sa.json.enc` | 团队 SA key,AES-256 加密(可安全 commit) | +| `secrets/README.md` | 加密机制说明 + 轮换流程 | +| `DEPLOY.md` | 面向非技术用户的逐步安装指南(英文) | ## License diff --git a/scripts/decrypt-key.sh b/scripts/decrypt-key.sh new file mode 100755 index 0000000..6f0ce8d --- /dev/null +++ b/scripts/decrypt-key.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# decrypt-key.sh — decrypt the bundled service-account key into +# ~/.config/gcp/bookkeeping-sa.json. You'll be prompted for the passphrase +# (ask your admin; it's stored in the team password manager). +# +# Usage: +# bash scripts/decrypt-key.sh +# +# Idempotent: re-running overwrites the existing decrypted file. +set -euo pipefail + +REPO_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +SRC="$REPO_DIR/secrets/bookkeeping-sa.json.enc" +DEST_DIR="$HOME/.config/gcp" +DEST="$DEST_DIR/bookkeeping-sa.json" + +if [[ ! -f "$SRC" ]]; then + echo "error: encrypted key not found at $SRC" >&2 + exit 1 +fi + +mkdir -p "$DEST_DIR" + +# AES-256-CBC + PBKDF2 (100k iter) + salt. Passphrase read interactively. +openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -d -in "$SRC" -out "$DEST" + +chmod 600 "$DEST" +echo "Decrypted to $DEST" + +SA_EMAIL=$(python3 -c "import json; print(json.load(open('$DEST'))['client_email'])" 2>/dev/null || true) +if [[ -n "$SA_EMAIL" ]]; then + echo "" + echo "Service-account email: $SA_EMAIL" + echo "Next step: share your Google Sheet with this email (Editor)." +fi diff --git a/scripts/setup.md b/scripts/setup.md index 987a649..d04fb91 100644 --- a/scripts/setup.md +++ b/scripts/setup.md @@ -23,39 +23,65 @@ The shared-key model: you create **one** Google Cloud service account and distri 5. **Copy the service account's email** (e.g. `autoacct@.iam.gserviceaccount.com`). 6. Rename the downloaded file to `autoacct-sa.json` (recommended — DEPLOY.md assumes this name). -### A.2 — Distribute the key + email to users +### A.2 — Encrypt the JSON and commit it to the repo -The JSON is a private key. Use a secure channel: +Generate a strong random passphrase (48 chars; alphanumeric + `-_`): -| Channel | Verdict | -|---|---| -| **Password manager shared vault** (1Password, Bitwarden, Vaultwarden) | **Recommended.** Easy to revoke, no copies floating in inboxes. | -| Encrypted email / Signal / private DM | OK for small teams. | -| Cloud drive with strict per-user ACLs | OK if your org already uses one. | -| **Plain email** | Do not. | -| **Git repo (even private)** | Do not — `.gitignore` already excludes `*-sa.json`. | +```bash +python3 -c "import secrets, string; print(''.join(secrets.choice(string.ascii_letters + string.digits + '_-') for _ in range(48)))" +``` + +Encrypt with openssl (AES-256-CBC + PBKDF2, 100k iterations): + +```bash +PASSPHRASE='' openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -salt \ + -pass env:PASSPHRASE \ + -in ~/Downloads/.json \ + -out secrets/bookkeeping-sa.json.enc + +git add secrets/bookkeeping-sa.json.enc +git commit -m "secrets: add encrypted SA key" +git push +``` + +Then **store the passphrase in your team password manager** (1Password / Bitwarden shared vault). The passphrase is the only out-of-band thing your users need. + +Move the plaintext key out of the repo dir and protect it on your own machine: + +```bash +mv ~/Downloads/.json ~/.config/gcp/bookkeeping-sa.json +chmod 600 ~/.config/gcp/bookkeeping-sa.json +``` Tell each user: -- The JSON file (attach / share) -- The service-account email (so they know who to share their sheet with) -- Pointer to [`DEPLOY.md`](../DEPLOY.md) if they want hand-holding, or [`README.md`](../README.md) if they're comfortable with the terminal +- A pointer to the repo (`git clone https://github.com/CharlesZhang2023/AutoACCT.git`) +- The passphrase (via 1Password share — **never via plain email / chat**) +- Link to [`DEPLOY.md`](../DEPLOY.md) for hand-holding or [`README.md`](../README.md) if they're comfortable in the terminal ### A.3 — Verify your own install first -Before sending the JSON to anyone, run through the user-side install yourself (Steps 1–11 of `DEPLOY.md`) to confirm everything works end-to-end. It's also a chance to catch any GCP-side misconfiguration before users hit it. +Before announcing it to anyone, run through the user-side install yourself (`DEPLOY.md` Parts 1–4) on a clean directory to confirm `git clone` + `decrypt-key.sh` + sheet creation + smoke test all work end-to-end. Catches any GCP-side misconfiguration before users hit it. -### A.4 — Key rotation (when you need to) +### A.4 — Rotation -Rotate the JSON key when: -- A user leaves the team (so you stop trusting their copy of the key) -- You suspect a leak -- Every 6–12 months as routine hygiene +**Passphrase rotation** (when a user leaves, or every 6–12 months): +1. Generate a new passphrase as in A.2. +2. Decrypt with the old passphrase, re-encrypt with the new one: + ```bash + openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -d \ + -in secrets/bookkeeping-sa.json.enc -out /tmp/sa.json + PASSPHRASE='' openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -salt \ + -pass env:PASSPHRASE -in /tmp/sa.json -out secrets/bookkeeping-sa.json.enc + shred -u /tmp/sa.json # rm -P on macOS + ``` +3. Commit + push the new `.enc`. Update the passphrase entry in the team password manager. Users `git pull` + re-run `decrypt-key.sh`. -To rotate: -1. In GCP Console → Service Accounts → keys → **Add Key** (creates a new one) → download. -2. **Delete the old key** in the same panel. (After deletion, any existing copies stop working.) -3. Re-distribute the new JSON to all current users via your secure channel. -4. Users replace their `~/.config/gcp/autoacct-sa.json` with the new file. No other changes needed — `config.json`, sheet sharing, etc. all stay intact. +**Underlying GCP key rotation** (when the passphrase leaks, or a user with a decrypted copy leaves): +- Passphrase rotation alone is **not enough** if someone already has the decrypted JSON on their machine — they retain a working credential. +- GCP Console → Service Accounts → Keys → **Add Key** (download new) → **Delete old**. The deleted key stops working immediately, globally. +- Re-encrypt the new JSON (A.2 flow), commit, push. Users pull + decrypt. + +See [`secrets/README.md`](../secrets/README.md) for the same procedures with copy-pasteable commands. --- @@ -70,13 +96,16 @@ The `worksheet` value in `config.json` doesn't match the actual tab name. Most c ### `HTTP 404` / `Requested entity was not found` `sheet_id` in `config.json` is wrong. Tell user to re-copy the long string from `/d/.../edit` in their browser's URL bar. -### `FileNotFoundError ... autoacct-sa.json` -The JSON file isn't where `config.json` expects. Common causes: -- User saved with a different filename (e.g. `autoacct-project-12345.json`) and never renamed it. -- User skipped the `mv` step and the file is still in Downloads. +### `FileNotFoundError ... bookkeeping-sa.json` +User skipped `bash scripts/decrypt-key.sh`, or decryption failed and they didn't notice. Have them re-run it and confirm the success line `Decrypted to ~/.config/gcp/bookkeeping-sa.json`. Run `ls -la ~/.config/gcp/` to check. +### `bad decrypt` from openssl +Wrong passphrase. Most common causes: +- They pasted the wrong entry from the password manager. +- The passphrase has been rotated since last time. Have them check the password manager for the latest version. + ### `ImportError: No module named 'googleapiclient'` Python deps not installed. Run `pip install google-api-python-client google-auth`. If `pip` is missing, try `pip3` or `python3 -m pip install ...`. diff --git a/secrets/README.md b/secrets/README.md new file mode 100644 index 0000000..e5fcb18 --- /dev/null +++ b/secrets/README.md @@ -0,0 +1,81 @@ +# secrets/ + +This directory holds the team's shared Google Cloud service-account key, +encrypted with **AES-256-CBC + PBKDF2 (100k iterations)** using a single +team passphrase. + +| File | What it is | +|---|---| +| `bookkeeping-sa.json.enc` | The SA private key, encrypted. Safe to commit. | + +## To decrypt (end users) + +```bash +bash scripts/decrypt-key.sh +``` + +You'll be prompted for the team passphrase. Ask your admin if you don't have +it — it's stored in the team password manager (1Password / Bitwarden), never +in this repo or in chat history. + +On success the decrypted JSON lands at `~/.config/gcp/bookkeeping-sa.json` +with mode 600, and the script prints the service-account email you need to +share your Sheet with. + +## To rotate the passphrase (admin) + +```bash +# Decrypt with old passphrase +openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -d \ + -in secrets/bookkeeping-sa.json.enc \ + -out /tmp/sa.json + +# Re-encrypt with new passphrase +openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -salt \ + -in /tmp/sa.json \ + -out secrets/bookkeeping-sa.json.enc + +shred -u /tmp/sa.json # or: rm -P on macOS + +git add secrets/bookkeeping-sa.json.enc +git commit -m "secrets: rotate passphrase" +git push +``` + +Then update the passphrase in the team password manager and notify users. + +## To rotate the SA key itself (admin) + +When the underlying GCP key needs replacement (key leak, periodic rotation, +team member departure): + +1. In GCP Console → Service Accounts → `bookkeeping@autoacct...` → Keys → **Add Key** → JSON → download. +2. **Delete the old key** in the same panel. +3. Re-encrypt: + ```bash + openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -salt \ + -in ~/Downloads/.json \ + -out secrets/bookkeeping-sa.json.enc + ``` +4. `git add` + `commit` + `push`. +5. Tell users to `git pull` and re-run `bash scripts/decrypt-key.sh`. + +## Why this works + +- AES-256 + PBKDF2 with 100k iterations makes brute-forcing infeasible for a + strong (>32 char) random passphrase, even with the ciphertext public. +- Encrypted blob in git history means anyone with the passphrase can install + with just `git clone` + run the decrypt script — no out-of-band file + transfer needed. +- Passphrase distribution is the only remaining out-of-band step; that + belongs in a password manager. + +## Threats this does NOT protect against + +- Anyone with the passphrase can read/write **all team sheets** shared with + the service account. The passphrase is the team's collective trust boundary. +- If the passphrase leaks publicly: rotate both passphrase **and** the + underlying GCP key (see above). Don't just rotate the passphrase. +- A user who has previously decrypted the key has a plaintext copy on their + machine. Removing them from the team requires rotating the GCP key (the + passphrase rotation is not enough — they already have a decrypted JSON). diff --git a/secrets/bookkeeping-sa.json.enc b/secrets/bookkeeping-sa.json.enc new file mode 100644 index 0000000000000000000000000000000000000000..16b8ab29d10a9933064034edca54041c3537d816 GIT binary patch literal 2368 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 literal 0 HcmV?d00001