From b135aac8531c1e1488147ad8c6f98eddbdbe0c99 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Mon, 17 Apr 2023 18:53:55 +0000 Subject: Initial commit Created from https://vercel.com/new --- .eslintrc.json | 3 + .gitignore | 36 + .vscode/settings.json | 4 + LICENSE | 21 + README.md | 139 ++ app/[compositeKey]/page.tsx | 7 + app/components/analytics.tsx | 22 + app/components/error.tsx | 11 + app/components/stats.tsx | 57 + app/components/testimony.tsx | 125 ++ app/components/title.tsx | 9 + app/deploy/page.tsx | 89 + app/globals.css | 15 + app/head.tsx | 42 + app/header.tsx | 60 + app/layout.tsx | 58 + app/page.tsx | 50 + app/share/page.tsx | 227 ++ app/unseal/page.tsx | 159 ++ img/envshare.png | Bin 0 -> 275552 bytes jest.config.js | 6 + next.config.js | 11 + package.json | 42 + pages/api/v1/load.ts | 36 + pages/api/v1/og.tsx | 64 + pages/api/v1/secret/[id].ts | 58 + pages/api/v1/secret/index.ts | 96 + pages/api/v1/store.ts | 38 + pkg/constants.ts | 3 + pkg/encoding.test.ts | 23 + pkg/encoding.ts | 31 + pkg/encryption.test.ts | 24 + pkg/encryption.ts | 51 + pkg/id.ts | 8 + pnpm-lock.yaml | 4505 +++++++++++++++++++++++++++++++++++++++ postcss.config.js | 6 + public/fonts/Inter-SemiBold.ttf | Bin 0 -> 315756 bytes rome.json | 26 + tailwind.config.js | 18 + tsconfig.json | 30 + util/base58.ts | 7 + 41 files changed, 6217 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/[compositeKey]/page.tsx create mode 100644 app/components/analytics.tsx create mode 100644 app/components/error.tsx create mode 100644 app/components/stats.tsx create mode 100644 app/components/testimony.tsx create mode 100644 app/components/title.tsx create mode 100644 app/deploy/page.tsx create mode 100644 app/globals.css create mode 100644 app/head.tsx create mode 100644 app/header.tsx create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/share/page.tsx create mode 100644 app/unseal/page.tsx create mode 100644 img/envshare.png create mode 100644 jest.config.js create mode 100644 next.config.js create mode 100644 package.json create mode 100644 pages/api/v1/load.ts create mode 100644 pages/api/v1/og.tsx create mode 100644 pages/api/v1/secret/[id].ts create mode 100644 pages/api/v1/secret/index.ts create mode 100644 pages/api/v1/store.ts create mode 100644 pkg/constants.ts create mode 100644 pkg/encoding.test.ts create mode 100644 pkg/encoding.ts create mode 100644 pkg/encryption.test.ts create mode 100644 pkg/encryption.ts create mode 100644 pkg/id.ts create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.js create mode 100644 public/fonts/Inter-SemiBold.ttf create mode 100644 rome.json create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 util/base58.ts diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a333bff --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e769a45 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "typescript.tsdk": "node_modules/.pnpm/typescript@4.9.4/node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e4712ee --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Andreas Thomas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f890f9d --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +
+

EnvShare

