mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add tests for flx Bundle.
Also cleaned up the flx code a bit. Replaced custom KeyPair class with cipher's AsymmetricKeyPair.
This commit is contained in:
parent
766eda6bf6
commit
713d654330
@ -18,6 +18,8 @@ const String kBundleMagic = '#!mojo mojo:sky_viewer\n';
|
||||
// more flexbile about what we accept.
|
||||
const String kBundleMagicPrefix = '#!mojo ';
|
||||
|
||||
typedef Stream<List<int>> StreamOpener();
|
||||
|
||||
Future<List<int>> _readBytesWithLength(RandomAccessFile file) async {
|
||||
ByteData buffer = new ByteData(4);
|
||||
await file.readInto(buffer.buffer.asUint8List());
|
||||
@ -39,13 +41,13 @@ Future<String> _readLine(RandomAccessFile file) async {
|
||||
}
|
||||
|
||||
// Writes a 32-bit length followed by the content of [bytes].
|
||||
void _writeBytesWithLengthSync(File outputFile, List<int> bytes) {
|
||||
void _writeBytesWithLengthSync(RandomAccessFile outputFile, List<int> 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(<List<int>>[_contentBytes]);
|
||||
}
|
||||
|
||||
final String path;
|
||||
@ -84,9 +87,8 @@ class Bundle {
|
||||
List<int> manifestBytes;
|
||||
Map<String, dynamic> 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<int> _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<bool> verifyContent() async {
|
||||
assert(_contentOffset != null);
|
||||
if (!verifyManifestSignature(manifest, manifestBytes, signatureBytes))
|
||||
return false;
|
||||
|
||||
Stream<List<int>> content = await new File(path).openRead(_contentOffset);
|
||||
Stream<List<int>> 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();
|
||||
}
|
||||
}
|
||||
|
@ -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<int> 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<int> 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);
|
||||
}
|
||||
}
|
||||
|
70
packages/unit/test/flx/bundle_test.dart
Normal file
70
packages/unit/test/flx/bundle_test.dart
Normal file
@ -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<int> kPrivateKeyDER = BASE64.decode(kPrivateKeyBase64);
|
||||
|
||||
// Test manifest.
|
||||
final Map<String, dynamic> kManifest = <String, dynamic>{
|
||||
'name': 'test app',
|
||||
'version': '1.0.0'
|
||||
};
|
||||
|
||||
// Simple test byte pattern.
|
||||
final Uint8List kTestBytes = new Uint8List.fromList(<int>[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<String, dynamic> 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);
|
||||
});
|
||||
}
|
@ -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<String, dynamic> 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<String, dynamic> manifest = JSON.decode(UTF8.decode(
|
||||
serializeManifest(kManifest, keyPair.publicKey, kTestBytes)));
|
||||
Uint8List signatureBytes = signManifest(kTestBytes, keyPair.privateKey);
|
||||
|
Loading…
Reference in New Issue
Block a user