···2929 let width = img.width;
3030 let height = img.height;
31313232- // If image is already small enough, return original
3333- if (file.size <= maxSize) {
3434- console.log('skipping compression+resizing, already small enough');
3535- return resolve({
3636- blob: file,
3737- aspectRatio: {
3838- width,
3939- height
4040- }
4141- });
4242- }
3232+ const isSmallEnough = file.size <= maxSize;
43334434 if (width > maxDimension || height > maxDimension) {
4535 if (width > height) {
···6858 if (!blob) {
6959 return reject(new Error('Compression failed.'));
7060 }
7171- if (blob.size <= maxSize || quality < 0.3) {
6161+ if (isSmallEnough || blob.size <= maxSize || quality < 0.3) {
7262 resolve({
7363 blob,
7464 aspectRatio: {
+13-35
src/lib/cards/media/SecretImageCard/crypto.ts
···11/**
22- * AES-GCM encryption/decryption using Web Crypto API with password-derived keys.
22+ * AES-GCM encryption/decryption using Web Crypto API with SHA-256 derived keys.
33 */
4455-async function deriveKey(password: string, salt: Uint8Array<ArrayBuffer>): Promise<CryptoKey> {
66- const encoder = new TextEncoder();
77- const keyMaterial = await crypto.subtle.importKey(
88- 'raw',
99- encoder.encode(password),
1010- 'PBKDF2',
1111- false,
1212- ['deriveKey']
1313- );
1414-1515- return crypto.subtle.deriveKey(
1616- {
1717- name: 'PBKDF2',
1818- salt,
1919- iterations: 10000,
2020- hash: 'SHA-256'
2121- },
2222- keyMaterial,
2323- { name: 'AES-GCM', length: 256 },
2424- false,
2525- ['encrypt', 'decrypt']
2626- );
55+async function deriveKey(password: string): Promise<CryptoKey> {
66+ const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password));
77+ return crypto.subtle.importKey('raw', hash, 'AES-GCM', false, ['encrypt', 'decrypt']);
278}
2892910/**
3030- * Encrypt a Blob with a password. Returns a Blob containing salt + iv + ciphertext.
1111+ * Encrypt a Blob with a password. Returns a Blob containing iv + ciphertext.
3112 */
3213export async function encryptBlob(blob: Blob, password: string): Promise<Blob> {
3333- const salt = crypto.getRandomValues(new Uint8Array(16));
3414 const iv = crypto.getRandomValues(new Uint8Array(12));
3535- const key = await deriveKey(password, salt);
1515+ const key = await deriveKey(password);
36163717 const plaintext = await blob.arrayBuffer();
3818 const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext);
39194040- // Pack: salt (16) + iv (12) + ciphertext
4141- const result = new Uint8Array(16 + 12 + ciphertext.byteLength);
4242- result.set(salt, 0);
4343- result.set(iv, 16);
4444- result.set(new Uint8Array(ciphertext), 28);
2020+ // Pack: iv (12) + ciphertext
2121+ const result = new Uint8Array(12 + ciphertext.byteLength);
2222+ result.set(iv, 0);
2323+ result.set(new Uint8Array(ciphertext), 12);
45244625 return new Blob([result], { type: 'application/octet-stream' });
4726}
···5332export async function decryptBlob(encryptedBlob: Blob, password: string): Promise<Blob> {
5433 const data = new Uint8Array(await encryptedBlob.arrayBuffer());
55345656- const salt = data.slice(0, 16);
5757- const iv = data.slice(16, 28);
5858- const ciphertext = data.slice(28);
3535+ const iv = data.slice(0, 12);
3636+ const ciphertext = data.slice(12);
59376060- const key = await deriveKey(password, salt);
3838+ const key = await deriveKey(password);
6139 const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
62406341 return new Blob([plaintext]);
+4
src/lib/cards/media/SecretImageCard/index.ts
···2222 },
23232424 upload: async (item) => {
2525+ if (item.cardData.rawImage?.blob && !item.cardData.password) {
2626+ throw new Error('Password is required for secret image');
2727+ }
2828+2529 // If there's a new raw image + password, encrypt and upload
2630 if (item.cardData.rawImage?.blob && item.cardData.password) {
2731 const rawBlob = item.cardData.rawImage.blob as Blob;