Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ SLASHED_BOND_REWARD_SPLIT = 0.5
# Username for HTLCs escrows
ESCROW_USERNAME = 'admin'

# Blossom image upload in chat. Requires running a Blossom blob server behind the coordinator.
BLOSSOM_ENABLED = False

#Social
NOSTR_NSEC = 'nsec1vxhs2zc4kqe0dhz4z2gfrdyjsrwf8pg3neeqx6w4nl8djfzdp0dqwd6rxh'
STRFRY_HOST = 'localhost'
Expand Down
3 changes: 3 additions & 0 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class InfoSerializer(serializers.Serializer):
current_swap_fee_rate = serializers.FloatField(
help_text="Swap fees to perform on-chain transaction (percent)"
)
blossom_enabled = serializers.BooleanField(
help_text="Whether the coordinator offers encrypted image uploads via Blossom in chat"
)
version = VersionSerializer()
notice_severity = serializers.ChoiceField(
choices=[
Expand Down
1 change: 1 addition & 0 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,7 @@ def get(self, request):
context["max_order_size"] = config("MAX_ORDER_SIZE", cast=int, default=250000)
context["swap_enabled"] = not config("DISABLE_ONCHAIN", cast=bool, default=True)
context["max_swap"] = config("MAX_SWAP_AMOUNT", cast=int, default=0)
context["blossom_enabled"] = config("BLOSSOM_ENABLED", cast=bool, default=False)

try:
context["current_swap_fee_rate"] = Logics.compute_swap_fee_rate(
Expand Down
4 changes: 4 additions & 0 deletions docs/assets/schemas/api-latest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,9 @@ components:
type: number
format: double
description: Swap fees to perform on-chain transaction (percent)
blossom_enabled:
type: boolean
description: Whether the coordinator offers encrypted image uploads via Blossom in chat
version:
$ref: '#/components/schemas/Version'
notice_severity:
Expand Down Expand Up @@ -1285,6 +1288,7 @@ components:
- swap_enabled
- taker_fee
- version
- blossom_enabled
ListNotification:
type: object
properties:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { Dispatch, SetStateAction, useContext, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, TextField, Grid, Paper, Typography } from '@mui/material';
import { Button, TextField, Grid, Paper, Typography, Tooltip, IconButton } from '@mui/material';
import { decryptMessage } from '../../../../pgp';

// Icons
import CircularProgress from '@mui/material/CircularProgress';
import { useTheme } from '@mui/material';
import MessageCard from '../MessageCard';
import ChatHeader from '../ChatHeader';
Expand All @@ -16,7 +17,7 @@ import {
import { type UseGarageStoreType, GarageContext } from '../../../../contexts/GarageContext';
import { type Order } from '../../../../models';
import getSettings from '../../../../utils/settings';
import { Send } from '@mui/icons-material';
import { AttachFile, Send } from '@mui/icons-material';
import PrivacyWarningDialog from '../PrivacyWarningDialog';
import { ParsedFileMessage, parseImageMetadataJson } from '../../../../utils/nip17File';

Expand Down Expand Up @@ -45,6 +46,7 @@ interface Props {
setPeerPubKey: (peerPubKey: string) => void;
setError: Dispatch<SetStateAction<string>>;
setLastIndex: Dispatch<SetStateAction<number>>;
blossomEnabled: boolean;
}

const audioPath =
Expand All @@ -69,6 +71,7 @@ const EncryptedApiChat: React.FC<Props> = ({
onSendFile,
setError,
setLastIndex,
blossomEnabled,
}: Props): React.JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
Expand All @@ -81,7 +84,7 @@ const EncryptedApiChat: React.FC<Props> = ({
const [waitingEcho, setWaitingEcho] = useState<boolean>(false);
const [messageCount, setMessageCount] = useState<number>(0);
const [serverMessages, setServerMessages] = useState<ServerMessage[]>([]);
const [_uploading, setUploading] = useState<boolean>(false);
const [uploading, setUploading] = useState<boolean>(false);
const [imageUrls, setImageUrls] = useState<Record<number, string>>({});
const [privacyWarningOpen, setPrivacyWarningOpen] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -219,7 +222,7 @@ const EncryptedApiChat: React.FC<Props> = ({
}
};

const _handleAttachClick = (): void => {
const handleAttachClick = (): void => {
// Clear any previous errors
setError('');
setPrivacyWarningOpen(true);
Expand All @@ -233,7 +236,7 @@ const EncryptedApiChat: React.FC<Props> = ({
}
};

const _handleFileChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const file = e.target.files?.[0];
if (!file) {
// User cancelled file selection
Expand Down Expand Up @@ -338,24 +341,32 @@ const EncryptedApiChat: React.FC<Props> = ({
}}
fullWidth={true}
/>
{/* <input
<input
type='file'
ref={fileInputRef}
style={{ display: 'none' }}
accept='image/*'
onChange={handleFileChange}
/>
<Tooltip title={peerPubKey === undefined ? t('Waiting for peer...') : ''}>
<Tooltip
title={
!blossomEnabled
? t('This coordinator does not offer image uploads')
: peerPubKey === undefined
? t('Waiting for peer...')
: ''
}
>
<span>
<IconButton
disabled={uploading || peerPubKey === undefined}
disabled={uploading || peerPubKey === undefined || !blossomEnabled}
onClick={handleAttachClick}
color='primary'
>
{uploading ? <CircularProgress size={24} /> : <AttachFile />}
</IconButton>
</span>
</Tooltip> */}
</Tooltip>
<Button
disabled={waitingEcho || peerPubKey === undefined}
type='submit'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { Dispatch, SetStateAction, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, TextField, Grid, Paper, Typography } from '@mui/material';
import { Button, TextField, Grid, Paper, Typography, IconButton, Tooltip } from '@mui/material';

// Icons
import CircularProgress from '@mui/material/CircularProgress';
import KeyIcon from '@mui/icons-material/Key';
import { AttachFile } from '@mui/icons-material';
import PrivacyWarningDialog from '../PrivacyWarningDialog';
import { useTheme } from '@mui/material';
import MessageCard from '../MessageCard';
Expand Down Expand Up @@ -74,7 +75,7 @@ const EncryptedNostrChat: React.FC<Props> = ({
const [value, setValue] = useState<string>('');
const [waitingEcho, setWaitingEcho] = useState<boolean>(false);
const [messageCount, setMessageCount] = useState<number>(0);
const [_uploading, setUploading] = useState<boolean>(false);
const [uploading, setUploading] = useState<boolean>(false);
const [privacyWarningOpen, setPrivacyWarningOpen] = useState<boolean>(false);
const [peerConnected, setPeerConnected] = useState<boolean>(false);
const [imageUrls, setImageUrls] = useState<Record<number, string>>({});
Expand Down Expand Up @@ -191,7 +192,7 @@ const EncryptedNostrChat: React.FC<Props> = ({
}
};

const _handleAttachClick = (): void => {
const handleAttachClick = (): void => {
// Clear any previous errors
setError('');
setPrivacyWarningOpen(true);
Expand All @@ -205,7 +206,7 @@ const EncryptedNostrChat: React.FC<Props> = ({
}
};

const _handleFileChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const file = e.target.files?.[0];
if (!file) {
// User cancelled file selection
Expand Down Expand Up @@ -327,7 +328,7 @@ const EncryptedNostrChat: React.FC<Props> = ({
}}
fullWidth={true}
/>
{/* <input
<input
type='file'
ref={fileInputRef}
style={{ display: 'none' }}
Expand All @@ -344,7 +345,7 @@ const EncryptedNostrChat: React.FC<Props> = ({
{uploading ? <CircularProgress size={24} /> : <AttachFile />}
</IconButton>
</span>
</Tooltip> */}
</Tooltip>
<Button
disabled={waitingEcho || !peerPubKey}
type='submit'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import React, { useEffect, useLayoutEffect, useState, useContext, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, TextField, Grid, Paper, Typography } from '@mui/material';
import {
Button,
TextField,
Grid,
Paper,
Typography,
Tooltip,
IconButton,
CircularProgress,
} from '@mui/material';
import { encryptMessage, decryptMessage } from '../../../../pgp';
import { websocketClient, type WebsocketConnection } from '../../../../services/Websocket';
import { GarageContext, type UseGarageStoreType } from '../../../../contexts/GarageContext';
Expand All @@ -17,7 +26,7 @@ import {
FederationContext,
} from '../../../../contexts/FederationContext';
import getSettings from '../../../../utils/settings';
import { Send } from '@mui/icons-material';
import { AttachFile, Send } from '@mui/icons-material';
import { UseAppStoreType, AppContext } from '../../../../contexts/AppContext';
import PrivacyWarningDialog from '../PrivacyWarningDialog';
import { ParsedFileMessage, parseImageMetadataJson } from '../../../../utils/nip17File';
Expand All @@ -42,6 +51,7 @@ interface Props {
onSendFile: (file: File) => Promise<void>;
peerPubKey?: string;
setPeerPubKey: (peerPubKey: string) => void;
blossomEnabled: boolean;
}

const EncryptedSocketChat: React.FC<Props> = ({
Expand All @@ -57,6 +67,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
onSendFile,
peerPubKey,
setPeerPubKey,
blossomEnabled,
}: Props): React.JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
Expand All @@ -76,7 +87,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
const [receivedIndexes, setReceivedIndexes] = useState<number[]>([]);
const [error, setError] = useState<string>('');
const [imageUrls, setImageUrls] = useState<Record<number, string>>({});
const [_uploading, setUploading] = useState<boolean>(false);
const [uploading, setUploading] = useState<boolean>(false);
const [privacyWarningOpen, setPrivacyWarningOpen] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null);

Expand Down Expand Up @@ -276,7 +287,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
}
};

const _handleAttachClick = (): void => {
const handleAttachClick = (): void => {
// Clear any previous errors
setError('');
setPrivacyWarningOpen(true);
Expand All @@ -290,7 +301,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
}
};

const _handleFileChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const file = e.target.files?.[0];
if (!file) {
// User cancelled file selection
Expand Down Expand Up @@ -395,24 +406,32 @@ const EncryptedSocketChat: React.FC<Props> = ({
}}
fullWidth
/>
{/* <input
<input
type='file'
ref={fileInputRef}
style={{ display: 'none' }}
accept='image/*'
onChange={handleFileChange}
/>
<Tooltip title={peerPubKey === undefined ? t('Waiting for peer...') : ''}>
<Tooltip
title={
!blossomEnabled
? t('This coordinator does not offer image uploads')
: peerPubKey === undefined
? t('Waiting for peer...')
: ''
}
>
<span>
<IconButton
disabled={uploading || peerPubKey === undefined}
disabled={uploading || peerPubKey === undefined || !blossomEnabled}
onClick={handleAttachClick}
color='primary'
>
{uploading ? <CircularProgress size={24} /> : <AttachFile />}
</IconButton>
</span>
</Tooltip> */}
</Tooltip>
<Button
disabled={!connected || waitingEcho || peerPubKey === undefined}
type='submit'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,19 @@ const MessageCard: React.FC<Props> = ({
return;
}

const plaintext = await decryptFile(ciphertext, fileData.key, fileData.nonce);
const plaintextBuffer = await decryptFile(ciphertext, fileData.key, fileData.nonce);
const plaintext = new Uint8Array(plaintextBuffer);

// Defence-in-depth: verify the plaintext hash matches the sender's declared originalSha256.
// AEAD already guarantees integrity, but this confirms we decrypted the right content.
if (fileData.originalSha256) {
const plaintextValid = await verifyBlobHash(plaintext, fileData.originalSha256);
if (!plaintextValid) {
setImageError(t('Image content verification failed'));
return;
}
}

const blob = new Blob([plaintext], { type: fileData.mimeType });
const url = URL.createObjectURL(blob);

Expand Down
Loading
Loading