浏览代码

App Distribution: Implement code hash generation and verification (#5432)

Add initial code hash generation implementation

Generate a code hash by getting the architecture and uuids for the app, sorting the list lexicographically by architecture, concatenating the uuids and generate a hash using SHA1 (for legacy purposes). We then use the code hash to verify against a hash returned from the server to determine whether the current running version is the latest version.
Jeremy Durham 6 年之前
父节点
当前提交
7757c73670

+ 2 - 1
FirebaseAppDistribution.podspec

@@ -36,7 +36,8 @@ iOS SDK for App Distribution for Firebase.
   }
 
   s.test_spec 'unit' do |unit_tests|
-   unit_tests.source_files = 'FirebaseAppDistribution/Tests/Unit*/*.[mh]'
+    unit_tests.source_files = 'FirebaseAppDistribution/Tests/Unit*/*.[mh]'
+    unit_tests.resources = 'FirebaseAppDistribution/Tests/Unit/Resources/*'
   end
 
   # end

+ 20 - 3
FirebaseAppDistribution/Sources/FIRAppDistribution.m

@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #import "FIRAppDistribution+Private.h"
+#import "FIRAppDistributionMachO+Private.h"
 #import "FIRAppDistributionRelease+Private.h"
 
 #import <FirebaseCore/FIRAppInternal.h>
@@ -219,10 +220,26 @@ NSString *const kAppDistroLibraryName = @"fire-fad";
 
 - (void)handleReleasesAPIResponseWithData:data
                                completion:(FIRAppDistributionUpdateCheckCompletion)completion {
-  // TODO Implement parsing of releases API response
-  completion(nil, nil);
+  NSError *error = nil;
+  NSDictionary *object = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
+
+  NSArray *releaseList = [object objectForKey:@"releases"];
+  for (NSDictionary *releaseDict in releaseList) {
+    if (![[releaseDict objectForKey:@"latest"] boolValue]) continue;
+
+    NSString *codeHash = [releaseDict objectForKey:@"codeHash"];
+    FIRAppDistributionMachO *machO =
+        [[FIRAppDistributionMachO alloc] initWithPath:[[NSBundle mainBundle] executablePath]];
+
+    if (![codeHash isEqualToString:machO.codeHash]) {
+      FIRAppDistributionRelease *release =
+          [[FIRAppDistributionRelease alloc] initWithDictionary:releaseDict];
+      dispatch_async(dispatch_get_main_queue(), ^{
+        completion(release, nil);
+      });
+    }
+  }
 }
