mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-11-13 03:22:37 +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.",
|
"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"
|
"title": "String URL decoder"
|
||||||
},
|
},
|
||||||
|
|
||||||
"inputTitle": "Input String(URL-escaped)",
|
"inputTitle": "Input String(URL-escaped)",
|
||||||
"resultTitle": "Output string"
|
"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 stringRemoveDuplicateLines } from './remove-duplicate-lines/meta';
|
||||||
import { tool as stringRotate } from './rotate/meta';
|
import { tool as stringRotate } from './rotate/meta';
|
||||||
import { tool as stringQuote } from './quote/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