Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gpg encryption #21

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion public/locales/de-DE/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"title": "Einstellungen",
"actions": {
"enable2fa": "Zwei-Faktor-Authentifizierung",
"aliasPreferences": "Alias-Präferenzen"
"aliasPreferences": "Alias-Präferenzen",
}
}
22 changes: 22 additions & 0 deletions public/locales/en-US/settings-email-pgp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"title": "Set up PGP encryption",
"description": "By providing your public key, we will encrypt all emails sent to you. This will ensure that only you can read the emails we send you. Your email provider will not be able to read your emails anymore.",
"form": {
"fields": {
"publicKey": {
"label": "Your public key",
"helperText": "Paste your raw public key in armored format here.",
"placeholder": "----BEGIN PGP PUBLIC KEY BLOCK----\n\n...\n----END PGP PUBLIC KEY BLOCK----"
}
},
"continueActionLabel": "Enable PGP encryption"
},
"findPublicKey": {
"label": "Find public key automatically",
"title": "Use this key?",
"description": "We found a public key for your email! Would you like to use it? The key has been created on {{createdAt}} and is of type {{type}}. This is the fingerprint:",
"continueActionLabel": "Use Key"
},
"alreadyConfigured": "PGP encryption is activated. You are using a public key with this fingerprint: ",
"remove": "Disable PGP encryption"
}
3 changes: 2 additions & 1 deletion public/locales/en-US/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"actions": {
"enable2fa": "Two-Factor-Authentication",
"aliasPreferences": "Alias Preferences",
"apiKeys": "Manage API Keys"
"apiKeys": "Manage API Keys",
"emailPgp": "PGP Encryption for Emails"
}
}
6 changes: 6 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import RootRoute from "~/routes/Root"
import Settings2FARoute from "~/routes/Settings2FARoute"
import SettingsAPIKeysRoute from "~/routes/SettingsAPIKeysRoute"
import SettingsAliasPreferencesRoute from "~/routes/SettingsAliasPreferencesRoute"
import SettingsEmailPGPRoute from "~/routes/SettingsEmailPGPRoute"
import SettingsRoute from "~/routes/SettingsRoute"
import SignupRoute from "~/routes/SignupRoute"
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
Expand Down Expand Up @@ -113,6 +114,11 @@ const router = createBrowserRouter([
loader: getServerSettings,
element: <SettingsAPIKeysRoute />,
},
{
path: "/settings/email-pgp",
loader: getServerSettings,
element: <SettingsEmailPGPRoute />,
},
{
path: "/reports",
loader: getServerSettings,
Expand Down
19 changes: 19 additions & 0 deletions src/apis/find-public-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {client} from "~/constants/axios-client"

export interface FindPublicKeyResponse {
publicKey: string
type: string
createdAt: Date
}

export default async function findPublicKey(): Promise<FindPublicKeyResponse> {
const {data} = await client.post(
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/preferences/find-public-key`,
{},
{
withCredentials: true,
},
)

return data
}
2 changes: 2 additions & 0 deletions src/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@ export * from "./get-api-keys"
export {default as getAPIKeys} from "./get-api-keys"
export * from "./delete-api-key"
export {default as deleteAPIKey} from "./delete-api-key"
export * from "./find-public-key"
export {default as findPublicKey} from "./find-public-key"
1 change: 1 addition & 0 deletions src/apis/update-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface UpdatePreferencesData {
aliasProxyUserAgent?: ProxyUserAgentType
aliasExpandUrlShorteners?: boolean
aliasRejectOnPrivacyLeak?: boolean
emailGpgPublicKey?: string | null
}

export default async function updatePreferences(
Expand Down
5 changes: 1 addition & 4 deletions src/route-widgets/AliasDetailRoute/SelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ export default function SelectField({
const labelId = `${name}-label`
const preferenceName = `alias${name.charAt(0).toUpperCase() + name.slice(1)}`
const value = user.preferences[preferenceName as keyof User["preferences"]]
console.log(user.preferences)
console.log(preferenceName)
console.log(value)
const defaultValueText = valueTextMap[value.toString()]
const defaultValueText = valueTextMap[value!.toString()]

return (
<FormControl fullWidth>
Expand Down
76 changes: 76 additions & 0 deletions src/route-widgets/SettingsEmailPGPRoute/AlreadyConfigured.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {AxiosError} from "axios"
import {useAsync} from "react-use"
import {readKey} from "openpgp"
import {FaLockOpen} from "react-icons/fa"
import {ReactElement, useContext} from "react"
import {useTranslation} from "react-i18next"

import {Alert, CircularProgress, Grid} from "@mui/material"
import {LoadingButton} from "@mui/lab"
import {useMutation} from "@tanstack/react-query"

import {SimpleDetailResponse, User} from "~/server-types"
import {UpdatePreferencesData, updatePreferences} from "~/apis"
import {useErrorSuccessSnacks} from "~/hooks"
import {AuthContext} from "~/components"

export default function AlreadyConfigured(): ReactElement {
const {t} = useTranslation(["settings-email-pgp", "common"])
const {user, _updateUser} = useContext(AuthContext)
const {showSuccess, showError} = useErrorSuccessSnacks()

const {mutateAsync, isLoading} = useMutation<
SimpleDetailResponse,
AxiosError,
UpdatePreferencesData
>(updatePreferences, {
onSuccess: (response, values) => {
const newUser = {
...user,
preferences: {
...user!.preferences,
...values,
},
} as User

if (response.detail) {
showSuccess(response?.detail)
}

_updateUser(newUser)
},
onError: showError,
})
const {value: fingerprint, loading: isLoadingFingerprint} = useAsync(async () => {
const key = await readKey({
armoredKey: user!.preferences.emailGpgPublicKey!,
})

return key.getFingerprint()
}, [user?.preferences?.emailGpgPublicKey])

return (
<Grid container spacing={4}>
<Grid item>
<Alert severity="success" variant="standard">
{t("alreadyConfigured")}
{isLoadingFingerprint ? <CircularProgress /> : <code>{fingerprint}</code>}
</Alert>
</Grid>
<Grid item>
<LoadingButton
variant="contained"
loading={isLoading}
startIcon={<FaLockOpen />}
onClick={() =>
mutateAsync({
emailGpgPublicKey: null,
})
}
>
{t("remove")}
</LoadingButton>
</Grid>
</Grid>
)
}
71 changes: 71 additions & 0 deletions src/route-widgets/SettingsEmailPGPRoute/ImportKeyDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {ReactElement} from "react"
import {useTranslation} from "react-i18next"
import {HiKey} from "react-icons/hi"
import {TiCancel} from "react-icons/ti"
import {useAsync} from "react-use"
import {readKey} from "openpgp"

import {
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from "@mui/material"

import {FindPublicKeyResponse} from "~/apis"

export interface ImportKeyDialogProps {
open: boolean
publicKeyResult: FindPublicKeyResponse | null
onClose: () => void
onImport: () => void
}

export default function ImportKeyDialog({
open,
publicKeyResult,
onClose,
onImport,
}: ImportKeyDialogProps): ReactElement {
const {t} = useTranslation(["settings-email-pgp", "common"])
const {value: fingerprint, loading: isLoadingFingerprint} = useAsync(async () => {
if (publicKeyResult === null) {
return
}

const key = await readKey({
armoredKey: publicKeyResult!.publicKey,
})

return key.getFingerprint()
}, [publicKeyResult])

return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t("findPublicKey.title")}</DialogTitle>
<DialogContent>
<DialogContentText>
{t("findPublicKey.description", {
createdAt: publicKeyResult?.createdAt,
type: publicKeyResult?.type,
})}
</DialogContentText>
<Box my={2}>
{isLoadingFingerprint ? <CircularProgress /> : <code>{fingerprint}</code>}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} startIcon={<TiCancel />}>
{t("general.cancelLabel", {ns: "common"})}
</Button>
<Button onClick={onImport} startIcon={<HiKey />} variant="contained">
{t("findPublicKey.continueActionLabel")}
</Button>
</DialogActions>
</Dialog>
)
}
Loading