-
 - (void)checkForUpdateWithCompletion:(FIRAppDistributionUpdateCheckCompletion)completion {
   if (self.isTesterSignedIn) {
     [self fetchReleases:completion];

+ 157 - 0
FirebaseAppDistribution/Sources/FIRAppDistributionMachO.m

@@ -0,0 +1,157 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import <CommonCrypto/CommonHMAC.h>
+#include <mach-o/arch.h>
+#import <mach-o/fat.h>
+#import <mach-o/loader.h>
+#import "FIRAppDistributionMachO+Private.h"
+#import "FIRAppDistributionMachOSlice+Private.h"
+
+@interface FIRAppDistributionMachO ()
+@property(nonatomic, copy) NSFileHandle* file;
+@property(nonatomic, copy) NSMutableArray* slices;
+@end
+
+@implementation FIRAppDistributionMachO
+
+- (instancetype)initWithPath:(NSString*)path {
+  self = [super init];
+
+  if (self) {
+    _file = [NSFileHandle fileHandleForReadingAtPath:path];
+    _slices = [NSMutableArray new];
+    [self extractSlices];
+  }
+
+  return self;
+}
+
+- (void)extractSlices {
+  uint32_t magicValue;
+
+  struct fat_header fheader;
+  NSData* data = [self.file readDataOfLength:sizeof(fheader)];
+  [data getBytes:&fheader length:sizeof(fheader)];
+
+  magicValue = CFSwapInt32BigToHost(fheader.magic);
+
+  // Check to see if the file is a FAT binary (has multiple architectures)
+  if (magicValue == FAT_MAGIC) {
+    uint32_t archCount = CFSwapInt32BigToHost(fheader.nfat_arch);
+    NSUInteger archOffsets[archCount];
+
+    // Gather the offsets for each architecture
+    for (uint32_t i = 0; i < archCount; i++) {
+      struct fat_arch arch;
+
+      data = [self.file readDataOfLength:sizeof(arch)];
+      [data getBytes:&arch length:sizeof(arch)];
+
+      archOffsets[i] = CFSwapInt32BigToHost(arch.offset);
+    }
+
+    // Iterate the slices based on the offsets we extracted above
+    for (uint32_t i = 0; i < archCount; i++) {
+      FIRAppDistributionMachOSlice* slice = [self extractSliceAtOffset:archOffsets[i]];
+
+      if (slice) [_slices addObject:slice];
+    }
+  } else {
+    // If the binary is not FAT, we're dealing with a single architecture
+    FIRAppDistributionMachOSlice* slice = [self extractSliceAtOffset:0];
+
+    if (slice) [_slices addObject:slice];
+  }
+}
+
+- (FIRAppDistributionMachOSlice*)extractSliceAtOffset:(NSUInteger)offset {
+  [self.file seekToFileOffset:offset];
+
+  struct mach_header header;
+
+  NSData* data = [self.file readDataOfLength:sizeof(header)];
+  [data getBytes:&header length:sizeof(header)];
+  uint32_t magicValue = CFSwapInt32BigToHost(header.magic);
+
+  // If we didn't read a valid magic value, something is wrong
+  // Bail out immediately to prevent reading random data
+  if (magicValue != MH_CIGAM_64 && magicValue != MH_CIGAM) {
+    return nil;
+  }
+
+  // If the binary is 64-bit, read the reserved bit and discard it
+  if (magicValue == MH_CIGAM_64) {
+    uint32_t reserved;
+
+    [self.file readDataOfLength:sizeof(reserved)];
+  }
+
+  for (uint32_t i = 0; i < header.ncmds; i++) {
+    struct load_command lc;
+
+    data = [self.file readDataOfLength:sizeof(lc)];
+    [data getBytes:&lc length:sizeof(lc)];
+
+    if (lc.cmd != LC_UUID) {
+      // Move to the next load command
+      [self.file seekToFileOffset:[self.file offsetInFile] + lc.cmdsize - sizeof(lc)];
+      continue;
+    }
+
+    // Re-read the load command, but this time as a UUID command
+    // so we can easily fetch the UUID
+    [self.file seekToFileOffset:[self.file offsetInFile] - sizeof(lc)];
+    struct uuid_command uc;
+    data = [self.file readDataOfLength:sizeof(uc)];
+    [data getBytes:&uc length:sizeof(uc)];
+
+    NSUUID* uuid = [[NSUUID alloc] initWithUUIDBytes:uc.uuid];
+    const NXArchInfo* arch = NXGetArchInfoFromCpuType(header.cputype, header.cpusubtype);
+
+    return [[FIRAppDistributionMachOSlice alloc] initWithArch:arch uuid:uuid];
+  }
+
+  // If we got here, something is wrong. We iterated the load commands
+  // and didn't find a UUID load command. This means the binary is corrupt.
+  return nil;
+}
+
+- (NSString*)codeHash {
+  NSMutableString* prehashedString = [NSMutableString new];
+
+  NSArray* sortedSlices =
+      [_slices sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
+        return [[obj1 architectureName] compare:[obj2 architectureName]];
+      }];
+
+  for (FIRAppDistributionMachOSlice* slice in sortedSlices) {
+    [prehashedString appendString:[slice uuidString]];
+  }
+
+  return [self sha1:prehashedString];
+}
+
+- (NSString*)sha1:(NSString*)prehashedString {
+  NSData* data = [prehashedString dataUsingEncoding:NSUTF8StringEncoding];
+  uint8_t digest[CC_SHA1_DIGEST_LENGTH];
+  CC_SHA1(data.bytes, (int)data.length, digest);
+  NSMutableString* hashedString = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2];
+  for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) [hashedString appendFormat:@"%02x", digest[i]];
+
+  return hashedString;
+}
+@end

