canvas-website/src/lib/auth/crypto.ts

209 lines
5.5 KiB
TypeScript

// This module contains browser-specific WebCrypto API utilities
// Check if we're in a browser environment
export const isBrowser = (): boolean => typeof window !== 'undefined';
// Use the polyfill if available, otherwise fall back to native WebCrypto
const getCrypto = (): Crypto => {
if (typeof window !== 'undefined' && window.crypto) {
return window.crypto;
}
// Fallback to native WebCrypto if polyfill is not available
return window.crypto;
};
// Get registered users from localStorage
export const getRegisteredUsers = (): string[] => {
if (!isBrowser()) return [];
try {
return JSON.parse(window.localStorage.getItem('registeredUsers') || '[]');
} catch (error) {
console.error('Error getting registered users:', error);
return [];
}
};
// Add a user to the registered users list
export const addRegisteredUser = (username: string): void => {
if (!isBrowser()) return;
try {
const users = getRegisteredUsers();
if (!users.includes(username)) {
users.push(username);
window.localStorage.setItem('registeredUsers', JSON.stringify(users));
}
} catch (error) {
console.error('Error adding registered user:', error);
}
};
// Check if a username is available
export const isUsernameAvailable = async (username: string): Promise<boolean> => {
try {
// Get the list of registered users
const users = getRegisteredUsers();
// Check if the username is already taken
const isAvailable = !users.includes(username);
return isAvailable;
} catch (error) {
console.error('Error checking username availability:', error);
return false;
}
};
// Check if username is valid format (letters, numbers, underscores, hyphens)
export const isUsernameValid = (username: string): boolean => {
const usernameRegex = /^[a-zA-Z0-9_-]{3,20}$/;
return usernameRegex.test(username);
};
// Store a public key for a user
export const storePublicKey = (username: string, publicKey: string): void => {
if (!isBrowser()) return;
try {
window.localStorage.setItem(`${username}_publicKey`, publicKey);
} catch (error) {
console.error('Error storing public key:', error);
}
};
// Get a user's public key
export const getPublicKey = (username: string): string | null => {
if (!isBrowser()) return null;
try {
return window.localStorage.getItem(`${username}_publicKey`);
} catch (error) {
console.error('Error getting public key:', error);
return null;
}
};
// Generate a key pair using Web Crypto API
export const generateKeyPair = async (): Promise<CryptoKeyPair | null> => {
if (!isBrowser()) return null;
try {
const crypto = getCrypto();
return await crypto.subtle.generateKey(
{
name: 'ECDSA',
namedCurve: 'P-256',
},
true,
['sign', 'verify']
);
} catch (error) {
console.error('Error generating key pair:', error);
return null;
}
};
// Export a public key to a base64 string
export const exportPublicKey = async (publicKey: CryptoKey): Promise<string | null> => {
if (!isBrowser()) return null;
try {
const crypto = getCrypto();
const publicKeyBuffer = await crypto.subtle.exportKey(
'raw',
publicKey
);
return btoa(
String.fromCharCode.apply(null, Array.from(new Uint8Array(publicKeyBuffer)))
);
} catch (error) {
console.error('Error exporting public key:', error);
return null;
}
};
// Import a public key from a base64 string
export const importPublicKey = async (base64Key: string): Promise<CryptoKey | null> => {
if (!isBrowser()) return null;
try {
const crypto = getCrypto();
const binaryString = atob(base64Key);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return await crypto.subtle.importKey(
'raw',
bytes,
{
name: 'ECDSA',
namedCurve: 'P-256',
},
true,
['verify']
);
} catch (error) {
console.error('Error importing public key:', error);
return null;
}
};
// Sign data with a private key
export const signData = async (privateKey: CryptoKey, data: string): Promise<string | null> => {
if (!isBrowser()) return null;
try {
const crypto = getCrypto();
const encoder = new TextEncoder();
const encodedData = encoder.encode(data);
const signature = await crypto.subtle.sign(
{
name: 'ECDSA',
hash: { name: 'SHA-256' },
},
privateKey,
encodedData
);
return btoa(
String.fromCharCode.apply(null, Array.from(new Uint8Array(signature)))
);
} catch (error) {
console.error('Error signing data:', error);
return null;
}
};
// Verify a signature
export const verifySignature = async (
publicKey: CryptoKey,
signature: string,
data: string
): Promise<boolean> => {
if (!isBrowser()) return false;
try {
const crypto = getCrypto();
const encoder = new TextEncoder();
const encodedData = encoder.encode(data);
const binarySignature = atob(signature);
const signatureBytes = new Uint8Array(binarySignature.length);
for (let i = 0; i < binarySignature.length; i++) {
signatureBytes[i] = binarySignature.charCodeAt(i);
}
return await crypto.subtle.verify(
{
name: 'ECDSA',
hash: { name: 'SHA-256' },
},
publicKey,
signatureBytes,
encodedData
);
} catch (error) {
console.error('Error verifying signature:', error);
return false;
}
};