Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Gekidou] Get all existing server credentials on init #5468

Merged
merged 4 commits into from
Jun 21, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions app/init/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,56 @@
import {Platform} from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When sorting imports, the @react-native-community/async-storage should be treated as the character a and sorted amongst the first modules.

import * as KeyChain from 'react-native-keychain';
import urlParse from 'url-parse';

import DatabaseManager from '@database/manager';
import * as analytics from '@init/analytics';
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
import {getCSRFFromCookie} from '@utils/security';
import {normalizeServerUrl} from '@utils/url';

import type {ServerCredentials} from '@typings/credentials';
import type {ServerCredential} from '@typings/credentials';

const ASYNC_STORAGE_CURRENT_SERVER_KEY = '@currentServerUrl';

export const getAllServerCredentials = async (): Promise<ServerCredential[]> => {
const serverCredentials: ServerCredential[] = [];

let serverUrls: string[];
if (Platform.OS === 'ios') {
serverUrls = await KeyChain.getAllInternetPasswordServers();
} else {
serverUrls = await KeyChain.getAllGenericPasswordServices();
}

for await (const serverUrl of serverUrls) {
let serverCredential: ServerCredential | null;

const parsed = urlParse(serverUrl);
if (parsed.protocol) {
serverCredential = await convertV1Credential(serverUrl);
} else {
serverCredential = await getServerCredentials(serverUrl);
}

if (serverCredential) {
serverCredentials.push(serverCredential);
}
}

return serverCredentials;
};

const convertV1Credential = async (serverUrl: string) => {
const credentials = await getServerCredentials(serverUrl);
await removeServerCredentials(serverUrl);

setServerCredentials(serverUrl, credentials!.userId, credentials!.token);

const normalizedServerUrl = normalizeServerUrl(serverUrl);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having my doubts about normalizing.. let's chat about this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed normalization. We're now OK with the the server URL containing the protocol in both the keychain and in the db. If we want to match against a deeplink URL, we can always use a LIKE query or replace the deeplink protocol prior to querying.

return getServerCredentials(normalizedServerUrl);
};

// TODO: This function is only necessary to support pre-Gekidou
// versions as the active server URL may be stored in AsyncStorage.
// At some point we can remove this function and rely solely on
Expand Down Expand Up @@ -42,6 +82,8 @@ export const setServerCredentials = (serverUrl: string, userId: string, token: s
return;
}

const normalizedServerUrl = normalizeServerUrl(serverUrl);

try {
let accessGroup;
if (Platform.OS === 'ios') {
Expand All @@ -53,7 +95,7 @@ export const setServerCredentials = (serverUrl: string, userId: string, token: s
accessGroup,
securityLevel: KeyChain.SECURITY_LEVEL.SECURE_SOFTWARE,
};
KeyChain.setInternetCredentials(serverUrl, userId, token, options);
KeyChain.setInternetCredentials(normalizedServerUrl, userId, token, options);
} catch (e) {
console.warn('could not set credentials', e); //eslint-disable-line no-console
}
Expand Down Expand Up @@ -83,7 +125,7 @@ export const removeActiveServerCredentials = async () => {
}
};

export const getServerCredentials = async (serverUrl: string): Promise<ServerCredentials|null> => {
export const getServerCredentials = async (serverUrl: string): Promise<ServerCredential|null> => {
try {
const credentials = await KeyChain.getInternetCredentials(serverUrl);

Expand All @@ -106,7 +148,7 @@ export const getServerCredentials = async (serverUrl: string): Promise<ServerCre

// TODO: Create client and set token / CSRF
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do in the PR that brings in the network lib


return {userId, token};
return {serverUrl, userId, token};
}
}

Expand Down
6 changes: 6 additions & 0 deletions app/utils/url/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export function sanitizeUrl(url: string, useHttp = false) {
);
}

export function normalizeServerUrl(serverUrl: string) {
const parsed = urlParse(serverUrl);

return stripTrailingSlashes(`${parsed.host}${parsed.pathname}`);
}

export async function getServerUrlAfterRedirect(serverUrl: string, useHttp = false) {
let url = sanitizeUrl(serverUrl, useHttp);

Expand Down
9 changes: 9 additions & 0 deletions app/utils/url/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,4 +285,13 @@ describe('UrlUtils', () => {
expect(onSuccess).not.toHaveBeenCalled();
});
});

describe('normalizeServerUrl', () => {
it('should return hostname, port, and path without a trailing slash', () => {
const serverUrl = 'https://some.domain.com:12345/some/path/?q=query';
const expectedUrl = 'some.domain.com:12345/some/path';

expect(UrlUtils.normalizeServerUrl(serverUrl)).toEqual(expectedUrl);
});
});
});
13 changes: 11 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'react-native-gesture-handler';
import {ComponentDidAppearEvent, ComponentDidDisappearEvent, Navigation} from 'react-native-navigation';

