├── LICENSE ├── README.md ├── assets ├── calendar.svg ├── copy.svg ├── delete.svg ├── header.png ├── setting.svg └── vault.png ├── css ├── popup.css └── settings.css ├── js ├── popup.js ├── settings.js ├── storage.js └── sync.js ├── manifest.json ├── popup.html └── settings.html /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jhen-Jie Hong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | banner 2 | 3 | # KeyChain 4 | 5 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) 6 | 7 | > Keep your API Keys organized and secure. 8 | 9 | With the growing popularity of GPT and the increasing importance of APIs, many applications rely on API keys to function. However, people may struggle with managing their API keys effectively, which is time-consuming and susceptible to leaks. 10 | 11 | That's where KeyChain comes into play - it serves as a reliable and well-structured solution for users to securely store and efficiently manage their API keys. 12 | 13 | ## Features 14 | 15 | - 🛡️ **Securely store API keys** in your browser's local storage 16 | - ⚡ **Fetch API keys quickly** with just a simple click 17 | - 📅 **Easily view key expiration dates** at a glance 18 | - ☁️ **Seamless sync** with Google Drive, OneDrive, and Amazon S3 19 | - 🔒 **Robust encryption** using Web Crypto API for maximum security 20 | 21 | ## Roadmap 22 | 23 | - [x] Implement basic user interface 24 | - [x] Add support for encrypted export and import of API keys 25 | - [ ] Integrate with cloud storage platforms such as Google Drive, OneDrive, and Amazon S3 for syncing 26 | - [ ] Add automatic detection of API keys from web pages 27 | - [ ] A web-based version that is accessible on mobile devices. 28 | - [ ] Anonymous replacement APIs to improve privacy and security (considered as long-term goal) 29 | 30 | ## Installation 31 | 32 | Search for [KeyChain](https://chrome.google.com/webstore/detail/keychain/oahaommahieejlnckcacdggkecikkebg?hl=en&authuser=0) in the Google Chrome App Store or 33 | 34 | 1. Clone the repository or download the source code: 35 | 36 | ``` 37 | git clone https://github.com/jwjoel/keychain.git 38 | ``` 39 | 40 | 2. In your browser, open the Extensions page: 41 | 42 | - In Chrome, navigate to `chrome://extensions/` 43 | - In Firefox, navigate to `about:addons` 44 | 45 | 3. Enable developer mode (usually a toggle in the top-right corner of the Extensions page). 46 | 47 | 4. Click "Load unpacked extension" (Chrome) or "Load Temporary Add-on" (Firefox) and select the `keychain` directory that you cloned or downloaded. 48 | 49 | 5. The KeyChain extension should now be installed and visible in your browser's toolbar. 50 | 51 | ## Data Security 52 | 53 | Data security is at the core of KeyChain. Using AES-GCM encryption, API keys are stored as ciphertext in IndexedDB. All keys are saved locally, so make sure to export your API keys before changing environments. The exported file format is: 54 | 55 | ```json 56 | { 57 | "keys": [ 58 | { 59 | "id": 1, 60 | "source": "Github", 61 | "key": { 62 | "ciphertext": "6DR7vprpOw5C78kaTRLT7p1eIQ==", 63 | "iv": "w5JOmjtrvfcpo+JH" 64 | }, 65 | "expiration": 1687167800822 66 | } 67 | ], 68 | "encryptionKey": { 69 | "alg": "A256GCM", 70 | "ext": true, 71 | "k": "HyrZg2KSkBCBVbl3n6qxgb2MwRIX8l6EBCBstkhITCQ", 72 | "key_ops": ["encrypt", "decrypt"], 73 | "kty": "oct" 74 | } 75 | } 76 | ``` 77 | 78 | API keys are stored as encrypted ciphertext in base64 format. You can separate the encryption key and API keys as needed to ensure maximum security. Additionally, keys added to KeyChain are not directly displayed on the interface and cannot be edited. To modify a key, you must first delete the original API key and then add a new one. 79 | 80 | ## Usage 81 | 82 | https://github.com/jwjoel/KeyChain/assets/25562443/85afd114-52c0-4862-b57d-715b834ed0a8 83 | 84 | ### Add API Key 85 | 86 | 1. Click the KeyChain icon in your browser's toolbar to open the extension popup. 87 | 88 | 2. Use the "Chain" tab to view your stored keys. If no keys are stored, an "empty message" will be displayed. 89 | 90 | 3. Click the "AddKey" tab to add a new API key. 91 | 92 | ### Export and Import 93 | 94 | 1. Click the settings icon in the top-right corner of the interface to enter the settings page. 95 | 96 | 2. Choose the Export option, and the system will automatically download the file after the export is complete. 97 | 98 | 3. On the device where you want to import the keys, choose Import. 99 | 100 | ## Contributing 101 | 102 | Feel free to open issues or submit pull requests for bug fixes or feature requests. 103 | 104 | 111 | 112 | ## License 113 | 114 | This project is released under the [MIT License](https://opensource.org/licenses/MIT). 115 | -------------------------------------------------------------------------------- /assets/calendar.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /assets/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwjoel/KeyChain/9577784f85493ccd82b9d974dd8ca53ea46780d2/assets/header.png -------------------------------------------------------------------------------- /assets/setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/vault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwjoel/KeyChain/9577784f85493ccd82b9d974dd8ca53ea46780d2/assets/vault.png -------------------------------------------------------------------------------- /css/popup.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | user-select: none; 8 | } 9 | 10 | body { 11 | font-family: "Inter", sans-serif; 12 | } 13 | 14 | .main-wrapper { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | padding: 22px; 19 | } 20 | 21 | .form-wrapper { 22 | margin: 0 auto; 23 | width: 270px; 24 | height: 470px; 25 | background: white; 26 | } 27 | 28 | .fixed-container { 29 | position: sticky; 30 | top: 0; 31 | background-color: #fff; 32 | z-index: 10; 33 | width: 100%; 34 | } 35 | 36 | .title { 37 | margin-bottom: 20px; 38 | } 39 | 40 | .title h3 { 41 | color: #07074d; 42 | font-weight: 700; 43 | font-size: 24px; 44 | line-height: 24px; 45 | width: 60%; 46 | margin-bottom: 8px; 47 | transition: all 0.3s ease; 48 | } 49 | 50 | .title p { 51 | font-size: 15px; 52 | line-height: 15px; 53 | color: #536387; 54 | width: 70%; 55 | } 56 | 57 | .title-block { 58 | position: absolute; 59 | left: 0px; 60 | top: 20px; 61 | height: 55px; 62 | width: 6px; 63 | background-color: #6a64f1; 64 | } 65 | 66 | .steps { 67 | padding-bottom: 10px; 68 | margin-bottom: 20px; 69 | border-bottom: 1px solid #dde3ec; 70 | } 71 | 72 | .steps ul { 73 | padding: 0; 74 | margin: 0; 75 | list-style: none; 76 | display: flex; 77 | gap: 0px; 78 | } 79 | 80 | .steps li { 81 | cursor: pointer; 82 | display: flex; 83 | align-items: center; 84 | gap: 10px; 85 | font-weight: 500; 86 | font-size: 16px; 87 | line-height: 24px; 88 | color: #536387; 89 | width: 120px; 90 | transition: transform 0.2s ease, color 0.2s ease; 91 | } 92 | 93 | .steps li:hover { 94 | color: #07074d; 95 | } 96 | 97 | .steps li:active { 98 | transform: scale(0.95); 99 | } 100 | 101 | .steps li span { 102 | display: flex; 103 | align-items: center; 104 | justify-content: center; 105 | background: #dde3ec; 106 | border-radius: 50%; 107 | height: 30px; 108 | width: 30px; 109 | font-weight: 500; 110 | font-size: 16px; 111 | line-height: 24px; 112 | color: #536387; 113 | } 114 | 115 | .steps li.active { 116 | color: #07074d; 117 | transition: all 0.3s ease; 118 | } 119 | 120 | .steps li.active span { 121 | background: #6a64f1; 122 | color: #ffffff; 123 | } 124 | 125 | .form-input-container { 126 | margin-top: 15px; 127 | } 128 | 129 | .form-input { 130 | width: 100%; 131 | padding: 10px 12px; 132 | border-radius: 5px; 133 | border: 1px solid #dde3ec; 134 | background: #ffffff; 135 | font-weight: 500; 136 | font-size: 15px; 137 | color: #536387; 138 | outline: none; 139 | resize: none; 140 | } 141 | 142 | .form-input:focus { 143 | border-color: #6a64f1; 144 | box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.05); 145 | transition: border-color 0.3s ease, box-shadow 0.3s ease; 146 | } 147 | 148 | .form-label { 149 | color: #07074d; 150 | font-weight: 500; 151 | font-size: 14px; 152 | line-height: 24px; 153 | display: block; 154 | margin-bottom: 2px; 155 | } 156 | 157 | .form-step-1 { 158 | max-height: 365px; 159 | overflow-y: overlay; 160 | } 161 | 162 | .form-step-2 { 163 | margin-left: 50px; 164 | } 165 | 166 | .form-step-1::-webkit-scrollbar { 167 | width: 0px; 168 | } 169 | 170 | .form-step-1, 171 | .form-step-2 { 172 | position: absolute; 173 | width: 270px; 174 | transition: transform 0.5s ease; 175 | transform: translateX(0%); 176 | transition: transform 0.5s all; 177 | } 178 | 179 | .form-step-1.active { 180 | transform: translateX(0%); 181 | } 182 | 183 | .form-step-1:not(.active) { 184 | transform: translateX(-120%); 185 | } 186 | 187 | .form-step-2.active { 188 | margin-left: 0px; 189 | } 190 | 191 | .form-step-2:not(.active) { 192 | transform: translateX(100%); 193 | } 194 | 195 | .form-btn-wrapper { 196 | display: flex; 197 | align-items: center; 198 | justify-content: flex-end; 199 | gap: 25px; 200 | margin-top: 25px; 201 | } 202 | 203 | .back-btn { 204 | cursor: pointer; 205 | background: #ffffff; 206 | border: none; 207 | color: #07074d; 208 | font-weight: 500; 209 | font-size: 16px; 210 | line-height: 24px; 211 | display: none; 212 | } 213 | 214 | .back-btn.active { 215 | display: block; 216 | } 217 | 218 | .btn { 219 | display: flex; 220 | align-items: center; 221 | gap: 5px; 222 | font-size: 15px; 223 | border-radius: 5px; 224 | padding: 10px 25px; 225 | border: none; 226 | font-weight: 500; 227 | background-color: #6a64f1; 228 | color: white; 229 | cursor: pointer; 230 | transition: transform 0.2s ease, color 0.2s ease; 231 | } 232 | 233 | .btn:hover { 234 | box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.05); 235 | } 236 | 237 | .btn:active { 238 | transform: scale(0.98); 239 | } 240 | 241 | .key { 242 | background-color: #fff; 243 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 244 | padding: 8px; 245 | margin-bottom: 10px; 246 | position: relative; 247 | display: flex; 248 | justify-content: space-between; 249 | } 250 | 251 | .key:hover { 252 | background-color: #f5f5f5; 253 | } 254 | 255 | .key > div { 256 | margin-bottom: 4px; 257 | } 258 | 259 | .key > div:last-child { 260 | margin-bottom: 0; 261 | } 262 | 263 | .icons { 264 | cursor: pointer; 265 | width: 20px; 266 | height: 20px; 267 | margin-right: 3px; 268 | transition: transform 0.2s ease, filter 0.2s ease; 269 | } 270 | 271 | .icons:hover { 272 | filter: brightness(0.8); 273 | } 274 | 275 | .icons:active { 276 | transform: scale(0.9); 277 | } 278 | 279 | .icons :hover { 280 | background: rgba(0, 0, 0, 0.5); 281 | border-radius: 10px; 282 | } 283 | 284 | .key-details { 285 | background: #fafafa; 286 | border: 1px solid #dde3ec; 287 | border-radius: 5px; 288 | margin: 10px 0 10px; 289 | transition: background-color 0.3s ease; 290 | } 291 | 292 | .key-header { 293 | display: flex; 294 | position: relative; 295 | } 296 | .key-header img { 297 | width: 17px; 298 | } 299 | .key-header-icon { 300 | right: 8px; 301 | gap: 10px; 302 | top: 8px; 303 | position: absolute; 304 | } 305 | .key-details h5 { 306 | color: #07074d; 307 | font-weight: 600; 308 | font-size: 14px; 309 | line-height: 14px; 310 | padding: 10px 15px; 311 | } 312 | .key-details ul { 313 | border-top: 1px solid #edeef2; 314 | padding: 15px; 315 | margin: 0; 316 | list-style: none; 317 | flex-wrap: wrap; 318 | row-gap: 14px; 319 | } 320 | .key-details ul li { 321 | color: #536387; 322 | font-size: 12px; 323 | line-height: 14px; 324 | width: 100%; 325 | display: flex; 326 | align-items: center; 327 | gap: 10px; 328 | user-select: none; 329 | } 330 | .key-content { 331 | display: flex; 332 | justify-content: space-between; 333 | align-items: center; 334 | width: 100%; 335 | cursor: pointer; 336 | transition: background-color 0.3s ease; 337 | } 338 | 339 | .key-content:hover { 340 | background-color: #f5f5f5; 341 | } 342 | 343 | .key-header-icons { 344 | display: flex; 345 | gap: 5px; 346 | margin-right: 7px; 347 | } 348 | 349 | .empty-message { 350 | color: #536387; 351 | } 352 | 353 | .popup-notification { 354 | position: fixed; 355 | top: 30px; 356 | right: -10px; 357 | background-color: rgb(90, 161, 88); 358 | border-radius: 4px; 359 | width: 80px; 360 | height: 30px; 361 | display: flex; 362 | align-items: center; 363 | justify-content: center; 364 | z-index: 999; 365 | } 366 | 367 | #popup-text { 368 | color: white; 369 | padding-left: 15px; 370 | line-height: 30px; 371 | font-size: 13px; 372 | } 373 | 374 | #expiration { 375 | transition: all 0.3s ease; 376 | } 377 | 378 | .import-export-buttons { 379 | position: fixed; 380 | display: flex; 381 | bottom: -60px; 382 | gap: 10px; 383 | } 384 | 385 | .settings-btn-wrapper { 386 | position: absolute; 387 | top: 15px; 388 | right: 15px; 389 | } 390 | 391 | .settings-btn-wrapper button { 392 | font-family: "Inter", sans-serif; 393 | font-weight: 500; 394 | font-size: 15px; 395 | color: #6a64f1; 396 | border: none; 397 | background-color: transparent; 398 | cursor: pointer; 399 | } 400 | 401 | .settings-btn-wrapper :hover { 402 | filter: brightness(0.8); 403 | transition: 0.5s all; 404 | } 405 | -------------------------------------------------------------------------------- /css/settings.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | font-family: "Inter", sans-serif; 11 | } 12 | 13 | .main-wrapper { 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | padding: 22px; 18 | } 19 | 20 | .form-wrapper { 21 | margin: 0 auto; 22 | width: 270px; 23 | height: 470px; 24 | background: white; 25 | padding: 20px; 26 | } 27 | 28 | .title { 29 | margin-bottom: 20px; 30 | } 31 | 32 | .title h3 { 33 | color: #07074d; 34 | font-weight: 700; 35 | font-size: 24px; 36 | line-height: 24px; 37 | margin-bottom: 8px; 38 | } 39 | 40 | .buttons-wrapper { 41 | display: flex; 42 | gap: 10px; 43 | margin-bottom: 20px; 44 | } 45 | 46 | button { 47 | display: flex; 48 | align-items: center; 49 | gap: 5px; 50 | font-size: 15px; 51 | border-radius: 5px; 52 | padding: 10px 33px; 53 | border: none; 54 | font-weight: 500; 55 | background-color: #6a64f1; 56 | color: white; 57 | cursor: pointer; 58 | transition: transform 0.2s ease, color 0.2s ease; 59 | } 60 | 61 | .button:hover { 62 | box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.05); 63 | transform: scale(0.98); 64 | } 65 | 66 | .button:active { 67 | transform: scale(0.98); 68 | } 69 | 70 | .footer { 71 | font-size: 12px; 72 | line-height: 24px; 73 | color: #536387; 74 | } 75 | 76 | .footer p { 77 | margin-top: 10px; 78 | } 79 | 80 | .footer a { 81 | color: #6a64f1; 82 | text-decoration: none; 83 | } 84 | 85 | .footer a:hover { 86 | color: #07074d; 87 | } 88 | 89 | .popup-notification { 90 | position: fixed; 91 | color: #fff; 92 | top: 40px; 93 | right: calc(50% - 111px); 94 | background-color: rgb(90, 161, 88); 95 | border-radius: 4px; 96 | width: 65px; 97 | height: 30px; 98 | display: flex; 99 | align-items: center; 100 | justify-content: center; 101 | z-index: 999; 102 | } 103 | 104 | #popup-text { 105 | color: white; 106 | padding-left: 15px; 107 | line-height: 30px; 108 | font-size: 13px; 109 | } 110 | 111 | #expiration { 112 | transition: all 0.3s ease; 113 | } 114 | -------------------------------------------------------------------------------- /js/popup.js: -------------------------------------------------------------------------------- 1 | const keyStorage = new KeyStorage(); 2 | 3 | document.addEventListener("DOMContentLoaded", async () => { 4 | await displayKeys(); 5 | 6 | const form = document.getElementById("add-key-form"); 7 | form.addEventListener("submit", async (event) => { 8 | event.preventDefault(); 9 | 10 | const source = form.source.value; 11 | const key = form.key.value; 12 | const expirationOption = form["expiration-option"].value; 13 | const expirationDateElement = form.expiration; 14 | 15 | let expiration = null; 16 | if (expirationOption !== "never") { 17 | if (expirationOption === "custom") { 18 | expiration = expirationDateElement.valueAsNumber; 19 | } else { 20 | const days = parseInt(expirationOption, 10); 21 | const currentTime = new Date().getTime(); 22 | expiration = currentTime + days * 24 * 60 * 60 * 1000; 23 | } 24 | } 25 | 26 | await keyStorage.addKey({ 27 | source, 28 | key, 29 | expiration, 30 | }); 31 | await displayKeys(); 32 | stepMenuOne.click(); 33 | form.reset(); 34 | }); 35 | 36 | const expirationSelect = document.getElementById("expiration-option"); 37 | expirationSelect.addEventListener("change", () => { 38 | if (expirationSelect.value === "custom") { 39 | document.getElementById("expiration").style.display = "block"; 40 | } else { 41 | document.getElementById("expiration").style.display = "none"; 42 | } 43 | }); 44 | 45 | // Tab navigation 46 | const stepMenuOne = document.querySelector(".step-menu1"); 47 | const stepMenuTwo = document.querySelector(".step-menu2"); 48 | 49 | const stepOne = document.querySelector(".form-step-1"); 50 | const stepTwo = document.querySelector(".form-step-2"); 51 | 52 | stepMenuOne.addEventListener("click", function (event) { 53 | event.preventDefault(); 54 | 55 | stepMenuOne.classList.add("active"); 56 | stepMenuTwo.classList.remove("active"); 57 | 58 | stepOne.classList.add("active"); 59 | stepTwo.classList.remove("active"); 60 | }); 61 | 62 | stepMenuTwo.addEventListener("click", function (event) { 63 | event.preventDefault(); 64 | 65 | stepMenuOne.classList.remove("active"); 66 | stepMenuTwo.classList.add("active"); 67 | 68 | stepOne.classList.remove("active"); 69 | stepTwo.classList.add("active"); 70 | }); 71 | }); 72 | 73 | async function displayKeys() { 74 | const keysList = document.getElementById("keys-list"); 75 | const keys = await keyStorage.getKeys(); 76 | keys.reverse(); 77 | keysList.innerHTML = ""; 78 | if (keys.length === 0) { 79 | // Display the empty message when there are no keys 80 | const emptyMessage = document.createElement("div"); 81 | emptyMessage.className = "key-details"; 82 | emptyMessage.innerHTML = ` 83 |
84 |
85 |
Welcome to Key Chain
86 |
87 |
88 | 94 | `; 95 | keysList.appendChild(emptyMessage); 96 | } else { 97 | keys.forEach((key) => { 98 | const keyElement = document.createElement("div"); 99 | keyElement.className = "key-details"; 100 | 101 | keyElement.innerHTML = ` 102 |
103 |
104 |
${key.source}
105 |
106 | Copy 109 | Delete 112 |
113 |
114 |
115 | 129 | `; 130 | 131 | keysList.appendChild(keyElement); 132 | 133 | keyElement 134 | .querySelector(".delete-icon") 135 | .addEventListener("click", async (e) => { 136 | e.stopPropagation(); // 阻止事件冒泡 137 | const id = key.id; 138 | await keyStorage.removeKey(id); 139 | await displayKeys(); 140 | }); 141 | 142 | keyElement.querySelector(".key-content").addEventListener("click", () => { 143 | const keyToCopy = key.key; 144 | const el = document.createElement("textarea"); 145 | el.value = keyToCopy; 146 | document.body.appendChild(el); 147 | el.select(); 148 | document.execCommand("copy"); 149 | document.body.removeChild(el); 150 | showPopupNotification("Copied"); 151 | }); 152 | }); 153 | } 154 | } 155 | 156 | function showPopupNotification(message) { 157 | const popupNotification = document.getElementById("popup-notification"); 158 | const popupText = document.getElementById("popup-text"); 159 | 160 | popupText.innerText = message; 161 | popupNotification.style.opacity = 0; 162 | popupNotification.style.display = "block"; 163 | 164 | setTimeout(() => { 165 | // Fade in animation 166 | popupNotification.style.transition = "opacity 0.5s ease"; 167 | popupNotification.style.opacity = 1; 168 | }, 50); 169 | 170 | setTimeout(() => { 171 | // Fade out animation 172 | popupNotification.style.transition = "opacity 0.5s ease"; 173 | popupNotification.style.opacity = 0; 174 | }, 2000); 175 | 176 | setTimeout(() => { 177 | popupNotification.style.display = "none"; 178 | }, 2500); 179 | } 180 | -------------------------------------------------------------------------------- /js/settings.js: -------------------------------------------------------------------------------- 1 | function arrayBufferToBase64(buffer) { 2 | const byteArray = new Uint8Array(buffer); 3 | return btoa(String.fromCharCode(...byteArray)); 4 | } 5 | 6 | function base64ToArrayBuffer(base64) { 7 | const binaryString = atob(base64); 8 | const len = binaryString.length; 9 | const bytes = new Uint8Array(len); 10 | for (let i = 0; i < len; i++) { 11 | bytes[i] = binaryString.charCodeAt(i); 12 | } 13 | return bytes.buffer; 14 | } 15 | 16 | const keyStorage = new KeyStorage(); 17 | async function handleExport() { 18 | const exportData = await keyStorage.exportEncryptedKeys(); 19 | const dataJSON = JSON.stringify(exportData); 20 | const blob = new Blob([dataJSON], { type: "application/json" }); 21 | const url = URL.createObjectURL(blob); 22 | const link = document.createElement("a"); 23 | link.href = url; 24 | link.download = "keychain_backup.json"; 25 | document.body.appendChild(link); 26 | link.click(); 27 | document.body.removeChild(link); 28 | showPopupNotification("Done"); 29 | } 30 | 31 | async function handleImport() { 32 | const input = document.createElement("input"); 33 | input.type = "file"; 34 | input.accept = "application/json"; 35 | input.onchange = async (event) => { 36 | const file = event.target.files[0]; 37 | if (file) { 38 | const reader = new FileReader(); 39 | reader.onload = async (event) => { 40 | try { 41 | const data = JSON.parse(event.target.result); 42 | await keyStorage.importEncryptedKeys(data); 43 | showPopupNotification("Done"); 44 | } catch (error) { 45 | console.error("Error importing keys:", error); 46 | } 47 | }; 48 | reader.readAsText(file); 49 | } 50 | }; 51 | document.body.appendChild(input); 52 | input.click(); 53 | document.body.removeChild(input); 54 | } 55 | 56 | document 57 | .getElementById("import-button") 58 | .addEventListener("click", handleImport); 59 | document 60 | .getElementById("export-button") 61 | .addEventListener("click", handleExport); 62 | 63 | function showPopupNotification(message) { 64 | const popupNotification = document.getElementById("popup-notification"); 65 | const popupText = document.getElementById("popup-text"); 66 | 67 | popupText.innerText = message; 68 | popupNotification.style.opacity = 0; 69 | popupNotification.style.display = "block"; 70 | 71 | setTimeout(() => { 72 | // Fade in animation 73 | popupNotification.style.transition = "opacity 0.5s ease"; 74 | popupNotification.style.opacity = 1; 75 | }, 50); 76 | 77 | setTimeout(() => { 78 | // Fade out animation 79 | popupNotification.style.transition = "opacity 0.5s ease"; 80 | popupNotification.style.opacity = 0; 81 | }, 2000); 82 | 83 | setTimeout(() => { 84 | popupNotification.style.display = "none"; 85 | }, 2500); 86 | } 87 | -------------------------------------------------------------------------------- /js/storage.js: -------------------------------------------------------------------------------- 1 | class KeyStorage { 2 | constructor() { 3 | this.dbName = "KeyVault"; 4 | this.objectStoreName = "keys"; 5 | this.keyStoreName = "encryptionKeys"; 6 | this.init(); 7 | } 8 | 9 | async init() { 10 | const request = indexedDB.open(this.dbName); 11 | request.onupgradeneeded = (event) => { 12 | const db = event.target.result; 13 | db.createObjectStore(this.objectStoreName, { 14 | keyPath: "id", 15 | autoIncrement: true, 16 | }); 17 | db.createObjectStore(this.keyStoreName, { keyPath: "id" }); 18 | }; 19 | request.onsuccess = (event) => { 20 | const db = event.target.result; 21 | const keyTransaction = db.transaction(this.keyStoreName); 22 | const keyObjectStore = keyTransaction.objectStore(this.keyStoreName); 23 | const getRequest = keyObjectStore.get(1); 24 | getRequest.onsuccess = async () => { 25 | if (!getRequest.result) { 26 | const generatedKey = await this.generateKey(); 27 | const exportedKey = await crypto.subtle.exportKey( 28 | "jwk", 29 | generatedKey 30 | ); 31 | this.encryptionKey = generatedKey; 32 | const keyRequest = indexedDB.open(this.dbName); 33 | keyRequest.onsuccess = (event) => { 34 | const db = event.target.result; 35 | const transaction = db.transaction(this.keyStoreName, "readwrite"); 36 | const objectStore = transaction.objectStore(this.keyStoreName); 37 | objectStore.add({ id: 1, key: exportedKey }); 38 | }; 39 | } else { 40 | this.encryptionKey = await crypto.subtle.importKey( 41 | "jwk", 42 | getRequest.result.key, 43 | { 44 | name: "AES-GCM", 45 | length: 256, 46 | }, 47 | true, 48 | ["encrypt", "decrypt"] 49 | ); 50 | } 51 | }; 52 | }; 53 | } 54 | 55 | generateKey() { 56 | return crypto.subtle.generateKey( 57 | { 58 | name: "AES-GCM", 59 | length: 256, 60 | }, 61 | true, 62 | ["encrypt", "decrypt"] 63 | ); 64 | } 65 | 66 | async encrypt(plaintext, key) { 67 | const encoder = new TextEncoder(); 68 | const encoded = encoder.encode(plaintext); 69 | const iv = crypto.getRandomValues(new Uint8Array(12)); 70 | const ciphertext = await crypto.subtle.encrypt( 71 | { 72 | name: "AES-GCM", 73 | iv: iv, 74 | }, 75 | key, 76 | encoded 77 | ); 78 | return { 79 | iv: iv, 80 | ciphertext: ciphertext, 81 | }; 82 | } 83 | 84 | async decrypt(ciphertext, iv, key) { 85 | const decoder = new TextDecoder(); 86 | const decrypted = await crypto.subtle.decrypt( 87 | { 88 | name: "AES-GCM", 89 | iv: iv, 90 | }, 91 | key, 92 | ciphertext 93 | ); 94 | return String(decoder.decode(decrypted)); 95 | } 96 | 97 | async exportEncryptedKeys() { 98 | const keys = await this.getEncryptedKeys(); 99 | const encryptedKeys = []; 100 | 101 | for (const key of keys) { 102 | const encryptedKeyInfo = { 103 | id: key.id, 104 | source: key.source, 105 | key: { 106 | ciphertext: arrayBufferToBase64(key.key.ciphertext), 107 | iv: arrayBufferToBase64(key.key.iv), 108 | }, 109 | expiration: key.expiration, 110 | }; 111 | encryptedKeys.push(encryptedKeyInfo); 112 | } 113 | 114 | const exportedKey = await crypto.subtle.exportKey( 115 | "jwk", 116 | this.encryptionKey 117 | ); 118 | return { 119 | keys: encryptedKeys, 120 | encryptionKey: exportedKey, 121 | }; 122 | } 123 | 124 | async importEncryptedKeys(data) { 125 | const importedKey = await crypto.subtle.importKey( 126 | "jwk", 127 | data.encryptionKey, 128 | { 129 | name: "AES-GCM", 130 | length: 256, 131 | }, 132 | true, 133 | ["encrypt", "decrypt"] 134 | ); 135 | this.encryptionKey = importedKey; 136 | const keyRequest = indexedDB.open(this.dbName); 137 | keyRequest.onsuccess = (event) => { 138 | const db = event.target.result; 139 | const transaction = db.transaction(this.keyStoreName, "readwrite"); 140 | const objectStore = transaction.objectStore(this.keyStoreName); 141 | objectStore.put({ id: 1, key: data.encryptionKey }); 142 | }; 143 | await this.removeAllKeys(); 144 | for (const key of data.keys) { 145 | const decryptedApikey = await this.decrypt( 146 | base64ToArrayBuffer(key.key.ciphertext), 147 | base64ToArrayBuffer(key.key.iv), 148 | this.encryptionKey 149 | ); 150 | const keyInfo = { 151 | id: key.id, 152 | source: key.source, 153 | key: decryptedApikey, 154 | expiration: key.expiration, 155 | }; 156 | await this.addKey(keyInfo); 157 | } 158 | } 159 | 160 | async exportPlainKeys() { 161 | let plaintextKeys = await this.getKeys(); 162 | 163 | const exportedKey = await crypto.subtle.exportKey( 164 | "jwk", 165 | this.encryptionKey 166 | ); 167 | return { 168 | keys: plaintextKeys, 169 | encryptionKey: exportedKey, 170 | }; 171 | } 172 | 173 | async importPlainKeys(data) { 174 | const importedKey = await crypto.subtle.importKey( 175 | "jwk", 176 | data.encryptionKey, 177 | { 178 | name: "AES-GCM", 179 | length: 256, 180 | }, 181 | true, 182 | ["encrypt", "decrypt"] 183 | ); 184 | this.encryptionKey = importedKey; 185 | await this.removeAllKeys(); 186 | 187 | for (let tempKey of data.keys) { 188 | const keyInfo = { 189 | id: tempKey.id, 190 | source: tempKey.source, 191 | key: tempKey.key, 192 | expiration: tempKey.expiration, 193 | }; 194 | await this.addKey(keyInfo); 195 | } 196 | } 197 | 198 | async removeAllKeys() { 199 | return new Promise((resolve, reject) => { 200 | const request = indexedDB.open(this.dbName); 201 | request.onsuccess = (event) => { 202 | const db = event.target.result; 203 | const transaction = db.transaction(this.objectStoreName, "readwrite"); 204 | const objectStore = transaction.objectStore(this.objectStoreName); 205 | const clearRequest = objectStore.clear(); 206 | 207 | clearRequest.onsuccess = () => resolve(clearRequest.result); 208 | transaction.onerror = () => reject(transaction.error); 209 | }; 210 | }); 211 | } 212 | 213 | async addKey(keyInfo) { 214 | return new Promise(async (resolve, reject) => { 215 | const encryptedApikey = await this.encrypt( 216 | keyInfo.key, 217 | this.encryptionKey 218 | ); 219 | keyInfo.key = encryptedApikey; 220 | const request = indexedDB.open(this.dbName); 221 | request.onsuccess = (event) => { 222 | const db = event.target.result; 223 | const transaction = db.transaction(this.objectStoreName, "readwrite"); 224 | const objectStore = transaction.objectStore(this.objectStoreName); 225 | const addRequest = objectStore.add(keyInfo); 226 | 227 | addRequest.onsuccess = () => resolve(addRequest.result); 228 | transaction.onerror = () => reject(transaction.error); 229 | }; 230 | }); 231 | } 232 | 233 | async getEncryptedKeys() { 234 | return new Promise((resolve, reject) => { 235 | const request = indexedDB.open(this.dbName); 236 | request.onsuccess = (event) => { 237 | const db = event.target.result; 238 | const transaction = db.transaction(this.objectStoreName); 239 | const objectStore = transaction.objectStore(this.objectStoreName); 240 | const keys = []; 241 | 242 | objectStore.openCursor().onsuccess = (event) => { 243 | const cursor = event.target.result; 244 | if (cursor) { 245 | keys.push(cursor.value); 246 | cursor.continue(); 247 | } else { 248 | resolve(keys); 249 | } 250 | }; 251 | }; 252 | }); 253 | } 254 | 255 | async getKeys() { 256 | return new Promise((resolve, reject) => { 257 | try { 258 | const request = indexedDB.open(this.dbName); 259 | 260 | request.onerror = (event) => { 261 | reject(new Error("Unable to open DB")); 262 | }; 263 | 264 | request.onsuccess = (event) => { 265 | const db = event.target.result; 266 | const transaction = db.transaction(this.objectStoreName); 267 | const objectStore = transaction.objectStore(this.objectStoreName); 268 | const keys = []; 269 | 270 | objectStore.openCursor().onsuccess = (event) => { 271 | const cursor = event.target.result; 272 | if (cursor) { 273 | keys.push(cursor.value); 274 | cursor.continue(); 275 | } 276 | }; 277 | 278 | transaction.oncomplete = async () => { 279 | try { 280 | for (const key of keys) { 281 | key.key = await this.decrypt( 282 | key.key.ciphertext, 283 | key.key.iv, 284 | this.encryptionKey 285 | ); 286 | } 287 | resolve(keys); 288 | } catch (error) { 289 | reject(new Error("Decrypt error:" + error)); 290 | } 291 | }; 292 | 293 | transaction.onerror = () => reject(transaction.error); 294 | }; 295 | } catch (error) { 296 | reject(new Error("DB Operation error:" + error)); 297 | } 298 | }); 299 | } 300 | 301 | async removeKey(id) { 302 | return new Promise((resolve, reject) => { 303 | const request = indexedDB.open(this.dbName); 304 | request.onsuccess = (event) => { 305 | const db = event.target.result; 306 | const transaction = db.transaction(this.objectStoreName, "readwrite"); 307 | const objectStore = transaction.objectStore(this.objectStoreName); 308 | const deleteRequest = objectStore.delete(id); 309 | 310 | deleteRequest.onsuccess = () => resolve(deleteRequest.result); 311 | transaction.onerror = () => reject(transaction.error); 312 | }; 313 | }); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /js/sync.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwjoel/KeyChain/9577784f85493ccd82b9d974dd8ca53ea46780d2/js/sync.js -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "KeyChain", 4 | "version": "0.1.1", 5 | "description": "A browser extension for managing API Keys locally.", 6 | "action": { 7 | "default_popup": "popup.html" 8 | }, 9 | "permissions": ["storage"], 10 | "icons": { 11 | "16": "assets/vault.png", 12 | "48": "assets/vault.png", 13 | "128": "assets/vault.png" 14 | }, 15 | "content_security_policy": { 16 | "extension_pages": "script-src 'self'; object-src 'self'" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 |
19 |
20 |
21 |
22 |

Key Chain

23 |

Safeguard API Keys

24 |
25 |
26 | 35 |
36 |
37 |
    38 |
  • 39 | C 40 | Chain 41 |
  • 42 |
  • 43 | A 44 | AddKey 45 |
  • 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | 54 |
55 |
56 |
57 | 58 | 65 |
66 | 67 |
68 | 69 | 76 |
77 | 78 |
79 | 82 | 89 |
90 | 91 |
92 | 98 |
99 | 100 |
101 | 102 |
103 |
104 |
105 |
106 |
107 | 108 | 109 | -------------------------------------------------------------------------------- /settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 |
19 |
20 |
21 |

Settings

22 |
23 |

Backup

24 |
25 |
26 | 27 | 28 |
29 | 38 | 53 |
54 |
55 | 56 | 57 | --------------------------------------------------------------------------------