mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-05 10:28:54 +00:00
* perf: skip Android Keystore listing on cold start Open the app DB first and decrypt credentials only for known server URLs so logged-in launches no longer enumerate every generic-password service. Co-authored-by: Cursor <cursoragent@cursor.com> * perf: load server credentials in one parallel pass Drop the extra preauth Set and second keystore probe so cold start decrypts each known URL once, and read active servers from the existing query. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: retry failed startup and cache credentials only after Keychain writes Reset base initialization so a rejected launch can retry, and keep the in-memory credential cache aligned with successful secure-storage writes. Co-authored-by: Cursor <cursoragent@cursor.com> * test: drop mock-harness init tests and unused session cache The initialize() retry test only asserted mock wiring. Remove leftover hasCachedCredentials/session_cache APIs from closed stack floors, and keep credential tests that lock listing-skip and cache behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: drop leftover launch profiler instrumentation Cold-start timing marks were debug logInfo breadcrumbs, not part of the Keystore skip. Co-authored-by: Cursor <cursoragent@cursor.com> * test: assert empty known-URL fallback still returns credentials Listing-called alone would miss a path that enumerates Keystore then drops the results. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
197 lines
6.3 KiB
TypeScript
197 lines
6.3 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {Platform} from 'react-native';
|
|
import * as KeyChain from 'react-native-keychain';
|
|
|
|
import DatabaseManager from '@database/manager';
|
|
import {logWarning} from '@utils/log';
|
|
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
|
|
|
// After initialize(), this is the logged-in DB-active set — not a live Keystore listing.
|
|
let cachedServerCredentials: ServerCredential[] | undefined;
|
|
|
|
export const clearCachedServerCredentials = () => {
|
|
cachedServerCredentials = undefined;
|
|
};
|
|
|
|
const replaceCachedCredential = (serverUrl: string, credential: ServerCredential | null) => {
|
|
if (cachedServerCredentials === undefined) {
|
|
return;
|
|
}
|
|
|
|
const rest = cachedServerCredentials.filter((c) => c.serverUrl !== serverUrl);
|
|
cachedServerCredentials = credential ? [...rest, credential] : rest;
|
|
};
|
|
|
|
const getAllKeychainServerUrls = async (): Promise<string[]> => {
|
|
if (Platform.OS === 'ios') {
|
|
return KeyChain.getAllInternetPasswordServers();
|
|
}
|
|
return KeyChain.getAllGenericPasswordServices();
|
|
};
|
|
|
|
export const getAllServerCredentials = async (knownServerUrls?: string[]): Promise<ServerCredential[]> => {
|
|
if (cachedServerCredentials !== undefined) {
|
|
return cachedServerCredentials.map((c) => ({...c}));
|
|
}
|
|
|
|
let serverUrls: string[];
|
|
|
|
// Empty knownServerUrls intentionally lists (wiped/fresh DB).
|
|
if (knownServerUrls?.length) {
|
|
serverUrls = knownServerUrls;
|
|
} else {
|
|
serverUrls = await getAllKeychainServerUrls();
|
|
}
|
|
|
|
const serverCredentials = (await Promise.all(
|
|
serverUrls.map((serverUrl) => getServerCredentials(serverUrl)),
|
|
)).filter((credential): credential is ServerCredential => Boolean(credential));
|
|
|
|
cachedServerCredentials = serverCredentials;
|
|
return serverCredentials.map((c) => ({...c}));
|
|
};
|
|
|
|
export const getActiveServerUrl = async () => {
|
|
let serverUrl = await DatabaseManager.getActiveServerUrl();
|
|
if (!serverUrl) {
|
|
const serverUrls = await getAllKeychainServerUrls();
|
|
serverUrl = serverUrls[0];
|
|
}
|
|
return serverUrl || undefined;
|
|
};
|
|
|
|
export const setServerCredentials = async (serverUrl: string, token: string, preauthSecret?: string) => {
|
|
if (!(serverUrl && token)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let accessGroup;
|
|
if (Platform.OS === 'ios') {
|
|
const appGroup = getIOSAppGroupDetails();
|
|
accessGroup = appGroup.appGroupIdentifier;
|
|
}
|
|
|
|
const options: KeyChain.SetOptions = {
|
|
accessGroup,
|
|
securityLevel: KeyChain.SECURITY_LEVEL.SECURE_SOFTWARE,
|
|
};
|
|
|
|
const stored = await KeyChain.setInternetCredentials(serverUrl, token, token, options);
|
|
if (stored === false) {
|
|
throw new Error('failed to store credentials');
|
|
}
|
|
|
|
if (preauthSecret) {
|
|
const storedSecret = await KeyChain.setGenericPassword('preshared_secret', preauthSecret, {
|
|
server: serverUrl,
|
|
...options,
|
|
});
|
|
if (storedSecret === false) {
|
|
throw new Error('failed to store preauth secret');
|
|
}
|
|
} else {
|
|
const reset = await KeyChain.resetGenericPassword({
|
|
server: serverUrl,
|
|
...options,
|
|
});
|
|
if (reset === false) {
|
|
throw new Error('failed to reset preauth secret');
|
|
}
|
|
}
|
|
|
|
replaceCachedCredential(serverUrl, {serverUrl, userId: token, token, preauthSecret});
|
|
} catch (e) {
|
|
logWarning('could not set credentials', e);
|
|
}
|
|
};
|
|
|
|
export const removeServerCredentials = async (serverUrl: string) => {
|
|
await KeyChain.resetInternetCredentials({server: serverUrl});
|
|
replaceCachedCredential(serverUrl, null);
|
|
};
|
|
|
|
export const removePreauthSecret = async (serverUrl: string) => {
|
|
try {
|
|
const reset = await KeyChain.resetGenericPassword({server: serverUrl});
|
|
if (reset === false) {
|
|
return;
|
|
}
|
|
const existing = cachedServerCredentials?.find((c) => c.serverUrl === serverUrl);
|
|
if (existing) {
|
|
existing.preauthSecret = undefined;
|
|
}
|
|
} catch (e) {
|
|
// Preauth secret might not exist, ignore errors
|
|
}
|
|
};
|
|
|
|
export const getPreauthSecret = async (serverUrl: string): Promise<string | undefined> => {
|
|
try {
|
|
const preauthCredentials = await KeyChain.getGenericPassword({
|
|
server: serverUrl,
|
|
});
|
|
const secret = preauthCredentials ? preauthCredentials.password : undefined;
|
|
return secret;
|
|
} catch (e) {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
export const removeActiveServerCredentials = async () => {
|
|
const serverUrl = await getActiveServerUrl();
|
|
if (serverUrl) {
|
|
await removeServerCredentials(serverUrl);
|
|
}
|
|
};
|
|
|
|
export const getServerCredentials = async (serverUrl: string): Promise<ServerCredential|null> => {
|
|
const cached = cachedServerCredentials?.find((c) => c.serverUrl === serverUrl);
|
|
if (cached) {
|
|
return {...cached};
|
|
}
|
|
|
|
try {
|
|
// Get main credentials
|
|
const credentials = await KeyChain.getInternetCredentials(serverUrl);
|
|
|
|
if (!credentials) {
|
|
return null;
|
|
}
|
|
|
|
// TODO: Pre-Gekidou we were concatenating the deviceToken and the userId in
|
|
// credentials.username so we need to check the length of credentials.username.split(',').
|
|
// This check should be removed at some point. https://mattermost.atlassian.net/browse/MM-43483
|
|
const parts = credentials.username.split(',');
|
|
const userId = parts[parts.length - 1];
|
|
const token = credentials.password;
|
|
|
|
if (!token || token === 'undefined') {
|
|
return null;
|
|
}
|
|
|
|
// Get preauth secret separately
|
|
let preauthSecret: string | undefined;
|
|
try {
|
|
const preauthCredentials = await KeyChain.getGenericPassword({
|
|
server: serverUrl,
|
|
});
|
|
preauthSecret = preauthCredentials ? preauthCredentials.password : undefined;
|
|
} catch (e) {
|
|
// Preauth secret is optional, so ignore errors
|
|
preauthSecret = undefined;
|
|
}
|
|
|
|
return {
|
|
serverUrl,
|
|
userId,
|
|
token,
|
|
preauthSecret,
|
|
};
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
};
|