diff --git a/app/lib/import_pass/import_page.dart b/app/lib/import_pass/import_page.dart index 4e385d1..099233a 100644 --- a/app/lib/import_pass/import_page.dart +++ b/app/lib/import_pass/import_page.dart @@ -22,7 +22,7 @@ class PkPassImportSource { final String? contentResolverPath; final String? filePath; - final List? bytes; + final Uint8List? bytes; Future getPass() async { if (contentResolverPath != null) { diff --git a/app/pubspec.lock b/app/pubspec.lock index 6928c1b..2799aa0 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -46,6 +46,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "2ca377ad4d677bbadca278e0ba4da4e057b80a10b927bfc8f7d8bda8fe2ceb75" + url: "https://pub.dev" + source: hosted + version: "1.5.4" async: dependency: transitive description: @@ -262,6 +270,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0+11" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" fake_async: dependency: transitive description: @@ -712,14 +728,14 @@ packages: path: "../passkit" relative: true source: path - version: "0.0.5" + version: "0.0.6" passkit_ui: dependency: "direct main" description: path: "../passkit_ui" relative: true source: path - version: "0.0.4" + version: "0.0.5" path: dependency: "direct main" description: diff --git a/apple_passkit/example/pubspec.lock b/apple_passkit/example/pubspec.lock index 45a6e09..edbb800 100644 --- a/apple_passkit/example/pubspec.lock +++ b/apple_passkit/example/pubspec.lock @@ -258,10 +258,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" webdriver: dependency: transitive description: @@ -271,5 +271,5 @@ packages: source: hosted version: "3.0.3" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/passkit/CHANGELOG.md b/passkit/CHANGELOG.md index 95c37d1..24c1e2b 100644 --- a/passkit/CHANGELOG.md +++ b/passkit/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.0.7 + +- The library is now able to create properly signed `pkpass` files that work with Apple Wallet. + Follow the guide [here](https://github.com/ueman/passkit/blob/master/passkit/SIGNING.md) to learn more. +- Pretty much every use of `List` has been changed to `Uint8List`. This is potentially breaking. + ## 0.0.6 - Add ability to create PkPass signature via OpenSSL or other command line tools diff --git a/passkit/README.md b/passkit/README.md index 0dd8064..9a691b8 100644 --- a/passkit/README.md +++ b/passkit/README.md @@ -69,19 +69,18 @@ void main() { > [!WARNING] > This is experimental. -> The resulting file not yet get accepted by Apple Wallet due to missing support for writing the pass signature. -> -> If you know how to create the PkPass signature it in pure Dart code, please add an example -> [here](https://github.com/ueman/passkit/issues/74) or create -> a PR for [this](https://github.com/ueman/passkit/issues/74) issue. +> Follow the guide [here](https://github.com/ueman/passkit/blob/master/passkit/SIGNING.md) to learn more. ```dart import 'package:passkit/passkit.dart'; void main() { - final pkPass = PkPass(...); - final pkPassFile = pass.write(); - await File('path/to/pass.pkpass').writeAsBytes(pkPassFile); +final pass = PkPass(...); +final binaryData pass.write( + certificatePem: File('pass_certificate.pem').readAsStringSync(), + privateKeyPem: File('private_key.pem').readAsStringSync(), +); +File('pass.pkpass').writeAsBytesSync(binaryData); } ``` @@ -136,7 +135,6 @@ Please feel encouraged to create PRs for the following features. - PassKit Web Service: This functionality is existing, but might not work. Please file an issue or create a PR with a fix for bugs you encounter. - Push Notification update registration is only working on iOS due to this whole specification being an Apple thingy. - Localization: Existing, but still inconvenient to use. There might be issues due to localizations being UTF-16 formatted, but the library currently uses UTF-8 to read localizations. -- [Passkit creation](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/Creating.html#//apple_ref/doc/uid/TP40012195-CH4-SW54) is partially supported. See note further above. ## Bugs and parsing issues diff --git a/passkit/SIGNING.md b/passkit/SIGNING.md new file mode 100644 index 0000000..67fa9d4 --- /dev/null +++ b/passkit/SIGNING.md @@ -0,0 +1,42 @@ +# Signing a PkPass file + +> [!WARNING] +> THIS SHOULD NOT BE USED WITHIN AN APP. +> IT SHOULD ONLY BE USED SERVER SIDE, OTHERWISE YOU RISK EXPOSING YOUR CERTIFICATE AND PRIVATE KEY. + +This guide assumes you're working on a macOS system. + +## Step 1: Get a Pass ID and Pass Certificate from the Apple Developer Portal + +Follow for example this guide: https://www.kodeco.com/2855-beginning-passbook-in-ios-6-part-1-2?page=3#toc-anchor-007 +At the end you should have a certificate and private key in Keychain Access. + +## Step 2: Create `.pem` files + +This is the important step! + +Export your certificate and private key from Keychain Access as `Certificate.p12`. It will ask you to set a password. + +You should replace `` with your password in the following commands. +(The `--legacy` part at the end of the following command may or may not be needed depending on your openssl version) + +Create the certificate `.pem` file. +```shell +openssl pkcs12 -in Certificates.p12 -clcerts -nokeys -out pass_certificate.pem -passin pass: --legacy +``` + +Create the private key `.pem` file. Unfortunately, it's not yet possible to use a password protected private key file. +```shell +openssl pkcs12 -in Certificates.p12 -out private_key.pem -nocerts -nodes -passin pass: --legacy +``` + +Then you can the generated file to sign PkPass files: + +```dart +final pass = PkPass(...); +final binaryData pass.write( + certificatePem: File('pass_certificate.pem').readAsStringSync(), + privateKeyPem: File('private_key.pem').readAsStringSync(), +); +File('pass.pkpass').writeAsBytesSync(binaryData); // The file ending is important +``` diff --git a/passkit/lib/src/apple_wwdr_certificate.dart b/passkit/lib/src/apple_wwdr_certificate.dart index 129d8e4..10f8eb8 100644 --- a/passkit/lib/src/apple_wwdr_certificate.dart +++ b/passkit/lib/src/apple_wwdr_certificate.dart @@ -12,8 +12,8 @@ X509 get wwdrG4 => /// More info at: /// https://developer.apple.com/help/account/reference/wwdr-intermediate-certificates/ /// https://www.apple.com/certificateauthority/ -// ignore: constant_identifier_names -const worldwide_Developer_Relations_G4 = [ +// ignore: constant_identifier_names, non_constant_identifier_names +final worldwide_Developer_Relations_G4 = Uint8List.fromList([ 48, 130, 4, @@ -1127,4 +1127,4 @@ const worldwide_Developer_Relations_G4 = [ 207, 242, 159, -]; +]); diff --git a/passkit/lib/src/archive_extensions.dart b/passkit/lib/src/archive_extensions.dart index 2ff0e6a..a69f7cd 100644 --- a/passkit/lib/src/archive_extensions.dart +++ b/passkit/lib/src/archive_extensions.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:crypto/crypto.dart'; +import 'package:passkit/src/archive_file_extension.dart'; import 'package:passkit/src/pkpass/exceptions.dart'; import 'package:passkit/src/pkpass/pk_pass_image.dart'; import 'package:passkit/src/strings_parser/naive_strings_file_parser.dart'; @@ -15,13 +16,8 @@ import 'package:passkit/src/strings_parser/naive_strings_file_parser.dart'; final _utf8JsonDecoder = const Utf8Decoder().fuse(const JsonDecoder()); extension ArchiveX on Archive { - List? findBytesForFile(String fileName) => - findFile(fileName)?.content as List?; - - Uint8List? findUint8ListForFile(String fileName) { - final data = findBytesForFile(fileName); - return data == null ? null : Uint8List.fromList(data); - } + Uint8List? findBytesForFile(String fileName) => + findFile(fileName)?.binaryContent; Map? findFileAndReadAsJson(String fileName) { final bytes = findBytesForFile(fileName); @@ -33,9 +29,9 @@ extension ArchiveX on Archive { PkImage? loadImage(String name) { return PkImage.fromImages( - image1: findUint8ListForFile('$name.png'), - image2: findUint8ListForFile('$name@2.png'), - image3: findUint8ListForFile('$name@3.png'), + image1: findBytesForFile('$name.png'), + image2: findBytesForFile('$name@2.png'), + image3: findBytesForFile('$name@3.png'), ); } @@ -52,8 +48,7 @@ extension ArchiveX on Archive { for (final languageFile in translationFiles) { final language = languageFile.name.split('.').first; - languageData[language] = - parseStringsFile(languageFile.content as List); + languageData[language] = parseStringsFile(languageFile.binaryContent); } return languageData; } @@ -88,7 +83,7 @@ extension ArchiveX on Archive { for (final file in filesWithoutSignatureAndManifest) { final checksumInManifest = manifest[file.name] as String?; - final digest = sha1.convert(file.content as List); + final digest = sha1.convert(file.binaryContent); if (checksumInManifest != digest.toString()) { throw ChecksumMismatchException(file.name); } diff --git a/passkit/lib/src/archive_file_extension.dart b/passkit/lib/src/archive_file_extension.dart new file mode 100644 index 0000000..38d0a8d --- /dev/null +++ b/passkit/lib/src/archive_file_extension.dart @@ -0,0 +1,7 @@ +import 'dart:typed_data'; + +import 'package:archive/archive.dart'; + +extension ArchiveFileX on ArchiveFile { + Uint8List get binaryContent => Uint8List.fromList(content as List); +} diff --git a/passkit/lib/src/order/pk_order.dart b/passkit/lib/src/order/pk_order.dart index f43a648..79cd64f 100644 --- a/passkit/lib/src/order/pk_order.dart +++ b/passkit/lib/src/order/pk_order.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:passkit/src/archive_extensions.dart'; +import 'package:passkit/src/archive_file_extension.dart'; import 'package:passkit/src/pkpass/exceptions.dart'; import 'package:passkit/src/signature_verification.dart'; @@ -20,7 +21,7 @@ class PkOrder { /// verification and validation. // TODO(ueman): Provide an async method for this. static PkOrder fromBytes( - final List bytes, { + final Uint8List bytes, { bool skipChecksumVerification = false, bool skipSignatureVerification = false, }) { @@ -38,10 +39,8 @@ class PkOrder { if (skipSignatureVerification) { final manifestContent = - archive.findFile('manifest.json')!.content as List; - final signatureContent = Uint8List.fromList( - archive.findFile('signature')!.content as List, - ); + archive.findFile('manifest.json')!.binaryContent; + final signatureContent = archive.findFile('signature')!.binaryContent; verifySignature( signatureBytes: signatureContent, @@ -77,7 +76,7 @@ class PkOrder { final Map>? languageData; /// The bytes of this PkPass - final List sourceData; + final Uint8List sourceData; /// Indicates whether a webservices is available. bool get isWebServiceAvailable => order.webServiceURL != null; diff --git a/passkit/lib/src/pkpass/pass_data.dart b/passkit/lib/src/pkpass/pass_data.dart index 94b6957..1db69ca 100644 --- a/passkit/lib/src/pkpass/pass_data.dart +++ b/passkit/lib/src/pkpass/pass_data.dart @@ -1,3 +1,4 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first import 'package:csslib/parser.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:passkit/src/pkpass/barcode.dart'; @@ -266,4 +267,77 @@ class PassData { /// a pass and suggest related actions. @JsonKey(name: 'semantics') final Semantics? semantics; + + PassData copyWith({ + String? description, + int? formatVersion, + String? organizationName, + String? passTypeIdentifier, + String? serialNumber, + String? teamIdentifier, + String? appLaunchURL, + List? associatedStoreIdentifiers, + Map? userInfo, + DateTime? expirationDate, + bool? voided, + List? beacons, + List? locations, + num? maxDistance, + DateTime? relevantDate, + PassStructure? boardingPass, + PassStructure? coupon, + PassStructure? eventTicket, + PassStructure? generic, + PassStructure? storeCard, + Barcode? barcode, + List? barcodes, + Color? backgroundColor, + Color? foregroundColor, + String? groupingIdentifier, + Color? labelColor, + String? logoText, + bool? suppressStripShine, + bool? sharingProhibited, + String? authenticationToken, + Uri? webServiceURL, + Nfc? nfc, + Semantics? semantics, + }) { + return PassData( + description: description ?? this.description, + formatVersion: formatVersion ?? this.formatVersion, + organizationName: organizationName ?? this.organizationName, + passTypeIdentifier: passTypeIdentifier ?? this.passTypeIdentifier, + serialNumber: serialNumber ?? this.serialNumber, + teamIdentifier: teamIdentifier ?? this.teamIdentifier, + appLaunchURL: appLaunchURL ?? this.appLaunchURL, + associatedStoreIdentifiers: + associatedStoreIdentifiers ?? this.associatedStoreIdentifiers, + userInfo: userInfo ?? this.userInfo, + expirationDate: expirationDate ?? this.expirationDate, + voided: voided ?? this.voided, + beacons: beacons ?? this.beacons, + locations: locations ?? this.locations, + maxDistance: maxDistance ?? this.maxDistance, + relevantDate: relevantDate ?? this.relevantDate, + boardingPass: boardingPass ?? this.boardingPass, + coupon: coupon ?? this.coupon, + eventTicket: eventTicket ?? this.eventTicket, + generic: generic ?? this.generic, + storeCard: storeCard ?? this.storeCard, + barcode: barcode ?? this.barcode, + barcodes: barcodes ?? this.barcodes, + backgroundColor: backgroundColor ?? this.backgroundColor, + foregroundColor: foregroundColor ?? this.foregroundColor, + groupingIdentifier: groupingIdentifier ?? this.groupingIdentifier, + labelColor: labelColor ?? this.labelColor, + logoText: logoText ?? this.logoText, + suppressStripShine: suppressStripShine ?? this.suppressStripShine, + sharingProhibited: sharingProhibited ?? this.sharingProhibited, + authenticationToken: authenticationToken ?? this.authenticationToken, + webServiceURL: webServiceURL ?? this.webServiceURL, + nfc: nfc ?? this.nfc, + semantics: semantics ?? this.semantics, + ); + } } diff --git a/passkit/lib/src/pkpass/pkpass.dart b/passkit/lib/src/pkpass/pkpass.dart index 1860c41..e79c81e 100644 --- a/passkit/lib/src/pkpass/pkpass.dart +++ b/passkit/lib/src/pkpass/pkpass.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:crypto/crypto.dart'; import 'package:meta/meta.dart'; -import 'package:passkit/src/apple_wwdr_certificate.dart'; +import 'package:passkit/src/archive_file_extension.dart'; import 'package:passkit/src/pkpass/exceptions.dart'; import 'package:passkit/src/pkpass/pass_data.dart'; import 'package:passkit/src/pkpass/pass_type.dart'; @@ -12,23 +12,7 @@ import 'package:passkit/src/pkpass/personalization.dart'; import 'package:passkit/src/pkpass/pk_pass_image.dart'; import 'package:passkit/src/signature_verification.dart'; import 'package:passkit/src/strings_parser/naive_strings_file_parser.dart'; - -/// Follow [this](https://www.kodeco.com/2855-beginning-passbook-in-ios-6-part-1-2?page=4#toc-anchor-011) -/// tutorial for instructions on how to create the signature with OpenSSL. -/// -/// ```bash -/// /// openssl smime -binary -sign -certfile WWDR.pem -signer passcertificate.pem -inkey passkey.pem -in manifest.json -out signature -outform DER -passin pass:12345 -/// ``` -/// -/// [manifest] is the file you need to pass to OpenSSL as `manifest.json`. -/// [wwdrCertificate] is the file content you need to pass to OpenSSL as `WWDR.pem` -/// -/// If you know how to do it in pure Dart code, please add an example or create -/// a PR: https://github.com/ueman/passkit/issues/74 -typedef SignatureBuilder = Uint8List Function( - String manifest, - List wwdrCertificate, -); +import 'package:passkit/src/write_signature.dart'; /// Dart uses a special fast decoder when using a fused [Utf8Decoder] and [JsonDecoder]. /// This speeds up decoding. @@ -82,7 +66,7 @@ class PkPass { /// certificate. // TODO(any): Provide an async method for this. static PkPass fromBytes( - final List bytes, { + final Uint8List bytes, { bool skipChecksumVerification = false, bool skipSignatureVerification = false, }) { @@ -99,17 +83,17 @@ class PkPass { archive.checkSha1Checksums(manifest); if (!skipSignatureVerification) { final manifestContent = - archive.findFile('manifest.json')!.content as List; - final signatureContent = Uint8List.fromList( - archive.findFile('signature')!.content as List, - ); + archive.findFile('manifest.json')!.binaryContent; + final signatureContent = archive.findFile('signature')!.binaryContent; - verifySignature( + if (!verifySignature( signatureBytes: signatureContent, manifestBytes: manifestContent, teamIdentifier: passData.teamIdentifier, identifier: passData.passTypeIdentifier, - ); + )) { + throw Exception('validation failed'); + } } } @@ -148,7 +132,7 @@ class PkPass { // gracefully fall back to just parsing the PkPass file. // TODO(ueman): Provide an async method for this. static List passesFromBytes( - final List bytes, { + final Uint8List bytes, { bool skipChecksumVerification = false, bool skipSignatureVerification = false, }) { @@ -162,7 +146,7 @@ class PkPass { return pkPasses .map( (file) => fromBytes( - file.content as List, + file.binaryContent, skipChecksumVerification: skipChecksumVerification, skipSignatureVerification: skipSignatureVerification, ), @@ -243,7 +227,7 @@ class PkPass { final Map>? languageData; /// The bytes of this PkPass - final List sourceData; + final Uint8List sourceData; /// Indicates whether a webservices is available. bool get isWebServiceAvailable => pass.webServiceURL != null; @@ -253,17 +237,15 @@ class PkPass { /// /// When written to disk, the file should have an ending of `.pkpass`. /// - /// In order to sign the pkpass file, pass a [signatureBuilder]. - /// Follow [this](https://www.kodeco.com/2855-beginning-passbook-in-ios-6-part-1-2?page=4#toc-anchor-011) - /// tutorial for instructions on how to create the signature with OpenSSL. - /// ```bash - /// openssl smime -binary -sign -certfile WWDR.pem -signer passcertificate.pem -inkey passkey.pem -in manifest.json -out signature -outform DER -passin pass:12345 - /// ``` + /// [certificatePem] is the certificate to be used to sign the PkPass file. + /// + /// [privateKeyPem] is the private key PEM file. Right now, + /// it's only supported if it's not password protected. /// - /// The file that's created by OpenSSL should be returned via [signatureBuilder]. + /// Read more about signing [here](https://github.com/ueman/passkit/blob/master/passkit/SIGNING.md). /// - /// If you know how to do it in pure Dart code, please add an example or create - /// a PR: https://github.com/ueman/passkit/issues/74 + /// If either [certificatePem] or [privateKeyPem] is null, the resulting PkPass + /// will not be properly signed, but still generated. /// /// Remarks: /// - There's no support for verifying that the signature matches the PkPass @@ -272,7 +254,10 @@ class PkPass { /// - Image sizes aren't checked, which means it's possible to create passes /// that look odd and wrong in Apple wallet or [passkit_ui](https://pub.dev/packages/passkit_ui) @experimental - Uint8List? write({SignatureBuilder? signatureBuilder}) { + Uint8List? write({ + String? certificatePem, + String? privateKeyPem, + }) { final archive = Archive(); final encoder = JsonEncoder.withIndent(' '); @@ -301,7 +286,7 @@ class PkPass { final manifest = {}; for (final file in archive.files) { - manifest[file.name] = sha1.convert(file.content as List).toString(); + manifest[file.name] = sha1.convert(file.binaryContent).toString(); } final manifestContent = encoder.convert(manifest); @@ -311,9 +296,13 @@ class PkPass { ); archive.addFile(manifestFile); - if (signatureBuilder != null) { - final signature = - signatureBuilder(manifestContent, worldwide_Developer_Relations_G4); + if (certificatePem != null && privateKeyPem != null) { + final signature = writeSignature( + certificatePem, + privateKeyPem, + Uint8List.fromList(manifestFile.content as List), + ); + final signatureFile = ArchiveFile( 'signature', signature.length, @@ -321,6 +310,7 @@ class PkPass { ); archive.addFile(signatureFile); } + final pkpass = ZipEncoder().encode(archive); if (pkpass == null) { return null; @@ -332,13 +322,8 @@ class PkPass { // This is intentionally not exposed to keep this an implementation detail. // Tests should be written against the PkPass class directly. extension on Archive { - List? findBytesForFile(String fileName) => - findFile(fileName)?.content as List?; - - Uint8List? findUint8ListForFile(String fileName) { - final data = findBytesForFile(fileName); - return data == null ? null : Uint8List.fromList(data); - } + Uint8List? findBytesForFile(String fileName) => + findFile(fileName)?.binaryContent; /// Returns a map of locale to a map of resolution to image bytes. /// Returns null, if no image is localized @@ -367,11 +352,11 @@ extension on Archive { } if (fileName.endsWith('@2x.png')) { - map[language]![2] = Uint8List.fromList(file.content as List); + map[language]![2] = file.binaryContent; } else if (fileName.endsWith('@3x.png')) { - map[language]![3] = Uint8List.fromList(file.content as List); + map[language]![3] = file.binaryContent; } else { - map[language]![1] = Uint8List.fromList(file.content as List); + map[language]![1] = file.binaryContent; } } @@ -395,9 +380,9 @@ extension on Archive { PkImage? loadImage(String name) { return PkImage.fromImages( - image1: findUint8ListForFile('$name.png'), - image2: findUint8ListForFile('$name@2x.png'), - image3: findUint8ListForFile('$name@3x.png'), + image1: findBytesForFile('$name.png'), + image2: findBytesForFile('$name@2x.png'), + image3: findBytesForFile('$name@3x.png'), localizedImages: loadLocalizedImage(name), ); } @@ -415,8 +400,7 @@ extension on Archive { for (final languageFile in translationFiles) { final language = languageFile.name.split('.').first; - languageData[language] = - parseStringsFile(languageFile.content as List); + languageData[language] = parseStringsFile(languageFile.binaryContent); } return languageData; } @@ -468,7 +452,7 @@ extension on Archive { for (final file in filesWithoutSignatureAndManifest) { final checksumInManifest = manifest[file.name] as String?; - final digest = sha1.convert(file.content as List); + final digest = sha1.convert(file.binaryContent); if (checksumInManifest != digest.toString()) { throw ChecksumMismatchException(file.name); } diff --git a/passkit/lib/src/signature_verification.dart b/passkit/lib/src/signature_verification.dart index bad8288..6e43e5c 100644 --- a/passkit/lib/src/signature_verification.dart +++ b/passkit/lib/src/signature_verification.dart @@ -1,9 +1,10 @@ import 'dart:typed_data'; + +import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:passkit/src/apple_wwdr_certificate.dart'; import 'package:passkit/src/pkpass/exceptions.dart'; import 'package:pkcs7/pkcs7.dart'; -import 'package:collection/collection.dart'; /// [identifier] corresponds to the `passTypeIdentifier` in PkPasses or the /// `orderTypeIdentifier` for PkOrders. @@ -17,7 +18,7 @@ import 'package:collection/collection.dart'; // as long as the contents match? bool verifySignature({ required Uint8List signatureBytes, - required List manifestBytes, + required Uint8List manifestBytes, required String identifier, required String teamIdentifier, DateTime? now, @@ -51,7 +52,7 @@ bool verifySignature({ // Set the serialNumber key to the unique serial number for that identifier. final signerInfo = pkcs7.verify([wwdrG4]); - // final algo = si.getDigest(si.digestAlgorithm); Calculate hash based on the algo? + // final algo = signerInfo.getDigest(signerInfo.digestAlgorithm); Calculate hash based on the algo? return signerInfo.listEquality(manifestHash, signerInfo.messageDigest!); } diff --git a/passkit/lib/src/strings_parser/naive_strings_file_parser.dart b/passkit/lib/src/strings_parser/naive_strings_file_parser.dart index e2f27bc..5dfa999 100644 --- a/passkit/lib/src/strings_parser/naive_strings_file_parser.dart +++ b/passkit/lib/src/strings_parser/naive_strings_file_parser.dart @@ -1,8 +1,9 @@ import 'dart:convert'; +import 'dart:typed_data'; /// Parses [content] to a [Map] which contains the /// key-value-pairs for translations. -Map parseStringsFile(List content) { +Map parseStringsFile(Uint8List content) { final string = _stringsFileDecoder.convert(content); return naiveStringsFileParser(string); } diff --git a/passkit/lib/src/write_signature.dart b/passkit/lib/src/write_signature.dart new file mode 100644 index 0000000..cfbfe88 --- /dev/null +++ b/passkit/lib/src/write_signature.dart @@ -0,0 +1,49 @@ +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; +import 'package:passkit/src/apple_wwdr_certificate.dart'; +import 'package:pkcs7/pkcs7.dart'; +import 'package:pointycastle/pointycastle.dart'; + +/// [certificatePem] is the certificate to be used to sign the PkPass file. +/// +/// [privateKeyPem] is the private key PEM file. Right now, +/// it's only supported if it's not password protected. +/// +/// Read more about signing [here](https://github.com/ueman/passkit/blob/master/passkit/SIGNING.md). +// TODO(any): Add pkPassCertPem checks +// similar to the signature_verification.dart file, the identifier +// and teamIdentifier should match. But one step at a time. +Uint8List writeSignature( + String certificatePem, + String privateKeyPem, + Uint8List manifestBytes, +) { + final issuer = X509.fromPem(certificatePem); + + final pkcs7Builder = Pkcs7Builder(); + + pkcs7Builder.addCertificate(wwdrG4); + pkcs7Builder.addCertificate(issuer); + + final privateKey = + encrypt.RSAKeyParser().parse(privateKeyPem) as RSAPrivateKey; + + final signerInfo = Pkcs7SignerInfoBuilder.rsa( + issuer: issuer, + privateKey: privateKey, + digestAlgorithm: HashAlgorithm.sha256, + ); + + final manifestHash = Uint8List.fromList(sha256.convert(manifestBytes).bytes); + + signerInfo.addSMimeDigest( + digest: manifestHash, + signingTime: DateTime.now(), + ); + pkcs7Builder.addSignerInfo(signerInfo); + + final pkcs7 = pkcs7Builder.build(); + return pkcs7.der; +} diff --git a/passkit/pubspec.yaml b/passkit/pubspec.yaml index a011afb..ce2a47c 100644 --- a/passkit/pubspec.yaml +++ b/passkit/pubspec.yaml @@ -1,6 +1,6 @@ name: passkit description: Pure Dart library which can read Apple's PKPass files. Works on servers too. -version: 0.0.6 +version: 0.0.7 repository: https://github.com/ueman/passkit issue_tracker: https://github.com/ueman/passkit/issues topics: @@ -18,12 +18,14 @@ dependencies: collection: ^1.18.0 crypto: ^3.0.0 csslib: ^1.0.0 + encrypt: ^5.0.3 http: ^1.2.0 http_parser: ^4.0.0 intl: ^0.19.0 json_annotation: ^4.9.0 meta: ^1.0.0 - pkcs7: ^1.0.3 + pkcs7: ^1.0.0 + pointycastle: ^3.9.0 dev_dependencies: build_runner: ^2.3.2 diff --git a/passkit_ui/example/pubspec.lock b/passkit_ui/example/pubspec.lock index 1fdc051..7d015f2 100644 --- a/passkit_ui/example/pubspec.lock +++ b/passkit_ui/example/pubspec.lock @@ -9,6 +9,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.6.1" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "2ca377ad4d677bbadca278e0ba4da4e057b80a10b927bfc8f7d8bda8fe2ceb75" + url: "https://pub.dev" + source: hosted + version: "1.5.4" async: dependency: transitive description: @@ -89,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" fake_async: dependency: transitive description: @@ -217,14 +241,14 @@ packages: path: "../../passkit" relative: true source: path - version: "0.0.5" + version: "0.0.6" passkit_ui: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.0.4" + version: "0.0.5" path: dependency: transitive description: @@ -346,10 +370,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" web: dependency: transitive description: