One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links 馃搮
calendar.xyehr.cn
1import {
2 createHash,
3 createPrivateKey,
4 generateKeyPairSync,
5 randomUUID,
6 sign,
7} from 'crypto'
8
9export interface DpopPublicJwk {
10 kty: string
11 crv: string
12 x: string
13 y: string
14}
15
16function toBase64Url(input: Buffer | string) {
17 return Buffer.from(input).toString('base64url')
18}
19
20function jwkThumbprint(publicJwk: DpopPublicJwk) {
21 const canonical = JSON.stringify({
22 crv: publicJwk.crv,
23 kty: publicJwk.kty,
24 x: publicJwk.x,
25 y: publicJwk.y,
26 })
27
28 return createHash('sha256').update(canonical, 'utf8').digest('base64url')
29}
30
31export function generateDpopKeyMaterial() {
32 const { privateKey, publicKey } = generateKeyPairSync('ec', {
33 namedCurve: 'P-256',
34 })
35 const publicJwk = publicKey.export({ format: 'jwk' }) as DpopPublicJwk
36 const privateKeyPem = privateKey
37 .export({ type: 'pkcs8', format: 'pem' })
38 .toString()
39
40 if (!publicJwk?.kty || !publicJwk?.crv || !publicJwk?.x || !publicJwk?.y) {
41 throw new Error('Failed to generate DPoP key material')
42 }
43
44 return {
45 publicJwk,
46 privateKeyPem,
47 jkt: jwkThumbprint(publicJwk),
48 }
49}
50
51export function createDpopProof(params: {
52 htu: string
53 htm: string
54 privateKeyPem: string
55 publicJwk: DpopPublicJwk
56 accessToken?: string
57 nonce?: string
58}) {
59 const header = {
60 typ: 'dpop+jwt',
61 alg: 'ES256',
62 jwk: {
63 kty: params.publicJwk.kty,
64 crv: params.publicJwk.crv,
65 x: params.publicJwk.x,
66 y: params.publicJwk.y,
67 },
68 }
69
70 const payload: Record<string, string | number> = {
71 jti: randomUUID(),
72 iat: Math.floor(Date.now() / 1000),
73 htm: params.htm.toUpperCase(),
74 htu: params.htu,
75 }
76
77 if (params.accessToken) {
78 payload.ath = createHash('sha256')
79 .update(params.accessToken, 'utf8')
80 .digest('base64url')
81 }
82
83 if (params.nonce) {
84 payload.nonce = params.nonce
85 }
86
87 const encodedHeader = toBase64Url(JSON.stringify(header))
88 const encodedPayload = toBase64Url(JSON.stringify(payload))
89 const signingInput = `${encodedHeader}.${encodedPayload}`
90
91 const signature = sign('sha256', Buffer.from(signingInput, 'utf8'), {
92 key: createPrivateKey(params.privateKeyPem),
93 dsaEncoding: 'ieee-p1363',
94 })
95
96 return `${signingInput}.${toBase64Url(signature)}`
97}