import {Navigation as NavigationConstants, Screens} from './app/constants';
import {getAllServerCredentials} from './app/init/credentials';
import GlobalEventHandler from './app/init/global_event_handler';
import './app/init/fetch';
import {initialLaunch} from './app/init/launch';
Expand Down Expand Up @@ -42,11 +43,19 @@ if (Platform.OS === 'android') {
AppRegistry.registerComponent('MattermostShare', () => ShareExtension);
}

Navigation.events().registerAppLaunchedListener(() => {
Navigation.events().registerAppLaunchedListener(async () => {
GlobalEventHandler.init();
ManagedApp.init();
registerNavigationListeners();
registerScreens();

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const serverCredentials = await getAllServerCredentials();

// TODO:
// DatabaseManager.init(serverCredentials);
// NetworkClientManager.init(serverCredentials);

initialLaunch();
});

Expand All @@ -60,7 +69,7 @@ function componentDidAppearListener({componentId}: ComponentDidAppearEvent) {

switch (componentId) {
case 'MainSidebar':
DeviceEventEmitter.emit(NavigationConstants.MAIN_SIDEBAR_DID_OPEN, this.handleSidebarDidOpen);
DeviceEventEmitter.emit(NavigationConstants.MAIN_SIDEBAR_DID_OPEN);
DeviceEventEmitter.emit(NavigationConstants.BLUR_POST_DRAFT);
break;
case 'SettingsSidebar':
Expand Down
34 changes: 17 additions & 17 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ PODS:
- glog (0.3.5)
- jail-monkey (2.3.3):
- React
- libwebp (1.1.0):
- libwebp/demux (= 1.1.0)
- libwebp/mux (= 1.1.0)
- libwebp/webp (= 1.1.0)
- libwebp/demux (1.1.0):
- libwebp (1.2.0):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this changing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure :/

- libwebp/demux (= 1.2.0)
- libwebp/mux (= 1.2.0)
- libwebp/webp (= 1.2.0)
- libwebp/demux (1.2.0):
- libwebp/webp
- libwebp/mux (1.1.0):
- libwebp/mux (1.2.0):
- libwebp/demux
- libwebp/webp (1.1.0)
- libwebp/webp (1.2.0)
- Permission-Camera (3.0.2):
- RNPermissions
- Permission-PhotoLibrary (3.0.2):
Expand Down Expand Up @@ -403,10 +403,10 @@ PODS:
- React
- RNVectorIcons (8.1.0):
- React-Core
- Rudder (1.0.11)
- SDWebImage (5.9.4):
- SDWebImage/Core (= 5.9.4)
- SDWebImage/Core (5.9.4)
- Rudder (1.0.14)
- SDWebImage (5.11.1):
- SDWebImage/Core (= 5.11.1)
- SDWebImage/Core (5.11.1)
- SDWebImageWebPCoder (0.6.1):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.7)
Expand Down Expand Up @@ -441,7 +441,7 @@ PODS:
- React-jsi
- XCDYouTubeKit (2.8.2)
- Yoga (1.14.0)
- YoutubePlayer-in-WKWebView (0.3.4)
- YoutubePlayer-in-WKWebView (0.3.5)

DEPENDENCIES:
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
Expand Down Expand Up @@ -716,10 +716,10 @@ SPEC CHECKSUMS:
EXConstants: c00cd53a17a65b2e53ddb3890e4e74d3418e406e
EXFileSystem: 35769beb727d5341d1276fd222710f9704f7164e
FBLazyVector: 49cbe4b43e445b06bf29199b6ad2057649e4c8f5
FBReactNativeSpec: a804c9d6c798f94831713302354003ee54ea18cb
FBReactNativeSpec: 0eb0a77cec90f681f2a6e58baba2224a1597e3ed
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
jail-monkey: 80c9e34da2cd54023e5ad08bf7051ec75bd43d5b
libwebp: 946cb3063cea9236285f7e9a8505d806d30e07f3
libwebp: e90b9c01d99205d03b6bb8f2c8c415e5a4ef66f0
Permission-Camera: 4eb4a67be32698c5927cb12313c3c1f23ee5b7b2
Permission-PhotoLibrary: 5b198e177c0bc5c91c20e66ae7202d0183c78b87
RCT-Folly: ec7a233ccc97cc556cf7237f0db1ff65b986f27c
Expand Down Expand Up @@ -779,8 +779,8 @@ SPEC CHECKSUMS:
RNShare: 31fa0cedbd06c2744a78e0d2b7ba364778aa3506
RNSVG: 551acb6562324b1d52a4e0758f7ca0ec234e278f
RNVectorIcons: 31cebfcf94e8cf8686eb5303ae0357da64d7a5a4
Rudder: ef340f877a39653f19e69124dffae12fde3f881b
SDWebImage: b69257f4ab14e9b6a2ef53e910fdf914d8f757c1
Rudder: 40f3a255fab3f8bbe120e496f90019de68c1aca1
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: d0dac55073088d24b2ac1b191a71a8f8d0adac21
Sentry: 9d055e2de30a77685e86b219acf02e59b82091fc
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
Expand All @@ -800,7 +800,7 @@ SPEC CHECKSUMS:
WatermelonDB: e6706a0fac2221e2be4bdc93ff0e3990231d91bd
XCDYouTubeKit: 79baadb0560673a67c771eba45f83e353fd12c1f
Yoga: 8c8436d4171c87504c648ae23b1d81242bdf3bbf
YoutubePlayer-in-WKWebView: af2f5929fc78882d94bfdfeea999b661b78d9717
YoutubePlayer-in-WKWebView: cfbf46da51d7370662a695a8f351e5fa1d3e1008

PODFILE CHECKSUM: 34335a8d329b001fdfa695c4fb3c8928db80f375

Expand Down
12 changes: 5 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

93 changes: 93 additions & 0 deletions patches/react-native-keychain+7.0.0.patch
Original file line number Diff line number Diff line change
@@ -1,3 +1,57 @@
diff --git a/node_modules/react-native-keychain/RNKeychainManager/RNKeychainManager.m b/node_modules/react-native-keychain/RNKeychainManager/RNKeychainManager.m
index 38ccdf3..bdc4713 100644
--- a/node_modules/react-native-keychain/RNKeychainManager/RNKeychainManager.m
+++ b/node_modules/react-native-keychain/RNKeychainManager/RNKeychainManager.m
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to submit this patch upstream?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, just gotta fix the patch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -272,6 +272,34 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
return SecItemDelete((__bridge CFDictionaryRef) query);
}

+-(NSArray<NSString*>*)getAllServersForInternetPasswords
+{
+ NSMutableDictionary *query = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ (__bridge id)kCFBooleanTrue, (__bridge id)kSecReturnAttributes,
+ (__bridge id)kSecMatchLimitAll, (__bridge id)kSecMatchLimit,
+ nil];
+ NSMutableArray<NSString*> *servers = [NSMutableArray<NSString*> new];
+
+ [query setObject:(__bridge id)kSecClassInternetPassword forKey:(__bridge id)kSecClass];
+ NSArray *result = nil;
+ CFTypeRef resultRef = NULL;
+ OSStatus osStatus = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef*)&resultRef);
+ if (osStatus != noErr && osStatus != errSecItemNotFound) {
+ NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil];
+ @throw error;
+ } else if (osStatus != errSecItemNotFound) {
+ result = (__bridge NSArray*)(resultRef);
+ if (result != NULL) {
+ for (id entry in result) {
+ NSString *server = [entry objectForKey:(__bridge NSString *)kSecAttrServer];
+ [servers addObject:server];
+ }
+ }
+ }
+
+ return servers;
+}
+
-(NSArray<NSString*>*)getAllServicesForSecurityClasses:(NSArray *)secItemClasses
{
NSMutableDictionary *query = [NSMutableDictionary dictionaryWithObjectsAndKeys:
@@ -592,4 +620,14 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
}
}