+ 44 - 0
FirebaseAppDistribution/Sources/FIRAppDistributionMachOSlice.m

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import <mach-o/fat.h>
+#import <mach-o/loader.h>
+#import "FIRAppDistributionMachOSlice+Private.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAppDistributionMachOSlice
+
+- (instancetype)initWithArch:(const NXArchInfo *)arch uuid:(NSUUID *)uuid {
+  self = [super init];
+  if (!self) return nil;
+
+  _arch = arch;
+  _uuid = uuid;
+  return self;
+}
+
+- (NSString *)architectureName {
+  return [NSString stringWithUTF8String:_arch->name];
+}
+
+- (NSString *)uuidString {
+  return [[[_uuid UUIDString] lowercaseString] stringByReplacingOccurrencesOfString:@"-"
+                                                                         withString:@""];
+}
+@end
+
+NS_ASSUME_NONNULL_END

+ 29 - 0
FirebaseAppDistribution/Sources/Private/FIRAppDistributionMachO+Private.h

@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRAppDistributionMachOSlice+Private.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAppDistributionMachO : NSObject
+
+- (instancetype)initWithPath:(NSString *)path;
+- (NSString *)codeHash;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 34 - 0
FirebaseAppDistribution/Sources/Private/FIRAppDistributionMachOSlice+Private.h

@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import <Foundation/Foundation.h>
+#import <mach-o/arch.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAppDistributionMachOSlice : NSObject
+
+@property(nonatomic, assign) const NXArchInfo* arch;
+@property(nonatomic, copy) NSUUID* uuid;
+
+- (instancetype)initWithArch:(const NXArchInfo*)arch uuid:(NSUUID*)uuid;
+
+- (NSString*)architectureName;
+- (NSString*)uuidString;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 52 - 0
FirebaseAppDistribution/Tests/Unit/FIRAppDistributionMachOTests.m

@@ -0,0 +1,52 @@
+// Copyright 2020 Google
+//
+// 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.
+
+#import <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+
+#import <FirebaseCore/FIRAppInternal.h>
+#import "FIRAppDistributionMachO+Private.h"
+
+@interface FIRAppDistributionMachOTests : XCTestCase
+@end
+
+@implementation FIRAppDistributionMachOTests
+
+- (NSString*)resourcePath:(NSString*)path {
+  NSBundle* bundle = [NSBundle bundleForClass:[self class]];
+  NSString* resourcePath = [bundle resourcePath];
+
+  return [resourcePath stringByAppendingPathComponent:path];
+}
+
+- (void)testCodeHashForSingleArchIntelSimulator {
+  FIRAppDistributionMachO* macho;
+  macho = [[FIRAppDistributionMachO alloc] initWithPath:[self resourcePath:@"x86_64-executable"]];
+  XCTAssertEqualObjects([macho codeHash], @"442eb836efe1f56bf8a65b2a0a78b2f8d3e792e7");
+}
+
+- (void)testCodeHashForMultipleArch {
+  FIRAppDistributionMachO* macho;
+  macho =
+      [[FIRAppDistributionMachO alloc] initWithPath:[self resourcePath:@"armv7-armv7s-executable"]];
+  XCTAssertEqualObjects([macho codeHash], @"80cc0ec0af8a0169831abcc73177eb2b57990bc0");
+}
+
+- (void)testCodeHashForNonExistentBinary {
+  FIRAppDistributionMachO* macho;
+  macho = [[FIRAppDistributionMachO alloc] initWithPath:[self resourcePath:@"missing-file"]];
+  XCTAssertEqualObjects([macho codeHash], @"da39a3ee5e6b4b0d3255bfef95601890afd80709");
+}
+
+@end

二进制
FirebaseAppDistribution/Tests/Unit/Resources/armv7-armv7s-executable


二进制
FirebaseAppDistribution/Tests/Unit/Resources/x86_64-executable