clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

main
diagramzip / services / api / src / crypto.ts
crypto.ts 2.1K
1import { RequestError } from './validation'23const aliasPattern = /^[A-Za-z0-9_-]{16}$/4const capabilityPattern = /^[A-Za-z0-9_-]{43}$/56function base64Url(bytes: Uint8Array): string {7 let binary = ''8 for (const byte of bytes) binary += String.fromCharCode(byte)9 return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '')10}1112function decodeBase64Url(value: string): Uint8Array {13 const padded = value.replaceAll('-', '+').replaceAll('_', '/') + '='.repeat((4 - value.length % 4) % 4)14 const binary = atob(padded)15 return Uint8Array.from(binary, character => character.charCodeAt(0))16}1718export function isAliasId(value: string): boolean {19 return aliasPattern.test(value)20}2122export function randomAliasId(): string {23 return base64Url(crypto.getRandomValues(new Uint8Array(12)))24}2526export async function sha256(bytes: ArrayBuffer | ArrayBufferView): Promise<Uint8Array> {27 return new Uint8Array(await crypto.subtle.digest('SHA-256', bytes))28}2930export async function contentId(bytes: Uint8Array): Promise<{ id: string; digest: Uint8Array }> {31 const digest = await sha256(bytes)32 return { id: base64Url(digest), digest }33}3435export async function writeCapabilityHash(request: Request): Promise<Uint8Array> {36 const authorization = request.headers.get('Authorization')37 const match = authorization?.match(/^Bearer ([A-Za-z0-9_-]{43})$/)38 if (!match?.[1] || !capabilityPattern.test(match[1])) {39 throw new RequestError(401, 'write_capability_required', 'A valid write capability is required.')40 }41 let capability: Uint8Array42 try {43 capability = decodeBase64Url(match[1])44 } catch {45 throw new RequestError(401, 'write_capability_required', 'A valid write capability is required.')46 }47 if (capability.byteLength !== 32 || base64Url(capability) !== match[1]) {48 throw new RequestError(401, 'write_capability_required', 'A valid write capability is required.')49 }50 return sha256(capability)51}5253export function capabilityMatches(provided: Uint8Array, stored: ArrayBuffer): boolean {54 const expected = new Uint8Array(stored)55 return expected.byteLength === 32 && crypto.subtle.timingSafeEqual(provided, expected)56}