+
Share Environment Variables Securely
+
+ +
+ envshare.dev +
+
+ +EnvShare is a simple tool to share environment variables securely. It uses +**AES-GCM** to encrypt your data before sending it to the server. The encryption +key never leaves your browser. + +## Features + +- **Shareable Links:** Share your environment variables securely by sending a + link +- **End-to-End Encryption:** AES-GCM encryption is used to encrypt your data + before sending it to the server +- **Limit number of reads:** Limit the number of times a link can be read +- **Auto Expire:** Automatically expire links and delete data after a certain + time + +
+ +![](img/envshare.png) + +## Built with + +- [Next.js](https://nextjs.org) +- [tailwindcss](https://tailwindcss.com) +- Deployed on [Vercel](https://vercel.com?utm_source=envshare) +- Data stored on [Upstash](https://upstash.com?utm_source=envshare) + +## Deploy your own + +Detailed instructions can be found [here](https://envshare.dev/deploy) + +All you need is a Redis database on Upstash and a Vercel account. Click the +button below to clone and deploy: + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=EnvShare&demo-description=Simple%20Next.js%20%2B%20Upstash%20app%20to%20share%20environment%20variables%20securely%20using%20AES-GCM%20encryption.&demo-url=https%3A%2F%2Fenvshare.dev%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F5SaFBHXp5FBFJbsTzVqIJ3%2Ff0f8382369b7642fd8103debb9025c11%2Fenvshare.png&project-name=EnvShare&repository-name=envshare&repository-url=https%3A%2F%2Fgithub.com%2Fchronark%2Fenvshare&from=templates&integration-ids=oac_V3R1GIpkoJorr6fqyiwdhl17) + + +## Sponsors + + + + + +
+ + + Upstash + +

Upstash: Serverless Database for Redis

+ +
    +
  • Serverless Redis with global replication and durable storage
  • +
  • Price scales to zero with per request pricing
  • +
  • Built-in REST API designed for serverless and edge functions
  • +
