motion-provisioning 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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