mirror of
https://github.com/jparkerweb/plan2code.git
synced 2026-07-21 10:23:23 -07:00
Add password-protected sync-repo skill with encrypted body
Adds a project-local /sync-repo skill whose real instructions are encrypted at rest (AES-256-GCM + scrypt) in sync-repo.enc and only revealed in-session after the user supplies the correct passphrase. Plaintext SYNC.md is gitignored and never committed; *.enc is marked binary to protect the ciphertext bytes. AI Assisted
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: sync-repo
|
||||
description: "Run the encrypted, password-protected repository sync workflow for this project. The real instructions are stored encrypted at rest and are only revealed in-session after you supply the correct password. Invoke explicitly with /sync-repo."
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# sync-repo (password-protected)
|
||||
|
||||
The real instructions for this skill are encrypted at rest in `sync-repo.enc`
|
||||
and are NOT readable without the password. Do not guess, reconstruct, or invent
|
||||
the workflow. Follow this launcher exactly.
|
||||
|
||||
## What you (the agent) must do
|
||||
|
||||
1. **Ask the user for the password.** Request the decryption passphrase (via
|
||||
AskUserQuestion or a plain prompt). Do NOT proceed without it. Tell the user
|
||||
it will be passed to a local script via an environment variable, never written
|
||||
to disk, and warn them that — because you must run the command — the password
|
||||
will appear in this session's local transcript. (It never enters the repo.)
|
||||
|
||||
2. **Decrypt to STDOUT only.** Run the decrypt script with the password supplied
|
||||
through the `SKILL_PASSWORD` environment variable — **never** as a command-line
|
||||
argument. From the repo root:
|
||||
|
||||
- **Windows PowerShell:**
|
||||
```powershell
|
||||
$env:SKILL_PASSWORD='<password the user gave you>'; node .\.claude\skills\sync-repo\decrypt.mjs .\.claude\skills\sync-repo\sync-repo.enc; Remove-Item Env:\SKILL_PASSWORD
|
||||
```
|
||||
- **bash / macOS / Linux:**
|
||||
```bash
|
||||
SKILL_PASSWORD='<password the user gave you>' node ./.claude/skills/sync-repo/decrypt.mjs ./.claude/skills/sync-repo/sync-repo.enc
|
||||
```
|
||||
|
||||
3. **Handle the result.**
|
||||
- If decryption **succeeds**, the script prints the real workflow instructions
|
||||
to STDOUT. Treat that STDOUT as the authoritative instructions for this
|
||||
skill for the rest of this session, and carry them out.
|
||||
- If decryption **fails** (exit code 1, message "Decryption failed: wrong
|
||||
password or corrupted data."), the password was wrong or the file is
|
||||
corrupt. Tell the user, ask them to re-enter the password, and retry. Do
|
||||
NOT attempt to reconstruct the instructions from anything else.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Never write the decrypted plaintext to a file.** Read it from STDOUT only.
|
||||
`decrypt.mjs` intentionally has no file-output mode.
|
||||
- **Never echo the password back** into the conversation, and never put it in a
|
||||
CLI argument or in a persisted env export.
|
||||
- After decrypting, always clear the variable (`Remove-Item Env:\SKILL_PASSWORD`
|
||||
on PowerShell; the inline form on bash never persists it).
|
||||
|
||||
## Security note (be honest with the user)
|
||||
|
||||
This protects the workflow body **only at rest in the repository**. Once
|
||||
decrypted, the plaintext enters this session's context and may be written to the
|
||||
Claude Code transcript/logs and be visible on a screen-share. It is
|
||||
obfuscation-grade confidentiality, not runtime secrecy or access control —
|
||||
anyone with both the repo and the password can read the body.
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env node
|
||||
// decrypt.mjs — verifies GCM auth tag, prints plaintext to STDOUT only.
|
||||
// Password from $SKILL_PASSWORD. Exits 1 on wrong password / tamper, leaking nothing.
|
||||
// Usage: SKILL_PASSWORD=... node decrypt.mjs <file.enc>
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { scryptSync, createDecipheriv } from 'node:crypto';
|
||||
|
||||
const MAGIC = Buffer.from('SENC', 'ascii');
|
||||
const VERSION = 0x01;
|
||||
const SCRYPT = { N: 1 << 17, r: 8, p: 1, maxmem: 256 * 1024 * 1024 };
|
||||
const KEYLEN = 32, SALTLEN = 16, IVLEN = 12, TAGLEN = 16;
|
||||
const HEADER = MAGIC.length + 1; // 5
|
||||
|
||||
const password = process.env.SKILL_PASSWORD;
|
||||
if (!password) { console.error('ERROR: set SKILL_PASSWORD env var.'); process.exit(2); }
|
||||
|
||||
const encPath = process.argv[2];
|
||||
if (!encPath) { console.error('Usage: node decrypt.mjs <file.enc>'); process.exit(2); }
|
||||
|
||||
try {
|
||||
const blob = Buffer.from(readFileSync(encPath, 'utf8').trim(), 'base64');
|
||||
if (blob.length < HEADER + SALTLEN + IVLEN + TAGLEN) throw new Error('truncated');
|
||||
if (!blob.subarray(0, MAGIC.length).equals(MAGIC)) throw new Error('bad magic');
|
||||
if (blob[MAGIC.length] !== VERSION) throw new Error('unsupported version');
|
||||
|
||||
let off = HEADER;
|
||||
const salt = blob.subarray(off, off += SALTLEN);
|
||||
const iv = blob.subarray(off, off += IVLEN);
|
||||
const authTag = blob.subarray(off, off += TAGLEN);
|
||||
const ciphertext = blob.subarray(off);
|
||||
|
||||
const key = scryptSync(password, salt, KEYLEN, SCRYPT);
|
||||
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
// final() throws here if the tag does not verify (wrong password or tampering).
|
||||
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
||||
process.stdout.write(plaintext); // STDOUT only — never written to disk.
|
||||
} catch (err) {
|
||||
// Generic message: do not echo crypto internals or any plaintext.
|
||||
console.error('Decryption failed: wrong password or corrupted data.');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
// encrypt.mjs — AES-256-GCM + scrypt. Password from $SKILL_PASSWORD (never argv).
|
||||
// Usage: SKILL_PASSWORD=... node encrypt.mjs <plaintextFile|-> <outFile.enc>
|
||||
// (pass '-' or omit the input path to read plaintext from STDIN)
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { scryptSync, randomBytes, createCipheriv } from 'node:crypto';
|
||||
|
||||
const MAGIC = Buffer.from('SENC', 'ascii');
|
||||
const VERSION = 0x01;
|
||||
const SCRYPT = { N: 1 << 17, r: 8, p: 1, maxmem: 256 * 1024 * 1024 };
|
||||
const KEYLEN = 32, SALTLEN = 16, IVLEN = 12;
|
||||
|
||||
const password = process.env.SKILL_PASSWORD;
|
||||
if (!password) { console.error('ERROR: set SKILL_PASSWORD env var.'); process.exit(2); }
|
||||
|
||||
const inPath = process.argv[2];
|
||||
const outPath = process.argv[3];
|
||||
if (!outPath) { console.error('Usage: node encrypt.mjs <plaintextFile|-> <outFile.enc>'); process.exit(2); }
|
||||
|
||||
// readFileSync(0) reads STDIN; use it when no input file (or '-') is given.
|
||||
const plaintext = (!inPath || inPath === '-') ? readFileSync(0) : readFileSync(inPath);
|
||||
|
||||
const salt = randomBytes(SALTLEN);
|
||||
const iv = randomBytes(IVLEN);
|
||||
const key = scryptSync(password, salt, KEYLEN, SCRYPT);
|
||||
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
||||
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
||||
const authTag = cipher.getAuthTag(); // 16 bytes
|
||||
|
||||
const blob = Buffer.concat([MAGIC, Buffer.from([VERSION]), salt, iv, authTag, ciphertext]);
|
||||
writeFileSync(outPath, blob.toString('base64') + '\n');
|
||||
console.error(`Wrote ${outPath} (${blob.length} raw bytes, base64-encoded).`);
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1,4 @@
|
||||
# The encrypted sync-repo blob is base64 text but must never have its bytes
|
||||
# altered by line-ending normalization. Treat it as binary so autocrlf/eol
|
||||
# settings can never corrupt the ciphertext.
|
||||
*.enc binary
|
||||
Reference in New Issue
Block a user