Skip to content

Commit 07307df

Browse files
committed
update website, submit pubkey
1 parent 81c7264 commit 07307df

4 files changed

Lines changed: 157 additions & 98 deletions

File tree

website/src/app/favicon.ico

-25.3 KB
Binary file not shown.

website/src/app/layout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const jetbrains = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono" }
99
export const metadata: Metadata = {
1010
title: "KernelSU Keyring",
1111
description: "Developer Identity Management",
12+
icons: {
13+
icon: "/favicon.ico",
14+
},
1215
};
1316

1417
export default function RootLayout({

website/src/components/keyring-app.tsx

Lines changed: 132 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
2222

2323
// Zod Schemas
2424
const genSchema = z.object({
25-
name: z.string().min(2, "Name is too short"),
26-
email: z.string().email("Invalid email address"),
25+
curve: z.enum(["P-256", "P-384"]),
2726
});
2827

2928
const submitSchema = z.object({
3029
username: z.string().min(1, "Username required"),
31-
csr: z.string().includes("BEGIN CERTIFICATE REQUEST", { message: "Invalid CSR" }),
30+
csr: z.string().min(100, "Public key is required"),
3231
});
3332

3433
const revokeSchema = z.object({
@@ -95,9 +94,12 @@ const parseCertFile = async (file: File): Promise<{ fingerprint: string, text: s
9594

9695
export function KeyringApp() {
9796
const [lang, setLang] = useState<LocaleKey>("en");
97+
const [generatedPublicKey, setGeneratedPublicKey] = useState<string>("");
98+
const [mounted, setMounted] = useState(false);
9899
const t = locales[lang];
99100

100101
useEffect(() => {
102+
setMounted(true);
101103
const defaultLang = navigator.language.startsWith("zh") ? "zh" : "en";
102104
setLang(defaultLang);
103105
}, []);
@@ -107,8 +109,8 @@ export function KeyringApp() {
107109
{/* Header */}
108110
<div className="max-w-4xl w-full flex justify-between items-center mb-8">
109111
<div className="flex items-center gap-3">
110-
<div className="p-2 bg-indigo-600 rounded-lg shadow-lg shadow-indigo-500/20">
111-
<ShieldCheck className="w-6 h-6 text-white" />
112+
<div className="p-2 bg-white dark:bg-slate-800 rounded-lg shadow-lg">
113+
<img src="/logo.svg" alt="KernelSU Logo" className="w-8 h-8" />
112114
</div>
113115
<div>
114116
<h1 className="text-2xl font-bold tracking-tight">{t.title}</h1>
@@ -123,23 +125,37 @@ export function KeyringApp() {
123125

124126
{/* Main Content */}
125127
<Card className="max-w-4xl w-full shadow-xl border-slate-200 dark:border-slate-800">
126-
<Tabs defaultValue="generate" className="w-full">
127-
<div className="border-b px-6 py-2 bg-slate-50/50 dark:bg-slate-900/50">
128-
<TabsList className="grid w-full grid-cols-4 bg-slate-200/50 dark:bg-slate-800/50">
129-
<TabsTrigger value="generate">{t.tabs.generate}</TabsTrigger>
130-
<TabsTrigger value="submit">{t.tabs.submit}</TabsTrigger>
131-
<TabsTrigger value="query">{t.tabs.query}</TabsTrigger>
132-
<TabsTrigger value="revoke">{t.tabs.revoke}</TabsTrigger>
133-
</TabsList>
128+
{mounted ? (
129+
<Tabs defaultValue="generate" className="w-full">
130+
<div className="border-b px-6 py-2 bg-slate-50/50 dark:bg-slate-900/50">
131+
<TabsList className="grid w-full grid-cols-4 bg-slate-200/50 dark:bg-slate-800/50">
132+
<TabsTrigger value="generate">{t.tabs.generate}</TabsTrigger>
133+
<TabsTrigger value="submit">{t.tabs.submit}</TabsTrigger>
134+
<TabsTrigger value="query">{t.tabs.query}</TabsTrigger>
135+
<TabsTrigger value="revoke">{t.tabs.revoke}</TabsTrigger>
136+
</TabsList>
137+
</div>
138+
139+
<div className="p-6">
140+
<TabsContent value="generate" forceMount className="data-[state=inactive]:hidden">
141+
<GenerateForm t={t} onGenerated={setGeneratedPublicKey} />
142+
</TabsContent>
143+
<TabsContent value="submit" forceMount className="data-[state=inactive]:hidden">
144+
<SubmitForm t={t} initialPublicKey={generatedPublicKey} />
145+
</TabsContent>
146+
<TabsContent value="query" forceMount className="data-[state=inactive]:hidden">
147+
<QueryForm t={t} />
148+
</TabsContent>
149+
<TabsContent value="revoke" forceMount className="data-[state=inactive]:hidden">
150+
<RevokeForm t={t} />
151+
</TabsContent>
152+
</div>
153+
</Tabs>
154+
) : (
155+
<div className="p-6 min-h-[400px] flex items-center justify-center">
156+
<Loader2 className="w-8 h-8 animate-spin text-indigo-600" />
134157
</div>
135-
136-
<div className="p-6">
137-
<TabsContent value="generate"><GenerateForm t={t} /></TabsContent>
138-
<TabsContent value="submit"><SubmitForm t={t} /></TabsContent>
139-
<TabsContent value="query"><QueryForm t={t} /></TabsContent>
140-
<TabsContent value="revoke"><RevokeForm t={t} /></TabsContent>
141-
</div>
142-
</Tabs>
158+
)}
143159
</Card>
144160

145161
<footer className="mt-12 text-center text-sm text-slate-400">
@@ -151,50 +167,75 @@ export function KeyringApp() {
151167

152168
// --- Sub Components ---
153169

154-
function GenerateForm({ t }: { t: typeof locales.en }) {
170+
function GenerateForm({ t, onGenerated }: { t: typeof locales.en; onGenerated: (publicKey: string) => void }) {
155171
const [keys, setKeys] = useState<{
156172
privateKey: string;
157-
csr: string;
173+
publicKey: string;
158174
fingerprint: string;
159-
name: string
160175
} | null>(null);
161-
const form = useForm<z.infer<typeof genSchema>>({ resolver: zodResolver(genSchema) });
176+
const form = useForm<z.infer<typeof genSchema>>({
177+
resolver: zodResolver(genSchema),
178+
defaultValues: {
179+
curve: "P-256"
180+
}
181+
});
162182
const [loading, setLoading] = useState(false);
163183

164184
const onSubmit = async (data: z.infer<typeof genSchema>) => {
165185
setLoading(true);
166186
try {
167-
// Generate RSA key pair (2048 bits for compatibility)
168-
const keypair = forge.pki.rsa.generateKeyPair({ bits: 2048, workers: -1 });
169-
170-
// Create CSR
171-
const csr = forge.pki.createCertificationRequest();
172-
csr.publicKey = keypair.publicKey;
173-
csr.setSubject([
174-
{ name: 'commonName', value: data.name },
175-
{ name: 'emailAddress', value: data.email },
176-
{ name: 'organizationName', value: 'KernelSU Module Developers' }
177-
]);
178-
179-
// Sign CSR with private key
180-
csr.sign(keypair.privateKey, forge.md.sha256.create());
181-
182-
// Convert to PEM format
183-
const privateKeyPem = forge.pki.privateKeyToPem(keypair.privateKey);
184-
const csrPem = forge.pki.certificationRequestToPem(csr);
185-
186-
// Generate a fingerprint from public key for identification
187-
const publicKeyDer = forge.asn1.toDer(forge.pki.publicKeyToAsn1(keypair.publicKey)).getBytes();
188-
const md = forge.md.sha256.create();
189-
md.update(publicKeyDer);
190-
const fingerprint = md.digest().toHex().toUpperCase();
187+
let privateKeyPem: string;
188+
let publicKeyPem: string;
189+
190+
// Generate EC key pair based on selected curve
191+
if (data.curve === "P-256" || data.curve === "P-384") {
192+
// Generate EC keys using Web Crypto API
193+
const curveName = data.curve === "P-256" ? "P-256" : "P-384";
194+
const cryptoKeypair = await window.crypto.subtle.generateKey(
195+
{
196+
name: "ECDSA",
197+
namedCurve: curveName,
198+
},
199+
true,
200+
["sign", "verify"]
201+
);
202+
203+
// Export private key to PKCS#8 format
204+
const privateKeyBuffer = await window.crypto.subtle.exportKey("pkcs8", cryptoKeypair.privateKey);
205+
const privateKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(privateKeyBuffer)));
206+
privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyBase64.match(/.{1,64}/g)?.join('\n')}\n-----END PRIVATE KEY-----`;
207+
208+
// Export public key to SPKI format
209+
const publicKeyBuffer = await window.crypto.subtle.exportKey("spki", cryptoKeypair.publicKey);
210+
const publicKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(publicKeyBuffer)));
211+
publicKeyPem = `-----BEGIN PUBLIC KEY-----\n${publicKeyBase64.match(/.{1,64}/g)?.join('\n')}\n-----END PUBLIC KEY-----`;
212+
} else {
213+
throw new Error("Unsupported curve type");
214+
}
215+
216+
// Calculate fingerprint from public key
217+
const publicKeyBuffer = await window.crypto.subtle.digest(
218+
'SHA-256',
219+
new TextEncoder().encode(publicKeyPem)
220+
);
221+
const fingerprintArray = Array.from(new Uint8Array(publicKeyBuffer));
222+
const fingerprint = fingerprintArray.map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase();
223+
224+
const timestamp = Date.now();
225+
const fileName = `keypair_${timestamp}`;
191226

192227
setKeys({
193228
privateKey: privateKeyPem,
194-
csr: csrPem,
229+
publicKey: publicKeyPem,
195230
fingerprint,
196-
name: data.name.replace(/\s+/g, '_')
197231
});
232+
233+
// Auto download private key
234+
downloadFile(privateKeyPem, `${fileName}.key.pem`);
235+
236+
// Auto fill public key to submit form
237+
onGenerated(publicKeyPem);
238+
198239
toast.success(t.gen.success);
199240
} catch (e) {
200241
toast.error(e instanceof Error ? e.message : String(e));
@@ -210,17 +251,18 @@ function GenerateForm({ t }: { t: typeof locales.en }) {
210251
<p className="text-sm text-slate-500">{t.gen.desc}</p>
211252
</div>
212253

213-
<div className="grid gap-4 md:grid-cols-2">
214-
<div className="space-y-2">
215-
<Label>{t.gen.name}</Label>
216-
<Input {...form.register("name")} placeholder="Linus Torvalds" />
217-
{form.formState.errors.name && <p className="text-xs text-red-500">{form.formState.errors.name.message}</p>}
218-
</div>
219-
<div className="space-y-2">
220-
<Label>{t.gen.email}</Label>
221-
<Input {...form.register("email")} placeholder="linus@kernel.org" />
222-
{form.formState.errors.email && <p className="text-xs text-red-500">{form.formState.errors.email.message}</p>}
223-
</div>
254+
<div className="space-y-2">
255+
<Label>{t.gen.curve}</Label>
256+
<Select onValueChange={v => form.setValue("curve", v as "P-256" | "P-384")} defaultValue="P-256">
257+
<SelectTrigger>
258+
<SelectValue placeholder={t.gen.curve} />
259+
</SelectTrigger>
260+
<SelectContent>
261+
<SelectItem value="P-256">P-256 (NIST P-256 / secp256r1)</SelectItem>
262+
<SelectItem value="P-384">P-384 (NIST P-384 / secp384r1)</SelectItem>
263+
</SelectContent>
264+
</Select>
265+
{form.formState.errors.curve && <p className="text-xs text-red-500">{form.formState.errors.curve.message}</p>}
224266
</div>
225267

226268
<Button onClick={form.handleSubmit(onSubmit)} disabled={loading} className="w-full bg-indigo-600 hover:bg-indigo-700 text-white">
@@ -259,13 +301,13 @@ function GenerateForm({ t }: { t: typeof locales.en }) {
259301
title={t.gen.priv_warn}
260302
content={keys.privateKey}
261303
isSecret
262-
downloadName={`${keys.name}.key.pem`}
304+
downloadName={`keypair_${Date.now()}.key.pem`}
263305
downloadText={t.gen.download_priv}
264306
/>
265307
<KeyDisplay
266308
title={t.gen.pub_label}
267-
content={keys.csr}
268-
downloadName={`${keys.name}.csr.pem`}
309+
content={keys.publicKey}
310+
downloadName={`keypair_${Date.now()}.pub.pem`}
269311
downloadText={t.gen.download_pub}
270312
/>
271313
</div>
@@ -275,26 +317,42 @@ function GenerateForm({ t }: { t: typeof locales.en }) {
275317
);
276318
}
277319

278-
function SubmitForm({ t }: { t: typeof locales.en }) {
279-
const form = useForm<z.infer<typeof submitSchema>>({ resolver: zodResolver(submitSchema) });
320+
function SubmitForm({ t, initialPublicKey }: { t: typeof locales.en; initialPublicKey: string }) {
321+
const form = useForm<z.infer<typeof submitSchema>>({
322+
resolver: zodResolver(submitSchema),
323+
defaultValues: {
324+
csr: initialPublicKey
325+
}
326+
});
327+
328+
// Update form when initialPublicKey changes
329+
useEffect(() => {
330+
if (initialPublicKey) {
331+
form.setValue("csr", initialPublicKey);
332+
}
333+
}, [initialPublicKey, form]);
280334

281335
const handleFileImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
282336
const file = e.target.files?.[0];
283337
if (!file) return;
284338

285-
const result = await parseCertFile(file);
286-
if (result && result.text.includes('BEGIN CERTIFICATE REQUEST')) {
287-
form.setValue("csr", result.text);
288-
toast.success(t.common.import_success);
289-
} else {
339+
try {
340+
const text = await file.text();
341+
if (text.includes('BEGIN PUBLIC KEY') || text.includes('BEGIN CERTIFICATE REQUEST')) {
342+
form.setValue("csr", text);
343+
toast.success(t.common.import_success);
344+
} else {
345+
toast.error(t.common.import_error);
346+
}
347+
} catch (e) {
290348
toast.error(t.common.import_error);
291349
}
292350
e.target.value = "";
293351
};
294352

295353
const onSubmit = (data: z.infer<typeof submitSchema>) => {
296354
const title = `[keyring] ${data.username}`;
297-
const body = `## Submit Developer CSR (Certificate Signing Request)\n\n**CSR**:\n\n\`\`\`\n${data.csr}\n\`\`\`\n\n---\nPlease review and add \`approved\` label to issue certificate.`;
355+
const body = `## Submit Developer Public Key\n\n**Public Key**:\n\n\`\`\`\n${data.csr}\n\`\`\`\n\n---\nPlease review and add \`approved\` label to issue certificate.`;
298356
window.open(`https://github.com/KernelSU-Modules-Repo/developers/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`, "_blank");
299357
};
300358

@@ -323,7 +381,7 @@ function SubmitForm({ t }: { t: typeof locales.en }) {
323381
<Input id="import-submit" type="file" className="hidden" accept=".pem,.csr,.txt" onChange={handleFileImport} />
324382
</div>
325383
</div>
326-
<Textarea {...form.register("csr")} className="font-mono text-xs h-32" placeholder="-----BEGIN CERTIFICATE REQUEST-----" />
384+
<Textarea {...form.register("csr")} className="font-mono text-xs h-32" placeholder="-----BEGIN PUBLIC KEY-----" />
327385
{form.formState.errors.csr && <p className="text-xs text-red-500">{form.formState.errors.csr.message}</p>}
328386
</div>
329387

website/src/lib/locales.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,24 @@ export const locales = {
55
subtitle: "Developer Identity Management System (X.509 PKI)",
66
tabs: { generate: "Generate", submit: "Submit", query: "Query", revoke: "Revoke" },
77
gen: {
8-
title: "Generate Certificate Signing Request (CSR)",
9-
desc: "Create a private key and CSR locally for certificate issuance.",
10-
name: "Full Name",
11-
email: "Email Address",
12-
btn: "Generate Key & CSR",
13-
success: "Private key and CSR generated successfully!",
8+
title: "Generate Key Pair",
9+
desc: "Create a private key and public key locally for certificate issuance.",
10+
curve: "Curve Type",
11+
btn: "Generate Key Pair",
12+
success: "Private key and public key generated successfully!",
1413
download_priv: "Download Private Key",
15-
download_pub: "Download CSR",
14+
download_pub: "Download Public Key",
1615
fingerprint_label: "Public Key Fingerprint (SHA-256)",
1716
priv_warn: "Private Key (SECRET - SAVE NOW)",
18-
pub_label: "Certificate Signing Request (CSR)",
17+
pub_label: "Public Key",
1918
},
2019
sub: {
21-
title: "Submit CSR",
22-
desc: "Submit your CSR to get a signed certificate from Middle CA.",
20+
title: "Submit Public Key",
21+
desc: "Submit your public key to get a signed certificate from Middle CA.",
2322
gh: "GitHub Username",
24-
pub: "Certificate Signing Request (CSR)",
23+
pub: "Public Key",
2524
btn: "Create GitHub Issue",
26-
warn: "Security: Never submit your private key, only the CSR.",
25+
warn: "Security: Never submit your private key, only the public key.",
2726
},
2827
query: {
2928
title: "Query Certificate",
@@ -63,25 +62,24 @@ export const locales = {
6362
subtitle: "开发者身份认证管理系统 (X.509 PKI)",
6463
tabs: { generate: "生成", submit: "提交", query: "查询", revoke: "吊销" },
6564
gen: {
66-
title: "生成证书签名请求 (CSR)",
67-
desc: "在本地生成私钥和 CSR 以申请证书签发。",
68-
name: "全名",
69-
email: "电子邮箱",
70-
btn: "生成密钥与 CSR",
71-
success: "私钥和 CSR 生成成功!",
65+
title: "生成密钥对",
66+
desc: "在本地生成私钥和公钥以申请证书签发。",
67+
curve: "曲线类型",
68+
btn: "生成密钥对",
69+
success: "私钥和公钥生成成功!",
7270
download_priv: "下载私钥 (.key.pem)",
73-
download_pub: "下载 CSR (.csr.pem)",
71+
download_pub: "下载公钥 (.pub.pem)",
7472
fingerprint_label: "公钥指纹 (SHA-256)",
7573
priv_warn: "私钥 (绝密 - 立即保存)",
76-
pub_label: "证书签名请求 (CSR)",
74+
pub_label: "公钥",
7775
},
7876
sub: {
79-
title: "提交 CSR",
80-
desc: "提交您的 CSR 以获取 Middle CA 签发的证书。",
77+
title: "提交公钥",
78+
desc: "提交您的公钥以获取 Middle CA 签发的证书。",
8179
gh: "GitHub 用户名",
82-
pub: "证书签名请求 (CSR)",
80+
pub: "公钥",
8381
btn: "创建 GitHub Issue",
84-
warn: "安全提示:永远不要提交您的私钥,仅提交 CSR。",
82+
warn: "安全提示:永远不要提交您的私钥,仅提交公钥。",
8583
},
8684
query: {
8785
title: "查询证书",

0 commit comments

Comments
 (0)