+ +[Start for free in 30 seconds!](https://upstash.com/?utm_source=envshare) + +
+ +## Configuration + +### Environment Variables + +`ENABLE_VERCEL_ANALYTICS` Any truthy value will enable Vercel Analytics. This is turned off by default + +## Contributing + +This repository uses `pnpm` to manage dependencies. Install it using +`npm install -g pnpm` + +Please run `pnpm fmt` before committing to format the code. + +## Docs + +Docs in the README are temporary and will be moved to the website soon. + +### API + +#### Store a secret + +**PLEASE NEVER EVER UPLOAD UNENCRYPTED SECRETS.** + +This endpoint is only meant to store **already encrypted** secrets. The +encrypted secrets are stored in plain text. + +```sh-session +$ curl -XPOST -s https://envshare.dev/api/v1/secret -d "already-encrypted-secret" +``` + +You can add optional headers to configure the ttl and number of reads. + +```sh-session +$ curl -XPOST -s https://envshare.dev/api/v1/secret -d "already-encrypted-secret" -H "envshare-ttl: 3600" -H "envshare-reads: 10" +``` + +- Omitting the `envshare-ttl` header will set a default of 30 days. Disable the + ttl by setting it to 0. (`envshare-ttl: 0`) +- Omitting the `envshare-reads` header will simply disable it and allow reading + for an unlimited number of times. + +This endpoint returns a JSON response with the secret id: + +```json +{ + "data": { + "id": "HdPbXgpvUvNk43oxSdK97u", + "ttl": 86400, + "reads": 2, + "expiresAt": "2023-01-19T20:47:28.383Z", + "url": "http://envshare.dev/api/v1/secret/HdPbXgpvUvNk43oxSdK97u" + } +} +``` + +#### Retrieve a secret + +You need an id to retrieve a secret. The id is returned when you store a secret. + +```sh-session +$ curl -s https://envshare.dev/api/v1/secret/HdPbXgpvUvNk43oxSdK97u +``` + +```json +{ + "data": { + "secret": "Hello", + "remainingReads": 1 + } +} +``` diff --git a/app/[compositeKey]/page.tsx b/app/[compositeKey]/page.tsx new file mode 100644 index 0000000..d91f182 --- /dev/null +++ b/app/[compositeKey]/page.tsx @@ -0,0 +1,7 @@ +import { redirect } from "next/navigation"; + +// This page is here for backwards compatibility with old links. +// Old links were of the form /{compositeKey} and now they are of the form /unseal#{compositeKey} +export default function Page(props: { params: { compositeKey: string } }) { + return redirect(`/unseal#${props.params.compositeKey}`); +} diff --git a/app/components/analytics.tsx b/app/components/analytics.tsx new file mode 100644 index 0000000..ef6a2ae --- /dev/null +++ b/app/components/analytics.tsx @@ -0,0 +1,22 @@ +"use client"; +import { Analytics as VercelAnalytics } from "@vercel/analytics/react"; + +const track = ["/", "/share", "/deploy", "/unseal"]; + +export function Analytics() { + return ( + { + const url = new URL(event.url); + if (!track.includes(url.pathname)) { + url.pathname = "/__redacted"; + return { + ...event, + url: url.href, + }; + } + return event; + }} + /> + ); +} diff --git a/app/components/error.tsx b/app/components/error.tsx new file mode 100644 index 0000000..acf36d7 --- /dev/null +++ b/app/components/error.tsx @@ -0,0 +1,11 @@ +type Props = { + message: string; +}; + +export const ErrorMessage: React.FC = ({ message }) => { + return ( +
+ {message} +
+ ); +}; diff --git a/app/components/stats.tsx b/app/components/stats.tsx new file mode 100644 index 0000000..31d74bc --- /dev/null +++ b/app/components/stats.tsx @@ -0,0 +1,57 @@ +import { Redis } from "@upstash/redis"; + +const redis = Redis.fromEnv(); +export const revalidate = 60; + +export const Stats = asyncComponent(async () => { + const [reads, writes] = await redis + .pipeline() + .get("envshare:metrics:reads") + .get("envshare:metrics:writes") + .exec<[number, number]>(); + const stars = await fetch("https://api.github.com/repos/chronark/envshare") + .then((res) => res.json()) + .then((json) => json.stargazers_count as number); + + const stats = [ + { + label: "Documents Encrypted", + value: writes, + }, + { + label: "Documents Decrypted", + value: reads, + }, + ] satisfies { label: string; value: number }[]; + + if (stars) { + stats.push({ + label: "GitHub Stars", + value: stars, + }); + } + + return ( +
+
    + {stats.map(({ label, value }) => ( +
  • +
    + {Intl.NumberFormat("en-US", { notation: "compact" }).format(value)} +
    +
    {label}
    +
  • + ))} +
+
+ ); +}); + +// stupid hack to make "server components" actually work with components +// https://www.youtube.com/watch?v=h_9Vx6kio2s +function asyncComponent(fn: (arg: T) => Promise): (arg: T) => R { + return fn as (arg: T) => R; +} diff --git a/app/components/testimony.tsx b/app/components/testimony.tsx new file mode 100644 index 0000000..757a953 --- /dev/null +++ b/app/components/testimony.tsx @@ -0,0 +1,125 @@ +"use client"; +import Image from "next/image"; +import Link from "next/link"; +import { Props } from "next/script"; +import React, { PropsWithChildren } from "react"; + +const TwitterHandle: React.FC = ({ children }) => { + return {children}; +}; + +const Author: React.FC> = ({ children, href }) => ( + + {children} + +); + +const Title: React.FC> = ({ children, href }) => ( + + {children} + +); + +export const Testimonials = () => { + const posts: { + content: React.ReactNode; + link: string; + author: { + name: React.ReactNode; + title?: React.ReactNode; + image: string; + }; + }[] = [ + { + content: ( +
+

+ My cursory audit of @chronark_'s envshare: +

+

+ It is light, extremely functional, and does its symmetric block cipher correctly, unique initialization + vectors, decryption keys derived securely. +

+
+

Easily modified to remove minimal analytics. Superior to Privnote.

+
+

Self-hosting is easy. 👏

