mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
feat(native): client certificates for servers behind mutual TLS
A proxy in front of TREK (Cloudflare, nginx, Caddy, Traefik, HAProxy) can let in only devices that show a client certificate. The apps could not, so such a server was unusable from them. - Address screen: the server check runs natively and notices when the server asks for a certificate. Instead of "no server answered" it says so and offers to add a .p12/.pfx file; a wrong password asks again, an expired, not yet valid or keyless file is named as such. - The certificate is shown only to the host and port it was added for. iOS keeps it in the keychain (this device only), Android seals the key with an Android Keystore key. The file's password is never stored. - The web view answers the server's certificate request on both platforms; WebSocket, service worker and API requests go through with it. The offline copy, watch requests and the check use it too. - When the server stops letting the app in (certificate removed, expired or no longer trusted) while the page still comes from the service worker or the offline copy, a native check sends the app to the address screen, which offers to add another. A server that only asks and lets everyone in is left alone. - Cloudflare-style 403 and nginx-style 400 refusals without a certificate lead to the same screen instead of staying up as an error page. - Settings, Account: a Client certificate card (only with one stored) shows subject, issuer, server and expiry, with Replace and Remove. - Wiki: Native-Apps "Client certificates (mTLS)" with Cloudflare, nginx, Caddy and Traefik setup and how to make a certificate for the app. - The app icon on the unreachable screen is black on the light surface.
This commit is contained in:
@@ -119,6 +119,33 @@ export interface TrekShellPlugin {
|
||||
addToGoogleWallet(options: { url: string }): Promise<{ result: 'opened' }>;
|
||||
/** Android, feature windowLayout: the window's size classes and a foldable's posture and hinge. */
|
||||
getWindowLayout(): Promise<NativeWindowLayout>;
|
||||
/**
|
||||
* Feature clientCertificate: a GET against the user's server for the address
|
||||
* screen, made natively. It shows the stored client certificate to that server
|
||||
* when asked and reports whether the server asked (`certRequested`) and got
|
||||
* one (`certSent`). A failed connection resolves with status 0.
|
||||
*/
|
||||
requestServer(options: { url: string; timeoutMs?: number }): Promise<NativeServerResponse>;
|
||||
/**
|
||||
* Feature clientCertificate: the stored client certificate, or null, and the
|
||||
* last server that asked for one while the app ran (`served`: it got one).
|
||||
*/
|
||||
clientCertificate(): Promise<{
|
||||
certificate: ClientCertificateInfo | null;
|
||||
request?: { host: string; port: number; served: boolean };
|
||||
}>;
|
||||
/**
|
||||
* Feature clientCertificate: picks a .p12/.pfx file, asks for its password
|
||||
* (with `labels`, the UI's words) until it opens, and keeps it for the server
|
||||
* at host and port, replacing any other.
|
||||
*/
|
||||
importClientCertificate(options: {
|
||||
host: string;
|
||||
port: number;
|
||||
labels: ClientCertificateLabels;
|
||||
}): Promise<ClientCertificateImport>;
|
||||
/** Feature clientCertificate: forgets the stored client certificate. */
|
||||
removeClientCertificate(): Promise<void>;
|
||||
addListener(
|
||||
event: 'windowLayout',
|
||||
listener: (payload: NativeWindowLayout) => void
|
||||
@@ -229,6 +256,47 @@ export type NativeLocationPayload =
|
||||
}
|
||||
| { error: { code: number; message: string } };
|
||||
|
||||
interface NativeServerResponse {
|
||||
status: number;
|
||||
data: unknown;
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
certRequested: boolean;
|
||||
certSent: boolean;
|
||||
/** host:port of the server that asked. */
|
||||
certHost?: string;
|
||||
/** Why the connection failed, when status is 0. */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ClientCertificateInfo {
|
||||
host: string;
|
||||
port: number;
|
||||
/** The certificate's common name. */
|
||||
subject: string;
|
||||
/** The issuer's common name. */
|
||||
issuer: string;
|
||||
/** Expiry, in milliseconds since 1970. */
|
||||
notAfter: number;
|
||||
}
|
||||
|
||||
export interface ClientCertificateLabels {
|
||||
title: string;
|
||||
message: string;
|
||||
placeholder: string;
|
||||
ok: string;
|
||||
cancel: string;
|
||||
wrongPassword: string;
|
||||
}
|
||||
|
||||
export type ClientCertificateFailure = 'format' | 'no_key' | 'expired' | 'not_yet_valid' | 'store';
|
||||
|
||||
export interface ClientCertificateImport {
|
||||
result: 'imported' | 'cancelled' | 'failed';
|
||||
failure?: ClientCertificateFailure;
|
||||
certificate?: ClientCertificateInfo;
|
||||
}
|
||||
|
||||
export const TrekShell = registerPlugin<TrekShellPlugin>('TrekShell');
|
||||
|
||||
export const APP_URL_SCHEME = 'com.liketrek.trek';
|
||||
|
||||
Reference in New Issue
Block a user