Skip to content

Commit eb41525

Browse files
committed
refactor(ci): simplify to whole-file encryption
Replace per-value encryption with single-blob encryption: - Encrypt entire secrets file as one payload - Decrypt once, then parse with dotenv - Simpler, faster, smaller encrypted file
1 parent f531579 commit eb41525

File tree

3 files changed

+32
-153
lines changed

3 files changed

+32
-153
lines changed

.github/secrets.env.encrypted

Lines changed: 10 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,12 @@
11
{
2-
"CS_DEFAULT_KEYSET_ID": {
3-
"k": "ct",
4-
"c": "mBbM0?Nl*Qu~k~!?IMwm(qwtWHu>HeEmn8T4a(l7Q2ep~@2y2p^Y1vQS{!GRU-)lG{ysSLcEnRYnH3F}iR6+(smAqKtl@XWAfLV&!nG*uPP}T~-r&G*#J3lNz%2t5ZP7a`*ZI=R*`;=2Y;|SC5Hwu<&vtJ>^t3onC{{VkhX",
5-
"ob": null,
6-
"bf": null,
7-
"hm": null,
8-
"i": {
9-
"t": "ci_secrets",
10-
"c": "value"
11-
},
12-
"v": 2
13-
},
14-
"CS_TENANT_KEYSET_ID_1": {
15-
"k": "ct",
16-
"c": "mBbKa^tk|l%%@u_arbG%UnJ|qHjRA(OZ~O~aEFq|Uc*O6?2pFqI4tsmify9c35DEDw5q)}F>^yg<kB^+Szu&AFA`3pS@2=RAYOMmRZ$<5hkwnC-*By{kj}=Rq^n`0?h~)(Y#zco%B6N;Y;|SC5Hwu<&vtJ>^t3onC{{VkhX",
17-
"ob": null,
18-
"bf": null,
19-
"hm": null,
20-
"i": {
21-
"t": "ci_secrets",
22-
"c": "value"
23-
},
24-
"v": 2
25-
},
26-
"CS_TENANT_KEYSET_ID_2": {
27-
"k": "ct",
28-
"c": "mBbLD*8EXQH6XEOK;w-geHlW;Hcz#WkN6K1cJ)L8Il`dD=s-(DNfCehdrxUhA=?^ltY2~a>ltl>eOb}jO|b-_#B9kfh2@RJAn@8tcumPN3B<gHPD#0XIUqX;A9X4~?g<6EAM1V<i=}p9Y;|SC5Hwu<&vtJ>^t3onC{{VkhX",
29-
"ob": null,
30-
"bf": null,
31-
"hm": null,
32-
"i": {
33-
"t": "ci_secrets",
34-
"c": "value"
35-
},
36-
"v": 2
37-
},
38-
"CS_TENANT_KEYSET_ID_3": {
39-
"k": "ct",
40-
"c": "mBbM86sosp5K0i7^KEo0`t7I0HXCW)>~VFO&LzfWcEs~u?jfrxx~{6@WR)}XfQBFSj3FDJ!DpHH>*W?X3)L;xfUNGCTmGWNAZ4;VmI-6*9I7}bl;);r{9C+95IEyT(n9|(mdx|EyQOwvY;|SC5Hwu<&vtJ>^t3onC{{VkhX",
41-
"ob": null,
42-
"bf": null,
43-
"hm": null,
44-
"i": {
45-
"t": "ci_secrets",
46-
"c": "value"
47-
},
48-
"v": 2
49-
},
50-
"CS_TENANT_KEYSET_NAME_1": {
51-
"k": "ct",
52-
"c": "mBbJ+(r74pg5s5`J;<6!;3&n!7qFLrdkua!vBgow7y#K5HGW~ayhSXH#2`0o!W21kgS_p^5HtcI%n39_NeYh-0@8ppT$?~FLY}2|VQh6}#1J%G{m*u9K=iaYPbgM7%ZC",
53-
"ob": null,
54-
"bf": null,
55-
"hm": null,
56-
"i": {
57-
"t": "ci_secrets",
58-
"c": "value"
59-
},
60-
"v": 2
61-
},
62-
"CS_TENANT_KEYSET_NAME_2": {
63-
"k": "ct",
64-
"c": "mBbJTqjoEaJ-CTMWn{Xk(gsY#7Yu1{4D?y85h;F%$M0wwF4J$$oh{29#31%gYxd^m5@QCp&&_m5v;-{C(+w&IFi(EQy{IYeUzVkIVQh6}#1J%G{m*u9K=iaYPbgM7%ZC",
65-
"ob": null,
66-
"bf": null,
67-
"hm": null,
68-
"i": {
69-
"t": "ci_secrets",
70-
"c": "value"
71-
},
72-
"v": 2
73-
},
74-
"CS_TENANT_KEYSET_NAME_3": {
75-
"k": "ct",
76-
"c": "mBbJa8JfC8cf;O(@6B(CT+-;o7g*nt^_G8wfwl^!USRDMv2)t`&x-2W#31i!G%X)KSwL1P-_O|!FD?N`{m?8>Cp+R1!^~d5yIG}nVQh6}#1J%G{m*u9K=iaYPbgM7%ZC",
77-
"ob": null,
78-
"bf": null,
79-
"hm": null,
80-
"i": {
81-
"t": "ci_secrets",
82-
"c": "value"
83-
},
84-
"v": 2
85-
},
86-
"DOCKER_HUB_USERNAME": {
87-
"k": "ct",
88-
"c": "mBbKFBRV%>F|ABa9GSR+5dsjzA*Xjz65=6Opeh)SCg=TpK2bNREA5BaLw4;<P#C&)u$aUk0k`!?^!e;1*iD3SFHJE9jb^LfO6>oPYF{#`Q=G|+rFLO#b!Eg5G+h19c5guRv^Y;FRyoUu",
89-
"ob": null,
90-
"bf": null,
91-
"hm": null,
92-
"i": {
93-
"t": "ci_secrets",
94-
"c": "value"
95-
},
96-
"v": 2
97-
},
98-
"DOCKER_HUB_PASSWORD": {
99-
"k": "ct",
100-
"c": "mBbJN?&s^Jst!Y@DOM$^8w{|-IvwJgJ5RK$$!h6!&)@=iTbz!tLFvp-JQKR$nd9i-bGm`I!bl^WPa_Ts#fX;<C|t>jGdKtp4t<5hAl7r7XvT##hIQ<u7ZsVcW}5K4b3a%Nk?i_EEs9NAgr#<2Y;|SC5Hwu<&vtJ>^t3onC{{VkhX",
101-
"ob": null,
102-
"bf": null,
103-
"hm": null,
104-
"i": {
105-
"t": "ci_secrets",
106-
"c": "value"
107-
},
108-
"v": 2
109-
},
110-
"MULTITUDES_ACCESS_TOKEN": {
111-
"k": "ct",
112-
"c": "mBbMB3Qx9={MGQF%x>r6239u30d{5koTb&8J&>*7xLp;n!_q}|Cfu7z_Cuo(uYCH8tFxS+e`Ts_$GHOCTv$x_Y?($V<IvvDVriKjkQ=P`F+04q8%A4@=z4Ss$##lq4FI8~0NY7cLBy!bR$Er&%p6#31g`)f32lAE;%VL~tL1!ReslE$2zMx{r!nXt#=9kojKCkVBmUZU75Vl}e?Ry@vah4%Qw=8<%NKn;jJEA;xf4nIZazqTc?0+7^d{l<Us0Z+UEQD(c!>~#f%qaO@le7zqJBKfo1xwNMHSCryogwW?T1M+WNNH2`QP&coXm7*vL?{v38>j6h$SMgGg<hLSd4DdEf|`=s;LQV6yrsg!mrU;$L-=QSfxg}ZKgi1gm%T+gb2Bc>M$T)hH^}97=J~?Uh}vGN@t~2yUi?CWi{Q!tSt;CI8@L6^hV!`!9ACFmr4>^GnWbN`ul!nBc2026(i37{c%{c&nyI?pB^VvAVXFewEKHuF(70R#2_j}fWJu4MW4a+wcnx5NF*Yls8(sRP$Yl*3m{Yb4$h@^VQh6}#1J%G{m*u9K=iaYPbgM7%ZC",
113-
"ob": null,
114-
"bf": null,
115-
"hm": null,
116-
"i": {
117-
"t": "ci_secrets",
118-
"c": "value"
119-
},
120-
"v": 2
121-
}
2+
"k": "ct",
3+
"c": "mBbKAoNc(qCyj1dv~cffmKx^812qC-mMd8i2N)OP5M3E)uk=Yn{4kUIe-v{dQ+cJ=D+$HeAONGotT3y<*<#>k_o$T{KS;Nz!;7fwmbuVYJ_hzcdybtS2&z(C1CSv%;uS__hclT8wUKkQW3GqR70E!W>MB+5zq@1F>rqBLmw(G!fK0Lhc7>=eiUA9BSOa>5{3AIvBpWg4&hw&OlC(uk&H%$h%>tZ`QG)sMah4!fB+ry_YC0lHb>b^T*n!!%YI&4~rZTc%6~TRETmlc<^FH!HhiK0>=bI6HV(V!&$m5-T=E$ziHSfEPSP5a1=eskcjrWx5@HRq@zmMkBB7dq*Wy+Ts-<oBJO|g^I{H)-$RQcM_RoE!=qRGbx$|IC#6}e_>4kCWHu&gN>rJ!Piwt=;u%;IAawIKXxxX1Z)nSV+|T<uJm<oPB9_+<rfjNUeJP#moM_=ilEn&xI#C!mb_#oGY15%r9O__UU1cguTi)rbwJXu2o<w`oX*Kms?R*ZY9l+{V6Xq`CD?I`1a)i_<`RdxgK)!(#(n2eOuq^<rUiGPCQ;4O`lLBlz1de7utIR0*Wo*_$MeVOy>_KB1}u>Y|Dkp}i32WPAW3$R|vRky(Vl;<$Vb4&CLI&JdW{MD=R$sT)#1g<G{}WL{OOOQ8hJNw>}Z5>+7*A;V3dZ}9Qy+Ghg{70uIo@$=pweSy;(DJHMlYjvP_T70n`BCq_hP9`n;<r4Al;gO<&&ihjdPw})wDpvqHwRR`@EfT4gr@Ya4R)yW!L1o&7ef=44;I-P2(^_{@gm8b4)^$1FFH(Z3o@aAJ-#>pph=Z4dD4%kfn)~z&M}}cbmJy8Sk!Tz)W8$Ju9T0v<B{}){MVblB0Oq-pT{&d{&{;dZ$G>44m$14hXjTM$<gO3;+chFH9`yS)s_5pI<Yn>B6O^ohdxKeUV(nvnmeni;#)m<6Veou9#DPak%mzbqA;tZ5Y2S2AOS@Lq97|$J$thFlY~`vY=6s=>>`pDg71Q_XSX{x~#5Ep;;-;)iId*3@SDNf|u)g&bf(P*Yjol+9l*gG%6bHj}T2f1IX0DXPAkYy0y^tZcM*Wcw_PHZQNC|ec!FFR-8TOPB@`E*k>ZNvJY;|SC5Hwu<&vtJ>^t3onC{{VkhX",
4+
"ob": null,
5+
"bf": null,
6+
"hm": null,
7+
"i": {
8+
"t": "ci_secrets",
9+
"c": "value"
10+
},
11+
"v": 2
12212
}

