mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 00:07:01 +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",
|
||||
"clear": "Clear",
|
||||
"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?",
|
||||
"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_ios": "Pick the certificate file (.p12 or .pfx) in Files and enter its password.",
|
||||
"not_set": "Not set",
|
||||
"pick": "Pick certificate",
|
||||
"pick_failed": "Pick failed",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 {
|
||||
Key, Smartphone, Lock, Eye, EyeOff, ShieldCheck, Monitor, Trash2,
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { useAuthStore } from '../../stores/auth-store';
|
||||
import { passwordChangeOutcome } from './password-change';
|
||||
import { clientCertCopy } from './client-cert-copy';
|
||||
import {
|
||||
AccountSecurityForbiddenError,
|
||||
isLimitedStalwartSignIn,
|
||||
@@ -50,7 +51,7 @@ import {
|
||||
import { generateTotpEnrolment, type TotpEnrolment } from '../../lib/totp';
|
||||
|
||||
// ── 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
|
||||
// servers.
|
||||
function ClientCertSection() {
|
||||
@@ -59,6 +60,7 @@ function ClientCertSection() {
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const [certAlias, setCertAlias] = useState<string | null>(null);
|
||||
const [certBusy, setCertBusy] = useState(false);
|
||||
const copy = clientCertCopy(Platform.OS, t);
|
||||
|
||||
useEffect(() => {
|
||||
void getClientCertAlias().then(setCertAlias);
|
||||
@@ -82,7 +84,7 @@ function ClientCertSection() {
|
||||
if (certBusy) return;
|
||||
Alert.alert(
|
||||
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' },
|
||||
{
|
||||
@@ -105,7 +107,7 @@ function ClientCertSection() {
|
||||
return (
|
||||
<SettingsSection
|
||||
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}>
|
||||
<ShieldCheck size={18} color={certAlias ? c.success : c.mutedForeground} />
|
||||
@@ -116,7 +118,7 @@ function ClientCertSection() {
|
||||
: t('settings.security.client_cert.not_set', "Not set")}
|
||||
</Text>
|
||||
<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>
|
||||
</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