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;