scripts/decrypt-secrets.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { protect, csTable, csColumn, Encrypted } from "@cipherstash/protect";
22
import * as fs from "fs";
33
import * as path from "path";
4+
import * as dotenv from "dotenv";
45

56
const schema = csTable("ci_secrets", {
67
value: csColumn("value"),
78
});
89

9-
type EncryptedSecrets = Record<string, Encrypted>;
10-
1110
async function main(): Promise<void> {
12-
// Find .env.encrypted relative to repo root (one level up from scripts/)
1311
const repoRoot = path.resolve(import.meta.dirname, "..");
1412
const encryptedPath = path.join(repoRoot, ".github", "secrets.env.encrypted");
1513

@@ -19,33 +17,31 @@ async function main(): Promise<void> {
1917
}
2018

2119
const client = await protect({ schemas: [schema] });
22-
const encrypted: EncryptedSecrets = JSON.parse(
20+
const encrypted: Encrypted = JSON.parse(
2321
fs.readFileSync(encryptedPath, "utf-8")
2422
);
2523

24+
const result = await client.decrypt(encrypted);
25+
if (result.failure) {
26+
console.error(`Failed to decrypt: ${result.failure.message}`);
27+
process.exit(1);
28+
}
29+
30+
const env = dotenv.parse(String(result.data));
2631
const githubEnvPath = process.env.GITHUB_ENV;
2732
const isCI = !!githubEnvPath;
2833

29-
for (const [key, payload] of Object.entries(encrypted)) {
30-
const result = await client.decrypt(payload);
31-
if (result.failure) {
32-
console.error(`Failed to decrypt ${key}: ${result.failure.message}`);
33-
process.exit(1);
34-
}
35-
const value = String(result.data);
36-
34+
for (const [key, value] of Object.entries(env)) {
3735
if (isCI) {
38-
// GitHub Actions: use heredoc syntax for multiline values
3936
const delimiter = `EOF_${key}_${Date.now()}`;
4037
fs.appendFileSync(githubEnvPath, `${key}<<${delimiter}\n${value}\n${delimiter}\n`);
4138
} else {
42-
// Local: simple KEY=value output (for testing)
4339
console.log(`${key}=${value}`);
4440
}
4541
}
4642

4743
if (isCI) {
48-
console.error(`Decrypted ${Object.keys(encrypted).length} secrets to $GITHUB_ENV`);
44+
console.error(`Decrypted ${Object.keys(env).length} secrets to $GITHUB_ENV`);
4945
}
5046
}
5147

scripts/encrypt-secrets.ts

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { protect, csTable, csColumn } from "@cipherstash/protect";
22
import * as fs from "fs";
33
import * as path from "path";
4-
import * as dotenv from "dotenv";
54

65
const schema = csTable("ci_secrets", {
76
value: csColumn("value"),
@@ -14,30 +13,24 @@ async function main(): Promise<void> {
1413

1514
if (!fs.existsSync(plaintextPath)) {
1615
console.error(`Error: ${plaintextPath} not found`);
17-
console.error("Create this file with your plaintext secrets (KEY=value format)");
1816
process.exit(1);
1917
}
2018

21-
const env = dotenv.parse(fs.readFileSync(plaintextPath));
19+
const fileContent = fs.readFileSync(plaintextPath, "utf-8");
2220
const client = await protect({ schemas: [schema] });
2321

24-
const encrypted: Record<string, unknown> = {};
25-
26-
for (const [key, value] of Object.entries(env)) {
27-
const result = await client.encrypt(value, {
28-
table: schema,
29-
column: schema.value,
30-
});
31-
if (result.failure) {
32-
console.error(`Failed to encrypt ${key}: ${result.failure.message}`);
33-
process.exit(1);
34-
}
35-
encrypted[key] = result.data;
36-
console.error(`Encrypted: ${key}`);
22+
const result = await client.encrypt(fileContent, {
23+
table: schema,
24+
column: schema.value,
25+
});
26+
27+
if (result.failure) {
28+
console.error(`Failed to encrypt: ${result.failure.message}`);
29+
process.exit(1);
3730
}
3831

39-
fs.writeFileSync(encryptedPath, JSON.stringify(encrypted, null, 2) + "\n");
40-
console.error(`\nWrote ${Object.keys(encrypted).length} secrets to ${encryptedPath}`);
32+
fs.writeFileSync(encryptedPath, JSON.stringify(result.data, null, 2) + "\n");
33+
console.error(`Encrypted secrets file to ${encryptedPath}`);
4134
}
4235

4336
main().catch((err) => {

0 commit comments

Comments
 (0)