diff --git a/public/locales/en/string.json b/public/locales/en/string.json
index 2994890..d5c7f96 100644
--- a/public/locales/en/string.json
+++ b/public/locales/en/string.json
@@ -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."
}
}
diff --git a/src/pages/tools/string/index.ts b/src/pages/tools/string/index.ts
index bd645f7..5ae998a 100644
--- a/src/pages/tools/string/index.ts
+++ b/src/pages/tools/string/index.ts
@@ -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';
diff --git a/src/pages/tools/string/privatebin/index.tsx b/src/pages/tools/string/privatebin/index.tsx
new file mode 100644
index 0000000..8c3d84a
--- /dev/null
+++ b/src/pages/tools/string/privatebin/index.tsx
@@ -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 (
+
+ {value === index && {children} }
+
+ );
+}
+
+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('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ 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 = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: 'Paste Settings',
+ component: (
+
+
+ Expiration
+
+ updateField(
+ 'expiration',
+ e.target.value as InitialValuesType['expiration']
+ )
+ }
+ label="Expiration"
+ >
+ 1 Hour
+ 1 Day
+ 1 Week
+ 1 Month
+ Never
+
+
+
+ updateField('password', e.target.value)}
+ type="password"
+ sx={{ mb: 2 }}
+ />
+
+
+ updateField('burnAfterReading', e.target.checked)
+ }
+ />
+ }
+ label="Burn after reading"
+ />
+
+ )
+ }
+ ];
+
+ const renderCustomInput = (values: InitialValuesType) => (
+
+
+
+
+
+
+
+
+
+
+ Create a new paste
+
+
+ Enter your text below and configure the settings. Your content
+ will be encrypted and stored securely.
+
+
+
+
+
+
+ compute(values, input)}
+ disabled={loading || !input.trim()}
+ fullWidth
+ >
+ {loading ? 'Creating...' : 'Create Paste'}
+
+
+
+
+
+
+
+ Retrieve a paste
+
+
+ Enter the paste ID and password to retrieve the content.
+
+
+
+ setRetrieveId(e.target.value)}
+ sx={{ mb: 2 }}
+ />
+
+ setRetrievePassword(e.target.value)}
+ type="password"
+ sx={{ mb: 3 }}
+ />
+
+
+ {loading ? 'Retrieving...' : 'Retrieve Paste'}
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+
+ return (
+ }
+ 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.'
+ }}
+ />
+ );
+}
diff --git a/src/pages/tools/string/privatebin/meta.ts b/src/pages/tools/string/privatebin/meta.ts
new file mode 100644
index 0000000..94702ff
--- /dev/null
+++ b/src/pages/tools/string/privatebin/meta.ts
@@ -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'))
+});
diff --git a/src/pages/tools/string/privatebin/privatebin.service.test.ts b/src/pages/tools/string/privatebin/privatebin.service.test.ts
new file mode 100644
index 0000000..116d3be
--- /dev/null
+++ b/src/pages/tools/string/privatebin/privatebin.service.test.ts
@@ -0,0 +1,6 @@
+import { expect, describe, it } from 'vitest';
+// import { main } from './service';
+//
+// describe('privatebin', () => {
+//
+// })
diff --git a/src/pages/tools/string/privatebin/service.ts b/src/pages/tools/string/privatebin/service.ts
new file mode 100644
index 0000000..49bcdee
--- /dev/null
+++ b/src/pages/tools/string/privatebin/service.ts
@@ -0,0 +1,178 @@
+import { InitialValuesType, PasteData } from './types';
+
+// Simple encryption using Web Crypto API
+async function encryptText(text: string, password: string): Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ return createPaste(input, options);
+}
diff --git a/src/pages/tools/string/privatebin/types.ts b/src/pages/tools/string/privatebin/types.ts
new file mode 100644
index 0000000..b4822e1
--- /dev/null
+++ b/src/pages/tools/string/privatebin/types.ts
@@ -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;
+}