diff --git a/packages/flx/lib/bundle.dart b/packages/flx/lib/bundle.dart index 52485c8efa1..f5afe4492d0 100644 --- a/packages/flx/lib/bundle.dart +++ b/packages/flx/lib/bundle.dart @@ -18,6 +18,8 @@ const String kBundleMagic = '#!mojo mojo:sky_viewer\n'; // more flexbile about what we accept. const String kBundleMagicPrefix = '#!mojo '; +typedef Stream> StreamOpener(); + Future> _readBytesWithLength(RandomAccessFile file) async { ByteData buffer = new ByteData(4); await file.readInto(buffer.buffer.asUint8List()); @@ -39,13 +41,13 @@ Future _readLine(RandomAccessFile file) async { } // Writes a 32-bit length followed by the content of [bytes]. -void _writeBytesWithLengthSync(File outputFile, List bytes) { +void _writeBytesWithLengthSync(RandomAccessFile outputFile, List bytes) { if (bytes == null) bytes = new Uint8List(0); assert(bytes.length < 0xffffffff); ByteData length = new ByteData(4)..setUint32(0, bytes.length, Endianness.LITTLE_ENDIAN); - outputFile.writeAsBytesSync(length.buffer.asUint8List(), mode: FileMode.APPEND); - outputFile.writeAsBytesSync(bytes, mode: FileMode.APPEND); + outputFile.writeFromSync(length.buffer.asUint8List()); + outputFile.writeFromSync(bytes); } // Represents a parsed .flx Bundle. Contains information from the bundle's @@ -70,13 +72,14 @@ class Bundle { this.path, this.manifest, contentBytes, - KeyPair keyPair: null + AsymmetricKeyPair keyPair: null }) : _contentBytes = contentBytes { assert(path != null); assert(manifest != null); assert(_contentBytes != null); manifestBytes = serializeManifest(manifest, keyPair?.publicKey, _contentBytes); signatureBytes = signManifest(manifestBytes, keyPair?.privateKey); + _openContentStream = () => new Stream.fromIterable(>[_contentBytes]); } final String path; @@ -84,9 +87,8 @@ class Bundle { List manifestBytes; Map manifest; - // File byte offset of the start of the zip content. Only valid when opened - // from a file. - int _contentOffset; + // Callback to open a Stream containing the bundle content data. + StreamOpener _openContentStream; // Zip content bytes. Only valid when created in memory. List _contentBytes; @@ -100,7 +102,8 @@ class Bundle { } signatureBytes = await _readBytesWithLength(file); manifestBytes = await _readBytesWithLength(file); - _contentOffset = await file.position(); + int contentOffset = await file.position(); + _openContentStream = () => new File(path).openRead(contentOffset); file.close(); String manifestString = UTF8.decode(manifestBytes); @@ -115,14 +118,12 @@ class Bundle { return bundle; } - // When opened from a file, verifies that the package has a valid signature - // and content. + // Verifies that the package has a valid signature and content. Future verifyContent() async { - assert(_contentOffset != null); if (!verifyManifestSignature(manifest, manifestBytes, signatureBytes)) return false; - Stream> content = await new File(path).openRead(_contentOffset); + Stream> content = _openContentStream(); BigInteger expectedHash = new BigInteger(manifest['content-hash'], 10); if (!await verifyContentHash(expectedHash, content)) return false; @@ -133,10 +134,11 @@ class Bundle { // Writes the in-memory representation to disk. void writeSync() { assert(_contentBytes != null); - File outputFile = new File(path); - outputFile.writeAsStringSync('#!mojo mojo:sky_viewer\n'); + RandomAccessFile outputFile = new File(path).openSync(mode: FileMode.WRITE); + outputFile.writeStringSync('#!mojo mojo:sky_viewer\n'); _writeBytesWithLengthSync(outputFile, signatureBytes); _writeBytesWithLengthSync(outputFile, manifestBytes); - outputFile.writeAsBytesSync(_contentBytes, mode: FileMode.APPEND, flush: true); + outputFile.writeFromSync(_contentBytes); + outputFile.close(); } } diff --git a/packages/flx/lib/signing.dart b/packages/flx/lib/signing.dart index c9a1a0668fc..8ec65c842c9 100644 --- a/packages/flx/lib/signing.dart +++ b/packages/flx/lib/signing.dart @@ -12,6 +12,8 @@ import 'package:bignum/bignum.dart'; import 'package:cipher/cipher.dart'; import 'package:cipher/impl/client.dart'; +export 'package:cipher/cipher.dart' show AsymmetricKeyPair; + // The ECDSA algorithm parameters we're using. These match the parameters used // by the Flutter updater package. class CipherParameters { @@ -143,26 +145,32 @@ ECPublicKey _publicKeyFromPrivateKey(ECPrivateKey privateKey) { return new ECPublicKey(Q, privateKey.parameters); } -class KeyPair { - KeyPair(this.publicKey, this.privateKey); +AsymmetricKeyPair keyPairFromPrivateKeyFileSync(String privateKeyPath) { + File file = new File(privateKeyPath); + if (!file.existsSync()) + return null; + return keyPairFromPrivateKeyBytes(file.readAsBytesSync()); +} - ECPublicKey publicKey; - ECPrivateKey privateKey; +AsymmetricKeyPair keyPairFromPrivateKeyBytes(List privateKeyBytes) { + ECPrivateKey privateKey = _asn1ParsePrivateKey( + _params.domain, new Uint8List.fromList(privateKeyBytes)); + if (privateKey == null) + return null; + + ECPublicKey publicKey = _publicKeyFromPrivateKey(privateKey); + return new AsymmetricKeyPair(publicKey, privateKey); +} + +// TODO(mpcomplete): remove this class when flutter_tools is updated. +class KeyPair extends AsymmetricKeyPair { + KeyPair(PublicKey publicKey, PrivateKey privateKey) + : super(publicKey, privateKey); static KeyPair readFromPrivateKeySync(String privateKeyPath) { - File file = new File(privateKeyPath); - if (!file.existsSync()) + AsymmetricKeyPair pair = keyPairFromPrivateKeyFileSync(privateKeyPath); + if (pair == null) return null; - return fromPrivateKeyBytes(file.readAsBytesSync()); - } - - static KeyPair fromPrivateKeyBytes(List privateKeyBytes) { - ECPrivateKey privateKey = _asn1ParsePrivateKey( - _params.domain, new Uint8List.fromList(privateKeyBytes)); - if (privateKey == null) - return null; - - ECPublicKey publicKey = _publicKeyFromPrivateKey(privateKey); - return new KeyPair(publicKey, privateKey); + return new KeyPair(pair.publicKey, pair.privateKey); } } diff --git a/packages/unit/test/flx/bundle_test.dart b/packages/unit/test/flx/bundle_test.dart new file mode 100644 index 00000000000..12b6af3c0c1 --- /dev/null +++ b/packages/unit/test/flx/bundle_test.dart @@ -0,0 +1,70 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:io'; + +import 'package:flx/signing.dart'; +import 'package:flx/bundle.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +main() async { + // The following constant was generated via the openssl shell commands: + // openssl ecparam -genkey -name prime256v1 -out privatekey.pem + // openssl ec -in privatekey.pem -outform DER | base64 + const String kPrivateKeyBase64 = 'MHcCAQEEIG4Xt+MgsdP/o89kAHz7EVVLKkN+DUfpaBtZfMyFGbUgoAoGCCqGSM49AwEHoUQDQgAElPtbBVPPqKHYXYAgHaxB2hL6sXeFc99YLijTAuAPe2Nbhywan+v4k+nFm0TJJW/mkV+nH+fyBZ98t4UcFCqkOg=='; + final List kPrivateKeyDER = BASE64.decode(kPrivateKeyBase64); + + // Test manifest. + final Map kManifest = { + 'name': 'test app', + 'version': '1.0.0' + }; + + // Simple test byte pattern. + final Uint8List kTestBytes = new Uint8List.fromList([1, 2, 3]); + + // Create a temp dir and file for the bundle. + Directory tempDir = await Directory.systemTemp.createTempSync('bundle_test'); + String bundlePath = path.join(tempDir.path, 'bundle.flx'); + + AsymmetricKeyPair keyPair = keyPairFromPrivateKeyBytes(kPrivateKeyDER); + Map manifest = JSON.decode(UTF8.decode( + serializeManifest(kManifest, keyPair.publicKey, kTestBytes))); + + test('verifyContent works', () async { + Bundle bundle = new Bundle.fromContent( + path: bundlePath, + manifest: manifest, + contentBytes: kTestBytes, + keyPair: keyPair + ); + + bool verifies = await bundle.verifyContent(); + expect(verifies, equals(true)); + }); + + test('write/read works', () async { + Bundle bundle = new Bundle.fromContent( + path: bundlePath, + manifest: manifest, + contentBytes: kTestBytes, + keyPair: keyPair + ); + + bundle.writeSync(); + + Bundle diskBundle = await Bundle.readHeader(bundlePath); + expect(diskBundle != null, equals(true)); + expect(diskBundle.manifestBytes, equals(bundle.manifestBytes)); + expect(diskBundle.signatureBytes, equals(bundle.signatureBytes)); + expect(diskBundle.manifest['key'], equals(bundle.manifest['key'])); + expect(diskBundle.manifest['key'], equals(manifest['key'])); + + bool verifies = await diskBundle.verifyContent(); + expect(verifies, equals(true)); + }); + + test('cleanup', () async { + tempDir.deleteSync(recursive: true); + }); +} diff --git a/packages/unit/test/flx/signing_test.dart b/packages/unit/test/flx/signing_test.dart index d7cec6e37c9..5c275fa2015 100644 --- a/packages/unit/test/flx/signing_test.dart +++ b/packages/unit/test/flx/signing_test.dart @@ -32,7 +32,7 @@ void main() { final int kTestHash = 0x039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81; test('can read openssl key pair', () { - KeyPair keyPair = KeyPair.fromPrivateKeyBytes(kPrivateKeyDER); + AsymmetricKeyPair keyPair = keyPairFromPrivateKeyBytes(kPrivateKeyDER); expect(keyPair != null, equals(true)); expect(keyPair.privateKey.d.intValue(), equals(kPrivateKeyD)); expect(keyPair.publicKey.Q.x.toBigInteger().intValue(), equals(kPublicKeyQx)); @@ -40,7 +40,7 @@ void main() { }); test('serializeManifest adds key and content-hash', () { - KeyPair keyPair = KeyPair.fromPrivateKeyBytes(kPrivateKeyDER); + AsymmetricKeyPair keyPair = keyPairFromPrivateKeyBytes(kPrivateKeyDER); Uint8List manifestBytes = serializeManifest(kManifest, keyPair.publicKey, kTestBytes); Map decodedManifest = JSON.decode(UTF8.decode(manifestBytes)); String expectedKey = BASE64.encode(keyPair.publicKey.Q.getEncoded()); @@ -52,7 +52,7 @@ void main() { }); test('signManifest and verifyManifestSignature work', () { - KeyPair keyPair = KeyPair.fromPrivateKeyBytes(kPrivateKeyDER); + AsymmetricKeyPair keyPair = keyPairFromPrivateKeyBytes(kPrivateKeyDER); Map manifest = JSON.decode(UTF8.decode( serializeManifest(kManifest, keyPair.publicKey, kTestBytes))); Uint8List signatureBytes = signManifest(kTestBytes, keyPair.privateKey);