diff --git a/src/lib/cards/core/SecretImageCard/CreateSecretImageCardModal.svelte b/src/lib/cards/core/SecretImageCard/CreateSecretImageCardModal.svelte
new file mode 100644
index 0000000..4285e5c
--- /dev/null
+++ b/src/lib/cards/core/SecretImageCard/CreateSecretImageCardModal.svelte
@@ -0,0 +1,100 @@
+
+
+
+
+
diff --git a/src/lib/cards/core/SecretImageCard/SecretImageCard.svelte b/src/lib/cards/core/SecretImageCard/SecretImageCard.svelte
new file mode 100644
index 0000000..1bc07b6
--- /dev/null
+++ b/src/lib/cards/core/SecretImageCard/SecretImageCard.svelte
@@ -0,0 +1,106 @@
+
+
+{#if item.cardData.preview}
+
+{/if}
+
+{#if decryptedUrl}
+
+{/if}
+
+{#if !decryptedUrl}
+
+
+ {#if decrypting}
+
+ {:else}
+
+ {/if}
+
+
+{/if}
diff --git a/src/lib/cards/core/SecretImageCard/crypto.ts b/src/lib/cards/core/SecretImageCard/crypto.ts
new file mode 100644
index 0000000..085c4b9
--- /dev/null
+++ b/src/lib/cards/core/SecretImageCard/crypto.ts
@@ -0,0 +1,98 @@
+/**
+ * AES-GCM encryption/decryption using Web Crypto API with password-derived keys.
+ */
+
+async function deriveKey(password: string, salt: Uint8Array): Promise {
+ const encoder = new TextEncoder();
+ const keyMaterial = await crypto.subtle.importKey(
+ 'raw',
+ encoder.encode(password),
+ 'PBKDF2',
+ false,
+ ['deriveKey']
+ );
+
+ return crypto.subtle.deriveKey(
+ {
+ name: 'PBKDF2',
+ salt,
+ iterations: 100000,
+ hash: 'SHA-256'
+ },
+ keyMaterial,
+ { name: 'AES-GCM', length: 256 },
+ false,
+ ['encrypt', 'decrypt']
+ );
+}
+
+/**
+ * Encrypt a Blob with a password. Returns a Blob containing salt + iv + ciphertext.
+ */
+export async function encryptBlob(blob: Blob, password: string): Promise {
+ const salt = crypto.getRandomValues(new Uint8Array(16));
+ const iv = crypto.getRandomValues(new Uint8Array(12));
+ const key = await deriveKey(password, salt);
+
+ const plaintext = await blob.arrayBuffer();
+ const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext);
+
+ // Pack: salt (16) + iv (12) + ciphertext
+ const result = new Uint8Array(16 + 12 + ciphertext.byteLength);
+ result.set(salt, 0);
+ result.set(iv, 16);
+ result.set(new Uint8Array(ciphertext), 28);
+
+ return new Blob([result], { type: 'application/octet-stream' });
+}
+
+/**
+ * Decrypt a Blob that was encrypted with encryptBlob. Returns the original Blob.
+ * Throws on wrong password.
+ */
+export async function decryptBlob(encryptedBlob: Blob, password: string): Promise {
+ const data = new Uint8Array(await encryptedBlob.arrayBuffer());
+
+ const salt = data.slice(0, 16);
+ const iv = data.slice(16, 28);
+ const ciphertext = data.slice(28);
+
+ const key = await deriveKey(password, salt);
+ const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
+
+ return new Blob([plaintext]);
+}
+
+/**
+ * Create a tiny pixelated preview of an image (16x16 pixels stored as a base64 data URL).
+ */
+export function createPixelatedPreview(
+ file: Blob,
+ size: number = 16
+): Promise {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ const reader = new FileReader();
+
+ reader.onload = (e) => {
+ if (!e.target?.result) return reject(new Error('Failed to read file'));
+ img.src = e.target.result as string;
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+
+ img.onload = () => {
+ const canvas = document.createElement('canvas');
+ canvas.width = size;
+ canvas.height = size;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return reject(new Error('Failed to get canvas context'));
+
+ ctx.imageSmoothingEnabled = true;
+ ctx.drawImage(img, 0, 0, size, size);
+
+ resolve(canvas.toDataURL('image/webp', 0.5));
+ };
+ img.onerror = reject;
+ });
+}
diff --git a/src/lib/cards/core/SecretImageCard/index.ts b/src/lib/cards/core/SecretImageCard/index.ts
new file mode 100644
index 0000000..03026b9
--- /dev/null
+++ b/src/lib/cards/core/SecretImageCard/index.ts
@@ -0,0 +1,57 @@
+import { uploadBlob } from '$lib/atproto/methods';
+import type { CardDefinition } from '../../types';
+import CreateSecretImageCardModal from './CreateSecretImageCardModal.svelte';
+import SecretImageCard from './SecretImageCard.svelte';
+
+export const SecretImageCardDefinition = {
+ type: 'secretImage',
+ contentComponent: SecretImageCard,
+
+ creationModalComponent: CreateSecretImageCardModal,
+
+ createNew: (card) => {
+ card.cardType = 'secretImage';
+ card.cardData = {
+ encryptedImage: '',
+ preview: ''
+ };
+ },
+
+ upload: async (item) => {
+ const img = item.cardData.encryptedImage;
+ if (!img) return item;
+
+ // Already uploaded
+ if (typeof img === 'object' && img.$type === 'blob') return item;
+
+ // Local blob from creation modal
+ if (img?.blob) {
+ if (img.objectUrl) {
+ URL.revokeObjectURL(img.objectUrl);
+ }
+ item.cardData.encryptedImage = await uploadBlob({ blob: img.blob });
+ }
+
+ return item;
+ },
+
+ name: 'Secret Image',
+
+ keywords: ['secret', 'encrypted', 'password', 'hidden', 'private', 'locked'],
+ groups: ['Core'],
+
+ icon: ``
+} as CardDefinition & { type: 'secretImage' };
diff --git a/src/lib/cards/index.ts b/src/lib/cards/index.ts
index 640d06d..685de1b 100644
--- a/src/lib/cards/index.ts
+++ b/src/lib/cards/index.ts
@@ -56,6 +56,7 @@ import { GermDMCardDefinition } from './social/GermDMCard';
import { KichRecipeCardDefinition } from './social/KichRecipeCard';
import { KichRecipeCollectionCardDefinition } from './social/KichRecipeCollectionCard';
import { KichCookingLogCardDefinition } from './social/KichCookingLogCard';
+import { SecretImageCardDefinition } from './core/SecretImageCard';
// import { Model3DCardDefinition } from './visual/Model3DCard';
export const AllCardDefinitions = [
@@ -117,7 +118,8 @@ export const AllCardDefinitions = [
GermDMCardDefinition,
KichRecipeCardDefinition,
KichRecipeCollectionCardDefinition,
- KichCookingLogCardDefinition
+ KichCookingLogCardDefinition,
+ SecretImageCardDefinition
] as const;
export const CardDefinitionsByType = AllCardDefinitions.reduce(