aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--client/public/assets/icons/ic_close.pngbin0 -> 534 bytes
-rw-r--r--client/src/components/Editor/Editor.jsx164
-rw-r--r--client/src/components/Modal/Modal.jsx25
-rw-r--r--client/src/components/Modal/Modal.module.css61
-rw-r--r--client/src/utils/encryption.js81
-rw-r--r--server/main.go9
6 files changed, 310 insertions, 30 deletions
diff --git a/client/public/assets/icons/ic_close.png b/client/public/assets/icons/ic_close.png
new file mode 100644
index 0000000..90d5759
--- /dev/null
+++ b/client/public/assets/icons/ic_close.png
Binary files differ
diff --git a/client/src/components/Editor/Editor.jsx b/client/src/components/Editor/Editor.jsx
index 868036f..e1675fc 100644
--- a/client/src/components/Editor/Editor.jsx
+++ b/client/src/components/Editor/Editor.jsx
@@ -4,11 +4,16 @@ import Prism from "prismjs";
import styles from "./Editor.module.css";
import "../prism-themes/prism-gruvbox-dark.css";
import "../prism-themes/prism-line-numbers.css";
-import {
- BASE_URL,
- URL_REGEX,
-} from "../../utils/constants";
+import { BASE_URL, URL_REGEX } from "../../utils/constants";
import Header from "../Header/Header";
+import {
+ generateAESKey,
+ keyToString,
+ stringToKey,
+ encryptAES,
+ decryptAES,
+} from "../../utils/encryption";
+import Modal from "../Modal/Modal";
const Editor = () => {
const { id } = useParams();
@@ -16,8 +21,10 @@ const Editor = () => {
const location = useLocation();
const [text, setText] = useState("");
const [language, setLanguage] = useState("none");
+ const [openModal, setOpenModal] = useState(false);
const textareaRef = useRef(null);
const lineNumberRef = useRef(null);
+ const queryParams = new URLSearchParams(location.search);
const handleTextChange = (event) => {
setText(event.target.value);
@@ -29,11 +36,56 @@ const Editor = () => {
}
};
- const handleClick = async () => {
+ const handleSaveClick = async () => {
if (!text) {
alert("Please enter some text!");
return;
}
+ if (URL_REGEX.test(text)) {
+ const response = await fetch(`${BASE_URL}/bin`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ language,
+ content: text,
+ }),
+ });
+ const data = await response.json();
+ if (response.ok) {
+ navigator.clipboard
+ .writeText(`${window.location.origin}/r/${data.id}`)
+ .then(
+ function () {
+ alert("Short URL copied to clipboard!");
+ },
+ function () {
+ try {
+ document.execCommand("copy");
+ alert("Short URL copied to clipboard!");
+ } catch (err) {
+ console.log("Oops, unable to copy");
+ }
+ },
+ );
+ }
+ navigate(`/${data.id}`);
+ return;
+ }
+ setOpenModal(true);
+ };
+
+ const handleSuccessClick = async () => {
+ setOpenModal(false);
+ const key = await generateAESKey();
+ const keyString = await keyToString(key);
+ const { encrypted, iv } = await encryptAES(text, key);
+ const encryptedBase64 = btoa(
+ String.fromCharCode.apply(null, new Uint8Array(encrypted)),
+ );
+ const ivBase64 = btoa(String.fromCharCode.apply(null, iv));
+
const response = await fetch(`${BASE_URL}/bin`, {
method: "POST",
headers: {
@@ -41,42 +93,68 @@ const Editor = () => {
},
body: JSON.stringify({
language,
- content: text,
+ content: encryptedBase64,
+ iv: ivBase64,
}),
});
const data = await response.json();
if (response.ok) {
- const isURL = URL_REGEX.test(text);
- if (isURL) {
- navigator.clipboard.writeText(`${BASE_URL}/r/${data.id}`).then(
+ navigator.clipboard
+ .writeText(`${window.location.origin}/${data.id}?key=${keyString}`)
+ .then(
function () {
- alert("Short URL copied to clipboard!");
+ navigator.clipboard.writeText(
+ `${window.location.origin}/${data.id}?key=${keyString}`,
+ );
+ alert("URL copied to clipboard!");
},
- function (err) {
+ function () {
try {
- var successful = document.execCommand("copy");
- alert("Short URL copied to clipboard!");
+ document.execCommand("copy");
+ alert("URL copied to clipboard!");
} catch (err) {
console.log("Oops, unable to copy");
}
},
);
- } else {
- navigator.clipboard.writeText(`${BASE_URL}/r/${data.id}`).then(
+ navigate(`/${data.id}?key=${keyString}`);
+ } else {
+ console.error(data);
+ }
+ };
+
+ const handleCancelClick = async () => {
+ setOpenModal(false);
+ const response = await fetch(`${BASE_URL}/bin`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ language,
+ content: text,
+ }),
+ });
+ const data = await response.json();
+ if (response.ok) {
+ navigator.clipboard
+ .writeText(`${window.location.origin}/${data.id}`)
+ .then(
function () {
- navigator.clipboard.writeText(`${BASE_URL}/${data.id}`);
+ navigator.clipboard.writeText(
+ `${window.location.origin}/${data.id}`,
+ );
alert("URL copied to clipboard!");
},
- function (err) {
+ function () {
try {
- var successful = document.execCommand("copy");
- alert("URL copied to clipboard!");
+ document.execCommand("copy");
+ alert("URL copied to clip`board!");
} catch (err) {
console.log("Oops, unable to copy");
}
},
);
- }
navigate(`/${data.id}`);
} else {
console.error(data);
@@ -96,12 +174,30 @@ const Editor = () => {
const response = await fetch(`${BASE_URL}/bin/${id}`);
const data = await response.json();
if (response.ok) {
- const isURL = URL_REGEX.test(data.content);
- if (isURL) {
- setText(`Your shortened URL: ${BASE_URL}/r/${id}`);
- } else {
+ if (data.iv) {
+ const keyString = queryParams.get("key");
+ const key = await stringToKey(keyString);
+ const encrypted = new Uint8Array(
+ atob(data.content)
+ .split("")
+ .map((char) => char.charCodeAt(0)),
+ ).buffer;
+ const ivArray = new Uint8Array(
+ atob(data.iv)
+ .split("")
+ .map((char) => char.charCodeAt(0)),
+ );
+ const decryptedContent = await decryptAES(encrypted, key, ivArray);
setLanguage(data.language);
- setText(data.content);
+ setText(decryptedContent);
+ } else {
+ const isURL = URL_REGEX.test(data.content);
+ if (isURL) {
+ setText(`Your shortened URL: ${window.location.origin}/r/${id}`);
+ } else {
+ setLanguage(data.language);
+ setText(data.content);
+ }
}
}
};
@@ -117,9 +213,24 @@ const Editor = () => {
return (
<>
<Header isSelectVisible={!id} onLanguageChange={handleLanguageChange} />
+ <Modal
+ openModal={openModal}
+ setOpenModal={setOpenModal}
+ onSuccessClick={() => {
+ handleSuccessClick();
+ }}
+ onCancelClick={() => {
+ handleCancelClick();
+ }}
+ />
<div className={styles.container}>
{!id && (
- <button className={styles.btn__save} onClick={handleClick}>
+ <button
+ className={styles.btn__save}
+ onClick={() => {
+ handleSaveClick();
+ }}
+ >
<img src="assets/icons/save.svg" className={styles.btn__icon} />
</button>
)}
@@ -150,3 +261,4 @@ const Editor = () => {
};
export default Editor;
+
diff --git a/client/src/components/Modal/Modal.jsx b/client/src/components/Modal/Modal.jsx
new file mode 100644
index 0000000..54c816e
--- /dev/null
+++ b/client/src/components/Modal/Modal.jsx
@@ -0,0 +1,25 @@
+import React from "react";
+import styles from "./Modal.module.css";
+
+const Modal = ({ openModal, setOpenModal, onSuccessClick, onCancelClick }) => {
+ return (
+ <div className={`${styles.background} ${openModal && styles.active}`}>
+ <div className={styles.container}>
+ <button
+ className={styles.container__close}
+ onClick={() => setOpenModal(false)}
+ >
+ <img src="assets/icons/ic_close.png" className={styles.close__img} />
+ </button>
+ <p className={styles.container__title}>Encrypt content?</p>
+ <div className={styles.container__actions}>
+ <button onClick={onSuccessClick}>Yes</button>
+ <button onClick={onCancelClick}>No</button>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default Modal;
+
diff --git a/client/src/components/Modal/Modal.module.css b/client/src/components/Modal/Modal.module.css
new file mode 100644
index 0000000..9d79c62
--- /dev/null
+++ b/client/src/components/Modal/Modal.module.css
@@ -0,0 +1,61 @@
+.background {
+ display: none;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ background-color: #00000062;
+ z-index: 10;
+}
+
+.active {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.container {
+ top: 0;
+ left: 0;
+ background-color: var(--color-dark);
+ padding: 40px;
+ width: 300px;
+ border-radius: 10px;
+ z-index: 11;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.container__close {
+ cursor: pointer;
+ background: none;
+ border: none;
+ margin-left: auto;
+}
+
+.close__img {
+ width: 20px;
+}
+
+.container__actions {
+ margin-top: 20px;
+ display: flex;
+ width: 100%;
+ justify-content: space-around;
+}
+
+.container__actions button {
+ padding: 10px 20px;
+ cursor: pointer;
+ width: 12rem;
+ text-align: center;
+ background-color: var(--color-yellow);
+ color: var(--color-dark);
+ border-radius: 10px;
+ font-size: 1rem;
+ margin: 0 10px;
+}
+
diff --git a/client/src/utils/encryption.js b/client/src/utils/encryption.js
new file mode 100644
index 0000000..e0e6c33
--- /dev/null
+++ b/client/src/utils/encryption.js
@@ -0,0 +1,81 @@
+async function generateAESKey() {
+ try {
+ const key = await window.crypto.subtle.generateKey(
+ {
+ name: "AES-GCM",
+ length: 256,
+ },
+ true,
+ ["encrypt", "decrypt"],
+ );
+ return key;
+ } catch (error) {
+ console.error("Error generating AES key:", error);
+ }
+}
+
+async function keyToString(key) {
+ try {
+ const exportedKey = await window.crypto.subtle.exportKey("raw", key);
+ const keyString = btoa(String.fromCharCode(...new Uint8Array(exportedKey)));
+ return keyString.replace(/\+/g, "-").replace(/\//g, "_");
+ } catch (error) {
+ console.error("Error converting key to string:", error);
+ }
+}
+
+async function stringToKey(keyString) {
+ try {
+ const originalKeyString = keyString.replace(/-/g, "+").replace(/_/g, "/");
+ const buffer = Uint8Array.from(atob(originalKeyString), (c) =>
+ c.charCodeAt(0),
+ ).buffer;
+
+ const key = await window.crypto.subtle.importKey(
+ "raw",
+ buffer,
+ { name: "AES-GCM", length: 256 },
+ true,
+ ["encrypt", "decrypt"],
+ );
+
+ return key;
+ } catch (error) {
+ console.error("Error converting string to key:", error);
+ }
+}
+
+async function encryptAES(plaintext, key) {
+ try {
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
+ const encrypted = await window.crypto.subtle.encrypt(
+ {
+ name: "AES-GCM",
+ iv: iv,
+ },
+ key,
+ new TextEncoder().encode(plaintext),
+ );
+ return { encrypted, iv };
+ } catch (error) {
+ console.error("Error encrypting:", error);
+ }
+}
+
+async function decryptAES(encrypted, key, iv) {
+ try {
+ const decrypted = await window.crypto.subtle.decrypt(
+ {
+ name: "AES-GCM",
+ iv: iv,
+ },
+ key,
+ encrypted,
+ );
+ return new TextDecoder().decode(decrypted);
+ } catch (error) {
+ console.error("Error decrypting:", error);
+ }
+}
+
+export { generateAESKey, keyToString, stringToKey, encryptAES, decryptAES };
diff --git a/server/main.go b/server/main.go
index 1ce935c..5aaeba7 100644
--- a/server/main.go
+++ b/server/main.go
@@ -22,6 +22,7 @@ var port string
type Bin struct {
Content string `json:"content"`
Language string `json:"language"`
+ IV string `json:"iv"`
}
const (
@@ -110,19 +111,19 @@ func redirectToURL(echoContext echo.Context) error {
}
func createTable() error {
- _, err := db.Exec("CREATE TABLE IF NOT EXISTS bins (id TEXT PRIMARY KEY, content TEXT, language TEXT)")
+ _, err := db.Exec("CREATE TABLE IF NOT EXISTS bins (id TEXT PRIMARY KEY, content TEXT, language TEXT, iv TEXT)")
return err
}
func getBinById(id string) (Bin, error) {
- row := db.QueryRow("SELECT content, language FROM bins WHERE id = ?", id)
+ row := db.QueryRow("SELECT content, language, iv FROM bins WHERE id = ?", id)
bin := Bin{}
- err := row.Scan(&bin.Content, &bin.Language)
+ err := row.Scan(&bin.Content, &bin.Language, &bin.IV)
return bin, err
}
func saveBin(id string, bin Bin) error {
- _, err := db.Exec("INSERT INTO bins (id, content, language) VALUES (?, ?, ?)", id, bin.Content, bin.Language)
+ _, err := db.Exec("INSERT INTO bins (id, content, language, iv) VALUES (?, ?, ?, ?)", id, bin.Content, bin.Language, bin.IV)
return err
}