mirror of
https://codeberg.org/comaps/comaps
synced 2025-12-20 13:23:59 +00:00
110 lines
3.1 KiB
JavaScript
110 lines
3.1 KiB
JavaScript
/**
|
|
* Crypto utilities for decrypting location data
|
|
* Uses Web Crypto API (AES-GCM-256)
|
|
*/
|
|
|
|
class LocationCrypto {
|
|
/**
|
|
* Decode base64url to base64
|
|
*/
|
|
static base64UrlToBase64(base64url) {
|
|
let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
while (base64.length % 4) {
|
|
base64 += '=';
|
|
}
|
|
return base64;
|
|
}
|
|
|
|
/**
|
|
* Parse the encoded credentials from URL
|
|
* Format: sessionId:encryptionKey (base64url encoded)
|
|
*/
|
|
static parseCredentials(encodedCredentials) {
|
|
try {
|
|
const base64 = this.base64UrlToBase64(encodedCredentials);
|
|
const decoded = atob(base64);
|
|
const [sessionId, encryptionKey] = decoded.split(':');
|
|
|
|
if (!sessionId || !encryptionKey) {
|
|
throw new Error('Invalid credentials format');
|
|
}
|
|
|
|
return { sessionId, encryptionKey };
|
|
} catch (err) {
|
|
console.error('Failed to parse credentials:', err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert base64 encryption key to CryptoKey
|
|
*/
|
|
static async importKey(base64Key) {
|
|
try {
|
|
const keyData = Uint8Array.from(atob(base64Key), c => c.charCodeAt(0));
|
|
|
|
return await window.crypto.subtle.importKey(
|
|
'raw',
|
|
keyData,
|
|
{ name: 'AES-GCM', length: 256 },
|
|
false,
|
|
['decrypt']
|
|
);
|
|
} catch (err) {
|
|
console.error('Failed to import key:', err);
|
|
throw new Error('Invalid encryption key');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypt the encrypted payload
|
|
* Payload format (JSON): { iv: base64, ciphertext: base64, authTag: base64 }
|
|
*/
|
|
static async decryptPayload(encryptedPayloadJson, encryptionKey) {
|
|
try {
|
|
// Parse the encrypted payload
|
|
const payload = JSON.parse(encryptedPayloadJson);
|
|
const { iv, ciphertext, authTag } = payload;
|
|
|
|
if (!iv || !ciphertext || !authTag) {
|
|
throw new Error('Invalid payload format');
|
|
}
|
|
|
|
// Import the key
|
|
const cryptoKey = await this.importKey(encryptionKey);
|
|
|
|
// Decode base64 components
|
|
const ivBytes = Uint8Array.from(atob(iv), c => c.charCodeAt(0));
|
|
const ciphertextBytes = Uint8Array.from(atob(ciphertext), c => c.charCodeAt(0));
|
|
const authTagBytes = Uint8Array.from(atob(authTag), c => c.charCodeAt(0));
|
|
|
|
// Combine ciphertext and auth tag (GCM mode requires them together)
|
|
const combined = new Uint8Array(ciphertextBytes.length + authTagBytes.length);
|
|
combined.set(ciphertextBytes, 0);
|
|
combined.set(authTagBytes, ciphertextBytes.length);
|
|
|
|
// Decrypt
|
|
const decryptedBuffer = await window.crypto.subtle.decrypt(
|
|
{
|
|
name: 'AES-GCM',
|
|
iv: ivBytes,
|
|
tagLength: 128 // 16 bytes = 128 bits
|
|
},
|
|
cryptoKey,
|
|
combined
|
|
);
|
|
|
|
// Convert to string and parse JSON
|
|
const decoder = new TextDecoder();
|
|
const decryptedText = decoder.decode(decryptedBuffer);
|
|
return JSON.parse(decryptedText);
|
|
} catch (err) {
|
|
console.error('Decryption failed:', err);
|
|
throw new Error('Failed to decrypt location data');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export for use in app.js
|
|
window.LocationCrypto = LocationCrypto;
|