From 0d3933c9fb044175a46249ea2bc7890622d26071 Mon Sep 17 00:00:00 2001 From: deep2web <88986286+deep2web@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:01:59 +0200 Subject: [PATCH 01/10] ad mTLS Client authentication --- app.config.ts | 6 +- components/ServerForm.tsx | 130 +++++++++++++++++++++++++++++++++++++- i18n/de.json | 6 +- i18n/en.json | 6 +- package-lock.json | 32 ++++++++++ package.json | 2 + screens/MainScreen.tsx | 21 ++++++ types.ts | 6 ++ utils/server.ts | 6 ++ 9 files changed, 207 insertions(+), 8 deletions(-) diff --git a/app.config.ts b/app.config.ts index 426d2f9a..8225376e 100644 --- a/app.config.ts +++ b/app.config.ts @@ -66,11 +66,9 @@ export default ({ config }: ConfigContext) => plugins: [ "@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/withClientCertPatch.ts"], ["./scripts/increaseGradleMemory.ts"], [ "expo-build-properties", diff --git a/components/ServerForm.tsx b/components/ServerForm.tsx index 25f2f04e..c1926c69 100644 --- a/components/ServerForm.tsx +++ b/components/ServerForm.tsx @@ -3,9 +3,13 @@ import { Text, Button, Input, CheckBox } from "@ui-kitten/components"; import { cleanServerUrl, sameServer, verifyEvccServer } from "../utils/server"; import LoadingIndicator from "./animations/LoadingIndicator"; import { useTranslation } from "react-i18next"; -import { BasicAuth, Server } from "types"; +import { BasicAuth, ClientCert, 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/test-module/src/TestModule"; interface ServerFormProps { server: Server | undefined; @@ -44,6 +48,7 @@ export default function ServerForm({ title: internalServer?.title, url, basicAuth: internalServer?.basicAuth || {}, + clientCert: internalServer?.clientCert, }); }; const setInternalAuth = (basicAuth: BasicAuth) => { @@ -51,9 +56,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 +120,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 (e) { + 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 +253,62 @@ export default function ServerForm({ )} + + {t("servers.manually.clientCertRequired")} + + + {!!internalServer?.clientCert && ( + <> + + + {certLabel && ( + + {t("servers.manually.clientCertNote")} + + )} + + {!!certBase64 && ( + + )} + + {!certLabel && !!internalServer?.clientCert && ( + + {t("servers.manually.clientCertNote")} + + )} + + )} + + {t("servers.manually.clientCertRequired")} + - {certLabel && ( - - {t("servers.manually.clientCertNote")} - - )} + {!!internalServer?.clientCert && ( + <> + - {!!certBase64 && ( - - )} + {certLabel && ( + + {t("servers.manually.clientCertNote")} + + )} + + {!!certBase64 && ( + + )} - {!certLabel && !!internalServer?.clientCert && ( - - {t("servers.manually.clientCertNote")} - + {!certLabel && !!internalServer?.clientCert && ( + + {t("servers.manually.clientCertNote")} + + )} + )} )} From 6fd3bffdf180b16c98b10fa133e1e2658157b222 Mon Sep 17 00:00:00 2001 From: deep2web <88986286+deep2web@users.noreply.github.com> Date: Sun, 26 Jul 2026 16:02:39 +0200 Subject: [PATCH 09/10] remove mTLS modules from iOS build --- modules/client-cert/expo-module.config.json | 5 +--- .../client-cert/ios/ClientCertModule.podspec | 23 ------------------- .../client-cert/ios/ClientCertModule.swift | 7 ------ .../client-cert/src/ClientCertModule.web.ts | 5 ---- 4 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 modules/client-cert/ios/ClientCertModule.podspec delete mode 100644 modules/client-cert/ios/ClientCertModule.swift delete mode 100644 modules/client-cert/src/ClientCertModule.web.ts diff --git a/modules/client-cert/expo-module.config.json b/modules/client-cert/expo-module.config.json index 63af2adc..4d58e107 100644 --- a/modules/client-cert/expo-module.config.json +++ b/modules/client-cert/expo-module.config.json @@ -1,8 +1,5 @@ { - "platforms": ["apple", "android", "web"], - "apple": { - "modules": ["ClientCertModule"] - }, + "platforms": ["android"], "android": { "modules": ["expo.modules.clientcert.ClientCertModule"] } diff --git a/modules/client-cert/ios/ClientCertModule.podspec b/modules/client-cert/ios/ClientCertModule.podspec deleted file mode 100644 index f9525ae8..00000000 --- a/modules/client-cert/ios/ClientCertModule.podspec +++ /dev/null @@ -1,23 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'TestModule' - s.version = '1.0.0' - s.summary = 'A sample project summary' - s.description = 'A sample project description' - s.author = '' - s.homepage = 'https://docs.expo.dev/modules/' - s.platforms = { - :ios => '16.4', - :tvos => '16.4' - } - s.source = { git: '' } - s.static_framework = true - - s.dependency 'ExpoModulesCore' - - # Swift/Objective-C compatibility - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - } - - s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" -end diff --git a/modules/client-cert/ios/ClientCertModule.swift b/modules/client-cert/ios/ClientCertModule.swift deleted file mode 100644 index 900fca36..00000000 --- a/modules/client-cert/ios/ClientCertModule.swift +++ /dev/null @@ -1,7 +0,0 @@ -import ExpoModulesCore - -public class TestModule: Module { - public func definition() -> ModuleDefinition { - Name("TestModule") - } -} diff --git a/modules/client-cert/src/ClientCertModule.web.ts b/modules/client-cert/src/ClientCertModule.web.ts deleted file mode 100644 index 486be3f5..00000000 --- a/modules/client-cert/src/ClientCertModule.web.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { registerWebModule, NativeModule } from 'expo'; - -class ClientCertModule extends NativeModule> {} - -export default registerWebModule(ClientCertModule, 'ClientCertModule'); From 88d7264d389dbf63f0be4c66f6dabef69f51ecb5 Mon Sep 17 00:00:00 2001 From: deep2web <88986286+deep2web@users.noreply.github.com> Date: Sun, 26 Jul 2026 16:07:33 +0200 Subject: [PATCH 10/10] delete obsolete types file --- modules/client-cert/src/ClientCertModule.types.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 modules/client-cert/src/ClientCertModule.types.ts diff --git a/modules/client-cert/src/ClientCertModule.types.ts b/modules/client-cert/src/ClientCertModule.types.ts deleted file mode 100644 index d5ed3238..00000000 --- a/modules/client-cert/src/ClientCertModule.types.ts +++ /dev/null @@ -1 +0,0 @@ -// Define your exported module types here.