gogcli/internal/backup
Peter Steinberger f26af3adba
feat(safety): add baked safety profiles (#536)
* feat(safety): add baked safety profiles

Co-authored-by: Drew Burchfield <1084679+drewburchfield@users.noreply.github.com>

* fix(safety): narrow readonly profile parent allows

* fix(safety): verify basename safe-build outputs

* fix(backup): promote Gmail checkpoints into final manifest

* docs(safety): explain baked safety profiles

* feat(safety): filter profiled help and schema

* fix(safety): avoid help filter shadow warnings

* fix(backup): make plaintext export resilient

* docs(changelog): mention safety help filtering

* fix(backup): satisfy export lint checks

---------

Co-authored-by: Drew Burchfield <1084679+drewburchfield@users.noreply.github.com>
2026-04-29 03:35:18 +01:00
..
async_push.go feat(backup): push gmail checkpoints asynchronously 2026-04-28 04:52:13 +01:00
backup_test.go feat(safety): add baked safety profiles (#536) 2026-04-29 03:35:18 +01:00
backup.go feat(safety): add baked safety profiles (#536) 2026-04-29 03:35:18 +01:00
config.go feat(backup): add markdown Gmail export 2026-04-28 07:29:27 +01:00
crypto.go perf(backup): stream gmail backup shards 2026-04-27 13:13:52 +01:00
git.go feat(backup): push gmail checkpoints asynchronously 2026-04-28 04:52:13 +01:00
read.go feat(safety): add baked safety profiles (#536) 2026-04-29 03:35:18 +01:00
readme.go feat(backup): add plaintext read and export commands 2026-04-27 10:15:26 +01:00

package backup

import (
	"fmt"
	"os"
	"path/filepath"
)

func writeBackupReadme(repo string) error {
	path := filepath.Join(repo, "README.md")
	if _, err := os.Stat(path); err == nil {
		return nil
	}

	const body = `# backup-gog

Encrypted Git backup for Google account data exported by gog.

This repository is written by ` + "`gog backup push`" + `. It is safe to keep on
GitHub because service payloads are encrypted before Git sees them.

## Layout

` + "```text" + `
README.md
manifest.json
data/<service>/<account-hash>/...
` + "```" + `

` + "`manifest.json`" + ` is cleartext and contains format version, export time,
public age recipients, service names, account hashes, shard paths, row counts,
encrypted byte sizes, and plaintext hashes used for verification. Email bodies,
subjects, senders, Drive filenames, contacts, event titles, and other private
Google data stay inside encrypted ` + "`*.jsonl.gz.age`" + ` shards.

## Security Model

Shard contents are deterministic JSONL, gzip-compressed with a fixed timestamp,
and encrypted with age for every configured public recipient. The local
` + "`~/.gog/age.key`" + ` identity is required to decrypt.

Git can still see manifest metadata: export time, public recipients, service
names, account hashes, shard paths, encrypted byte sizes, plaintext shard
hashes, backup cadence, and which encrypted shards changed. Git cannot read
Google content without an age identity.

Anyone who can push to this repository can replace encrypted backup data with
different data encrypted to your public recipient. Keep repository write access
restricted and review unexpected backup commits. If an age identity is
compromised, remove its public recipient and push a new backup; old Git history
may still contain shards decryptable by the compromised key.

## Push

` + "```bash" + `
gog backup push --services gmail
` + "```" + `

The command pulls/rebases this checkout, exports selected Google services,
writes encrypted shards, updates the manifest, commits, and pushes this
repository.

## Verify

` + "```bash" + `
gog backup verify
` + "```" + `

` + "`verify`" + ` decrypts every shard with the local age identity and verifies the
manifest hashes and row counts. It does not restore or write Google data.

## Recovery

Install gog, clone this repo to the path in ` + "`~/.gog/backup.json`" + `,
restore the local age identity file, then run:

` + "```bash" + `
gog backup verify
` + "```" + `

Do not commit the age identity. Only public ` + "`age1...`" + ` recipients belong in
config; ` + "`AGE-SECRET-KEY-...`" + ` values must stay local or in a password manager.
`
	if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
		return fmt.Errorf("write backup readme: %w", err)
	}

	return nil
}