+
+ ), + link: "https://twitter.com/FrederikMarkor/status/1615299856205250560", + author: { + name: Frederik Markor, + title: CEO @discreet, + image: "https://pbs.twimg.com/profile_images/1438061314010664962/NecuMIGR_400x400.jpg", + }, + }, + { + content: ( +
+

I'm particularly chuffed about this launch, for a couple of reasons:

+
    +
  • + ◆ Built on @nextjs + @upstash, hosted on{" "} + @vercel +
  • +
  • ◆ 100% free to use & open source
  • +
  • ◆ One-click deploy via Vercel + Upstash integration
  • +
+

Deploy your own → http://vercel.fyi/envshare

+
+ ), + link: "https://twitter.com/steventey/status/1615035241772482567?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1615035241772482567%7Ctwgr%5E1db44bb10c690189e24c980fcd787299961c34c6%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fpublish.twitter.com%2F%3Fquery%3Dhttps3A2F2Ftwitter.com2Fsteventey2Fstatus2F1615035241772482567widget%3DTweet", + author: { + name: Steven Tey, + title: Senior Developer Advocate at Vercel, + image: "https://pbs.twimg.com/profile_images/1506792347840888834/dS-r50Je_400x400.jpg", + }, + }, + { + content: ( +
+

+ Congratulations on the launch @chronark_👏! This is such a valuable product + for developers. Icing on the cake is that it's open source! ✨ +

+
+ ), + link: "https://twitter.com/DesignSiddharth/status/1615293209164546048", + author: { + name: @DesignSiddharth, + image: "https://pbs.twimg.com/profile_images/1613772710009765888/MbSblJYf_400x400.jpg", + }, + }, + ]; + + return ( +
+
    + {posts.map((post, i) => ( +
    + + {post.content} + +
    +
    +
    {post.author.name}
    +
    {post.author.title}
    +
    +
    + +
    +
    +
    + ))} +
+
+ ); +}; diff --git a/app/components/title.tsx b/app/components/title.tsx new file mode 100644 index 0000000..b9b7f3c --- /dev/null +++ b/app/components/title.tsx @@ -0,0 +1,9 @@ +import React, { PropsWithChildren } from "react"; + +export const Title: React.FC = ({ children }): JSX.Element => { + return ( +

+ {children} +

+ ); +}; diff --git a/app/deploy/page.tsx b/app/deploy/page.tsx new file mode 100644 index 0000000..b515144 --- /dev/null +++ b/app/deploy/page.tsx @@ -0,0 +1,89 @@ +"use client"; +import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid"; +import Link from "next/link"; +import { Title } from "@components/title"; +import React from "react"; +const steps: { + name: string; + description: string | React.ReactNode; + cta?: React.ReactNode; +}[] = [ + { + name: "Create a new Redis database on Upstash", + description: ( + <> + Upstash offers a serverless Redis database with a generous free tier of up to 10,000 requests per day. That's + more than enough. +
+ Click the button below to sign up and create a new Redis database on Upstash. + + ), + cta: ( + + Create Database + + + ), + }, + { + name: "Copy the REST connection credentials", + description: ( +

+ After creating the database, scroll to the bottom and make a note of UPSTASH_REDIS_REST_URL and{" "} + UPSTASH_REDIS_REST_TOKEN, you need them in the next step +

+ ), + }, + { + name: "Deploy to Vercel", + description: "Deploy the app to Vercel and paste the connection credentials into the environment variables.", + cta: ( + + Deploy + + + ), + }, +]; + +export default function Deploy() { + return ( +
+ Deploy EnvShare for Free +

+ You can deploy your own hosted version of EnvShare, you just need an Upstash and Vercel account. +

+
    + {steps.map((step, stepIdx) => ( +
  1. +
  2. + ))} +
+
+ ); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..b764a11 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,15 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer utilities { + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { + @apply appearance-none; + } + + + input[type="file"] { + @apply appearance-none; + } +} \ No newline at end of file diff --git a/app/head.tsx b/app/head.tsx new file mode 100644 index 0000000..aecaa44 --- /dev/null +++ b/app/head.tsx @@ -0,0 +1,42 @@ +export default function Head({ title, subtitle }: { title: string; subtitle: string }) { + // Fallback tagline + title ??= "Share Environment Variables Securely"; + subtitle ??= "EnvShare"; + + const baseUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"; + + const url = new URL("/api/v1/og", baseUrl); + url.searchParams.set("title", title); + url.searchParams.set("subtitle", subtitle); + + return ( + <> + EnvShare + + + + + + + + + + + {/* Open Graph / Facebook */} + + + + + + + + + {/* Twitter */} + + + + + + + ); +} diff --git a/app/header.tsx b/app/header.tsx new file mode 100644 index 0000000..872459a --- /dev/null +++ b/app/header.tsx @@ -0,0 +1,60 @@ +"use client"; +import React from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +const navigation = [ + { + name: "Share", + href: "/share", + }, + { + name: "Unseal", + href: "/unseal", + }, + + { + name: "Deploy", + href: "/deploy", + }, + { + name: "GitHub", + href: "https://github.com/chronark/envshare", + external: true, + }, +] satisfies { name: string; href: string; external?: boolean }[]; + +export const Header: React.FC = () => { + const pathname = usePathname(); + return ( +
+
+
+ + EnvShare + + {/* Desktop navigation */} + +
+
+ + {/* Fancy fading bottom border */} +
+ ); +}; diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..557407d --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,58 @@ +import "./globals.css"; +import { Inter } from "@next/font/google"; +import Link from "next/link"; +import { Header } from "./header"; + +import { Analytics } from "@components/analytics"; +const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + { + // Not everyone will want to host envshare on Vercel, so it makes sense to make this opt-in. + process.env.ENABLE_VERCEL_ANALYTICS ? : null + } + +
+ +
{children}
+ +
+
+

+ Built by{" "} + + @chronark_ + + and{" "} + + many others{" "} + +

+

+ EnvShare is deployed on{" "} + + Vercel + {" "} + and uses{" "} + + Upstash + {" "} + for storing encrypted data. +

+
+
+ + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..ba659ca --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; +import { Stats } from "./components/stats"; +import { Testimonials } from "./components/testimony"; + +export default function Home() { + return ( +
+
+
+ + EnvShare is Open Source on{" "} + + GitHub + + +
+
+

+ Share Environment Variables Securely +

+

+ Your document is encrypted in your browser before being stored for a limited period of time and read + operations. Unencrypted data never leaves your browser. +

+
+ + Deploy + + + Share + + +
+
+
+

Used and trusted by a growing community

+ + +
+ ); +} diff --git a/app/share/page.tsx b/app/share/page.tsx new file mode 100644 index 0000000..b6089f0 --- /dev/null +++ b/app/share/page.tsx @@ -0,0 +1,227 @@ +"use client"; +import { toBase58 } from "util/base58"; +import { useState, Fragment } from "react"; +import { Cog6ToothIcon, ClipboardDocumentIcon, ClipboardDocumentCheckIcon } from "@heroicons/react/24/outline"; +import { Title } from "@components/title"; +import { encrypt } from "pkg/encryption"; +import { ErrorMessage } from "@components/error"; +import { encodeCompositeKey } from "pkg/encoding"; +import { LATEST_KEY_VERSION } from "pkg/constants"; + +export default function Home() { + const [text, setText] = useState(""); + const [reads, setReads] = useState(999); + + const [ttl, setTtl] = useState(7); + const [ttlMultiplier, setTtlMultiplier] = useState(60 * 60 * 24); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [copied, setCopied] = useState(false); + + const [link, setLink] = useState(""); + + const onSubmit = async () => { + try { + setError(""); + setLink(""); + setLoading(true); + + const { encrypted, iv, key } = await encrypt(text); + + const { id } = (await fetch("/api/v1/store", { + method: "POST", + body: JSON.stringify({ + ttl: ttl * ttlMultiplier, + reads, + encrypted: toBase58(encrypted), + iv: toBase58(iv), + }), + }).then((r) => r.json())) as { id: string }; + + const compositeKey = encodeCompositeKey(LATEST_KEY_VERSION, id, key); + + const url = new URL(window.location.href); + url.pathname = "/unseal"; + url.hash = compositeKey; + setCopied(false); + setLink(url.toString()); + } catch (e) { + console.error(e); + setError((e as Error).message); + } finally { + setLoading(false); + } + }; + + return ( +
+ {error ? : null} + + {link ? ( +
+ Share this link with others +
+
+              {link}
+            
+ +
+
+ ) : ( +
{ + e.preventDefault(); + if (text.length <= 0) return; + onSubmit(); + }} + > + Encrypt and Share + +
+            
+ + +