├── 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 |
2 |
3 | # KeyChain
4 |
5 | [](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 |
--------------------------------------------------------------------------------
/assets/copy.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/delete.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/assets/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jwjoel/KeyChain/9577784f85493ccd82b9d974dd8ca53ea46780d2/assets/header.png
--------------------------------------------------------------------------------
/assets/setting.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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 |