+RCT_EXPORT_METHOD(getAllInternetPasswordServers:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+{
+ @try {
+ NSArray *servers = [self getAllServersForInternetPasswords];
+ return resolve(servers);
+ } @catch (NSError *nsError) {
+ return rejectWithError(reject, nsError);
+ }
+}
+
@end
diff --git a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java
index 3da433f..112cc74 100644
--- a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java
Expand Down Expand Up @@ -57,3 +111,42 @@ index 3da433f..112cc74 100644

promise.resolve(credentials);
} catch (KeyStoreAccessException e) {
diff --git a/node_modules/react-native-keychain/index.js b/node_modules/react-native-keychain/index.js
index b73cfb2..a754505 100644
--- a/node_modules/react-native-keychain/index.js
+++ b/node_modules/react-native-keychain/index.js
@@ -348,6 +348,21 @@ export function canImplyAuthentication(options?: Options): Promise<boolean> {
return RNKeychainManager.canCheckAuthentication(options);
}

+/**
+ * Gets all `kSecAttrServer` values used in internet credentials for iOS.
+ * @return {Promise} Resolves to an array of strings
+ */
+ export async function getAllInternetPasswordServers(): Promise<string[]> {
+ if (Platform.OS !== 'ios') {
+ return Promise.reject(
+ new Error(
+ `getAllInternetPasswordServers() is not supported on ${Platform.OS}`
+ )
+ );
+ }
+ return RNKeychainManager.getAllInternetPasswordServers();
+}
+
//* ANDROID ONLY */

/**
diff --git a/node_modules/react-native-keychain/typings/react-native-keychain.d.ts b/node_modules/react-native-keychain/typings/react-native-keychain.d.ts
index af2eb56..038047b 100644
--- a/node_modules/react-native-keychain/typings/react-native-keychain.d.ts
+++ b/node_modules/react-native-keychain/typings/react-native-keychain.d.ts
@@ -97,6 +97,8 @@ declare module 'react-native-keychain' {

function getAllGenericPasswordServices(): Promise<string[]>;

+ function getAllInternetPasswordServers(): Promise<string[]>;
+
function hasInternetCredentials(server: string): Promise<false | Result>;

function setInternetCredentials(
3 changes: 2 additions & 1 deletion types/credentials/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

export type ServerCredentials = {
export type ServerCredential = {
serverUrl: string;
userId: string;
token: string;
};