mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-11-10 01:59:54 +05:30
feat(string): Created PrivateBin tool
This commit is contained in:
parent
fc18dc0dc0
commit
1984733c86
7 changed files with 489 additions and 1 deletions
|
|
@ -280,8 +280,13 @@
|
|||
"longDescription": "This tool URL-decodes a previously URL-encoded string. URL-decoding is the inverse operation of URL-encoding. All percent-encoded characters get decoded to characters that you can understand. Some of the most well known percent-encoded values are %20 for a space, %3a for a colon, %2f for a slash, and %3f for a question mark. The two digits following the percent sign are character's char code values in hex.",
|
||||
"title": "String URL decoder"
|
||||
},
|
||||
|
||||
"inputTitle": "Input String(URL-escaped)",
|
||||
"resultTitle": "Output string"
|
||||
},
|
||||
"privatebin": {
|
||||
"title": "PrivateBin",
|
||||
"description": "A secure paste service for sharing text content with encryption and expiration options.",
|
||||
"shortDescription": "Share text securely with encryption and expiration",
|
||||
"longDescription": "PrivateBin is a secure paste service that allows you to share text content with others. Your content is encrypted and can be configured with expiration times and burn-after-reading options. Perfect for sharing sensitive information, code snippets, or any text content that needs to be shared temporarily and securely."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { tool as stringPrivatebin } from './privatebin/meta';
|
||||
import { tool as stringRemoveDuplicateLines } from './remove-duplicate-lines/meta';
|
||||
import { tool as stringRotate } from './rotate/meta';
|
||||
import { tool as stringQuote } from './quote/meta';
|
||||
|
|
|
|||
269
src/pages/tools/string/privatebin/index.tsx
Normal file
269
src/pages/tools/string/privatebin/index.tsx
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
TextField,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Typography,
|
||||
Alert,
|
||||
Paper,
|
||||
Tabs,
|
||||
Tab
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { InitialValuesType } from './types';
|
||||
import { createPaste, retrievePaste } from './service';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`privatebin-tabpanel-${index}`}
|
||||
aria-labelledby={`privatebin-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
expiration: '1day',
|
||||
burnAfterReading: false,
|
||||
password: ''
|
||||
};
|
||||
|
||||
export default function PrivateBin({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const { t } = useTranslation('string');
|
||||
const [input, setInput] = useState('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [retrieveId, setRetrieveId] = useState('');
|
||||
const [retrievePassword, setRetrievePassword] = useState('');
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
setResult('');
|
||||
setError('');
|
||||
};
|
||||
|
||||
const compute = async (values: InitialValuesType, input: string) => {
|
||||
if (!input.trim()) return;
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const pasteId = await createPaste(input, values);
|
||||
setResult(
|
||||
`Paste created successfully!\n\nPaste ID: ${pasteId}\n\nShare this ID with others to retrieve the content.`
|
||||
);
|
||||
} catch (err) {
|
||||
setError(`Failed to create paste: ${err}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetrieve = async () => {
|
||||
if (!retrieveId.trim() || !retrievePassword.trim()) {
|
||||
setError('Please enter both paste ID and password');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const content = await retrievePaste(retrieveId, retrievePassword);
|
||||
setResult(`Retrieved content:\n\n${content}`);
|
||||
} catch (err) {
|
||||
setError(`Failed to retrieve paste: ${err}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Paste Settings',
|
||||
component: (
|
||||
<Box>
|
||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||
<InputLabel>Expiration</InputLabel>
|
||||
<Select
|
||||
value={values.expiration}
|
||||
onChange={(e) =>
|
||||
updateField(
|
||||
'expiration',
|
||||
e.target.value as InitialValuesType['expiration']
|
||||
)
|
||||
}
|
||||
label="Expiration"
|
||||
>
|
||||
<MenuItem value="1hour">1 Hour</MenuItem>
|
||||
<MenuItem value="1day">1 Day</MenuItem>
|
||||
<MenuItem value="1week">1 Week</MenuItem>
|
||||
<MenuItem value="1month">1 Month</MenuItem>
|
||||
<MenuItem value="never">Never</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password (optional)"
|
||||
value={values.password}
|
||||
onChange={(e) => updateField('password', e.target.value)}
|
||||
type="password"
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.burnAfterReading}
|
||||
onChange={(e) =>
|
||||
updateField('burnAfterReading', e.target.checked)
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Burn after reading"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const renderCustomInput = (values: InitialValuesType) => (
|
||||
<Box>
|
||||
<Paper sx={{ width: '100%', mb: 2 }}>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={handleTabChange}
|
||||
aria-label="privatebin tabs"
|
||||
>
|
||||
<Tab label="Create Paste" />
|
||||
<Tab label="Retrieve Paste" />
|
||||
</Tabs>
|
||||
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Create a new paste
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Enter your text below and configure the settings. Your content
|
||||
will be encrypted and stored securely.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<ToolTextInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title="Paste Content"
|
||||
placeholder="Enter your text here..."
|
||||
/>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => compute(values, input)}
|
||||
disabled={loading || !input.trim()}
|
||||
fullWidth
|
||||
>
|
||||
{loading ? 'Creating...' : 'Create Paste'}
|
||||
</Button>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Retrieve a paste
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Enter the paste ID and password to retrieve the content.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Paste ID"
|
||||
value={retrieveId}
|
||||
onChange={(e) => setRetrieveId(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password"
|
||||
value={retrievePassword}
|
||||
onChange={(e) => setRetrievePassword(e.target.value)}
|
||||
type="password"
|
||||
sx={{ mb: 3 }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleRetrieve}
|
||||
disabled={loading || !retrieveId.trim() || !retrievePassword.trim()}
|
||||
fullWidth
|
||||
>
|
||||
{loading ? 'Retrieving...' : 'Retrieve Paste'}
|
||||
</Button>
|
||||
</TabPanel>
|
||||
</Paper>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={renderCustomInput(initialValues)}
|
||||
resultComponent={<ToolTextResult value={result} />}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
compute={compute}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
toolInfo={{
|
||||
title: `What is ${title}?`,
|
||||
description:
|
||||
longDescription ||
|
||||
'A secure paste service for sharing text content with encryption and expiration options.'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
15
src/pages/tools/string/privatebin/meta.ts
Normal file
15
src/pages/tools/string/privatebin/meta.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:privatebin.title',
|
||||
description: 'string:privatebin.description',
|
||||
shortDescription: 'string:privatebin.shortDescription',
|
||||
longDescription: 'string:privatebin.longDescription'
|
||||
},
|
||||
path: 'privatebin',
|
||||
icon: 'material-symbols:content-paste',
|
||||
keywords: ['privatebin', 'share', 'text', 'secure', 'paste', 'temporary'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { expect, describe, it } from 'vitest';
|
||||
// import { main } from './service';
|
||||
//
|
||||
// describe('privatebin', () => {
|
||||
//
|
||||
// })
|
||||
178
src/pages/tools/string/privatebin/service.ts
Normal file
178
src/pages/tools/string/privatebin/service.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { InitialValuesType, PasteData } from './types';
|
||||
|
||||
// Simple encryption using Web Crypto API
|
||||
async function encryptText(text: string, password: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(text);
|
||||
|
||||
// Generate key from password
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(password),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveBits', 'deriveKey']
|
||||
);
|
||||
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: encoder.encode('privatebin-salt'),
|
||||
iterations: 100000,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true,
|
||||
['encrypt']
|
||||
);
|
||||
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
data
|
||||
);
|
||||
|
||||
return (
|
||||
btoa(String.fromCharCode(...new Uint8Array(encrypted))) +
|
||||
'.' +
|
||||
btoa(String.fromCharCode(...iv))
|
||||
);
|
||||
}
|
||||
|
||||
async function decryptText(
|
||||
encryptedData: string,
|
||||
password: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const [encrypted, iv] = encryptedData.split('.');
|
||||
const encryptedBytes = new Uint8Array(
|
||||
atob(encrypted)
|
||||
.split('')
|
||||
.map((c) => c.charCodeAt(0))
|
||||
);
|
||||
const ivBytes = new Uint8Array(
|
||||
atob(iv)
|
||||
.split('')
|
||||
.map((c) => c.charCodeAt(0))
|
||||
);
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(password),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveBits', 'deriveKey']
|
||||
);
|
||||
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: encoder.encode('privatebin-salt'),
|
||||
iterations: 100000,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true,
|
||||
['decrypt']
|
||||
);
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: ivBytes },
|
||||
key,
|
||||
encryptedBytes
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(decrypted);
|
||||
} catch (error) {
|
||||
throw new Error('Failed to decrypt: Invalid password or corrupted data');
|
||||
}
|
||||
}
|
||||
|
||||
function generateId(): string {
|
||||
return (
|
||||
Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15)
|
||||
);
|
||||
}
|
||||
|
||||
function getExpirationTime(expiration: string): number {
|
||||
const now = Date.now();
|
||||
switch (expiration) {
|
||||
case '1hour':
|
||||
return now + 60 * 60 * 1000;
|
||||
case '1day':
|
||||
return now + 24 * 60 * 60 * 1000;
|
||||
case '1week':
|
||||
return now + 7 * 24 * 60 * 60 * 1000;
|
||||
case '1month':
|
||||
return now + 30 * 24 * 60 * 60 * 1000;
|
||||
case 'never':
|
||||
return 0;
|
||||
default:
|
||||
return now + 24 * 60 * 60 * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createPaste(
|
||||
content: string,
|
||||
options: InitialValuesType
|
||||
): Promise<string> {
|
||||
const pasteData: PasteData = {
|
||||
id: generateId(),
|
||||
content: await encryptText(content, options.password || 'default'),
|
||||
expiration: options.expiration,
|
||||
burnAfterReading: options.burnAfterReading,
|
||||
password: options.password,
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
// Store in localStorage (in a real app, this would be server-side)
|
||||
const pastes = JSON.parse(localStorage.getItem('privatebin_pastes') || '{}');
|
||||
pastes[pasteData.id] = pasteData;
|
||||
localStorage.setItem('privatebin_pastes', JSON.stringify(pastes));
|
||||
|
||||
return pasteData.id;
|
||||
}
|
||||
|
||||
export async function retrievePaste(
|
||||
id: string,
|
||||
password: string
|
||||
): Promise<string> {
|
||||
const pastes = JSON.parse(localStorage.getItem('privatebin_pastes') || '{}');
|
||||
const paste = pastes[id];
|
||||
|
||||
if (!paste) {
|
||||
throw new Error('Paste not found');
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if (
|
||||
paste.expiration !== 'never' &&
|
||||
paste.createdAt + getExpirationTime(paste.expiration) < Date.now()
|
||||
) {
|
||||
delete pastes[id];
|
||||
localStorage.setItem('privatebin_pastes', JSON.stringify(pastes));
|
||||
throw new Error('Paste has expired');
|
||||
}
|
||||
|
||||
const decryptedContent = await decryptText(paste.content, password);
|
||||
|
||||
// Delete if burn after reading
|
||||
if (paste.burnAfterReading) {
|
||||
delete pastes[id];
|
||||
localStorage.setItem('privatebin_pastes', JSON.stringify(pastes));
|
||||
}
|
||||
|
||||
return decryptedContent;
|
||||
}
|
||||
|
||||
export function main(
|
||||
input: string,
|
||||
options: InitialValuesType
|
||||
): Promise<string> {
|
||||
return createPaste(input, options);
|
||||
}
|
||||
14
src/pages/tools/string/privatebin/types.ts
Normal file
14
src/pages/tools/string/privatebin/types.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export type InitialValuesType = {
|
||||
expiration: '1hour' | '1day' | '1week' | '1month' | 'never';
|
||||
burnAfterReading: boolean;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export interface PasteData {
|
||||
id: string;
|
||||
content: string;
|
||||
expiration: string;
|
||||
burnAfterReading: boolean;
|
||||
password?: string;
|
||||
createdAt: number;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue