1// Copyright 2017 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4import 'dart:async'; 5 6import 'package:quiver/strings.dart'; 7 8import '../application_package.dart'; 9import '../base/common.dart'; 10import '../base/io.dart'; 11import '../base/process.dart'; 12import '../base/terminal.dart'; 13import '../convert.dart' show utf8; 14import '../globals.dart'; 15 16/// User message when no development certificates are found in the keychain. 17/// 18/// The user likely never did any iOS development. 19const String noCertificatesInstruction = ''' 20════════════════════════════════════════════════════════════════════════════════ 21No valid code signing certificates were found 22You can connect to your Apple Developer account by signing in with your Apple ID 23in Xcode and create an iOS Development Certificate as well as a Provisioning\u0020 24Profile for your project by: 25$fixWithDevelopmentTeamInstruction 26 5- Trust your newly created Development Certificate on your iOS device 27 via Settings > General > Device Management > [your new certificate] > Trust 28 29For more information, please visit: 30 https://developer.apple.com/library/content/documentation/IDEs/Conceptual/ 31 AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html 32 33Or run on an iOS simulator without code signing 34════════════════════════════════════════════════════════════════════════════════'''; 35/// User message when there are no provisioning profile for the current app bundle identifier. 36/// 37/// The user did iOS development but never on this project and/or device. 38const String noProvisioningProfileInstruction = ''' 39════════════════════════════════════════════════════════════════════════════════ 40No Provisioning Profile was found for your project's Bundle Identifier or your\u0020 41device. You can create a new Provisioning Profile for your project in Xcode for\u0020 42your team by: 43$fixWithDevelopmentTeamInstruction 44 45It's also possible that a previously installed app with the same Bundle\u0020 46Identifier was signed with a different certificate. 47 48For more information, please visit: 49 https://flutter.dev/setup/#deploy-to-ios-devices 50 51Or run on an iOS simulator without code signing 52════════════════════════════════════════════════════════════════════════════════'''; 53/// Fallback error message for signing issues. 54/// 55/// Couldn't auto sign the app but can likely solved by retracing the signing flow in Xcode. 56const String noDevelopmentTeamInstruction = ''' 57════════════════════════════════════════════════════════════════════════════════ 58Building a deployable iOS app requires a selected Development Team with a\u0020 59Provisioning Profile. Please ensure that a Development Team is selected by: 60$fixWithDevelopmentTeamInstruction 61 62For more information, please visit: 63 https://flutter.dev/setup/#deploy-to-ios-devices 64 65Or run on an iOS simulator without code signing 66════════════════════════════════════════════════════════════════════════════════'''; 67const String fixWithDevelopmentTeamInstruction = ''' 68 1- Open the Flutter project's Xcode target with 69 open ios/Runner.xcworkspace 70 2- Select the 'Runner' project in the navigator then the 'Runner' target 71 in the project settings 72 3- In the 'General' tab, make sure a 'Development Team' is selected.\u0020 73 You may need to: 74 - Log in with your Apple ID in Xcode first 75 - Ensure you have a valid unique Bundle ID 76 - Register your device with your Apple Developer Account 77 - Let Xcode automatically provision a profile for your app 78 4- Build or run your project again'''; 79 80 81final RegExp _securityFindIdentityDeveloperIdentityExtractionPattern = 82 RegExp(r'^\s*\d+\).+"(.+Develop(ment|er).+)"$'); 83final RegExp _securityFindIdentityCertificateCnExtractionPattern = RegExp(r'.*\(([a-zA-Z0-9]+)\)'); 84final RegExp _certificateOrganizationalUnitExtractionPattern = RegExp(r'OU=([a-zA-Z0-9]+)'); 85 86/// Given a [BuildableIOSApp], this will try to find valid development code 87/// signing identities in the user's keychain prompting a choice if multiple 88/// are found. 89/// 90/// Returns a set of build configuration settings that uses the selected 91/// signing identities. 92/// 93/// Will return null if none are found, if the user cancels or if the Xcode 94/// project has a development team set in the project's build settings. 95Future<Map<String, String>> getCodeSigningIdentityDevelopmentTeam({ 96 BuildableIOSApp iosApp, 97 bool usesTerminalUi = true, 98}) async { 99 final Map<String, String> buildSettings = iosApp.project.buildSettings; 100 if (buildSettings == null) 101 return null; 102 103 // If the user already has it set in the project build settings itself, 104 // continue with that. 105 if (isNotEmpty(buildSettings['DEVELOPMENT_TEAM'])) { 106 printStatus( 107 'Automatically signing iOS for device deployment using specified development ' 108 'team in Xcode project: ${buildSettings['DEVELOPMENT_TEAM']}' 109 ); 110 return null; 111 } 112 113 if (isNotEmpty(buildSettings['PROVISIONING_PROFILE'])) 114 return null; 115 116 // If the user's environment is missing the tools needed to find and read 117 // certificates, abandon. Tools should be pre-equipped on macOS. 118 if (!exitsHappy(const <String>['which', 'security']) || !exitsHappy(const <String>['which', 'openssl'])) 119 return null; 120 121 const List<String> findIdentityCommand = 122 <String>['security', 'find-identity', '-p', 'codesigning', '-v']; 123 124 String findIdentityStdout; 125 try { 126 findIdentityStdout = runCheckedSync(findIdentityCommand); 127 } catch (error) { 128 printTrace('Unexpected failure from find-identity: $error.'); 129 return null; 130 } 131 132 final List<String> validCodeSigningIdentities = findIdentityStdout 133 .split('\n') 134 .map<String>((String outputLine) { 135 return _securityFindIdentityDeveloperIdentityExtractionPattern 136 .firstMatch(outputLine) 137 ?.group(1); 138 }) 139 .where(isNotEmpty) 140 .toSet() // Unique. 141 .toList(); 142 143 final String signingIdentity = await _chooseSigningIdentity(validCodeSigningIdentities, usesTerminalUi); 144 145 // If none are chosen, return null. 146 if (signingIdentity == null) 147 return null; 148 149 printStatus('Signing iOS app for device deployment using developer identity: "$signingIdentity"'); 150 151 final String signingCertificateId = 152 _securityFindIdentityCertificateCnExtractionPattern 153 .firstMatch(signingIdentity) 154 ?.group(1); 155 156 // If `security`'s output format changes, we'd have to update the above regex. 157 if (signingCertificateId == null) 158 return null; 159 160 String signingCertificateStdout; 161 try { 162 signingCertificateStdout = runCheckedSync( 163 <String>['security', 'find-certificate', '-c', signingCertificateId, '-p'] 164 ); 165 } catch (error) { 166 printTrace('Couldn\'t find the certificate: $error.'); 167 return null; 168 } 169 170 final Process opensslProcess = await runCommand(const <String>['openssl', 'x509', '-subject']); 171 await (opensslProcess.stdin..write(signingCertificateStdout)).close(); 172 173 final String opensslOutput = await utf8.decodeStream(opensslProcess.stdout); 174 // Fire and forget discard of the stderr stream so we don't hold onto resources. 175 // Don't care about the result. 176 unawaited(opensslProcess.stderr.drain<String>()); 177 178 if (await opensslProcess.exitCode != 0) 179 return null; 180 181 return <String, String>{ 182 'DEVELOPMENT_TEAM': _certificateOrganizationalUnitExtractionPattern 183 .firstMatch(opensslOutput) 184 ?.group(1), 185 }; 186} 187 188Future<String> _chooseSigningIdentity(List<String> validCodeSigningIdentities, bool usesTerminalUi) async { 189 // The user has no valid code signing identities. 190 if (validCodeSigningIdentities.isEmpty) { 191 printError(noCertificatesInstruction, emphasis: true); 192 throwToolExit('No development certificates available to code sign app for device deployment'); 193 } 194 195 if (validCodeSigningIdentities.length == 1) 196 return validCodeSigningIdentities.first; 197 198 if (validCodeSigningIdentities.length > 1) { 199 final String savedCertChoice = config.getValue('ios-signing-cert'); 200 201 if (savedCertChoice != null) { 202 if (validCodeSigningIdentities.contains(savedCertChoice)) { 203 printStatus('Found saved certificate choice "$savedCertChoice". To clear, use "flutter config".'); 204 return savedCertChoice; 205 } else { 206 printError('Saved signing certificate "$savedCertChoice" is not a valid development certificate'); 207 } 208 } 209 210 // If terminal UI can't be used, just attempt with the first valid certificate 211 // since we can't ask the user. 212 if (!usesTerminalUi) 213 return validCodeSigningIdentities.first; 214 215 final int count = validCodeSigningIdentities.length; 216 printStatus( 217 'Multiple valid development certificates available (your choice will be saved):', 218 emphasis: true, 219 ); 220 for (int i=0; i<count; i++) { 221 printStatus(' ${i+1}) ${validCodeSigningIdentities[i]}', emphasis: true); 222 } 223 printStatus(' a) Abort', emphasis: true); 224 225 final String choice = await terminal.promptForCharInput( 226 List<String>.generate(count, (int number) => '${number + 1}') 227 ..add('a'), 228 prompt: 'Please select a certificate for code signing', 229 displayAcceptedCharacters: true, 230 defaultChoiceIndex: 0, // Just pressing enter chooses the first one. 231 ); 232 233 if (choice == 'a') { 234 throwToolExit('Aborted. Code signing is required to build a deployable iOS app.'); 235 } else { 236 final String selectedCert = validCodeSigningIdentities[int.parse(choice) - 1]; 237 printStatus('Certificate choice "$selectedCert" saved'); 238 config.setValue('ios-signing-cert', selectedCert); 239 return selectedCert; 240 } 241 } 242 243 return null; 244} 245