mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
fix: describe the client certificate the iOS way on iPhone and iPad
This commit is contained in:
@@ -1518,9 +1518,12 @@
|
|||||||
"choose_another": "Choose another",
|
"choose_another": "Choose another",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"clear_message": "New requests will no longer present a certificate. The certificate stays installed in Android.",
|
"clear_message": "New requests will no longer present a certificate. The certificate stays installed in Android.",
|
||||||
|
"clear_message_ios": "New requests will no longer present a certificate. The certificate is removed from the app; to use it again, pick the file again.",
|
||||||
"clear_title": "Stop using client certificate?",
|
"clear_title": "Stop using client certificate?",
|
||||||
"description": "Picks an installed Android certificate to authenticate to the server during the TLS handshake. Useful when the reverse proxy enforces mTLS.",
|
"description": "Picks an installed Android certificate to authenticate to the server during the TLS handshake. Useful when the reverse proxy enforces mTLS.",
|
||||||
|
"description_ios": "Imports a certificate file to authenticate to the server during the TLS handshake. Useful when the reverse proxy enforces mTLS.",
|
||||||
"hint": "Install the certificate via Android Settings → Security → Encryption & credentials, then pick it here.",
|
"hint": "Install the certificate via Android Settings → Security → Encryption & credentials, then pick it here.",
|
||||||
|
"hint_ios": "Pick the certificate file (.p12 or .pfx) in Files and enter its password.",
|
||||||
"not_set": "Not set",
|
"not_set": "Not set",
|
||||||
"pick": "Pick certificate",
|
"pick": "Pick certificate",
|
||||||
"pick_failed": "Pick failed",
|
"pick_failed": "Pick failed",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Alert, View, Text, StyleSheet, Pressable, Linking, ActivityIndicator } from 'react-native';
|
import { Alert, View, Text, StyleSheet, Pressable, Linking, ActivityIndicator, Platform } from 'react-native';
|
||||||
import DateTimePicker, { type DateTimePickerEvent } from '@react-native-community/datetimepicker';
|
import DateTimePicker, { type DateTimePickerEvent } from '@react-native-community/datetimepicker';
|
||||||
import {
|
import {
|
||||||
Key, Smartphone, Lock, Eye, EyeOff, ShieldCheck, Monitor, Trash2,
|
Key, Smartphone, Lock, Eye, EyeOff, ShieldCheck, Monitor, Trash2,
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { useLocaleStore } from '../../stores/locale-store';
|
import { useLocaleStore } from '../../stores/locale-store';
|
||||||
import { useAuthStore } from '../../stores/auth-store';
|
import { useAuthStore } from '../../stores/auth-store';
|
||||||
import { passwordChangeOutcome } from './password-change';
|
import { passwordChangeOutcome } from './password-change';
|
||||||
|
import { clientCertCopy } from './client-cert-copy';
|
||||||
import {
|
import {
|
||||||
AccountSecurityForbiddenError,
|
AccountSecurityForbiddenError,
|
||||||
isLimitedStalwartSignIn,
|
isLimitedStalwartSignIn,
|
||||||
@@ -50,7 +51,7 @@ import {
|
|||||||
import { generateTotpEnrolment, type TotpEnrolment } from '../../lib/totp';
|
import { generateTotpEnrolment, type TotpEnrolment } from '../../lib/totp';
|
||||||
|
|
||||||
// ── TLS client certificate (mobile-only) ──────────────────
|
// ── TLS client certificate (mobile-only) ──────────────────
|
||||||
// Picks an installed Android cert for mTLS handshakes. Independent of the
|
// Picks an installed Android cert (iOS: imports a .p12 file) for mTLS handshakes. Independent of the
|
||||||
// Stalwart account features below, so it stays visible even on non-Stalwart
|
// Stalwart account features below, so it stays visible even on non-Stalwart
|
||||||
// servers.
|
// servers.
|
||||||
function ClientCertSection() {
|
function ClientCertSection() {
|
||||||
@@ -59,6 +60,7 @@ function ClientCertSection() {
|
|||||||
const t = useLocaleStore((s) => s.t);
|
const t = useLocaleStore((s) => s.t);
|
||||||
const [certAlias, setCertAlias] = useState<string | null>(null);
|
const [certAlias, setCertAlias] = useState<string | null>(null);
|
||||||
const [certBusy, setCertBusy] = useState(false);
|
const [certBusy, setCertBusy] = useState(false);
|
||||||
|
const copy = clientCertCopy(Platform.OS, t);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void getClientCertAlias().then(setCertAlias);
|
void getClientCertAlias().then(setCertAlias);
|
||||||
@@ -82,7 +84,7 @@ function ClientCertSection() {
|
|||||||
if (certBusy) return;
|
if (certBusy) return;
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t('settings.security.client_cert.clear_title', "Stop using client certificate?"),
|
t('settings.security.client_cert.clear_title', "Stop using client certificate?"),
|
||||||
t('settings.security.client_cert.clear_message', "New requests will no longer present a certificate. The certificate stays installed in Android."),
|
copy.clearMessage,
|
||||||
[
|
[
|
||||||
{ text: t('common.cancel', "Cancel"), style: 'cancel' },
|
{ text: t('common.cancel', "Cancel"), style: 'cancel' },
|
||||||
{
|
{
|
||||||
@@ -105,7 +107,7 @@ function ClientCertSection() {
|
|||||||
return (
|
return (
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
title={t('settings.security.client_cert.title', "TLS client certificate")}
|
title={t('settings.security.client_cert.title', "TLS client certificate")}
|
||||||
description={t('settings.security.client_cert.description', "Picks an installed Android certificate to authenticate to the server during the TLS handshake. Useful when the reverse proxy enforces mTLS.")}
|
description={copy.description}
|
||||||
>
|
>
|
||||||
<View style={styles.certRow}>
|
<View style={styles.certRow}>
|
||||||
<ShieldCheck size={18} color={certAlias ? c.success : c.mutedForeground} />
|
<ShieldCheck size={18} color={certAlias ? c.success : c.mutedForeground} />
|
||||||
@@ -116,7 +118,7 @@ function ClientCertSection() {
|
|||||||
: t('settings.security.client_cert.not_set', "Not set")}
|
: t('settings.security.client_cert.not_set', "Not set")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.certHint}>
|
<Text style={styles.certHint}>
|
||||||
{t('settings.security.client_cert.hint', "Install the certificate via Android Settings → Security → Encryption & credentials, then pick it here.")}
|
{copy.hint}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { clientCertCopy } from '../client-cert-copy';
|
||||||
|
|
||||||
|
// Settings > Security > TLS client certificate told iPhone users to install
|
||||||
|
// the certificate in Android settings. iOS imports a .p12/.pfx file into the
|
||||||
|
// app instead, and Clear removes it from the app.
|
||||||
|
|
||||||
|
const t = (_key: string, fallback: string) => fallback;
|
||||||
|
const keys = (os: string) => {
|
||||||
|
const used: string[] = [];
|
||||||
|
clientCertCopy(os, (key, fallback) => { used.push(key); return fallback; });
|
||||||
|
return used;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('client certificate texts', () => {
|
||||||
|
it('speaks of a certificate file on iOS', () => {
|
||||||
|
const copy = clientCertCopy('ios', t);
|
||||||
|
expect(copy.hint).toBe('Pick the certificate file (.p12 or .pfx) in Files and enter its password.');
|
||||||
|
expect(copy.clearMessage).toContain('removed from the app');
|
||||||
|
expect(`${copy.description} ${copy.hint} ${copy.clearMessage}`).not.toMatch(/Android/);
|
||||||
|
expect(keys('ios')).toEqual([
|
||||||
|
'settings.security.client_cert.description_ios',
|
||||||
|
'settings.security.client_cert.hint_ios',
|
||||||
|
'settings.security.client_cert.clear_message_ios',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the Android texts on Android', () => {
|
||||||
|
expect(keys('android')).toEqual([
|
||||||
|
'settings.security.client_cert.description',
|
||||||
|
'settings.security.client_cert.hint',
|
||||||
|
'settings.security.client_cert.clear_message',
|
||||||
|
]);
|
||||||
|
expect(clientCertCopy('android', t).clearMessage).toContain('stays installed in Android');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is what the settings section shows', () => {
|
||||||
|
const src = fs.readFileSync(path.resolve(__dirname, '..', 'AccountSecuritySettings.tsx'), 'utf8');
|
||||||
|
expect(src).toMatch(/const copy = clientCertCopy\(Platform\.OS, t\);/);
|
||||||
|
expect(src).toMatch(/description=\{copy\.description\}/);
|
||||||
|
expect(src).toMatch(/\{copy\.hint\}/);
|
||||||
|
expect(src).toMatch(/copy\.clearMessage,/);
|
||||||
|
expect(src).not.toMatch(/t\('settings\.security\.client_cert\.(hint|description|clear_message)'/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// The texts of Settings > Security > TLS client certificate, per platform.
|
||||||
|
// Android picks a certificate installed in the system (KeyChain); iOS has no
|
||||||
|
// such store apps can pick from, so the user brings a .p12 or .pfx file that
|
||||||
|
// is imported into the app's keychain and removed again on Clear
|
||||||
|
// (plugins/ios-client-cert).
|
||||||
|
|
||||||
|
type Translate = (key: string, fallback: string) => string;
|
||||||
|
|
||||||
|
export interface ClientCertCopy {
|
||||||
|
description: string;
|
||||||
|
hint: string;
|
||||||
|
clearMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clientCertCopy(os: string, t: Translate): ClientCertCopy {
|
||||||
|
if (os === 'ios') {
|
||||||
|
return {
|
||||||
|
description: t('settings.security.client_cert.description_ios', 'Imports a certificate file to authenticate to the server during the TLS handshake. Useful when the reverse proxy enforces mTLS.'),
|
||||||
|
hint: t('settings.security.client_cert.hint_ios', 'Pick the certificate file (.p12 or .pfx) in Files and enter its password.'),
|
||||||
|
clearMessage: t('settings.security.client_cert.clear_message_ios', 'New requests will no longer present a certificate. The certificate is removed from the app; to use it again, pick the file again.'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
description: t('settings.security.client_cert.description', 'Picks an installed Android certificate to authenticate to the server during the TLS handshake. Useful when the reverse proxy enforces mTLS.'),
|
||||||
|
hint: t('settings.security.client_cert.hint', 'Install the certificate via Android Settings → Security → Encryption & credentials, then pick it here.'),
|
||||||
|
clearMessage: t('settings.security.client_cert.clear_message', 'New requests will no longer present a certificate. The certificate stays installed in Android.'),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user