From 51f3e62648657fcc50def9086997fc51212f6098 Mon Sep 17 00:00:00 2001 From: luna Date: Wed, 12 Feb 2025 19:37:06 +0000 Subject: [PATCH 1/3] feat: decrypt private posts --- src/components/post-card.tsx | 57 ++++++++++++++++++++++++++++-- src/lib/bluesky/types/bsky-post.ts | 27 ++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/components/post-card.tsx b/src/components/post-card.tsx index 42e86f0..7fe5ad0 100644 --- a/src/components/post-card.tsx +++ b/src/components/post-card.tsx @@ -35,7 +35,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from './ui/dropdown-menu'; -import { memo, useState } from 'react'; +import { memo, useEffect, useState } from 'react'; import { toast } from 'sonner'; import { usePlausible } from '@/hooks/use-plausible'; import { useBlueskyStore } from '@/lib/bluesky/store'; @@ -207,6 +207,49 @@ const PostDropdownMenu = ({ post, setTranslatedText }: { post: BSkyPost; setTran ); }; +async function decryptPrivatePost(post: BSkyPost) { + // If no encryption data, return the regular text + if (!post.record.encryption || !post.record.encryptedText) { + return post.record.text; + } + + const { key } = post.record.encryption; + const encryptedText = post.record.encryptedText; + + try { + // Convert key string to array buffer + const keyData = new TextEncoder().encode(key); + + // Import the raw key + const cryptoKey = await window.crypto.subtle.importKey('raw', keyData, { name: 'AES-CBC', length: 256 }, false, [ + 'decrypt', + ]); + + // Decode base64 encrypted text + const encryptedData = Uint8Array.from(atob(encryptedText), (c) => c.charCodeAt(0)); + + // First 16 bytes should be IV + const iv = encryptedData.slice(0, 16); + const data = encryptedData.slice(16); + + // Decrypt the data + const decryptedData = await window.crypto.subtle.decrypt( + { + name: 'AES-CBC', + iv: iv, + }, + cryptoKey, + data, + ); + + // Convert the decrypted array buffer back to text + return new TextDecoder().decode(decryptedData); + } catch (error) { + console.error('Decryption failed:', error); + return 'decryption failed'; + } +} + type PostCardInnerProps = { post: BSkyPost; context?: string; @@ -225,6 +268,14 @@ function PostCardInner({ post, context, className, parent = false }: PostCardInn const navigate = useNavigate(); const [translatedText, setTranslatedText] = useState(null); const { moderation } = usePostLabels({ agent, post }); + const isPrivatePost = !!post.record.encryptedText; + const [postText, setPostText] = useState(isPrivatePost ? 'decrypting private post...' : post.record.text); + + useEffect(() => { + if (isPrivatePost) { + decryptPrivatePost(post).then(setPostText); + } + }, [isPrivatePost, post]); const handleLike = (event: React.MouseEvent) => { event.preventDefault(); @@ -330,9 +381,9 @@ function PostCardInner({ post, context, className, parent = false }: PostCardInn ) : post.record.facets ? ( - + ) : ( - + )}

diff --git a/src/lib/bluesky/types/bsky-post.ts b/src/lib/bluesky/types/bsky-post.ts index 7a1f7cf..9340601 100644 --- a/src/lib/bluesky/types/bsky-post.ts +++ b/src/lib/bluesky/types/bsky-post.ts @@ -29,6 +29,33 @@ export const BSkyPost = Type.Object({ }), ), text: Type.String(), + encryptedText: Type.Optional(Type.String()), + encryption: Type.Optional( + Type.Object({ + key: Type.String(), + algorithm: Type.String(), + encoding: Type.String(), + }), + ), + acl: Type.Optional( + Type.Object({ + user: Type.Array( + Type.Object({ + did: Type.String(), + permission: Type.Object({ + read: Type.Boolean(), + interact: Type.Object({ + reply: Type.Boolean(), + like: Type.Boolean(), + repost: Type.Boolean(), + quote: Type.Boolean(), + comment: Type.Boolean(), + }), + }), + }), + ), + }), + ), }), embed: Type.Optional(BSkyPostEmbed), replyCount: Type.Number(), From aa7c5ef0eac701162b6f74690da713365c6f9a90 Mon Sep 17 00:00:00 2001 From: luna Date: Wed, 12 Feb 2025 19:59:24 +0000 Subject: [PATCH 2/3] chore: add logging --- src/components/post-card.tsx | 25 +++++++++++++++++++------ src/lib/bluesky/types/bsky-post.ts | 2 +- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/components/post-card.tsx b/src/components/post-card.tsx index 7fe5ad0..74e473d 100644 --- a/src/components/post-card.tsx +++ b/src/components/post-card.tsx @@ -218,32 +218,45 @@ async function decryptPrivatePost(post: BSkyPost) { try { // Convert key string to array buffer - const keyData = new TextEncoder().encode(key); + console.info('key:', key); + const keyData = Uint8Array.from(atob(key), (c) => c.charCodeAt(0)); // Import the raw key - const cryptoKey = await window.crypto.subtle.importKey('raw', keyData, { name: 'AES-CBC', length: 256 }, false, [ - 'decrypt', - ]); + console.info('keyData:', keyData); + const cryptoKey = await window.crypto.subtle.importKey( + 'raw', + keyData, + { name: post.record.encryption.type, length: 256 }, + false, + ['decrypt'], + ); // Decode base64 encrypted text + console.info('encryptedText:', encryptedText); const encryptedData = Uint8Array.from(atob(encryptedText), (c) => c.charCodeAt(0)); // First 16 bytes should be IV const iv = encryptedData.slice(0, 16); const data = encryptedData.slice(16); + console.info('iv:', iv); + console.info('data:', data); // Decrypt the data const decryptedData = await window.crypto.subtle.decrypt( { - name: 'AES-CBC', + name: post.record.encryption.type, iv: iv, }, cryptoKey, data, ); + console.info('decryptedData:', decryptedData); + // Convert the decrypted array buffer back to text - return new TextDecoder().decode(decryptedData); + const text = new TextDecoder().decode(decryptedData); + console.info('decrypted text:', text); + return text; } catch (error) { console.error('Decryption failed:', error); return 'decryption failed'; diff --git a/src/lib/bluesky/types/bsky-post.ts b/src/lib/bluesky/types/bsky-post.ts index 9340601..a836cac 100644 --- a/src/lib/bluesky/types/bsky-post.ts +++ b/src/lib/bluesky/types/bsky-post.ts @@ -33,7 +33,7 @@ export const BSkyPost = Type.Object({ encryption: Type.Optional( Type.Object({ key: Type.String(), - algorithm: Type.String(), + type: Type.String(), encoding: Type.String(), }), ), From 2a4090d915bd3032658a31c3d24e5c48646c9bf3 Mon Sep 17 00:00:00 2001 From: luna Date: Wed, 12 Feb 2025 20:32:54 +0000 Subject: [PATCH 3/3] fix: decrypting posts --- src/components/post-card.tsx | 82 ++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/src/components/post-card.tsx b/src/components/post-card.tsx index 74e473d..b30286b 100644 --- a/src/components/post-card.tsx +++ b/src/components/post-card.tsx @@ -207,8 +207,41 @@ const PostDropdownMenu = ({ post, setTranslatedText }: { post: BSkyPost; setTran ); }; +async function createEncryptedPost(content: string) { + // Generate encryption key and IV + const key = await crypto.subtle.generateKey({ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); + + const iv = crypto.getRandomValues(new Uint8Array(16)); + const encoder = new TextEncoder(); + const data = encoder.encode(content); + + // Encrypt content + const encrypted = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, key, data); + + // Export key material + const rawKey = await crypto.subtle.exportKey('raw', key); + const keyString = btoa(String.fromCharCode(...new Uint8Array(rawKey))); + + // Combine IV and ciphertext + const combined = new Uint8Array(iv.byteLength + encrypted.byteLength); + combined.set(iv, 0); + combined.set(new Uint8Array(encrypted), iv.byteLength); + + return { + record: { + text: '', // Empty public text + encryption: { + type: 'AES-CBC', + key: keyString, + }, + encryptedText: btoa(String.fromCharCode(...combined)), + }, + }; +} + +window.createEncryptedPost = createEncryptedPost; + async function decryptPrivatePost(post: BSkyPost) { - // If no encryption data, return the regular text if (!post.record.encryption || !post.record.encryptedText) { return post.record.text; } @@ -217,52 +250,37 @@ async function decryptPrivatePost(post: BSkyPost) { const encryptedText = post.record.encryptedText; try { - // Convert key string to array buffer - console.info('key:', key); - const keyData = Uint8Array.from(atob(key), (c) => c.charCodeAt(0)); + // Decode base64 key + const keyData = new Uint8Array(Array.from(atob(key), (c) => c.charCodeAt(0))); - // Import the raw key - console.info('keyData:', keyData); - const cryptoKey = await window.crypto.subtle.importKey( + // Import key (without length parameter) + const cryptoKey = await crypto.subtle.importKey( 'raw', keyData, - { name: post.record.encryption.type, length: 256 }, + { name: post.record.encryption.type }, // Removed length false, ['decrypt'], ); - // Decode base64 encrypted text - console.info('encryptedText:', encryptedText); - const encryptedData = Uint8Array.from(atob(encryptedText), (c) => c.charCodeAt(0)); - - // First 16 bytes should be IV - const iv = encryptedData.slice(0, 16); - const data = encryptedData.slice(16); - console.info('iv:', iv); - console.info('data:', data); - - // Decrypt the data - const decryptedData = await window.crypto.subtle.decrypt( - { - name: post.record.encryption.type, - iv: iv, - }, - cryptoKey, - data, - ); + // Decode base64 payload + const encryptedData = new Uint8Array(Array.from(atob(encryptedText), (c) => c.charCodeAt(0))); + + // Extract IV and ciphertext with proper buffer boundaries + const iv = new Uint8Array(encryptedData.buffer, 0, 16); + const data = new Uint8Array(encryptedData.buffer, 16); - console.info('decryptedData:', decryptedData); + // Decrypt + const decryptedData = await crypto.subtle.decrypt({ name: post.record.encryption.type, iv }, cryptoKey, data); - // Convert the decrypted array buffer back to text - const text = new TextDecoder().decode(decryptedData); - console.info('decrypted text:', text); - return text; + return new TextDecoder().decode(decryptedData); } catch (error) { console.error('Decryption failed:', error); return 'decryption failed'; } } +window.decryptPrivatePost = decryptPrivatePost; + type PostCardInnerProps = { post: BSkyPost; context?: string;