diff --git a/.github/workflows/build_and_release.yml b/.github/workflows/build_and_release.yml index bbc282f..16c078b 100644 --- a/.github/workflows/build_and_release.yml +++ b/.github/workflows/build_and_release.yml @@ -12,7 +12,7 @@ jobs: - name: ⚙️ Setup node uses: actions/setup-node@v3 with: - node-version: '20.x' + node-version: "20.x" - name: 🔃 Checkout uses: actions/checkout@v2 @@ -27,11 +27,11 @@ jobs: uses: montudor/action-zip@v1 - name: Zip dist files 📦 - run: zip -qq -r dist.zip dist - + run: cd dist && zip -r ../dist.zip * + - name: Create release 📦 uses: "marvinpinto/action-automatic-releases@latest" with: repo_token: ${{ secrets.ChatGuard_PAT }} prerelease: true - files: ./*.zip \ No newline at end of file + files: ./*.zip diff --git a/.gitignore b/.gitignore index 2e91049..77ddc9c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ dist dist-ssr *.local .env - +README_MOZILA.md # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/LICENSE.md b/LICENSE.md index c61b663..cd46960 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright [yyyy] [name of copyright owner] +Copyright 2024 mostafa kheibary Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 89062d2..9c9971e 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,48 @@ +# ChatGuard -> " Freedom of speech is not just a right; it's a cornerstone of democracy. In its purest form, it allows ideas to clash, perspectives to evolve, and societies to progress. Yet, with this great power comes the responsibility to navigate the delicate balance between expression and respect. How do we safeguard this essential freedom while fostering a culture of inclusivity and understanding? " -> -

- -

+Simple and easy to use browser extension that allow end to end encryption on web messenger -# ChatGuard +![demo](https://github.com/PrivacyForge/ChatGuard/assets/58364608/6a51e555-cb58-4e88-960b-6e1c029d4cce) -Simple and easy to use browser extension that allow end to end encryption on [telegram/k](https://telegram.com/k), [telegram/a](https://telegram.com/a), [twitter](https://twitter.com), [bale](https://web.bale.ai/chat) ,[soroush](https://web.splus.ir/), [eitaa](https://web.eitaa.com/) +## Features -### ChatGuard is a global solution to make almost any standard web messenger to be end to end encrypted +- End-to-End Encryption (E2E): Enjoy secure and private conversations without compromising your data. +- Cross-Application Compatibility: While currently limited to Bale Messenger during the beta phase, ChatGuard aims to extend its support to a wide range of messaging applications. +- Serverless: No need of server for exchanging public key,chatGuard uses the Messenger that running on as messaging service to transfer public keys. ## How to use Chat Guard ? -1. Download and install the extension with [official documentation](https://chat-guard.vercel.app/getting-started/installation) +
+ + +
+
+ +1. Download and install the extension with [official documentation](https://chat-guard.vercel.app/getting-started/how-to-install) +1. Open one of the supported messenger +1. Now fallow the documentation about starting a secure conversation in [how to use documentation](https://chat-guard.vercel.app/getting-started/how-to-use) -1. Open one of the [supported messenger](https://chat-guard.vercel.app/getting-started/support) -1. Both user must use the action menu and click to the "Create Handshake" to initiate a encryption handshake -1. After the handshake complete successfully, you can enjoy a safe chat :) +## How Chat Guard Work? -## How Chat Guard encrypt the messages? +ChatGuard uses a hybrid encryption method that include a RSA handsake and after that sending every message as encrypted packet with unique secret key that encrypted by the user public key. -- We use hybrid (Symmetric and Asymmetric) encryption -- first both user initiate a handshake, the handshake contain user public key -- after the handshake, when each user want to send a message this proccess take place : +- for more information read the documentation for [how it work](https://chat-guard.vercel.app/encryption/introduction) -1. generate a secret key -2. encrypt the secret key with both public keys -3. encrypted the message with the secret key -4. append the encrypted secret keys and encrypted message to one singular message and send it to the user +## Special Thanks -#### Now other user can decrypt the secret key with their own private key and using the decrypted secret key, user can decrypt the actual message +to [Arman](https://github.com/ArmanTaheriGhaleTaki) that inspired me to embark on this project and provided me with the idea to pursue. ## Donation I appreciate every donation for this project every donation will be spend on growing the project and releasing it on different platform like safari, etc. +Ethereum wallet (eth): + +``` +0x4850d6B360d0fA6bb6ED1Df240f46dE08A7aDF0a +``` + +Coffeebede: + diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 1a7b783..beed5f4 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,46 +1,61 @@ import { defineConfig } from "vitepress"; export default defineConfig({ - title: "Chat Guard", - description: "ChatGuard is a browser extension designed to make any messenger app to End to End encrypted", + title: "ChatGuard", + description: "ChatGuard is a browser extension designed to enable End to End encryption to your favorite messenger", appearance: "dark", cleanUrls: true, lang: "en-US", - head: [["link", { rel: "icon", href: "../public/images/logo.svg" }]], + sitemap: { + hostname: "https://chat-guard.vercel.app", + }, + head: [ + ["link", { rel: "icon", href: "/images/logo.svg" }], + ["script", { async: "", src: "https://www.googletagmanager.com/gtag/js?id=G-J084XJ7N2C" }], + [ + "script", + {}, + `window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', 'G-J084XJ7N2C');`, + ], + ], + locales: { + root: { + label: "English", + lang: "en", + }, + fa: { + label: "فارسی", + lang: "fa", + link: "/fa", + }, + }, themeConfig: { - logo: "../public/images/logo.svg", + logo: "/images/logo.svg", siteTitle: "Chat Guard", nav: [ { text: "Home", link: "/" }, - { text: "Download & Install", link: "/getting-started/installation" }, - { text: "Documentation", link: "/api/cipher" }, + { text: "Download & Install", link: "/getting-started/how-to-install" }, ], - sidebar: [ { text: "Getting Started", items: [ { - text: "Installation", - link: "/getting-started/installation", + text: "How to install", + link: "/getting-started/how-to-install", }, { text: "How to use", - link: "/getting-started/usage", + link: "/getting-started/how-to-use", }, ], }, { text: "Encryption", - items: [ - { text: "Introduction", link: "/encryption/introduction" }, - { text: "(DRSAP) Encryption Process", link: "/encryption/process" }, - ], - }, - { - text: "API", - collapsed: true, - items: [{ text: "Cipher", link: "/api/cipher" }], + items: [{ text: "Introduction", link: "/encryption/introduction" }], }, { text: "Contribution", @@ -52,10 +67,7 @@ export default defineConfig({ }, ], - socialLinks: [ - { icon: "github", link: "https://github.com/PrivacyForge/ChatGuard" }, - { icon: "twitter", link: "" }, - ], + socialLinks: [{ icon: "github", link: "https://github.com/PrivacyForge/ChatGuard" }], footer: { message: "Released under the Apache-2.0 License.", diff --git a/docs/.vitepress/theme/index.css b/docs/.vitepress/theme/index.css index 215dadc..7589cad 100644 --- a/docs/.vitepress/theme/index.css +++ b/docs/.vitepress/theme/index.css @@ -1 +1,9 @@ @import url("https://fonts.googleapis.com/css2?family=Kranky&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100..900&display=swap"); + +*:lang(fa) { + font-family: "Vazirmatn", sans-serif; +} +.content-container:lang(fa) { + direction: rtl; +} diff --git a/docs/api/cipher.md b/docs/api/cipher.md deleted file mode 100644 index 1a3c36b..0000000 --- a/docs/api/cipher.md +++ /dev/null @@ -1,134 +0,0 @@ -# Cipher Class Documentation - -## Overview - -The `Cipher` class provides encryption and decryption functionality using the DRSAP (Double RSA with AES Payload) algorithm. It utilizes the [node-forge](https://github.com/digitalbazaar/forge) library for cryptographic operations and interacts with the Chrome Storage API for storing and retrieving data. - -## Class Structure - -### Constructor - -```typescript -constructor(storage: LocalStorage, config: Config) -``` - -- **Parameters:** - - `storage`: An instance of `LocalStorage` for managing local storage. - - `config`: An object of type `Config` containing configuration settings. - -### Methods - -#### `createDRSAP(message: string, to: string): Promise` - -Encrypts a message using the DRSAP algorithm and returns the encrypted template. - -- **Parameters:** - - - `message`: The message to be encrypted. - - `to`: The recipient's identifier. - -- **Returns:** A Promise that resolves to the encrypted DRSAP template or `null` if the recipient's public key is not available. - -#### `resolveDRSAP(packet: string): Promise` - -Decrypts a DRSAP-encrypted packet and returns the original message. - -- **Parameters:** - - - `packet`: The DRSAP-encrypted packet. - -- **Returns:** A Promise that resolves to the decrypted message or `null` if decryption fails. - -#### `createDRSAPHandshake(to: string): Promise` - -Creates a DRSAP handshake packet for establishing a secure connection. - -- **Parameters:** - - - `to`: The recipient's identifier. - -- **Returns:** A Promise that resolves to the DRSAP handshake packet. - -#### `resolveDRSAPHandshake(packet: string, from: string): Promise` - -Resolves a DRSAP handshake packet and validates the sender's public key. - -- **Parameters:** - - - `packet`: The DRSAP handshake packet. - - `from`: The sender's identifier. - -- **Returns:** A Promise that resolves to an boolean packet or `undefined` if validation fails. - -#### `static validatePublicPem(pem: string): boolean` - -Validates a public key in PEM format. - -- **Parameters:** - - - `pem`: The public key in PEM format. - -- **Returns:** `true` if the public key is valid, `false` otherwise. - -#### `static validatePrivatePem(pem: string): boolean` - -Validates a private key in PEM format. - -- **Parameters:** - - - `pem`: The private key in PEM format. - -- **Returns:** `true` if the private key is valid, `false` otherwise. - -#### `encryptAES(message: string, secretKey: string): Promise` - -Encrypts a message using AES-CBC encryption with a given secret key. - -- **Parameters:** - - - `message`: The message to be encrypted. - - `secretKey`: The secret key for encryption. - -- **Returns:** A Promise that resolves to the encrypted message. - -#### `decryptAES(encryptedMessage: string, secretKey: string): Promise` - -Decrypts an AES-CBC-encrypted message with a given secret key. - -- **Parameters:** - - - `encryptedMessage`: The encrypted message. - - `secretKey`: The secret key for decryption. - -- **Returns:** A Promise that resolves to the decrypted message. - -## Example Usage - -```typescript -import { Cipher } from "./Cipher"; -import { chromeStorage } from "src/store"; -import { Config } from "src/types/Config"; - -const storage = new LocalStorage(); // assuming LocalStorage class is available -const config: Config = { - // provide configuration settings -}; - -const cipher = new Cipher(storage, config); - -// Example: Creating DRSAP -const encryptedTemplate = await cipher.createDRSAP("Hello, World!", "recipientId"); -console.log("Encrypted Template:", encryptedTemplate); - -// Example: Resolving DRSAP -const decryptedMessage = await cipher.resolveDRSAP(encryptedTemplate); -console.log("Decrypted Message:", decryptedMessage); -``` - -## Dependencies - -- [node-forge](https://github.com/digitalbazaar/forge): A JavaScript implementation of cryptographic standards. - -## Disclaimer - -This documentation assumes a certain environment and dependencies. Ensure that the required libraries and APIs are available and properly configured for the class to function as intended. diff --git a/docs/encryption/introduction.md b/docs/encryption/introduction.md index e2968fd..72624eb 100644 --- a/docs/encryption/introduction.md +++ b/docs/encryption/introduction.md @@ -2,7 +2,7 @@ ## Overview -![encryption overview](../public/images/encryption-1.png) +![encryption overview](/images/encryption-1.png) Our application prioritizes user privacy and security by implementing `end-to-end` encryption. This document outlines the hybrid encryption technique employed, combining both `symmetric` and `asymmetric` encryption methods. diff --git a/docs/encryption/process.md b/docs/encryption/process.md deleted file mode 100644 index 8ede3f6..0000000 --- a/docs/encryption/process.md +++ /dev/null @@ -1,43 +0,0 @@ -# DRSAP Encryption Documentation - -## Introduction - -DRSAP (Dynamic RSA Packet) is a hybrid encryption protocol designed for secure communication within your application. It employs a modified version of hybrid encryption and includes metadata in both messages and handshakes to ensure secure and efficient communication. DRSAP utilizes a two-way handshake method to exchange public keys between users and encrypts messages uniquely for each recipient. - -## Overview of DRSAP Encryption - -### Hybrid Encryption - -DRSAP utilizes hybrid encryption, combining the strengths of symmetric and asymmetric encryption. Each message is encrypted with a unique secret key, and this secret key is then encrypted with the recipient's public key. This ensures that only the intended recipient, possessing the corresponding private key, can decrypt and access the original message. - -### Metadata in Messages and Handshakes - -To distinguish between handshakes and regular messages, a specific prefix is used. Handshake messages are identified by the `::HSH::` prefix, while regular encrypted messages are identified by `::cgm::`. Metadata such as timestamps and recipient user IDs are included in handshakes to enhance security and tracking. - -## DRSAP Handshake Content - -![handshake](../public/images/handshake.png) - -The DRSAP handshake consists of the following components: - -1. **Prefix:** `::HSH::` to identify the message as a handshake. -2. **Separator:** `__` for separating different parts of the handshake. -3. **Timestamp:** Indicates the time of the handshake. -4. **Separator:** `__` for further separation. -5. **To User ID:** The user ID of the recipient. -6. **Public Key PEM:** The public key of the sender in PEM format. - -## DRSAP Encrypted Message Content - -![packet](../public/images/Packet.png) - -The actual encrypted message in DRSAP consists of: - -1. **Prefix:** `::cgm::` to identify the message as encrypted. -2. **64-byte String:** Secret key encrypted with the recipient's public key. -3. **64-byte String:** Secret key encrypted with the sender's public key. -4. **Message:** Encrypted using the secret key. - -## Conclusion - -DRSAP provides a secure communication framework by combining the advantages of symmetric and asymmetric encryption. The use of unique secret keys for each message and the inclusion of metadata in handshakes enhance the overall security and reliability of communication within the application. diff --git a/docs/fa/getting-started/how-to-install.md b/docs/fa/getting-started/how-to-install.md new file mode 100644 index 0000000..da9302a --- /dev/null +++ b/docs/fa/getting-started/how-to-install.md @@ -0,0 +1,37 @@ +# آموزش نصب چت گارد + +به چت گارد خوش آمدید, داخل این صفحه نحوه نصب افزونه چت گارد را روی پلتفرم های مختلف قرار دادیم. + +## مرورگرهای پشتیبانی شده + +1. [کروم](#کروم) +2. [فایرفاکس](#فایرفاکس) +3. [کیوی (مرورگر اندروید)](#کیوی) + +## کروم + +[![chrome store](/images/chromeStore.svg)](https://chromewebstore.google.com/detail/chatguard-beta/fokigjblcpglhdmjimcpmjikdfmchccg) + +1. لینک بالا را باز کنید و بر روی "Add to Chrome" کلیک کنید. + +## فایرفاکس + +[![فروشگاه فایرفاکس](/images/firefoxStore.svg)](https://addons.mozilla.org/en-GB/firefox/addon/chatguard/) + +1. لینک بالا را باز کنید و بر روی "Add to Firefox" کلیک کنید. + +## کیوی + +1. فایل افزونه‌ی را از [صفحه دانلود](https://github.com/PrivacyForge/ChatGuard/releases) دانلود کنید. + +2. مرورگر Kiwi را در دستگاه موبایل خود باز کنید. + +3. بر روی منو در گوشه بالا و سمت راست کلیک کنید. + +4. بر روی "افزونه" کلیک کنید. + +5. مطمئن شوید که "حالت توسعه‌دهنده" فعال است. + +6. بر روی + (از .zip/...) کلیک کنید. + +7. فایل zip را که از مرحله 1 دانلود کرده‌اید انتخاب کنید. diff --git a/docs/fa/getting-started/how-to-use.md b/docs/fa/getting-started/how-to-use.md new file mode 100644 index 0000000..3c0f7a7 --- /dev/null +++ b/docs/fa/getting-started/how-to-use.md @@ -0,0 +1,34 @@ +# آموزش استفاده از چت گارد + +در این مرحله، یاد می‌گیریم که چگونه یک گفتگوی امن با گارد چت راه‌اندازی کنیم و از آن به درستی استفاده کنیم. + +## پیام‌رسان‌های پشتیبانی شده + +- [telegram/k](https://telegram.com/k) +- [telegram/a](https://telegram.com/a) +- [bale](https://web.bale.ai/chat) +- [soroush](https://web.splus.ir/) +- [eitaa](https://web.eitaa.com/) +- [shad](https://web.shad.ir/) +- [rubika](https://web.rubika.ir/) +- [igap](https://web.igap.net/) + +## ۱. نصب افزونه + +افزونه را با استفاده از [راهنمای نصب](https://github.com/PrivacyForge/ChatGuard/blob/main/docs/getting-started/how-to-install.md) نصب کنید. + +## ۲. باز کردن یکی از پیام‌رسان‌های پشتیبانی شده + +بعد از نصب موفق، باید یک دکمه قرمز در بالای پیام‌رسان ظاهر شود. + +![منوی عملیات](/images/action-menu.png) + +## ۳. انجام دادن هندشیک + +روی دکمه قرمز در یک گفتگو با یک شخص که همچنین افزونه را دارد کلیک کنید. پس از کلیک بر روی منوی عملیات، باید باز شود و می‌توانید یک دکمه "make handshake" را ببینید. شما و شخصی که می‌خواهید هندشیک بدهد باید روی دکمه کلیک کنید. + +## ۴. شروع گفتگو + +بعد از اتمام موفق هندشیک، دکمه قرمز باید به یک دکمه آبی تبدیل شود و شما قادر به ارسال پیام به دوست خود با رمزگذاری end-to-end خواهید بود. + +همچنین این diff --git a/docs/getting-started/how-to-install.md b/docs/getting-started/how-to-install.md new file mode 100644 index 0000000..b2062e6 --- /dev/null +++ b/docs/getting-started/how-to-install.md @@ -0,0 +1,39 @@ +# How to install ChatGuard + +Welcome to Chat Guard for. This guide will walk you through the steps to download and install the Chat Guard on various browsers and devices. + +## Supported Browser + +1. [Chrome](#chrome) + +2. [Mozilla Firefox](#firefox) + +3. [Kiwi (android browser)](#kiwi) + +## Chrome + +[![chrome store](/images/chromeStore.svg)](https://chromewebstore.google.com/detail/chatguard-beta/fokigjblcpglhdmjimcpmjikdfmchccg) + +1. Open the link above and Click on "Add to Chrome". + +## Firefox + +[![firefox store](/images/firefoxStore.svg)](https://addons.mozilla.org/en-GB/firefox/addon/chatguard/) + +1. Open the link above and Click on "Add to Firefox". + +## kiwi + +1. Download the latest extension file from [download page](https://github.com/PrivacyForge/ChatGuard/releases). + +2. Open Kiwi Browser on your mobile device. + +3. Click on the menu on top-right corner + +4. Click on "Extension" + +5. Make sure that "developer mode" is enabled + +6. Click on +(from .zip/...) + +7. select the zip file that you downloaded from the step 1 diff --git a/docs/getting-started/how-to-use.md b/docs/getting-started/how-to-use.md new file mode 100644 index 0000000..61a41d0 --- /dev/null +++ b/docs/getting-started/how-to-use.md @@ -0,0 +1,35 @@ +# How to use ChatGuard + +in this step we learn how to make a secure conversation with ChatGuard and how properly use it. + +## Supported Messenger + +- [telegram/k](https://telegram.com/k) +- [telegram/a](https://telegram.com/a) +- [bale](https://web.bale.ai/chat) +- [soroush](https://web.splus.ir/) +- [eitaa](https://web.eitaa.com/) +- [shad](https://web.shad.ir/) +- [rubika](https://web.rubika.ir/) +- [igap](https://web.igap.net/) + +## 1. Install the extension + +install the extension using [installation guid](https://github.com/PrivacyForge/ChatGuard/blob/main/docs/getting-started/how-to-install.md) + +## 2. Open one of supported messenger + +after a successful installation you should see a red button appear in the top of the messenger + +![action menu](/images/action-menu.png) + +## 3. Making a handshake + +click on the red button on a conversation with a person who have the extension as well +after clicking on the action menu, it should open and you can see a "make handshake" button +you and the person that you want to make a handshake with should click on the button + +## 4. Starts messaging + +after making handshake successfully ended the red button should turn into a blue button +and you be abel to message to your friend with end to end encryption diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md deleted file mode 100644 index 102e010..0000000 --- a/docs/getting-started/installation.md +++ /dev/null @@ -1,95 +0,0 @@ -# Chat Guard installation guide - -## Overview - -Welcome to Chat Guard for. This guide will walk you through the steps to download and install the Chat Guard on various browsers and devices. - -## Table of Contents - -1. [Chrome](#chrome) - - [From the Official Chrome Web Store](#from-the-official-chrome-web-store) - - [Sideload Extension (Using the Extension File)](#sideload-extension-using-the-extension-file) -2. [Firefox](#firefox) - - - [From the Official Mozilla Add-ons](#from-the-official-mozilla-add-ons) - - [Sideload Extension (Using the Extension File)](#sideload-extension-using-the-extension-file) - -3. [Safari](#safari) - - - [From the Official Safari Extension Gallery](#from-the-official-safari-extension-gallery) - - [Sideload Extension (Using the Extension File)](#sideload-extension-using-the-extension-file) - -4. [Mobile Browsers](#mobile-browsers) - - [Chrome Mobile](#chrome-mobile) - - [Kiwi Browser](#kiwi-browser) - - [Safari (iOS)](#safari-ios) - -## Chrome - -### From the Official Chrome Web Store - -[![chrome store](../public/images/chromeStore.svg)](https://chromewebstore.google.com/) - -1. Open [Chrome Web Store](https://chrome.google.com/webstore). - -2. Click on the "Add to Chrome" button. - -### Sideload Extension (Using the Extension File) - -1. Download the extension file (usually a .zip or .crx file) from [download page](https://github.com/PrivacyForge/ChatGuard/releases/tag/v0.5.3-beta). - -2. Open Chrome and go to `chrome://extensions/`. - -3. Enable "Developer mode" at the top-right corner. - -4. Click "Load unpacked" and select the folder containing the extension files. - -## Firefox - -### From the Official Mozilla Add-ons - -[![firefox store](../public/images/firefoxStore.svg)](https://addons.mozilla.org/en-US/firefox/) - -1. Open [Mozilla Add-ons](https://addons.mozilla.org/). - -2. Search for "[Your Browser Extension Name]". - -3. Click on "Add to Firefox". - -### Sideload Extension (Using the Extension File) - -1. This feature is not working anymore, so use the official store to install the extension. - -## Safari - -### From the Official Safari Extension Gallery - -[![apple app store](../public/images/appleStore.svg)](https://safari-extensions.apple.com/) - -1. Open [Safari Extensions](https://safari-extensions.apple.com/). - -2. Click "Install Now". - -### Sideload Extension (Using the Extension File) - -1. This feature is not working anymore, so use the official store to install the extension. - -## Mobile Browsers - -### Android (Kiwi Browser) - -1. Open Kiwi Browser on your mobile device. - -2. Visit the Chrome Web Store and search for "Chat Guard". - -3. Tap "Add to Chrome". - -### iOS (Safari) - -1. Open Safari on your iOS device. - -2. Go to the [official Safari Extensions](https://apps.apple.com/us/developer/apple-inc/id284417353). - -3. Search for "Chat Guard". - -4. Tap "Get" to download and install the extension. diff --git a/docs/getting-started/usage.md b/docs/getting-started/usage.md deleted file mode 100644 index af5eb2b..0000000 --- a/docs/getting-started/usage.md +++ /dev/null @@ -1,44 +0,0 @@ -# How to Chat safely with Chat Guard. - -### Prerequisites - -1. **Download and Install Extension** - - - Download and install `Chat Guard` using [official documentation](/getting-started/installation) - -2. **Restart Browser** - - Some browsers may require a restart after installing the extension. - -## How to Use and Encrypt a Conversation - -Chat Guard allows users to engage in `end-to-end` encrypted conversations. Both users need to have the extension installed for secure communication. - -### Initiating Secure Conversation - -1. **Open Messenger** - - - Open your preferred messenger platform (e.g., Telegram, Bale,...). - -2. **Click on the Lock Icon** - - - Within the messenger, locate the lock icon located in the toolbar or header. - - ![Lock Icon](../public/images/use-step1.png) - -3. **Initiate Handshake Process** - - - Both users must click on the lock icon simultaneously to initiate the handshake process. - - ![Initiate Handshake](../public/images/use-step2.png) - -4. **Handshake Completion** - - - Once the handshake process is completed, a confirmation message will appear, indicating that the secure connection has been established. - - ![Handshake Completed](../public/images/use-step3.png) - -### Enjoy Freedom of Speech - -Now that the handshake process is complete, users can enjoy the freedom of speech with confidence that their conversations are `End-to-End` encrypted. Chat Guard ensures a secure and private communication experience. - -**Note:** It is essential that both users have the Chat Guard extension installed and follow the steps mentioned above to establish a secure connection. diff --git a/docs/index.md b/docs/index.md index a110c8c..024148e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,18 +3,15 @@ layout: home hero: - name: Chat Guard - tagline: A browser extension designed to make any messenger app to End to End encrypted + name: ChatGuard + tagline: ChatGuard is a browser extension designed to enable End to End encryption to your favorite messenger image: src: /images/logo.svg alt: logo actions: - theme: brand text: Download & Install - link: /getting-started/installation - - theme: alt - text: Documentation - link: /api/cipher + link: /getting-started/how-to-install features: - title: Supporting major messengers diff --git a/docs/public/google7f1de7f44f3b5fd5.html b/docs/public/google7f1de7f44f3b5fd5.html new file mode 100644 index 0000000..32de08c --- /dev/null +++ b/docs/public/google7f1de7f44f3b5fd5.html @@ -0,0 +1 @@ +google-site-verification: google7f1de7f44f3b5fd5.html \ No newline at end of file diff --git a/docs/public/images/Acknowledgement.png b/docs/public/images/Acknowledgement.png deleted file mode 100644 index f1291ad..0000000 Binary files a/docs/public/images/Acknowledgement.png and /dev/null differ diff --git a/docs/public/images/Packet.png b/docs/public/images/Packet.png deleted file mode 100644 index 3e0ae16..0000000 Binary files a/docs/public/images/Packet.png and /dev/null differ diff --git a/docs/public/images/ack-msg.png b/docs/public/images/ack-msg.png deleted file mode 100644 index 3e71a25..0000000 Binary files a/docs/public/images/ack-msg.png and /dev/null differ diff --git a/docs/public/images/action-menu.png b/docs/public/images/action-menu.png new file mode 100644 index 0000000..c62c738 Binary files /dev/null and b/docs/public/images/action-menu.png differ diff --git a/docs/public/images/handshake-msg.png b/docs/public/images/handshake-msg.png deleted file mode 100644 index 2db31c3..0000000 Binary files a/docs/public/images/handshake-msg.png and /dev/null differ diff --git a/docs/public/images/handshake.png b/docs/public/images/handshake.png deleted file mode 100644 index 77318b9..0000000 Binary files a/docs/public/images/handshake.png and /dev/null differ diff --git a/docs/public/images/packet-msg.png b/docs/public/images/packet-msg.png deleted file mode 100644 index 45505c2..0000000 Binary files a/docs/public/images/packet-msg.png and /dev/null differ diff --git a/docs/public/images/use-step1.png b/docs/public/images/use-step1.png deleted file mode 100644 index 10f0103..0000000 Binary files a/docs/public/images/use-step1.png and /dev/null differ diff --git a/docs/public/images/use-step2.png b/docs/public/images/use-step2.png deleted file mode 100644 index 9d1c9dd..0000000 Binary files a/docs/public/images/use-step2.png and /dev/null differ diff --git a/docs/public/images/use-step3.png b/docs/public/images/use-step3.png deleted file mode 100644 index ec4eb72..0000000 Binary files a/docs/public/images/use-step3.png and /dev/null differ diff --git a/manifest.json b/manifest.json index e807f78..e3ce254 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, "name": "ChatGuard-beta", - "description": "Browser Add on to make any chat application (End to End) encrypted", - "version": "0.6.0", + "description": "Browser extension that allow (End to End) encrypted in web chat messenger", + "version": "0.9.6", "author": "https://github.com/mostafa-kheibary", "icons": { "16": "src/assets/icons/icon16.png", @@ -16,7 +16,10 @@ "https://web.bale.ai/*", "https://web.telegram.org/*", "https://web.splus.ir/*", - "https://web.eitaa.com/*" + "https://web.eitaa.com/*", + "https://web.shad.ir/*", + "https://web.rubika.ir/*", + "https://web.igap.net/*" ], "js": ["src/content/index.ts"] } @@ -29,5 +32,10 @@ "page": "src/view/options/index.html", "open_in_tab": true }, - "permissions": ["storage", "tabs"] + "browser_specific_settings": { + "gecko": { + "id": "mostafa.kheibary@gmail.com" + } + }, + "permissions": ["storage"] } diff --git a/package.json b/package.json index c0a97b9..1144c5d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "email": "mostafa.kheibary@gmail.com", "url": "https://github.com/mostafa-kheibary" }, - "version": "0.6.0", + "version": "0.9.6", "type": "module", "scripts": { "dev": "vite", diff --git a/src/assets/icons/export.svg b/src/assets/icons/export.svg new file mode 100644 index 0000000..07377d0 --- /dev/null +++ b/src/assets/icons/export.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/github.svg b/src/assets/icons/github.svg new file mode 100644 index 0000000..84296d9 --- /dev/null +++ b/src/assets/icons/github.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/icons/import.svg b/src/assets/icons/import.svg new file mode 100644 index 0000000..f1cf14c --- /dev/null +++ b/src/assets/icons/import.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/info.svg b/src/assets/icons/info.svg new file mode 100644 index 0000000..d8c8ceb --- /dev/null +++ b/src/assets/icons/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/lock.svg b/src/assets/icons/lock.svg new file mode 100644 index 0000000..fc97c29 --- /dev/null +++ b/src/assets/icons/lock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/reset.svg b/src/assets/icons/reset.svg new file mode 100644 index 0000000..dc9d40e --- /dev/null +++ b/src/assets/icons/reset.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/x.svg b/src/assets/icons/x.svg new file mode 100644 index 0000000..fc9ec5c --- /dev/null +++ b/src/assets/icons/x.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/class/Cipher.ts b/src/class/Cipher.ts index 02ab94f..8a3dafa 100644 --- a/src/class/Cipher.ts +++ b/src/class/Cipher.ts @@ -48,13 +48,13 @@ export class Cipher { const cleanedPublicKey = store.user!.publicKey.replace(/[\r\n]/g, ""); const timestamp = new Date().getTime().toString(); const packet = - config.HANDSHAKE_PREFIX + store.user?.guardId + cleanedPublicKey + timestamp.length + timestamp + btoa(to); + config.HANDSHAKE_PREFIX + btoa(store.user!.guardId) + cleanedPublicKey + timestamp.length + timestamp + btoa(to); return packet; } public async resolveDRSAPHandshake(packet: string, forId: string) { const store = await BrowserStorage.get(); const handshakeArray = packet.split(config.HANDSHAKE_PREFIX)[1].split(""); - const guardId = handshakeArray.splice(0, 36).join(""); + const guardId = atob(handshakeArray.splice(0, 48).join("")); const publicKey = handshakeArray.splice(0, 178).join(""); const timestampLength = handshakeArray.splice(0, 2).join(""); const timestamp = handshakeArray.splice(0, +timestampLength).join(""); diff --git a/src/components/modules/content/Actions.svelte b/src/components/modules/content/Actions.svelte index d13d1c8..8add6a8 100644 --- a/src/components/modules/content/Actions.svelte +++ b/src/components/modules/content/Actions.svelte @@ -4,14 +4,14 @@ import { clickTo, typeTo } from "src/utils/userAction"; import { url } from "src/store/url.store"; import { chatStore as state } from "src/store/chat.store"; - import type { Contact, Field } from "src/types/Config"; + import type { Contact } from "src/types/Config"; import type Cipher from "src/class/Cipher"; import { config } from "src/config"; import { wait } from "src/utils/wait"; import Lock from "src/components/icon/Lock.svelte"; + import { useConfig } from "src/hooks/useConfig"; export let cipher: Cipher; - export let selector: Field; export let id: string; let status: "safe" | "unsafe" = "unsafe"; @@ -19,8 +19,10 @@ let isMenuOpen = false; let openFromLeft = false; let intervalId: any | null = null; + let position = { left: 0, top: 0 }; $: !$state.loading && clearInterval(intervalId); + const { getSelector } = useConfig(); const checkStatus = () => { const contact = LocalStorage.getMap(config.CONTACTS_STORAGE_KEY, $url.id); @@ -36,10 +38,6 @@ }; onMount(() => { - const leftDistance = document.getElementById(id)?.getBoundingClientRect().left || 0; - if (leftDistance < window.innerWidth / 2) { - openFromLeft = true; - } document.addEventListener("pointerdown", handleCloseMenu); }); onDestroy(() => { @@ -47,19 +45,30 @@ document.removeEventListener("pointerdown", handleCloseMenu); }); + const handleMenuClicked = (e: MouseEvent) => { + const leftDistance = document.getElementById(id)?.getBoundingClientRect().left || 0; + if (leftDistance < window.innerWidth / 2) { + openFromLeft = true; + } + if (openFromLeft) position.left = e.clientX; + else { + position.left = window.innerWidth - e.clientX; + } + position.top = e.clientY; + isMenuOpen = !isMenuOpen; + }; const handleSendHandshake = async (e: MouseEvent) => { - let textFiled = document.querySelector(selector.textField) as HTMLElement; + let textFiled = document.querySelector(getSelector("textField")) as HTMLElement; textFiled.focus(); if ($state.loading) return; - const submitButton = selector.submitButton; const packet = await cipher.createDRSAPHandshake($url.id); - typeTo(selector.textField, packet); - textFiled = document.querySelector(selector.textField) as HTMLElement; + typeTo(getSelector("textField"), packet); + textFiled = document.querySelector(getSelector("textField")) as HTMLElement; textFiled.style.display = "none"; await wait(50); state.update((state) => ({ ...state, loading: true, submit: true })); textFiled.style.display = "block"; - clickTo(submitButton); + clickTo(getSelector("submitButton")); isMenuOpen = false; intervalId = setInterval(() => { const user = LocalStorage.getMap(config.CONTACTS_STORAGE_KEY, $url.id); @@ -77,16 +86,21 @@ }; -
+ + +
- - -
+
{#if status === "safe"}
@@ -127,24 +141,24 @@ .ctc_menu { display: flex; flex-direction: column; - position: absolute; - top: calc(100% + 8px); - width: 180px; + position: fixed; + width: fit-content; background-color: #fff; box-shadow: 0 4px 32px #00000028; color: #000; transform: scale(0); text-align: left; transform-origin: top right; - transition: all 200ms ease; - padding: 8px 0; + transition: transform 200ms ease; + padding: 8px; border-radius: 8px; z-index: 10000000; - right: 0; &.fromLeft { - right: auto; transform-origin: top left; - left: 0; + .ctc_menu__item { + justify-content: end; + flex-direction: row-reverse; + } } &.open { transform: scale(1); @@ -155,8 +169,14 @@ align-items: center; gap: 0.5rem; cursor: pointer; - font-size: 14px; - padding: 6px 14px; + padding: 6px 12px; + border-radius: 8px; + span, + svg { + color: #000 !important; + font-size: 14px !important; + } + &::after { content: ""; position: absolute; diff --git a/src/components/modules/popup/AdvancedSetting.svelte b/src/components/modules/popup/AdvancedSetting.svelte index 3315774..0fd4c92 100644 --- a/src/components/modules/popup/AdvancedSetting.svelte +++ b/src/components/modules/popup/AdvancedSetting.svelte @@ -3,6 +3,9 @@ import Cipher from "src/class/Cipher"; import { refreshPage } from "src/utils/refreshPage"; import { onMount } from "svelte"; + import importIcon from "../../../assets/icons/import.svg"; + import resetIcon from "../../../assets/icons/reset.svg"; + import exportIcon from "../../../assets/icons/export.svg"; let error = ""; let browserStore: IStorage | null = null; @@ -41,6 +44,9 @@ a.click(); URL.revokeObjectURL(a.href); }; + const handleImportChatGuardConfig = () => { + document.getElementById("import-upload")?.click(); + }; const importChatGuardConfig = async (e: Event) => { const store = await BrowserStorage.get(); const target = e.target as HTMLInputElement; @@ -68,18 +74,30 @@
-
-

Configs

-
- export config - - +
+

Config

+
+

The Config file includes your private key, public key, and Guard ID. You can export/import/reset your keys here + and use them on other devices or for backups. +

- reset config + + +
+ + + Export + + + + Import + +
+ + + Reset + {#if error}

{error} @@ -91,40 +109,44 @@ diff --git a/src/components/modules/popup/NavigationTab.svelte b/src/components/modules/popup/NavigationTab.svelte index deca2d1..a6bb136 100644 --- a/src/components/modules/popup/NavigationTab.svelte +++ b/src/components/modules/popup/NavigationTab.svelte @@ -15,15 +15,15 @@ display: flex; align-items: center; color: #000; - font-size: 1rem; - background-color: #fbfbfb; - border-bottom: 0.1rem solid #0f7bff0e; + font-size: 0.9rem; + border-bottom: 0.1rem solid #0f7bff16; .button { - padding: 1.2rem 1rem; + padding: 1rem 1rem; background-color: transparent; border: none; cursor: pointer; color: #0f7dff; + font-size: 1.1rem; transform: rotate(180deg); } } diff --git a/src/components/modules/popup/Popup.svelte b/src/components/modules/popup/Popup.svelte index 019c10d..6d3bd4f 100644 --- a/src/components/modules/popup/Popup.svelte +++ b/src/components/modules/popup/Popup.svelte @@ -23,8 +23,11 @@ diff --git a/src/config/index.ts b/src/config/index.ts index 63b6e73..59bb430 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -12,8 +12,8 @@ export const initLog = ` ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ██ ██ ███████ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ - ██████ ██████ ██ ██ ██ ██ ██████ - + ██████ ██████ ██ ██ ██ ██ ██████ + `; export const version = "V1"; @@ -25,94 +25,140 @@ export const config: Config = { export const selectors: Record = { "web.bale.ai": { selector: { - desktop: { - app: "#app_main_wrapper", - textField: "#editable-message-text", - submitButton: "#chat_footer :nth-child(2) :nth-child(5)", - header: "#toolbarWrapper", - message: "[data-sid]", - innerMessageText: "span", - }, - mobile: { - app: "#app_main_wrapper", - textField: "#main-message-input", - submitButton: "#chat_footer :nth-child(2) > div:last-child", - header: "#toolbarWrapper", - message: "[data-sid]", - innerMessageText: "span", - }, + textField: (type) => (type === "mobile" ? "#main-message-input" : "#editable-message-text"), + submitButton: (type) => + type === "mobile" + ? "#chat_footer > :has(#main-message-input) :nth-child(5)" + : "#chat_footer > :has(#editable-message-text) :nth-child(5)", + header: "#toolbarWrapper", + message: "[data-sid]", + innerMessageText: "span", + }, + events: { + onSubmitClick: "click", + onInput: "input", }, path: "*", idProvider: "uid", }, "web.telegram.org/k/": { selector: { - desktop: { - app: "#page-chats", - textField: ".input-message-input[data-peer-id][contenteditable]", - submitButton: ".btn-send-container button", - header: "[data-type=chat] .sidebar-header", - message: "[data-type=chat] [data-peer-id][data-mid]", - innerMessageText: ".message", - }, + textField: ".input-message-input[data-peer-id][contenteditable]", + submitButton: ".btn-send-container button", + header: ".chat .sidebar-header", + message: ".chat [data-peer-id][data-mid]", + innerMessageText: ".message", + }, + events: { + onSubmitClick: (type) => (type === "mobile" ? "mousedown" : "click"), + onInput: "input", }, path: "*", idProvider: "#", }, "web.splus.ir": { selector: { - desktop: { - app: "#root", - textField: "#editable-message-text", - header: "#MiddleColumn > div.messages-layout > div.MiddleHeader", - message: "[data-message-id]", - innerMessageText: ".contWrap", - submitButton: - "#MiddleColumn > div.messages-layout > div.Transition.slide > div > div.middle-column-footer > div > button", - }, + textField: "#editable-message-text", + header: "#MiddleColumn > div.messages-layout > div.MiddleHeader", + message: "[data-message-id]", + innerMessageText: ".contWrap", + submitButton: + "#MiddleColumn > div.messages-layout > div.Transition.slide > div > div.middle-column-footer > div > button", + }, + events: { + onSubmitClick: "click", + onInput: "input", }, path: "*", idProvider: "#", }, "web.telegram.org/a/": { selector: { - desktop: { - app: "#root", - textField: "#editable-message-text", - header: "#MiddleColumn > div.messages-layout > div.MiddleHeader", - message: "[data-message-id]", - innerMessageText: ".text-content", - submitButton: - "#MiddleColumn > div.messages-layout > div.Transition > div > div.middle-column-footer > div.Composer.shown.mounted > button", - }, + textField: "#editable-message-text", + header: "#MiddleColumn > div.messages-layout > div.MiddleHeader", + message: "[data-message-id]", + innerMessageText: ".text-content", + submitButton: + "#MiddleColumn > div.messages-layout > div.Transition > div > div.middle-column-footer > div.Composer.shown.mounted > button", + }, + events: { + onSubmitClick: "click", + onInput: "input", }, path: "*", idProvider: "#", }, "web.eitaa.com": { selector: { - desktop: { - app: "#main-columns", - textField: ".chats-container .input-message-input", - header: ".chats-container .sidebar-header", - message: ".bubble[data-mid]", - innerMessageText: ".message", - submitButton: ".btn-send-container button", - }, + textField: ".chats-container .input-message-input", + header: ".chats-container .sidebar-header", + message: ".bubble[data-mid]", + innerMessageText: ".message", + submitButton: ".btn-send-container button", + }, + events: { + onSubmitClick: (type) => (type === "mobile" ? "mousedown" : "click"), + onInput: "input", + }, + path: "*", + idProvider: "#", + }, + "web.shad.ir": { + selector: { + textField: ".input-message-input [contenteditable]", + header: ".chat-info-container", + message: "[data-msg-id]", + innerMessageText: ".message [rb-message-text] div", + submitButton: ".btn-send-container button .c-ripple", + }, + events: { + onSubmitClick: "click", + onInput: "keyup", + }, + path: "*", + idProvider: "#", + }, + "web.rubika.ir": { + selector: { + textField: ".input-message-input [contenteditable]", + header: ".chat-info-container", + message: "[data-msg-id]", + innerMessageText: ".message [rb-message-text] div", + submitButton: ".btn-send-container button .c-ripple", + }, + events: { + onSubmitClick: "click", + onInput: "keyup", }, path: "*", idProvider: "#", }, + "web.igap.net": { + selector: { + textField: "#textarea_ref[contenteditable]", + header: "header > div", + message: "[data-message-id]", + innerMessageText: "[data-message-id] > div [dir]", + submitButton: "main > :last-child > :last-child", + }, + events: { + onSubmitClick: "click", + onInput: "input", + }, + path: "/app", + idProvider: "q", + }, "twitter.com": { selector: { - desktop: { - app: "#react-root", - textField: "[contenteditable]", - header: "[role=main] > div > div > div > :nth-child(2) > div > div > div > div > div > div > div", - message: "[data-testid=messageEntry] > div > :nth-child(2) [role=presentation]", - innerMessageText: "span", - submitButton: "[role=complementary] > :nth-child(2) > [role=button]", - }, + textField: "[contenteditable]", + header: "[role=main] > div > div > div > :nth-child(2) > div > div > div > div > div > div > div", + message: "[data-testid=messageEntry] > div > :nth-child(2) [role=presentation]", + innerMessageText: "span", + submitButton: "[role=complementary] > :nth-child(2) > [role=button]", + }, + events: { + onSubmitClick: "click", + onInput: "input", }, path: "/messages", idProvider: "/", diff --git a/src/content/index.ts b/src/content/index.ts index d43441e..eb373c7 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -1,52 +1,46 @@ import Actions from "src/components/modules/content/Actions.svelte"; -import { config, initLog } from "src/config"; +import { initLog } from "src/config"; import Cipher from "src/class/Cipher"; import LoadingScreen from "src/components/modules/content/LoadingScreen.svelte"; -import LocalStorage from "src/utils/LocalStorage"; import useObserver from "src/hooks/useObserver"; import useRender from "src/hooks/useRender"; import useUrl from "src/hooks/useUrl"; import BrowserStorage from "src/utils/BrowserStorage"; -import { getConfig, getDeviceType } from "src/utils"; +import { getDeviceType } from "src/utils"; import logger from "src/utils/logger"; import { parseMessage } from "./scripts/messageParser"; import { registerEventListener } from "./scripts/listeners"; import { register } from "./scripts/register"; -import { logSelectors } from "src/utils/logSelectors"; +import { useConfig } from "src/hooks/useConfig"; (async function main() { let store = await BrowserStorage.get(); if (!store.enable) return null; await register(); const type = getDeviceType(); + const { getSelector, idProvider, name } = useConfig(); const isTouch = type === "mobile" ? true : false; - const { idProvider, selector, name } = getConfig(); - if (!selector || !idProvider) return logger.error(`config notfound for ${location.hostname}`); + if (!name || !idProvider) return logger.error(`config notfound for ${location.hostname}`); logger.info({ type, isTouch, idProvider, name }); const cipher = new Cipher(); - const appRoot = document.querySelector(selector.app) as HTMLElement; - const { onObserve: onRootObserver } = useObserver(appRoot); - const { render } = useRender(appRoot); - const { url, urlStore } = useUrl(idProvider); + const { onObserve: onRootObserver } = useObserver(); + const { render } = useRender(); + const { urlStore } = useUrl(idProvider); if (import.meta.env.MODE !== "development") console.log(initLog); new LoadingScreen({ target: document.body }); - render(selector.header, (target, id) => { + + render(getSelector("header"), (target, id) => { // Action Menu on the conversation header - new Actions({ target, props: { cipher, selector, id } }); + new Actions({ target, props: { cipher, id } }); }); // event listener for user action (type,click,sending message) registerEventListener(urlStore); - url.subscribe((newUrl) => { - if (LocalStorage.getMap(config.CONTACTS_STORAGE_KEY, newUrl.id).publicKey) { - document.querySelector(selector.textField)?.dispatchEvent(new Event("input")); - } - }); onRootObserver(() => { // On message receive will run and parse it - const messages = Array.from(document.querySelectorAll(selector.message)); + const messages = Array.from(document.querySelectorAll(getSelector("message"))); messages.forEach(async (message, index) => parseMessage(urlStore, message as HTMLElement, messages, index)); }); })(); diff --git a/src/content/scripts/listeners.ts b/src/content/scripts/listeners.ts index d56c196..02e987d 100644 --- a/src/content/scripts/listeners.ts +++ b/src/content/scripts/listeners.ts @@ -3,85 +3,75 @@ import { config } from "src/config"; import useListener from "src/hooks/useListener"; import { chatStore } from "src/store/chat.store"; import type { Url } from "src/store/url.store"; -import { getConfig, getDeviceType } from "src/utils"; +import { getDeviceType } from "src/utils"; import LocalStorage from "src/utils/LocalStorage"; import logger from "src/utils/logger"; import { clickTo, typeTo } from "src/utils/userAction"; -import { get } from "svelte/store"; import { wait } from "src/utils/wait"; -import { logSelectors } from "src/utils/logSelectors"; +import { makeElementInvisible, makeElementVisible } from "src/utils/elementVisibility"; +import { useConfig } from "src/hooks/useConfig"; export const registerEventListener = (urlStore: Url) => { const cipher = new Cipher(); const type = getDeviceType(); + const { getSelector } = useConfig(); const isTouch = type === "mobile" ? true : false; - const { selector } = getConfig(); - const appRoot = document.querySelector(selector.app) as HTMLElement; - const { on } = useListener(appRoot); + const { onClick } = useListener(); - if (import.meta.env.MODE === "development") { - document.addEventListener("keydown", (e) => { - if (e.key === "W" && e.shiftKey) logSelectors(); - }); - } - on(selector.submitButton, "click", async (e: Event) => { - const state = get(chatStore); + const handleSubmitClicked = async (e: Event) => { const contact = LocalStorage.getMap(config.CONTACTS_STORAGE_KEY, urlStore.id); - let textFieldElement = document.querySelector(selector.textField) as HTMLElement; + let textFieldElement = document.querySelector(getSelector("textField")) as HTMLElement; const messageLengthIsOk = (textFieldElement.textContent || "").length <= 1200; if (!textFieldElement.textContent?.trim() || !contact.enable) return; - if (state.clickSubmit || state.submit) - return chatStore.update((prev) => ({ ...prev, clickSubmit: false, submit: false })); e.preventDefault(); e.stopImmediatePropagation(); if (!messageLengthIsOk) return alert("character length should be bellow 1200 character"); + makeElementInvisible(textFieldElement); const encrypted = await cipher.createDRSAP(textFieldElement.textContent || "", urlStore.id); - if (!encrypted) return; - typeTo(selector.textField, encrypted); - textFieldElement = document.querySelector(selector.textField) as HTMLElement; - textFieldElement.style.display = "none"; + if (!encrypted) return makeElementVisible(textFieldElement); + + typeTo(getSelector("textField"), encrypted); + await wait(25); chatStore.update((prev) => ({ ...prev, clickSubmit: true })); - await wait(50); - textFieldElement.style.display = "block"; - clickTo(selector.submitButton); + clickTo(getSelector("submitButton")); + makeElementVisible(textFieldElement); + textFieldElement.focus(); logger.info("Message sent, Send button clicked"); - }); + }; - document.addEventListener( - "keydown", - async (event) => { - const e = event as KeyboardEvent; - // check if we writing text to our textfield or not - let isEqual = (e.target as HTMLElement).isEqualNode(document.querySelector(selector.textField)); - if (!isEqual) return; + const handleTextFieldKeyDown = async (e: KeyboardEvent) => { + // check if we writing text to our textfield or not + let isEqual = (e.target as HTMLElement).isEqualNode(document.querySelector(getSelector("textField"))); + if (!isEqual) return; - const contact = LocalStorage.getMap(config.CONTACTS_STORAGE_KEY, urlStore.id); - let textFieldElement = document.querySelector(selector.textField) as HTMLElement; - const messageLengthIsOk = (textFieldElement.textContent || "").length <= 1200; + const contact = LocalStorage.getMap(config.CONTACTS_STORAGE_KEY, urlStore.id); + let textFieldElement = document.querySelector(getSelector("textField")) as HTMLElement; + const messageLengthIsOk = (textFieldElement.textContent || "").length <= 1200; + + if (e.key !== "Enter" || !contact.enable || !textFieldElement.textContent?.trim() || e.shiftKey || isTouch) { + return; + } + e.preventDefault(); + e.stopImmediatePropagation(); + + if (!messageLengthIsOk) return alert("character length should be bellow 1200 character"); + makeElementInvisible(textFieldElement); + const encrypted = await cipher.createDRSAP(textFieldElement.textContent || "", urlStore.id); + if (!encrypted) return makeElementVisible(textFieldElement); - if (e.key !== "Enter" || !contact.enable || !textFieldElement.textContent?.trim() || e.shiftKey || isTouch) { - return; - } - e.preventDefault(); - e.stopImmediatePropagation(); + typeTo(getSelector("textField"), encrypted); + await wait(25); + chatStore.update((prev) => ({ ...prev, submit: true })); + clickTo(getSelector("submitButton")); + makeElementVisible(textFieldElement); + textFieldElement.focus(); + logger.info("Message sent, Form submitted"); + }; - if (!messageLengthIsOk) return alert("character length should be bellow 1200 character"); - const encrypted = await cipher.createDRSAP(textFieldElement.textContent || "", urlStore.id); - if (!encrypted) return; - typeTo(selector.textField, encrypted); - textFieldElement = document.querySelector(selector.textField) as HTMLElement; - textFieldElement.style.display = "none"; - await wait(20); - chatStore.update((prev) => ({ ...prev, submit: true })); - clickTo(selector.submitButton); - textFieldElement.focus(); - textFieldElement.style.display = "block"; - logger.info("Message sent, Form submitted"); - }, - { capture: true } - ); + document.addEventListener("keydown", handleTextFieldKeyDown, { capture: true }); + onClick(getSelector("submitButton"), handleSubmitClicked); }; diff --git a/src/content/scripts/messageParser.ts b/src/content/scripts/messageParser.ts index 3642e9c..ac821a6 100644 --- a/src/content/scripts/messageParser.ts +++ b/src/content/scripts/messageParser.ts @@ -1,35 +1,34 @@ import Cipher from "src/class/Cipher"; import { config } from "src/config"; +import { useConfig } from "src/hooks/useConfig"; import type { Url } from "src/store/url.store"; -import { getConfig } from "src/utils"; import { changeTextNode } from "src/utils/changeTextNode"; +import { findFirstTextNode, findTargetRecursive } from "src/utils/findMessageTarget"; export const parseMessage = async (urlStore: Url, message: Element, messages: Element[], index: number) => { const cipher = new Cipher(); - const { selector } = getConfig(); + const { getSelector } = useConfig(); - const targets = Array.from(messages[index].querySelectorAll(selector.innerMessageText)); - const target = targets.find((el) => { - let find = null; - el.childNodes.forEach((node: Node) => { - if (node.nodeType === 3 && node.textContent?.startsWith("::")) { - find = true; - } - }); - if (find) return el; - return false; - }) as HTMLElement | null; + const targets = Array.from(messages[index].querySelectorAll(getSelector("innerMessageText"))); + const target = targets.find((el) => findTargetRecursive(el)) as HTMLElement | null; if (!target) return; - const textNodeContent = Array.from(target.childNodes).find((node) => node.nodeType === 3)?.textContent || ""; + const textNodeContent = findFirstTextNode(target); // Messages if (textNodeContent.startsWith(config.ENCRYPT_PREFIX)) { changeTextNode(target, "Parsing ..."); try { const packet = await cipher.resolveDRSAP(textNodeContent); - if (!packet) changeTextNode(target, "⛔ Error in decryption"); - else changeTextNode(target, "🔒" + packet); + if (!packet) { + changeTextNode(target, "⛔ Error in decryption"); + } else { + const status = document.createElement("span"); + status.textContent = "🔒 "; + status.style.opacity = "0.4"; + changeTextNode(target, packet); + target.prepend(status); + } (target as any).dir = "auto"; } catch (error) { changeTextNode(target, "⛔ Error in decryption"); diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts new file mode 100644 index 0000000..8093bbb --- /dev/null +++ b/src/hooks/useConfig.ts @@ -0,0 +1,37 @@ +import { selectors } from "src/config"; +import type { Events, Field } from "src/types/Config"; +import { getDeviceType } from "src/utils"; + +interface Config { + getSelector: (key: keyof Field) => string; + getEvent: (key: keyof Events) => string; + idProvider: string; + name: string; +} + +export const useConfig = (): Config => { + const type = getDeviceType(); + let host: string = location.hostname; + + const getSelector = (selectorKey: keyof Field) => { + const selector = (selectors[host] as any).selector[selectorKey]; + if (typeof selector === "string") return selector; + return selector(type); + }; + const getEvent = (eventKey: keyof Events) => { + const event = (selectors[host] as any).events[eventKey]; + if (typeof event === "string") return event; + return event(type); + }; + + const key = location.host + location.pathname; + for (const selectorKey in selectors) { + if (selectorKey.startsWith(key)) host = selectorKey; + } + return { + name: host, + idProvider: selectors[host].idProvider, + getEvent, + getSelector, + }; +}; diff --git a/src/hooks/useListener.ts b/src/hooks/useListener.ts index fdcf5e3..206ea52 100644 --- a/src/hooks/useListener.ts +++ b/src/hooks/useListener.ts @@ -1,3 +1,4 @@ +import { useConfig } from "./useConfig"; import useObserver from "./useObserver"; /** @@ -7,20 +8,20 @@ import useObserver from "./useObserver"; * @example * const { on , onClick } = useListener() */ -const useListener = (appRoot: HTMLElement) => { +const useListener = () => { const eventsListener: Record void)[]> = {}; const clickMap: Record = {}; - - const { onObserve } = useObserver(appRoot); + const { getEvent } = useConfig(); + const { onObserve } = useObserver(); document.addEventListener( - "click", + getEvent("onSubmitClick"), (e) => { for (const selector in clickMap) { const el = document.querySelector(selector); if (el) { const { top, left, width, height } = el.getBoundingClientRect(); - const { pageX, pageY } = e; + const { pageX, pageY } = e as MouseEvent; if (pageX >= left && pageX <= left + width && pageY >= top && pageY <= top + height) { clickMap[selector].forEach((callback) => callback(e)); } diff --git a/src/hooks/useObserver.ts b/src/hooks/useObserver.ts index a5ed19c..b04d886 100644 --- a/src/hooks/useObserver.ts +++ b/src/hooks/useObserver.ts @@ -6,7 +6,8 @@ type CallBackFunction = (mutations: MutationRecord[]) => void; * @example * const { onObserve } = useObserver() */ -const useObserver = (targetElement: HTMLElement) => { +const useObserver = () => { + const targetElement = document.body; const callbacks: CallBackFunction[] = []; const onObserve = (callback: CallBackFunction) => { diff --git a/src/hooks/useRender.ts b/src/hooks/useRender.ts index aba499e..a6b7932 100644 --- a/src/hooks/useRender.ts +++ b/src/hooks/useRender.ts @@ -1,7 +1,7 @@ import { getElement } from "src/utils/getElement"; import useObserver from "./useObserver"; -import { getConfig } from "src/utils"; import { selectors } from "src/config"; +import { useConfig } from "./useConfig"; interface RenderMap { id: string; @@ -16,10 +16,10 @@ interface RenderMap { * @example * const { render } = useRender() */ -const useRender = (appRoot: HTMLElement) => { - const { name } = getConfig(); +const useRender = () => { + const { name } = useConfig(); const renderMap: Record = {}; - const { onObserve } = useObserver(appRoot); + const { onObserve } = useObserver(); onObserve((mutations) => { mutations.forEach((mutation) => { diff --git a/src/types/Config.d.ts b/src/types/Config.d.ts index ca5bceb..5ffc89c 100644 --- a/src/types/Config.d.ts +++ b/src/types/Config.d.ts @@ -1,16 +1,19 @@ +export type FieldItem = ((device: "desktop" | "mobile") => string) | string; + export interface Field { - app: string; - textField: string; - submitButton: string; - header: string; - message: string; - innerMessageText: string; + textField: FieldItem; + submitButton: FieldItem; + header: FieldItem; + message: FieldItem; + innerMessageText: FieldItem; +} +export interface Events { + onSubmitClick: FieldItem; + onInput: FieldItem; } export interface Selector { - selector: { - desktop: Field; - mobile?: Field; - }; + selector: Field; + events: Events; path: string; idProvider: string; } diff --git a/src/utils/changeTextNode.ts b/src/utils/changeTextNode.ts index 7ea315a..b04d99e 100644 --- a/src/utils/changeTextNode.ts +++ b/src/utils/changeTextNode.ts @@ -1,7 +1,10 @@ export const changeTextNode = (element: HTMLElement, replace: string) => { - element.childNodes.forEach((node) => { - if (node.nodeType === 3 && node.textContent) { - return (node.textContent = replace); + if (element.nodeType === 3 && element.textContent) { + element.textContent = replace; + } else if (element.childNodes.length > 0) { + const firstChild = element.childNodes[0]; + if (firstChild) { + changeTextNode(firstChild as HTMLElement, replace); } - }); + } }; diff --git a/src/utils/elementVisibility.ts b/src/utils/elementVisibility.ts new file mode 100644 index 0000000..708ffa6 --- /dev/null +++ b/src/utils/elementVisibility.ts @@ -0,0 +1,10 @@ +export const makeElementInvisible = (element: HTMLElement) => { + element.style.visibility = "hidden"; + element.style.position = "absolute"; + element.style.opacity = "0"; +}; +export const makeElementVisible = (element: HTMLElement) => { + element.style.visibility = ""; + element.style.position = ""; + element.style.opacity = ""; +}; diff --git a/src/utils/findMessageTarget.ts b/src/utils/findMessageTarget.ts new file mode 100644 index 0000000..e64b937 --- /dev/null +++ b/src/utils/findMessageTarget.ts @@ -0,0 +1,27 @@ +export function findTargetRecursive(element: HTMLElement | Node): null | HTMLElement { + if (element.nodeType === 3 && element.textContent?.startsWith("::")) { + return element.parentElement; // Return the parent element + } + // Check child nodes recursively + if (element.childNodes) { + for (let i = 0; i < element.childNodes.length; i++) { + const foundElement = findTargetRecursive(element.childNodes[i]); + if (foundElement) { + return foundElement; // Return the found element + } + } + } + return null; +} + +export function findFirstTextNode(node: any) { + if (node.nodeType === 3) { + return node.textContent.trim(); + } else if (node.nodeType === 1 && node.childNodes.length > 0) { + const firstChild = node.childNodes[0]; + if (firstChild) { + return findFirstTextNode(firstChild); + } + } + return ""; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index b92dd1d..8d4bfb2 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,3 @@ -import { selectors } from "src/config"; -import type { Field } from "src/types/Config"; - export const getDeviceType = () => { const ua = navigator.userAgent; if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) { @@ -11,41 +8,3 @@ export const getDeviceType = () => { } return "desktop"; }; -interface Config { - selector: Field; - idProvider: string; - name: string; -} -export const getConfig = (): Config => { - const type = getDeviceType(); - - const key = location.host + location.pathname; - for (const selectorKey in selectors) { - if (selectorKey.startsWith(key)) { - const selector = selectors[selectorKey].selector[type]; - if (!selector) - return { - name: selectorKey, - selector: selectors[selectorKey].selector.desktop, - idProvider: selectors[selectorKey].idProvider, - }; - return { - name: selectorKey, - selector, - idProvider: selectors[selectorKey].idProvider, - }; - } - } - const selector = selectors[location.hostname].selector[type]; - if (!selector) - return { - name: location.hostname, - selector: selectors[location.hostname].selector.desktop, - idProvider: selectors[location.hostname].idProvider, - }; - return { - name: location.hostname, - selector, - idProvider: selectors[location.hostname].idProvider, - }; -}; diff --git a/src/utils/logSelectors.ts b/src/utils/logSelectors.ts deleted file mode 100644 index 47b275a..0000000 --- a/src/utils/logSelectors.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { getConfig } from "."; - -export const logSelectors = () => { - const { selector: selectors } = getConfig(); - for (let selector in selectors) { - console.log(selector, document.querySelector((selectors as any)[selector])); - } -}; diff --git a/src/utils/openLink.ts b/src/utils/openLink.ts new file mode 100644 index 0000000..92441bf --- /dev/null +++ b/src/utils/openLink.ts @@ -0,0 +1,4 @@ +export const openLink = (link: string) => { + if (chrome.tabs) return chrome.tabs.create({ url: link }); + if (browser.tabs) browser.tabs.create({ url: link }); +}; diff --git a/src/utils/userAction.ts b/src/utils/userAction.ts index 314253d..a224688 100644 --- a/src/utils/userAction.ts +++ b/src/utils/userAction.ts @@ -1,5 +1,6 @@ -// IMPORTANT : temporary disable this feature for other type of input +import { useConfig } from "src/hooks/useConfig"; +// IMPORTANT : temporary disable this feature for other type of input // function insertElementToDeepestChild(parentNode: HTMLElement, text: string) { // let currentElement: any = parentNode; // const textEl = document.createElement("span"); @@ -12,16 +13,21 @@ // currentElement.appendChild(textEl); // } export const typeTo = async (textFiledSelector: string, message: string) => { + const { getEvent } = useConfig(); const textFiled = document.querySelector(textFiledSelector) as HTMLElement; textFiled.focus(); const textEl = document.createElement("span"); + (textFiled as any).value = message; textEl.textContent = message; textEl.style.display = "none"; textFiled.replaceChildren(textEl); - textFiled.dispatchEvent(new Event("input", { cancelable: false, bubbles: true })); + textFiled.dispatchEvent(new Event(getEvent("onInput"), { cancelable: false, bubbles: true })); }; export const clickTo = (elementSelector: string) => { + const { getEvent } = useConfig(); const el = document.querySelector(elementSelector) as HTMLElement; - el.click(); + const event = getEvent("onSubmitClick"); + if (event === "click") return el.click(); + el.dispatchEvent(new Event(event, { cancelable: false, bubbles: true })); }; diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..bd6fd8f --- /dev/null +++ b/vercel.json @@ -0,0 +1,3 @@ +{ + "cleanUrls": true +}