209 lines
5.5 KiB
TypeScript
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;
|
|
}
|
|
}; |