motion-provisioning 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+
5
+ require "irb"
6
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+ rake generae_certificates
8
+
@@ -0,0 +1,231 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+ require 'base64'
4
+ require 'date'
5
+
6
+ require 'plist'
7
+ require 'security'
8
+ require 'spaceship'
9
+ require 'motion-provisioning/spaceship/portal_client'
10
+ require 'motion-provisioning/spaceship/free_portal_client'
11
+
12
+ require 'motion-provisioning/utils'
13
+ require 'motion-provisioning/tasks'
14
+ require 'motion-provisioning/version'
15
+ require 'motion-provisioning/service'
16
+ require 'motion-provisioning/certificate'
17
+ require 'motion-provisioning/application'
18
+ require 'motion-provisioning/mobileprovision'
19
+ require 'motion-provisioning/provisioning_profile'
20
+
21
+ module MotionProvisioning
22
+
23
+ class << self
24
+ attr_accessor :free, :team
25
+ end
26
+
27
+ def self.client
28
+ Spaceship::Portal.client ||= begin
29
+
30
+ if File.exist?('.gitignore') && File.read('.gitignore').match(/^provisioning$/).nil?
31
+ answer = Utils.ask("Info", "Do you want to add the 'provisioning' folder fo your '.gitignore' file? (Recommended) (Y/n):")
32
+ `echo provisioning >> .gitignore` if answer.downcase == 'y'
33
+ end
34
+
35
+ client = if free
36
+ Spaceship::FreePortalClient.new
37
+ else
38
+ Spaceship::PortalClient.new
39
+ end
40
+
41
+ email = ENV['MOTION_PROVISIONING_EMAIL'] || MotionProvisioning.config['email'] || Utils.ask("Info", "Your Apple ID email:")
42
+
43
+ config_path = File.expand_path('./provisioning/config.yaml')
44
+
45
+ if ENV['MOTION_PROVISIONING_EMAIL'].nil? && !File.exist?(config_path)
46
+ answer = Utils.ask("Info", "Do you want to save the email to the config file ('provisioning/config.yaml') so you dont have to type it again? (Y/n):")
47
+ if answer.downcase == 'y'
48
+ FileUtils.mkdir_p(File.expand_path('./provisioning'))
49
+ File.write(config_path, { 'email' => email }.to_yaml)
50
+ end
51
+ end
52
+
53
+ password = ENV['MOTION_PROVISIONING_PASSWORD']
54
+
55
+ server_name = "motionprovisioning.#{email}"
56
+ item = Security::InternetPassword.find(server: server_name)
57
+ password ||= item.password if item
58
+
59
+ if password.nil?
60
+ puts "The login information you enter will be stored safely in the macOS keychain."
61
+ password = Utils.ask_password("Info", "Password for #{email}:")
62
+ Security::InternetPassword.add(server_name, email, password)
63
+ end
64
+
65
+ Utils.log("Info", "Logging into the Developer Portal with email '#{email}'.")
66
+ begin
67
+ client.user = email
68
+ client.send("do_login", email, password)
69
+ rescue Spaceship::Client::InvalidUserCredentialsError => ex
70
+ Utils.log("Error", "There was an error logging into your account. Your password may be wrong.")
71
+
72
+ if Utils.ask("Info", 'Do you want to reenter your password? (Y/n):').downcase == 'y'
73
+
74
+ # The 'delete' method is very verbose, temporarily disable output
75
+ orig_stdout = $stdout.dup
76
+ $stdout.reopen('/dev/null', 'w')
77
+ Security::InternetPassword.delete(server: server_name)
78
+ $stdout.reopen(orig_stdout)
79
+
80
+ password = Utils.ask_password("Info", "Password for #{email}:")
81
+ Security::InternetPassword.add(server_name, email, password)
82
+ retry
83
+ else
84
+ abort
85
+ end
86
+ end
87
+
88
+ if self.free
89
+ client.teams.each do |team|
90
+ if team['currentTeamMember']['roles'].include?('XCODE_FREE_USER')
91
+ client.team_id = team['teamId']
92
+ self.team = team
93
+ end
94
+ end
95
+
96
+ if client.team_id.nil?
97
+ raise "could not find free team"
98
+ end
99
+ else
100
+ if team_id = MotionProvisioning.config['team_id'] || ENV['MOTION_PROVISIONING_TEAM_ID']
101
+ found = false
102
+ client.teams.each do |team|
103
+ if team_id == team['teamId']
104
+ client.team_id = team_id
105
+ self.team = team
106
+ found = true
107
+ end
108
+ end
109
+
110
+ if found == false
111
+ raise "The current user does not belong to team with ID '#{team_id}' selected in config.yml."
112
+ end
113
+ else
114
+
115
+ team_id = client.select_team
116
+
117
+ if File.exist?(config_path) && ENV['MOTION_PROVISIONING_TEAM_ID'].nil?
118
+ answer = Utils.ask("Info", "Do you want to save the team id (#{team_id}) in the config file ('provisioning/config.yaml') so you dont have to select it again? (Y/n):")
119
+ if answer.downcase == 'y'
120
+ config = YAML.load(File.read(config_path))
121
+ config['team_id'] = team_id
122
+ File.write(config_path, config.to_yaml)
123
+ end
124
+ end
125
+
126
+ self.team = client.teams.detect { |team| team['teamId'] == team_id }
127
+ end
128
+ end
129
+
130
+ Utils.log("Info", "Selected team '#{self.team['name']} (#{self.team['teamId']})'.")
131
+
132
+ Spaceship::App.set_client(client)
133
+ Spaceship::AppGroup.set_client(client)
134
+ Spaceship::Device.set_client(client)
135
+ Spaceship::Certificate.set_client(client)
136
+ Spaceship::ProvisioningProfile.set_client(client)
137
+
138
+ client
139
+ end
140
+ end
141
+
142
+ def self.config
143
+ return @config if @config
144
+ config_path = File.expand_path('./provisioning/config.yaml')
145
+ if File.exist?(config_path)
146
+ @config = YAML.load(File.read(config_path)) || {}
147
+ else
148
+ @config = {}
149
+ end
150
+ end
151
+
152
+ def self.services
153
+ @services ||= []
154
+ end
155
+
156
+ def self.services=(services)
157
+ @services = services
158
+ end
159
+
160
+ def self.entitlements
161
+ self.services.map(&:to_hash).inject({}, &:merge!)
162
+ end
163
+
164
+ def self.certificate(opts = {})
165
+ unless opts[:platform]
166
+ Utils.log("Error", "Certificate 'platform' is required")
167
+ exit(1)
168
+ end
169
+
170
+ unless opts[:type]
171
+ Utils.log("Error", "Certificate 'type' is required")
172
+ exit(1)
173
+ end
174
+
175
+ if opts[:free] == true && opts[:type] != :development
176
+ Utils.log("Error", "You can only create a 'free' certificate for type 'development'. You selected type '#{opts[:type].to_s}'")
177
+ exit(1)
178
+ end
179
+
180
+ opts[:platform] = :ios if opts[:platform] == :tvos
181
+
182
+ supported_platforms = [:ios, :mac]
183
+ unless supported_platforms.include?(opts[:platform])
184
+ Utils.log("Error", "Invalid value'#{opts[:platform]}'for 'platorm'. Supported values: #{supported_platforms}")
185
+ exit(1)
186
+ end
187
+
188
+ supported_types = [:distribution, :development, :developer_id]
189
+ unless supported_types.include?(opts[:type])
190
+ Utils.log("Error", "Invalid value '#{opts[:type]}'for 'type'. Supported values: #{supported_types}")
191
+ exit(1)
192
+ end
193
+
194
+ MotionProvisioning.free = opts[:free]
195
+ Certificate.new.certificate_name(opts[:type], opts[:platform])
196
+ end
197
+
198
+
199
+ def self.profile(opts = {})
200
+ unless opts[:bundle_identifier]
201
+ Utils.log("Error", "'bundle_identifier' is required")
202
+ exit(1)
203
+ end
204
+
205
+ unless opts[:app_name]
206
+ Utils.log("Error", "'app_name' is required")
207
+ exit(1)
208
+ end
209
+
210
+ supported_platforms = [:ios, :tvos, :mac]
211
+ unless supported_platforms.include?(opts[:platform])
212
+ Utils.log("Error", "Invalid value'#{opts[:platform]}'for 'platorm'. Supported values: #{supported_platforms}")
213
+ exit(1)
214
+ end
215
+
216
+ supported_types = [:distribution, :adhoc, :development]
217
+ unless supported_types.include?(opts[:type])
218
+ Utils.log("Error", "Invalid value '#{opts[:type]}'for 'type'. Supported values: #{supported_types}")
219
+ exit(1)
220
+ end
221
+
222
+ if opts[:free] == true && opts[:type] != :development
223
+ Utils.log("Error", "You can only create a 'free' provisioning profile for type 'development'. You selected type '#{opts[:type].to_s}'")
224
+ exit(1)
225
+ end
226
+
227
+ MotionProvisioning.free = opts[:free]
228
+ ProvisioningProfile.new.provisioning_profile(opts[:bundle_identifier], opts[:app_name], opts[:platform], opts[:type])
229
+ end
230
+
231
+ end
@@ -0,0 +1,53 @@
1
+ module MotionProvisioning
2
+ class Application
3
+
4
+ # Finds or create app for the given bundle id and name
5
+ def self.find_or_create(bundle_id: nil, name: nil, mac: mac = false)
6
+ app = Spaceship::Portal::App.find(bundle_id, mac: mac)
7
+ if app
8
+ app = app.details if app.features.nil?
9
+ else
10
+ begin
11
+ app = Spaceship::Portal::App.create!(bundle_id: bundle_id, name: name, mac: mac)
12
+ app = app.details if app.features.nil?
13
+ rescue Spaceship::Client::UnexpectedResponse => e
14
+ if e.to_s.include?("is not a valid identifier")
15
+ Utils.log("Error", "'#{bundle_id}' is not a valid identifier for an app. Please choose an identifier containing only alphanumeric characters, dots and asterisk")
16
+ exit 1
17
+ elsif e.to_s.include?("is not available")
18
+ Utils.log("Error", "'#{bundle_id}' has already been taken. Please enter a different string.")
19
+ exit 1
20
+ else
21
+ raise e
22
+ end
23
+ end
24
+ end
25
+
26
+ # services = MotionProvisioning.services
27
+
28
+ # Disable all app services not enabled via entitlements
29
+ # app.enabled_features.each do |feature_id|
30
+ # # These services are always enabled and cannot be disabled
31
+ # next if ['inAppPurchase', 'gameCenter', 'push'].include?(feature_id)
32
+ # service = services.detect { |s| s.identifier == feature_id }
33
+ # if service.nil?
34
+ # Utils.log('Info', "Disabling unused app service '#{feature_id}' for '#{bundle_id}'")
35
+ # # To disable Data Protection we need to send an empty string as value
36
+ # value = feature_id == 'dataProtection' ? '' : false
37
+ # app.update_service(Spaceship::Portal::AppService.new(feature_id, value))
38
+ # end
39
+ # end
40
+
41
+ # # Enable all app services enabled via entitlements (or which have a different value)
42
+ # services.each do |service|
43
+ # value = service.identifier == 'dataProtection' ? 'complete' : true
44
+ # if app.features[service.identifier] != value
45
+ # Utils.log('Info', "Enabling app service '#{service.name.split("::").last}' for '#{bundle_id}'")
46
+ # app.update_service(Spaceship::Portal::AppService.new(service.identifier, value))
47
+ # end
48
+ # end
49
+
50
+ app
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,278 @@
1
+ module Spaceship
2
+ module Portal
3
+ class Certificate
4
+ # The PLIST request for the free certificate returns the certificate content
5
+ # in the certContent variable. It's stored in this attribute for later use.
6
+ attr_accessor :motionprovisioning_certContent,
7
+ :motionprovisioning_serialNumber
8
+ end
9
+ end
10
+ end
11
+
12
+ module MotionProvisioning
13
+ class Certificate
14
+
15
+ attr_accessor :type, :output_path, :platform
16
+
17
+ def client
18
+ MotionProvisioning.client
19
+ end
20
+
21
+ def mac?
22
+ self.platform == :mac
23
+ end
24
+
25
+ def certificate_name(type, platform)
26
+ self.type = type
27
+ self.platform = platform
28
+ self.output_path = File.expand_path('./provisioning')
29
+ certificate_path = File.expand_path("./provisioning/#{platform}_#{type}_certificate.cer")
30
+ private_key_path = File.expand_path("./provisioning/#{platform}_#{type}_private_key.p12")
31
+
32
+ # First check if there is a certificate and key file, and if it is installed
33
+ identities = available_identities
34
+ if File.exist?(certificate_path) && File.exist?(private_key_path) && ENV['recreate_certificate'].nil?
35
+ fingerprint = sha1_fingerprint(certificate_path)
36
+ installed_cert = identities.detect { |e| e[:fingerprint] == fingerprint }
37
+ if installed_cert
38
+ Utils.log("Info", "Using certificate '#{installed_cert[:name]}'.")
39
+ return installed_cert[:name]
40
+ else
41
+ # The certificate is not installed, so we install the cert and the key
42
+ import_file(private_key_path)
43
+ import_file(certificate_path)
44
+ name = common_name(certificate_path)
45
+ Utils.log("Info", "Using certificate '#{name}'.")
46
+ return name
47
+ end
48
+ end
49
+
50
+ # Create the folder to store the certs
51
+ FileUtils.mkdir_p(File.expand_path('./provisioning'))
52
+
53
+ # Make sure a client is created and logged in
54
+ client
55
+
56
+ # All the certificates for the specified type
57
+ user_certificates = certificates
58
+
59
+ # Lets see if any of the user certificates is in the keychain
60
+ installed_certificate = nil
61
+ if !certificates.empty?
62
+ installed_certs_sha1 = identities.map { |e| e[:fingerprint] }
63
+ installed_certificate = certificates.detect do |certificate|
64
+ sha1 = OpenSSL::Digest::SHA1.new(certificate.motionprovisioning_certContent || certificate.download_raw)
65
+ installed_certs_sha1.include?(sha1.to_s.upcase)
66
+ end
67
+ end
68
+
69
+ # There are no certificates in the server so we create a new one
70
+ if user_certificates.empty?
71
+ Utils.log("Warning", "Couldn't find any existing certificates... creating a new one.")
72
+ if certificate = create_certificate
73
+ return common_name(certificate)
74
+ else
75
+ Utils.log("Error", "Something went wrong when trying to create a new certificate.")
76
+ abort
77
+ end
78
+ # There are certificates in the server, but none is installed locally. Revoke all and create a new one.
79
+ elsif installed_certificate.nil?
80
+ Utils.log("Error", "None of the available certificates (#{user_certificates.count}) is installed on the local machine. Revoking...")
81
+
82
+ # For distribution, ask before revoking
83
+ if self.type == :distribution && !$test_mode
84
+ answer = Utils.ask("Info", "There are #{user_certificates.count} distribution certificates in your account, but none installed locally.\n" \
85
+ "Before revoking and creating a new one, ask other team members who might have them installed to share them with you.\n" \
86
+ "Do you want to continue revoking the certificates? (Y/n):")
87
+ abort if answer.downcase != "y"
88
+ end
89
+
90
+ # Revoke all and create new one
91
+ if MotionProvisioning.free
92
+ user_certificates.each do |certificate|
93
+ client.revoke_development_certificate(certificate.motionprovisioning_serialNumber)
94
+ end
95
+ else
96
+ user_certificates.each(&:revoke!)
97
+ end
98
+
99
+ if certificate = create_certificate
100
+ return common_name(certificate)
101
+ else
102
+ Utils.log("Error", "Something went wrong when trying to create a new certificate...")
103
+ abort
104
+ end
105
+ # There are certificates on the server, and one of them is installed locally.
106
+ else
107
+ path = store_certificate_raw(installed_certificate.motionprovisioning_certContent ||installed_certificate.download_raw)
108
+ private_key_path = File.expand_path(File.join(output_path, "#{installed_certificate.id}.p12"))
109
+
110
+ # This certificate is installed on the local machine
111
+ Utils.log("Info", "Using certificate '#{installed_certificate.name}'.")
112
+
113
+ return common_name(path)
114
+ end
115
+ end
116
+
117
+ # All certificates of this type
118
+ def certificates
119
+ @certificates ||= begin
120
+ if MotionProvisioning.free
121
+ client.development_certificates(mac: mac?).map do |cert|
122
+ certificate = Spaceship::Portal::Certificate.factory(cert)
123
+ certificate.motionprovisioning_certContent = cert['certContent'].read
124
+ certificate.motionprovisioning_serialNumber = cert['serialNumber']
125
+ certificate
126
+ end
127
+ else
128
+ certificates = certificate_type.all
129
+ # Filter out development certificates belonging to other team members
130
+ if self.type == :development
131
+ user_id = MotionProvisioning.team['currentTeamMember']['teamMemberId']
132
+ certificates.select! { |c| c.owner_id == user_id }
133
+ end
134
+ certificates
135
+ end
136
+ end
137
+ end
138
+
139
+ # The kind of certificate we're interested in
140
+ def certificate_type
141
+ cert_type = nil
142
+ case platform
143
+ when :ios, :tvos
144
+ cert_type = Spaceship.certificate.production
145
+ cert_type = Spaceship.certificate.in_house if Spaceship.client.in_house?
146
+ cert_type = Spaceship.certificate.development if self.type == :development
147
+ when :mac
148
+ cert_type = Spaceship.certificate.mac_development
149
+ cert_type = Spaceship.certificate.mac_app_distribution if self.type == :distribution
150
+ cert_type = Spaceship.certificate.developer_i_d_application if self.type == :developer_id
151
+ end
152
+ cert_type
153
+ end
154
+
155
+ def create_certificate_signing_request
156
+ $motion_provisioninig_csr || Spaceship.certificate.create_certificate_signing_request
157
+ end
158
+
159
+ def create_certificate
160
+ # Create a new certificate signing request
161
+ csr, pkey = create_certificate_signing_request
162
+
163
+ # Store all that onto the filesystem
164
+ request_path = File.expand_path(File.join(self.output_path, "#{platform}_#{type}.certSigningRequest"))
165
+ File.write(request_path, csr.to_pem)
166
+
167
+ private_key_path = File.expand_path(File.join(self.output_path, "#{platform}_#{type}_private_key.p12"))
168
+ File.write(private_key_path, pkey.export)
169
+
170
+ # Use the signing request to create a new distribution certificate
171
+ begin
172
+ certificate = nil
173
+ certificate_attrs = nil
174
+ if MotionProvisioning.free
175
+ certificate_attrs = client.create_development_certificate(csr.to_pem)
176
+ # Fetch the certificate again because the response does not contain
177
+ # the certContent key
178
+ certificate_attrs = client.development_certificates(mac: mac?).detect do |cert|
179
+ cert['certificateId'] == certificate_attrs['certificateId']
180
+ end
181
+ certificate_attrs['certificateTypeDisplayId'] = certificate_attrs['certificateType']['certificateTypeDisplayId']
182
+ certificate = Spaceship::Portal::Certificate.factory(certificate_attrs)
183
+ certificate.motionprovisioning_certContent = certificate_attrs['certContent'].read
184
+ certificate
185
+ else
186
+ certificate = certificate_type.create!(csr: csr)
187
+ end
188
+ rescue => ex
189
+ if ex.to_s.include?("You already have a current")
190
+ FileUtils.rm(private_key_path)
191
+ FileUtils.rm(request_path)
192
+ Utils.log("Error", "Could not create another certificate, reached the maximum number of available certificates. Manually revoke certificates in the Developer Portal.")
193
+ abort
194
+ end
195
+ raise ex
196
+ end
197
+ Utils.log("Info", "Successfully created certificate.")
198
+
199
+ cert_path = store_certificate_raw(certificate.motionprovisioning_certContent || certificate.download_raw)
200
+
201
+ # Import all the things into the Keychain
202
+ import_file(private_key_path)
203
+ import_file(cert_path)
204
+ Utils.log("Info", "Successfully installed certificate.")
205
+
206
+ if self.type == :distribution
207
+ Utils.log("Warning", "You have just created a distribution certificate. These certificates must be shared with other team members by sending them the private key (.p12) and certificate (.cer) files in your /provisioning folder and install them in the keychain.")
208
+ else
209
+ Utils.log("Warning", "You have just created a development certificate. If you want to use this certificate on another machine, transfer the private key (.p12) and certificate (.cer) files in your /provisioning folder and install them in the keychain.")
210
+ end
211
+ Utils.ask("Info", "Press any key to continue...") if !$test_mode
212
+
213
+ return cert_path
214
+ end
215
+
216
+ def store_certificate_raw(raw_data)
217
+ path = File.expand_path(File.join(self.output_path, "#{platform}_#{type}_certificate.cer"))
218
+ File.write(path, raw_data)
219
+ path
220
+ end
221
+
222
+ def available_identities
223
+ ids = []
224
+ keychain = $keychain || "#{Dir.home}/Library/Keychains/login.keychain"
225
+ available = `security find-identity -v -p codesigning #{keychain}`
226
+ available.split("\n").each do |current|
227
+ next if current.include? "REVOKED"
228
+ begin
229
+ id = current.match(/.*\) (.*) \"(.*)\"/)
230
+ ids << {
231
+ fingerprint: id[1],
232
+ name: id[2]
233
+ }
234
+ rescue
235
+ # the last line does not match
236
+ end
237
+ end
238
+ ids
239
+ end
240
+
241
+ def sha1_fingerprint(path)
242
+ result = `openssl x509 -in "#{path}" -inform der -noout -sha1 -fingerprint`
243
+ begin
244
+ result = result.match(/SHA1 Fingerprint=(.*)/)[1]
245
+ result.delete!(':')
246
+ return result
247
+ rescue
248
+ puts result
249
+ Utils.log("Error", "Error parsing certificate '#{path}'")
250
+ end
251
+ end
252
+
253
+ def common_name(path)
254
+ result = `openssl x509 -in "#{path}" -inform der -noout -sha1 -subject`
255
+ begin
256
+ return result.match(/\/CN=(.*?)\//)[1]
257
+ rescue
258
+ puts result
259
+ Utils.log("Error", "Error parsing certificate '#{path}'")
260
+ end
261
+ end
262
+
263
+ def import_file(path)
264
+ unless File.exist?(path)
265
+ Utils.log("Error", "Could not find file '#{path}'")
266
+ abort
267
+ end
268
+
269
+ keychain = $keychain || "#{Dir.home}/Library/Keychains/login.keychain"
270
+
271
+ command = "security import #{path.shellescape} -k '#{keychain}' -P ''"
272
+ command << " -T /usr/bin/codesign"
273
+ command << " -T /usr/bin/security"
274
+
275
+ `#{command} 2>&1`
276
+ end
277
+ end
278
+ end