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) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 11:24:48 +08:00
parent d9707aeba7
commit 56003b7f69
7 changed files with 254 additions and 95 deletions

View File

@@ -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 59 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.

View File

@@ -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 (AN) |
| `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

View File

@@ -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. 成员离职时轮换 passphrasepassphrase 或解密后的 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 列顺序AN |
| `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 keyAES-256 加密(可安全 commit |
| `secrets/README.md` | 加密机制说明 + 轮换流程 |
| `DEPLOY.md` | 面向非技术用户的逐步安装指南(英文) |
## License

35
scripts/decrypt-key.sh Executable file
View File

@@ -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

View File

@@ -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@<project>.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='<paste-passphrase-here>' openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -salt \
-pass env:PASSPHRASE \
-in ~/Downloads/<downloaded-key>.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/<downloaded-key>.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 111 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 14) 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 612 months as routine hygiene
**Passphrase rotation** (when a user leaves, or every 612 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='<new>' 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 ...`.

81
secrets/README.md Normal file
View File

@@ -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/<new-key>.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).

Binary file not shown.