/* * Copyright 2015-present Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.buck.apple; import com.dd.plist.NSArray; import com.dd.plist.NSObject; import com.facebook.buck.log.Logger; import com.facebook.buck.model.Pair; import com.facebook.buck.rules.RuleKeyAppendable; import com.facebook.buck.rules.RuleKeyObjectSink; import com.facebook.buck.util.ProcessExecutor; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.hash.HashCode; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Map.Entry; import java.util.Optional; import javax.annotation.Nullable; /** A collection of provisioning profiles. */ public class ProvisioningProfileStore implements RuleKeyAppendable { public static final Optional<ImmutableMap<String, NSObject>> MATCH_ANY_ENTITLEMENT = Optional.empty(); public static final Optional<ImmutableList<CodeSignIdentity>> MATCH_ANY_IDENTITY = Optional.empty(); public static final ImmutableList<String> DEFAULT_READ_COMMAND = ImmutableList.of("openssl", "smime", "-inform", "der", "-verify", "-noverify", "-in"); private static final Logger LOG = Logger.get(ProvisioningProfileStore.class); private final Supplier<ImmutableList<ProvisioningProfileMetadata>> provisioningProfilesSupplier; private static final ImmutableSet<String> FORCE_INCLUDE_ENTITLEMENTS = ImmutableSet.of( "keychain-access-groups", "application-identifier", "com.apple.developer.associated-domains", "com.apple.developer.icloud-container-development-container-identifiers", "com.apple.developer.icloud-container-environment", "com.apple.developer.icloud-container-identifiers", "com.apple.developer.icloud-services", "com.apple.developer.ubiquity-container-identifiers", "com.apple.developer.ubiquity-kvstore-identifier"); private ProvisioningProfileStore( Supplier<ImmutableList<ProvisioningProfileMetadata>> provisioningProfilesSupplier) { this.provisioningProfilesSupplier = provisioningProfilesSupplier; } public ImmutableList<ProvisioningProfileMetadata> getProvisioningProfiles() { return provisioningProfilesSupplier.get(); } public Optional<ProvisioningProfileMetadata> getProvisioningProfileByUUID( String provisioningProfileUUID) { for (ProvisioningProfileMetadata profile : getProvisioningProfiles()) { if (profile.getUUID().equals(provisioningProfileUUID)) { return Optional.of(profile); } } return Optional.empty(); } private static boolean matchesOrArrayIsSubsetOf(@Nullable NSObject lhs, @Nullable NSObject rhs) { if (lhs == null) { return (rhs == null); } if (lhs instanceof NSArray && rhs instanceof NSArray) { List<NSObject> lhsList = Arrays.asList(((NSArray) lhs).getArray()); List<NSObject> rhsList = Arrays.asList(((NSArray) rhs).getArray()); return rhsList.containsAll(lhsList); } return lhs.equals(rhs); } // If multiple valid ones, find the one which matches the most specifically. I.e., // XXXXXXXXXX.com.example.* will match over XXXXXXXXXX.* for com.example.TestApp public Optional<ProvisioningProfileMetadata> getBestProvisioningProfile( String bundleID, ApplePlatform platform, Optional<ImmutableMap<String, NSObject>> entitlements, Optional<? extends Iterable<CodeSignIdentity>> identities) { final Optional<String> prefix; if (entitlements.isPresent()) { prefix = ProvisioningProfileMetadata.prefixFromEntitlements(entitlements.get()); } else { prefix = Optional.empty(); } int bestMatchLength = -1; Optional<ProvisioningProfileMetadata> bestMatch = Optional.empty(); for (ProvisioningProfileMetadata profile : getProvisioningProfiles()) { if (profile.getExpirationDate().after(new Date())) { Pair<String, String> appID = profile.getAppID(); LOG.debug("Looking at provisioning profile " + profile.getUUID() + "," + appID.toString()); if (!prefix.isPresent() || prefix.get().equals(appID.getFirst())) { String profileBundleID = appID.getSecond(); boolean match; if (profileBundleID.endsWith("*")) { // Chop the ending * if wildcard. profileBundleID = profileBundleID.substring(0, profileBundleID.length() - 1); match = bundleID.startsWith(profileBundleID); } else { match = (bundleID.equals(profileBundleID)); } if (!match) { LOG.debug( "Ignoring non-matching ID for profile " + profile.getUUID() + ". Expected: " + profileBundleID + ", actual: " + bundleID); continue; } Optional<String> platformName = platform.getProvisioningProfileName(); if (platformName.isPresent() && !profile.getPlatforms().contains(platformName.get())) { LOG.debug( "Ignoring incompatible platform " + platformName.get() + " for profile " + profile.getUUID()); continue; } // Match against other keys of the entitlements. Otherwise, we could potentially select // a profile that doesn't have all the needed entitlements, causing a error when // installing to device. // // For example: get-task-allow, aps-environment, etc. if (entitlements.isPresent()) { ImmutableMap<String, NSObject> entitlementsDict = entitlements.get(); ImmutableMap<String, NSObject> profileEntitlements = profile.getEntitlements(); for (Entry<String, NSObject> entry : entitlementsDict.entrySet()) { NSObject profileEntitlement = profileEntitlements.get(entry.getKey()); if (!(FORCE_INCLUDE_ENTITLEMENTS.contains(entry.getKey()) || matchesOrArrayIsSubsetOf(entry.getValue(), profileEntitlement))) { match = false; LOG.debug( "Ignoring profile " + profile.getUUID() + " with mismatched entitlement " + entry.getKey() + "; value is " + profileEntitlement + " but expected " + entry.getValue()); break; } } } // Reject any certificate which we know we can't sign with the supplied identities. ImmutableSet<HashCode> validFingerprints = profile.getDeveloperCertificateFingerprints(); if (match && identities.isPresent() && !validFingerprints.isEmpty()) { match = false; for (CodeSignIdentity identity : identities.get()) { Optional<HashCode> fingerprint = identity.getFingerprint(); if (fingerprint.isPresent() && validFingerprints.contains(fingerprint.get())) { match = true; break; } } if (!match) { LOG.debug( "Ignoring profile " + profile.getUUID() + " because it can't be signed with any valid identity in the current keychain."); continue; } } if (match && profileBundleID.length() > bestMatchLength) { bestMatchLength = profileBundleID.length(); bestMatch = Optional.of(profile); } } } else { LOG.debug("Ignoring expired profile " + profile.getUUID()); } } LOG.debug("Found provisioning profile " + bestMatch.toString()); return bestMatch; } // TODO(yiding): remove this once the precise provisioning profile can be determined. @Override public void appendToRuleKey(RuleKeyObjectSink sink) { sink.setReflectively("provisioning-profile-store", getProvisioningProfiles()); } public static ProvisioningProfileStore fromSearchPath( final ProcessExecutor executor, final ImmutableList<String> readCommand, final Path searchPath) { LOG.debug("Provisioning profile search path: " + searchPath); return new ProvisioningProfileStore( Suppliers.memoize( () -> { final ImmutableList.Builder<ProvisioningProfileMetadata> profilesBuilder = ImmutableList.builder(); try { Files.walkFileTree( searchPath.toAbsolutePath(), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (file.toString().endsWith(".mobileprovision")) { try { ProvisioningProfileMetadata profile = ProvisioningProfileMetadata.fromProvisioningProfilePath( executor, readCommand, file); profilesBuilder.add(profile); } catch (IOException | IllegalArgumentException e) { LOG.error(e, "Ignoring invalid or malformed .mobileprovision file"); } catch (InterruptedException e) { throw new IOException(e); } } return FileVisitResult.CONTINUE; } }); } catch (IOException e) { if (e.getCause() instanceof InterruptedException) { LOG.error(e, "Interrupted while searching for mobileprovision files"); } else { LOG.error(e, "Error while searching for mobileprovision files"); } } return profilesBuilder.build(); })); } public static ProvisioningProfileStore fromProvisioningProfiles( Iterable<ProvisioningProfileMetadata> profiles) { return new ProvisioningProfileStore(Suppliers.ofInstance(ImmutableList.copyOf(profiles))); } }