diff --git a/app.config.ts b/app.config.ts index 426d2f9..e101a2d 100644 --- a/app.config.ts +++ b/app.config.ts @@ -67,8 +67,8 @@ export default ({ config }: ConfigContext) => "@bacons/apple-targets", ["./scripts/fdroid/configureFdroid.ts"], [ - "./scripts/detox/configureDetox.ts", - { subdomains: "*" }, // uncomment to debug app + "./scripts/detox/configureDetox.ts", + { subdomains: "*" }, // uncomment to debug app ], ["./scripts/trustUserCAs.ts"], ["./scripts/increaseGradleMemory.ts"], diff --git a/components/ServerForm.tsx b/components/ServerForm.tsx index 25f2f04..855ec22 100644 --- a/components/ServerForm.tsx +++ b/components/ServerForm.tsx @@ -1,4 +1,5 @@ import React, { useRef, useState } from "react"; +import { Platform } from "react-native"; import { Text, Button, Input, CheckBox } from "@ui-kitten/components"; import { cleanServerUrl, sameServer, verifyEvccServer } from "../utils/server"; import LoadingIndicator from "./animations/LoadingIndicator"; @@ -6,6 +7,10 @@ import { useTranslation } from "react-i18next"; import { BasicAuth, Server } from "types"; import { useAppContext } from "./AppContext"; import ScanQRCodeButton from "./ScanQRCodeButton"; +import * as DocumentPicker from "expo-document-picker"; +import * as FileSystem from "expo-file-system/legacy"; +import { storeCert } from "../utils/certStorage"; +import NativeClientCertModule from "../modules/client-cert/src/ClientCertModule"; interface ServerFormProps { server: Server | undefined; @@ -44,6 +49,7 @@ export default function ServerForm({ title: internalServer?.title, url, basicAuth: internalServer?.basicAuth || {}, + clientCert: internalServer?.clientCert, }); }; const setInternalAuth = (basicAuth: BasicAuth) => { @@ -51,9 +57,56 @@ export default function ServerForm({ title: internalServer?.title, url: internalServer?.url || "", basicAuth, + clientCert: internalServer?.clientCert, }); }; + const [certPassword, setCertPassword] = useState(""); + const [certBase64, setCertBase64] = useState(); + const [certLabel, setCertLabel] = useState(); + + const toggleClientCert = (enabled: boolean) => { + if (!enabled) { + setCertBase64(undefined); + setCertPassword(""); + setCertLabel(undefined); + setInternalServer({ + ...internalServer, + url: internalServer?.url || "", + basicAuth: internalServer?.basicAuth || {}, + clientCert: undefined, + }); + } else { + setInternalServer({ + ...internalServer, + url: internalServer?.url || "", + basicAuth: internalServer?.basicAuth || {}, + clientCert: { label: "", secureStoreKey: "" }, + }); + } + }; + + const pickCertificate = async () => { + try { + const result = await DocumentPicker.getDocumentAsync({ + type: ["application/x-pkcs12", "application/pkcs12", "application/octet-stream"], + copyToCacheDirectory: true, + }); + + if (result.canceled) return; + + const file = result.assets[0]; + const base64 = await FileSystem.readAsStringAsync(file.uri, { + encoding: FileSystem.EncodingType.Base64, + }); + + setCertBase64(base64); + setCertLabel(file.name); + } catch (e) { + console.log("Error picking certificate", e); + } + }; + const validateAndSaveURL = async () => { if (inProgress) return; if (!internalServer?.title?.trim()) return; @@ -68,14 +121,32 @@ export default function ServerForm({ const finalUrl = await verifyEvccServer({ url: cleanUrl, basicAuth: internalServer?.basicAuth || {}, + clientCert: internalServer?.clientCert, }); - const server = { + const server: Server = { title: internalServer?.title, url: finalUrl, basicAuth: internalServer?.basicAuth || {}, }; + if (internalServer?.clientCert && certBase64) { + try { + NativeClientCertModule.setCertificate(certBase64, certPassword); + } catch { + throw new Error(t("servers.manually.clientCertNote") + " (Password invalid?)"); + } + const key = "cert_" + Date.now(); + await storeCert(key, certBase64, certPassword); + server.clientCert = { + label: certLabel || "Certificate", + secureStoreKey: key, + }; + } else if (internalServer?.clientCert?.secureStoreKey) { + // keep existing cert during update + server.clientCert = internalServer.clientCert; + } + const sameServerCount = servers.filter((s) => sameServer(server, s), ).length; @@ -183,6 +254,66 @@ export default function ServerForm({ )} + {Platform.OS === "android" && ( + <> + + {t("servers.manually.clientCertRequired")} + + + {!!internalServer?.clientCert && ( + <> + + + {certLabel && ( + + {t("servers.manually.clientCertNote")} + + )} + + {!!certBase64 && ( + + )} + + {!certLabel && !!internalServer?.clientCert && ( + + {t("servers.manually.clientCertNote")} + + )} + + )} + + )} +