SSL Pinned Certificate (#8055)

This commit is contained in:
Elias Nahum 2024-07-23 18:26:20 +08:00 committed by GitHub
parent c34a131cdd
commit de6ccaef01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 440 additions and 115 deletions

View file

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View file

@ -9,7 +9,7 @@ import DatabaseManager from '@database/manager';
import {getConfigValue} from '@queries/servers/system';
import {hasReliableWebsocket} from '@utils/config';
import {toMilliseconds} from '@utils/datetime';
import {logError, logInfo, logWarning} from '@utils/log';
import {logDebug, logError, logInfo, logWarning} from '@utils/log';
const MAX_WEBSOCKET_FAILS = 7;
const WEBSOCKET_TIMEOUT = toMilliseconds({seconds: 30});
@ -18,6 +18,7 @@ const MAX_WEBSOCKET_RETRY_TIME = toMilliseconds({minutes: 5});
const DEFAULT_OPTIONS = {
forceConnection: true,
};
const TLS_HANDSHARE_ERROR = 1015;
export default class WebSocketClient {
private conn?: WebSocketClientInterface;
@ -179,7 +180,7 @@ export default class WebSocketClient {
this.connectFailCount = 0;
});
this.conn!.onClose(() => {
this.conn!.onClose((ev) => {
clearTimeout(this.connectionTimeout);
this.conn = undefined;
this.responseSequence = 1;
@ -190,6 +191,12 @@ export default class WebSocketClient {
// reliable websockets are enabled this won't trigger a new sync.
this.shouldSkipSync = false;
if (ev.message && typeof ev.message === 'object' && 'code' in ev.message && ev.message.code === TLS_HANDSHARE_ERROR) {
logDebug('websocket did not connect', this.url, ev.message.reason);
this.closeCallback?.(this.connectFailCount);
return;
}
if (this.connectFailCount === 0) {
logInfo('websocket closed', this.url);
}

View file

@ -11,6 +11,7 @@ import {
} from '@mattermost/react-native-network-client';
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application';
import {modelName, osName, osVersion} from 'expo-device';
import {defineMessages, createIntl} from 'react-intl';
import {Alert, DeviceEventEmitter} from 'react-native';
import urlParse from 'url-parse';
@ -19,18 +20,41 @@ import {Client} from '@client/rest';
import * as ClientConstants from '@client/rest/constants';
import ClientError from '@client/rest/error';
import {CERTIFICATE_ERRORS} from '@constants/network';
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
import ManagedApp from '@init/managed_app';
import {toMilliseconds} from '@utils/datetime';
import {logDebug, logError} from '@utils/log';
import {getCSRFFromCookie} from '@utils/security';
const CLIENT_CERTIFICATE_IMPORT_ERROR_CODES = [-103, -104, -105, -108];
const CLIENT_CERTIFICATE_MISSING_ERROR_CODE = -200;
const SERVER_CERTIFICATE_INVALID = -299;
const SERVER_TRUST_EVALUATION_FAILED = -298;
let showingServerTrustAlert = false;
const messages = defineMessages({
invalidSslTitle: {
id: 'server.invalid.certificate.title',
defaultMessage: 'Invalid SSL certificate',
},
invalidSslDescription: {
id: 'server.invalid.certificate.description',
defaultMessage: 'The certificate for this server is invalid.\nYou might be connecting to a server that is pretending to be “{hostname}” which could put your confidential information at risk.',
},
invalidPinningTitle: {
id: 'server.invalid.pinning.title',
defaultMessage: 'Invalid pinned SSL certificate',
},
});
class NetworkManager {
private clients: Record<string, Client> = {};
private intl = createIntl({
locale: DEFAULT_LOCALE,
messages: getTranslations(DEFAULT_LOCALE),
});
private DEFAULT_CONFIG: APIClientConfiguration = {
headers: {
'X-Requested-With': 'XMLHttpRequest',
@ -129,12 +153,23 @@ class NetworkManager {
logDebug('Invalid SSL certificate:', event.errorDescription);
const parsed = urlParse(event.serverUrl);
Alert.alert(
getLocalizedMessage(DEFAULT_LOCALE, t('server.invalid.certificate.title'), 'Invalid SSL certificate'),
getLocalizedMessage(
DEFAULT_LOCALE,
t('server.invalid.certificate.description'),
'The certificate for this server is invalid.\nYou might be connecting to a server that is pretending to be “{hostname}” which could put your confidential information at risk.',
).replace('{hostname}', parsed.hostname),
this.intl.formatMessage(messages.invalidSslTitle),
this.intl.formatMessage(messages.invalidSslDescription, {hostname: parsed.hostname}),
);
} else if (SERVER_TRUST_EVALUATION_FAILED === event.errorCode && !showingServerTrustAlert) {
logDebug('Invalid SSL Pinning:', event.errorDescription);
showingServerTrustAlert = true;
Alert.alert(
this.intl.formatMessage(messages.invalidPinningTitle),
event.errorDescription,
[{
text: this.intl.formatMessage({id: 'server_upgrade.dismiss', defaultMessage: 'Dismiss'}),
onPress: () => {
setTimeout(() => {
showingServerTrustAlert = false;
}, toMilliseconds({hours: 23}));
},
}],
);
}
};

View file

@ -1009,6 +1009,7 @@
"server_upgrade.learn_more": "Learn More",
"server.invalid.certificate.description": "The certificate for this server is invalid.\nYou might be connecting to a server that is pretending to be “{hostname}” which could put your confidential information at risk.",
"server.invalid.certificate.title": "Invalid SSL certificate",
"server.invalid.pinning.title": "Invalid pinned SSL certificate",
"server.logout.alert_description": "All associated data will be removed",
"server.logout.alert_title": "Are you sure you want to log out of {displayName}?",
"server.remove.alert_description": "This will remove it from your list of servers. All associated data will be removed",

4
assets/certs/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View file

@ -699,6 +699,7 @@ platform :android do
end
update_identifiers
replace_assets
pinned_certificates
link_sentry({:os_type => "android"})
build_android
move_android_to_root
@ -816,6 +817,10 @@ platform :android do
sh 'cp -R ../assets/sounds/* ../android/app/src/main/res/raw/'
end
lane :pinned_certificates do
sh 'cp ../assets/certs/* ../android/app/src/main/assets/certs'
end
lane :deploy do |options|
file_path = options[:file]

View file

@ -0,0 +1,15 @@
import Foundation
extension Bundle {
static var app: Bundle {
var components = main.bundleURL.path.split(separator: "/")
var bundle: Bundle?
if let index = components.lastIndex(where: { $0.hasSuffix(".app") }) {
components.removeLast((components.count - 1) - index)
bundle = Bundle(path: components.joined(separator: "/"))
}
return bundle ?? main
}
}

View file

@ -1,24 +1,120 @@
import Foundation
import os.log
extension Network: URLSessionDelegate, URLSessionTaskDelegate {
typealias ChallengeEvaluation = (disposition: URLSession.AuthChallengeDisposition, credential: URLCredential?, error: NetworkError?)
public func urlSession(_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
var credential: URLCredential? = nil
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
let authMethod = challenge.protectionSpace.authenticationMethod
if authMethod == NSURLAuthenticationMethodClientCertificate {
let host = task.currentRequest!.url!.host!
if let (identity, certificate) = try? Keychain.default.getClientIdentityAndCertificate(for: host) {
credential = URLCredential(identity: identity,
certificates: [certificate],
persistence: URLCredential.Persistence.permanent)
}
disposition = .useCredential
var evaluation: ChallengeEvaluation
switch challenge.protectionSpace.authenticationMethod {
#if canImport(Security)
case NSURLAuthenticationMethodServerTrust:
evaluation = attemptServerTrustAuthentication(with: challenge)
case NSURLAuthenticationMethodClientCertificate:
evaluation = attemptClientAuthentication(with: challenge)
#endif
default:
evaluation = (.performDefaultHandling, nil, nil)
}
if let error = evaluation.error {
os_log("Gekidou: %{public}@",
log: .default,
type: .error,
error.localizedDescription
)
}
completionHandler(evaluation.disposition, evaluation.credential)
}
func attemptClientAuthentication(with challenge: URLAuthenticationChallenge) -> ChallengeEvaluation{
let host = challenge.protectionSpace.host
guard let (identity, certificate) = try? Keychain.default.getClientIdentityAndCertificate(for: host) else {
return (.performDefaultHandling, nil, nil)
}
return (.useCredential, URLCredential(identity: identity,
certificates: [certificate],
persistence: URLCredential.Persistence.permanent
), nil)
}
func attemptServerTrustAuthentication(with challenge: URLAuthenticationChallenge) -> ChallengeEvaluation {
let host = challenge.protectionSpace.host
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let trust = challenge.protectionSpace.serverTrust
else {
return (.performDefaultHandling, nil, nil)
}
do {
guard let certs = certificates[host], !certs.isEmpty else {
return (.performDefaultHandling, nil, nil)
}
completionHandler(disposition, credential)
try performDefaultValidation(trust)
try performValidation(trust, forHost: host)
try evaluate(trust, forHost: host, withCerts: certs)
return (.useCredential, URLCredential(trust: trust), nil)
} catch {
os_log("Gekidou: %{public}@",
log: .default,
type: .error,
error.localizedDescription
)
return (.cancelAuthenticationChallenge, nil, error as? NetworkError)
}
}
private func getServerTrustCertificates(_ trust: SecTrust) -> [SecCertificate] {
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, visionOS 1, *) {
return (SecTrustCopyCertificateChain(trust) as? [SecCertificate]) ?? []
} else {
return (0..<SecTrustGetCertificateCount(trust)).compactMap { index in
SecTrustGetCertificateAtIndex(trust, index)
}
}
}
private func performDefaultValidation(_ trust: SecTrust) throws {
let policy = SecPolicyCreateSSL(true, nil)
try evaluate(trust, afterApplying: policy)
}
private func performValidation(_ trust: SecTrust, forHost host: String) throws {
let policy = SecPolicyCreateSSL(true, host as CFString)
try evaluate(trust, afterApplying: policy)
}
private func evaluate(_ trust: SecTrust, afterApplying policy: SecPolicy) throws {
let status = SecTrustSetPolicies(trust, policy)
guard status == errSecSuccess else {
throw NetworkError.serverTrustEvaluationFailed(reason: .policyApplicationFailed(trust: trust, policy: policy, status: status))
}
var error: CFError?
let evaluationSucceeded = SecTrustEvaluateWithError(trust, &error)
if !evaluationSucceeded {
throw NetworkError.serverTrustEvaluationFailed(reason: .trustEvaluationFailed(error: error))
}
}
private func evaluate(_ trust: SecTrust, forHost host: String, withCerts certs: [SecCertificate]) throws {
let serverCertificates = getServerTrustCertificates(trust)
let serverCertificatesData = Set(serverCertificates.map { SecCertificateCopyData($0) as Data })
let pinnedCertificatesData = Set(certs.map { SecCertificateCopyData($0) as Data })
let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData)
if !pinnedCertificatesInServerData {
throw NetworkError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host, trust: trust, pinnedCertificates: certs, serverCertificates: serverCertificates))
}
}
}

View file

@ -0,0 +1,68 @@
import Foundation
public enum NetworkError: Error {
public enum ServerTrustFailureReason {
/// The output of a server trust evaluation.
public struct Output {
/// The host for which the evaluation was performed.
public let host: String
/// The `SecTrust` value which was evaluated.
public let trust: SecTrust
/// The `OSStatus` of evaluation operation.
public let status: OSStatus
/// The result of the evaluation operation.
public let result: SecTrustResultType
/// Creates an `Output` value from the provided values.
init(_ host: String, _ trust: SecTrust, _ status: OSStatus, _ result: SecTrustResultType) {
self.host = host
self.trust = trust
self.status = status
self.result = result
}
}
/// No certificates were found with which to perform the trust evaluation.
case noCertificatesFound
/// During evaluation, application of the associated `SecPolicy` failed.
case policyApplicationFailed(trust: SecTrust, policy: SecPolicy, status: OSStatus)
/// `SecTrust` evaluation failed with the associated `Error`, if one was produced.
case trustEvaluationFailed(error: Error?)
/// Default evaluation failed with the associated `Output`.
case defaultEvaluationFailed(output: Output)
/// Host validation failed with the associated `Output`.
case hostValidationFailed(output: Output)
/// Certificate pinning failed.
case certificatePinningFailed(host: String, trust: SecTrust, pinnedCertificates: [SecCertificate], serverCertificates: [SecCertificate])
}
case serverTrustEvaluationFailed(reason: ServerTrustFailureReason)
}
extension NetworkError: LocalizedError {
public var errorDescription: String? {
switch self {
case let .serverTrustEvaluationFailed(reason):
return "Server trust evaluation failed due to reason: \(reason.localizedDescription)"
}
}
}
extension NetworkError.ServerTrustFailureReason {
var localizedDescription: String {
switch self {
case .noCertificatesFound:
return "No certificates were found or provided for evaluation."
case .policyApplicationFailed:
return "Attempting to set a SecPolicy failed."
case let .trustEvaluationFailed(error):
return "SecTrust evaluation failed with error: \(error?.localizedDescription ?? "None")"
case let .defaultEvaluationFailed(output):
return "Default evaluation failed for host \(output.host)."
case let .hostValidationFailed(output):
return "Host validation failed for host \(output.host)."
case let .certificatePinningFailed(host, _, pinnedCertificates, _):
return "Certificate pinning failed for host \(host) after evaluating \(pinnedCertificates.count) installed certificates."
}
}
}

View file

@ -6,6 +6,7 @@
//
import Foundation
import os.log
public typealias ResponseHandler = (_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Void
@ -13,12 +14,15 @@ public class Network: NSObject {
internal var session: URLSession?
internal let queue = OperationQueue()
internal let urlVersion = "/api/v4"
internal var certificates: [String: [SecCertificate]] = [:]
@objc public static let `default` = Network()
override private init() {
super.init()
loadPinnedCertificates()
queue.maxConcurrentOperationCount = 1
let config = URLSessionConfiguration.default
@ -33,6 +37,41 @@ public class Network: NSObject {
)
}
internal func loadPinnedCertificates() {
guard let certsPath = Bundle.app.resourceURL?.appendingPathComponent("certs") else {
return
}
let fileManager = FileManager.default
do {
let certsArray = try fileManager.contentsOfDirectory(at: certsPath, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
let certs = certsArray.filter{ $0.pathExtension == "crt" || $0.pathExtension == "cer"}
for cert in certs {
if let domain = URL(string: cert.absoluteString)?.deletingPathExtension().lastPathComponent,
let certData = try? Data(contentsOf: cert),
let certificate = SecCertificateCreateWithData(nil, certData as CFData){
if certificates[domain] != nil {
certificates[domain]?.append(certificate)
} else {
certificates[domain] = [certificate]
}
os_log("Gekidou: loaded certificate %{public}@ for domain %{public}@",
log: .default,
type: .info,
cert.lastPathComponent, domain
)
}
}
} catch {
os_log(
"Gekidou: Error loading pinned certificates -- %{public}@",
log: .default,
type: .error,
String(describing: error)
)
}
}
internal func buildApiUrl(_ serverUrl: String, _ endpoint: String) -> URL {
return URL(string: "\(serverUrl)\(urlVersion)\(endpoint)")!
}

View file

@ -61,8 +61,9 @@ extension ShareExtension: URLSessionDataDelegate {
let uploadData = getUploadSessionData(id: id)
else {
os_log(
OSLogType.default,
"Mattermost BackgroundSession: didReceived failed to getUploadSessionData identifier=%{public}@",
log: .default,
type: .error,
session.configuration.identifier ?? "no identifier"
)
return
@ -95,6 +96,8 @@ extension ShareExtension: URLSessionDataDelegate {
filename,
fileId
)
postMessageIfUploadFinished(forSession: session)
} else {
os_log(
OSLogType.default,
@ -105,8 +108,9 @@ extension ShareExtension: URLSessionDataDelegate {
}
} catch {
os_log(
OSLogType.default,
"Mattermost BackgroundSession: Failed to receive data identifier=%{public}@ error=%{public}",
log: .default,
type: .error,
id,
error.localizedDescription
)
@ -116,48 +120,22 @@ extension ShareExtension: URLSessionDataDelegate {
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error == nil,
let id = session.configuration.identifier {
guard let data = getUploadSessionData(id: id)
else {
os_log(
OSLogType.default,
"Mattermost BackgroundSession: didCompleteWithError delegate failed to getUploadSessionData identifier=%{public}@",
session.configuration.identifier ?? "no identifier"
)
return
}
let total = data.totalFiles
let count = data.fileIds.count
if error != nil {
os_log(
OSLogType.default,
"Mattermost BackgroundSession: didCompleteWithError delegate for identifier=%{public}@ total files %{public}@ of %{public}@",
id,
"\(count)",
"\(total)"
)
if data.fileIds.count == data.totalFiles {
ProcessInfo().performExpiringActivity(
withReason: "Need to post the message") {expires in
os_log(
OSLogType.default,
"Mattermost BackgroundSession: posting message for identifier=%{public}@",
id
)
self.postMessageForSession(withId: id)
self.urlSessionDidFinishEvents(forBackgroundURLSession: session)
}
}
} else if error != nil {
os_log(
OSLogType.default,
"Mattermost BackgroundSession: didCompleteWithError delegate failed identifier=%{public}@ with error %{public}@",
log: .default,
type: .error,
session.configuration.identifier ?? "no identifier",
error?.localizedDescription ?? "no error description available"
)
return
}
os_log(
OSLogType.default,
"Mattermost BackgroundSession: didCompleteWithError delegate for identifier=%{public}@ after calling postMessageIfUploadFinished",
session.configuration.identifier ?? "no identifier"
)
}
@ -180,4 +158,45 @@ extension ShareExtension: URLSessionDataDelegate {
}
})
}
private func postMessageIfUploadFinished(forSession session: URLSession) {
lock.lock()
defer { lock.unlock() }
guard let id = session.configuration.identifier, let data = getUploadSessionData(id: id)
else {
os_log(
"Mattermost BackgroundSession: postMessageIfUploadFinished failed to getUploadSessionData identifier=%{public}@",
log: .default,
type: .error,
session.configuration.identifier ?? "no identifier"
)
return
}
let total = data.totalFiles
let count = data.fileIds.count
if count == total {
os_log(
OSLogType.default,
"Mattermost BackgroundSession: posting message for identifier=%{public}@",
id
)
ProcessInfo().performExpiringActivity(
withReason: "Need to post the message") {expires in
self.postMessageForSession(withId: id)
self.urlSessionDidFinishEvents(forBackgroundURLSession: session)
}
} else {
os_log(
OSLogType.default,
"Mattermost BackgroundSession: finish uploading files %{public}@ of %{public}@ for identifier=%{public}@",
"\(count)",
"\(total)",
id
)
}
}
}

View file

@ -98,9 +98,11 @@ extension ShareExtension {
id
)
return
}
self.removeUploadSessionData(id: id)
self.deleteUploadedFiles(files: data.files)
if let serverUrl = data.serverUrl,
let channelId = data.channelId {
Network.default.createPost(
@ -109,8 +111,15 @@ extension ShareExtension {
message: data.message,
fileIds: data.fileIds,
completionHandler: {info, reponse, error in
self.removeUploadSessionData(id: id)
self.deleteUploadedFiles(files: data.files)
if let err = error {
os_log(
"Mattermost BackgroundSession: error to create post for session identifier=%{public}@ -- %{public}@",
log: .default,
type: .error,
id,
err.localizedDescription
)
}
if let handler = completionHandler {
os_log(

View file

@ -54,6 +54,7 @@ public class ShareExtension: NSObject {
private let fileMgr = FileManager.default
private let preferences = Preferences.default
public var completionHandler: (() -> Void)?
internal let lock = NSLock()
public override init() {
super.init()

View file

@ -34,7 +34,7 @@ public class PushNotification: NSObject {
"Mattermost Notifications: process receipt response for serverUrl %{public}@ does not contain data",
ackNotification.serverUrl
)
completionHandler(notification)
completionHandler(nil)
return
}
notification.userInfo["server_url"] = ackNotification.serverUrl
@ -75,9 +75,14 @@ public class PushNotification: NSObject {
url, withMethod: "POST", withBody: jsonData,
andHeaders: headers, forServerUrl: ackNotification.serverUrl) {[weak self] data, response, error in
if error != nil && ackNotification.isIdLoaded,
let nsError = error as? NSError,
let fibonacciBackoffsInSeconds = self?.fibonacciBackoffsInSeconds,
let retryIndex = self?.retryIndex,
fibonacciBackoffsInSeconds.count > retryIndex {
if nsError.code == NSURLErrorCancelled {
completionHandler(nil)
return
}
let backoffInSeconds = fibonacciBackoffsInSeconds[retryIndex]
self?.retryIndex += 1

View file

@ -168,6 +168,7 @@
7FD482672864DC5900A5B18B /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BC977883E2624E05975CA65B /* OpenSans-Regular.ttf */; };
7FD482682864DC5900A5B18B /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E5C16B14E1CE4868886A1A00 /* OpenSans-SemiBold.ttf */; };
7FD482692864DC5900A5B18B /* OpenSans-SemiBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DB14DFDF6E04FA69FE769DC /* OpenSans-SemiBoldItalic.ttf */; };
7FDEF5362C2D01CC0093AADB /* certs in Resources */ = {isa = PBXBuildFile; fileRef = 7FDEF5352C2D01CC0093AADB /* certs */; };
7FF9C03D2983E7C6005CDCF5 /* ErrorSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF9C03C2983E7C6005CDCF5 /* ErrorSharingView.swift */; };
83ABFD122C1C90D90029685B /* calls_dynamic.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 83ABFD0E2C1C90D90029685B /* calls_dynamic.mp3 */; };
83ABFD132C1C90D90029685B /* calls_cheerful.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 83ABFD0F2C1C90D90029685B /* calls_cheerful.mp3 */; };
@ -360,6 +361,7 @@
7FCEFB9126B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SDWebImageDownloaderOperation+Swizzle.h"; sourceTree = "<group>"; };
7FCEFB9226B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SDWebImageDownloaderOperation+Swizzle.m"; sourceTree = "<group>"; };
7FD482282864D69700A5B18B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
7FDEF5352C2D01CC0093AADB /* certs */ = {isa = PBXFileReference; lastKnownFileType = folder; name = certs; path = ../assets/certs; sourceTree = "<group>"; };
7FF9C03C2983E7C6005CDCF5 /* ErrorSharingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorSharingView.swift; sourceTree = "<group>"; };
7FFE32B51FD9CCAA0038C7A0 /* FLAnimatedImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FLAnimatedImage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7FFE32B61FD9CCAA0038C7A0 /* KSCrash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KSCrash.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -1004,6 +1006,7 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
7FDEF5352C2D01CC0093AADB /* certs */,
27C667AB2952425700E590D5 /* ErrorReporting */,
13B07FAE1A68108700A75B9A /* Mattermost */,
7F581D33221ED5C60099E66B /* NotificationService */,
@ -1214,6 +1217,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7FDEF5362C2D01CC0093AADB /* certs in Resources */,
83ABFD132C1C90D90029685B /* calls_cheerful.mp3 in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
7F0F4B0A24BA173900E14C60 /* LaunchScreen.storyboard in Resources */,

View file

@ -1169,7 +1169,7 @@ PODS:
- Yoga
- react-native-netinfo (11.3.2):
- React-Core
- react-native-network-client (1.6.0):
- react-native-network-client (1.7.0):
- Alamofire (~> 5.9.1)
- DoubleConversion
- glog
@ -2040,7 +2040,7 @@ SPEC CHECKSUMS:
react-native-emm: ecab44d78fb1cc7d7b7901914f48fb6309c46ff2
react-native-image-picker: c3afe5472ef870d98a4b28415fc0b928161ee5f7
react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc
react-native-network-client: a862d041763721fddb9802a24fb6bb28208781cb
react-native-network-client: 65294121f883670b5e3b238a82693c7a810d74f5
react-native-notifications: 4601a5a8db4ced6ae7cfc43b44d35fe437ac50c4
react-native-paste-input: 011a9916ef3acf809a7da122847c61ca0f93a60e
react-native-performance: ff93f8af3b2ee9519fd7879896aa9b8b8272691d

View file

@ -1,12 +1,14 @@
package com.mattermost.rnshare
import android.content.Context
import android.util.Base64
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.work.ForegroundInfo
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.mattermost.rnshare.helpers.RealPathUtil
import okhttp3.CertificatePinner
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -22,11 +24,60 @@ import org.json.JSONException
import org.json.JSONObject
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.Objects
class ShareWorker(context: Context, workerParameters: WorkerParameters) : Worker(context, workerParameters) {
private val okHttpClient = OkHttpClient()
class ShareWorker(private val context: Context, workerParameters: WorkerParameters) : Worker(context, workerParameters) {
private val jsonType: MediaType? = "application/json; charset=utf-8".toMediaTypeOrNull()
private val okHttpClient: OkHttpClient
get() {
val builder = OkHttpClient.Builder()
val fingerprintsMap = getCertificatesFingerPrints()
if (fingerprintsMap.isNotEmpty()) {
val pinner = CertificatePinner.Builder()
for ((domain, fingerprints) in fingerprintsMap) {
for (fingerprint in fingerprints) {
pinner.add(domain, "sha256/$fingerprint")
}
}
val certificatePinner = pinner.build()
builder.certificatePinner(certificatePinner)
}
return builder.build()
}
private fun getCertificateFingerPrint(certInputStream: InputStream): String {
val certFactory = CertificateFactory.getInstance("X.509")
val certificate = certFactory.generateCertificate(certInputStream) as X509Certificate
val sha256 = MessageDigest.getInstance("SHA-256")
Base64.encodeToString(sha256.digest(certificate.publicKey.encoded), Base64.NO_WRAP)
val fingerprintBytes = sha256.digest(certificate.publicKey.encoded)
return Base64.encodeToString(fingerprintBytes, Base64.NO_WRAP)
}
private fun getCertificatesFingerPrints(): Map<String, List<String>> {
val fingerprintsMap = mutableMapOf<String, MutableList<String>>()
val assetsManager = context.assets
val certFiles = assetsManager.list("certs")?.filter { it.endsWith(".cer") || it.endsWith(".crt") } ?: return emptyMap()
for (fileName in certFiles) {
val domain = fileName.substringBeforeLast(".")
val certInputStream = assetsManager.open("certs/$fileName")
certInputStream.use {
val fingerprint = getCertificateFingerPrint(it)
if (fingerprintsMap.containsKey(domain)) {
fingerprintsMap[domain]?.add(fingerprint)
} else {
fingerprintsMap[domain] = mutableListOf(fingerprint)
}
}
}
return fingerprintsMap
}
override fun doWork(): Result {
val jsonString = inputData.getString("json_data") ?: return Result.failure()

View file

@ -1,10 +1,8 @@
package com.mattermost.rnshare.helpers
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.text.TextUtils
import android.util.Log
@ -49,25 +47,10 @@ object RealPathUtil {
return null
}
}
} else if (isMediaDocument(uri)) {
}
else if (isMediaDocument(uri)) {
// MediaProvider
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split((":").toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
}
val selectionArgs = arrayOf(split[1])
return contentUri?.let { getDataColumn(context, it, selectionArgs) }
return getPathFromSavingTempFile(context, uri)
}
}
if ("content".equals(uri.scheme, ignoreCase = true)) {
@ -133,27 +116,6 @@ object RealPathUtil {
return f.name
}
private fun getDataColumn(context:Context, uri:Uri, selectionArgs:Array<String>): String? {
var cursor: Cursor? = null
val column = "_data"
val selection = "_id=?"
val projection = arrayOf(column)
try
{
cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
}
finally
{
cursor?.close()
}
return null
}
private fun isExternalStorageDocument(uri:Uri):Boolean {
return "com.android.externalstorage.documents" == uri.authority
}

10
package-lock.json generated
View file

@ -22,7 +22,7 @@
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
"@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker",
"@mattermost/react-native-emm": "1.5.0",
"@mattermost/react-native-network-client": "1.6.0",
"@mattermost/react-native-network-client": "1.7.0",
"@mattermost/react-native-paste-input": "0.8.0",
"@mattermost/react-native-turbo-log": "0.4.0",
"@mattermost/rnshare": "file:./libraries/@mattermost/rnshare",
@ -49,7 +49,7 @@
"emoji-regex": "10.3.0",
"expo": "51.0.14",
"expo-application": "5.9.1",
"expo-crypto": "~13.0.2",
"expo-crypto": "13.0.2",
"expo-device": "6.0.2",
"expo-image": "1.12.12",
"expo-linear-gradient": "13.0.2",
@ -5609,9 +5609,9 @@
}
},
"node_modules/@mattermost/react-native-network-client": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.6.0.tgz",
"integrity": "sha512-SUXmF0sF4iyAgVovK8VYtUCF38MchAI89GwrvhkvaB+MQKMruDsqaKBLiwW4Rs6jq4dVbmTLV/7FTP0akTiyRQ==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.7.0.tgz",
"integrity": "sha512-m/O1BVXHYZkDjJONifyAfFygjE0SVpzUCrvhaqK0icG9PGoMoycSVtMqN/cHFaG2jASgEQY4pTgDz0UhcNCWDw==",
"dependencies": {
"validator": "13.12.0",
"zod": "3.23.8"

View file

@ -23,7 +23,7 @@
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
"@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker",
"@mattermost/react-native-emm": "1.5.0",
"@mattermost/react-native-network-client": "1.6.0",
"@mattermost/react-native-network-client": "1.7.0",
"@mattermost/react-native-paste-input": "0.8.0",
"@mattermost/react-native-turbo-log": "0.4.0",
"@mattermost/rnshare": "file:./libraries/@mattermost/rnshare",