Commit 713d6543 authored by Matt Perry's avatar Matt Perry

Add tests for flx Bundle.

Also cleaned up the flx code a bit. Replaced custom KeyPair class with
cipher's AsymmetricKeyPair.
parent 766eda6b
...@@ -18,6 +18,8 @@ const String kBundleMagic = '#!mojo mojo:sky_viewer\n'; ...@@ -18,6 +18,8 @@ const String kBundleMagic = '#!mojo mojo:sky_viewer\n';
// more flexbile about what we accept. // more flexbile about what we accept.
const String kBundleMagicPrefix = '#!mojo '; const String kBundleMagicPrefix = '#!mojo ';
typedef Stream<List<int>> StreamOpener();
Future<List<int>> _readBytesWithLength(RandomAccessFile file) async { Future<List<int>> _readBytesWithLength(RandomAccessFile file) async {
ByteData buffer = new ByteData(4); ByteData buffer = new ByteData(4);
await file.readInto(buffer.buffer.asUint8List()); await file.readInto(buffer.buffer.asUint8List());
...@@ -39,13 +41,13 @@ Future<String> _readLine(RandomAccessFile file) async { ...@@ -39,13 +41,13 @@ Future<String> _readLine(RandomAccessFile file) async {
} }
// Writes a 32-bit length followed by the content of [bytes]. // 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) if (bytes == null)
bytes = new Uint8List(0); bytes = new Uint8List(0);
assert(bytes.length < 0xffffffff); assert(bytes.length < 0xffffffff);
ByteData length = new ByteData(4)..setUint32(0, bytes.length, Endianness.LITTLE_ENDIAN); ByteData length = new ByteData(4)..setUint32(0, bytes.length, Endianness.LITTLE_ENDIAN);
outputFile.writeAsBytesSync(length.buffer.asUint8List(), mode: FileMode.APPEND); outputFile.writeFromSync(length.buffer.asUint8List());
outputFile.writeAsBytesSync(bytes, mode: FileMode.APPEND); outputFile.writeFromSync(bytes);
} }
// Represents a parsed .flx Bundle. Contains information from the bundle's // Represents a parsed .flx Bundle. Contains information from the bundle's
...@@ -70,13 +72,14 @@ class Bundle { ...@@ -70,13 +72,14 @@ class Bundle {
this.path, this.path,
this.manifest, this.manifest,
contentBytes, contentBytes,
KeyPair keyPair: null AsymmetricKeyPair keyPair: null
}) : _contentBytes = contentBytes { }) : _contentBytes = contentBytes {
assert(path != null); assert(path != null);
assert(manifest != null); assert(manifest != null);
assert(_contentBytes != null); assert(_contentBytes != null);
manifestBytes = serializeManifest(manifest, keyPair?.publicKey, _contentBytes); manifestBytes = serializeManifest(manifest, keyPair?.publicKey, _contentBytes);
signatureBytes = signManifest(manifestBytes, keyPair?.privateKey); signatureBytes = signManifest(manifestBytes, keyPair?.privateKey);
_openContentStream = () => new Stream.fromIterable(<List<int>>[_contentBytes]);
} }
final String path; final String path;
...@@ -84,9 +87,8 @@ class Bundle { ...@@ -84,9 +87,8 @@ class Bundle {
List<int> manifestBytes; List<int> manifestBytes;
Map<String, dynamic> manifest; Map<String, dynamic> manifest;
// File byte offset of the start of the zip content. Only valid when opened // Callback to open a Stream containing the bundle content data.
// from a file. StreamOpener _openContentStream;
int _contentOffset;
// Zip content bytes. Only valid when created in memory. // Zip content bytes. Only valid when created in memory.
List<int> _contentBytes; List<int> _contentBytes;
...@@ -100,7 +102,8 @@ class Bundle { ...@@ -100,7 +102,8 @@ class Bundle {
} }
signatureBytes = await _readBytesWithLength(file); signatureBytes = await _readBytesWithLength(file);
manifestBytes = await _readBytesWithLength(file); manifestBytes = await _readBytesWithLength(file);
_contentOffset = await file.position(); int contentOffset = await file.position();
_openContentStream = () => new File(path).openRead(contentOffset);
file.close(); file.close();
String manifestString = UTF8.decode(manifestBytes); String manifestString = UTF8.decode(manifestBytes);
...@@ -115,14 +118,12 @@ class Bundle { ...@@ -115,14 +118,12 @@ class Bundle {
return bundle; return bundle;
} }
// When opened from a file, verifies that the package has a valid signature // Verifies that the package has a valid signature and content.
// and content.
Future<bool> verifyContent() async { Future<bool> verifyContent() async {
assert(_contentOffset != null);
if (!verifyManifestSignature(manifest, manifestBytes, signatureBytes)) if (!verifyManifestSignature(manifest, manifestBytes, signatureBytes))
return false; 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); BigInteger expectedHash = new BigInteger(manifest['content-hash'], 10);
if (!await verifyContentHash(expectedHash, content)) if (!await verifyContentHash(expectedHash, content))
return false; return false;
...@@ -133,10 +134,11 @@ class Bundle { ...@@ -133,10 +134,11 @@ class Bundle {
// Writes the in-memory representation to disk. // Writes the in-memory representation to disk.
void writeSync() { void writeSync() {
assert(_contentBytes != null); assert(_contentBytes != null);
File outputFile = new File(path); RandomAccessFile outputFile = new File(path).openSync(mode: FileMode.WRITE);
outputFile.writeAsStringSync('#!mojo mojo:sky_viewer\n'); outputFile.writeStringSync('#!mojo mojo:sky_viewer\n');
_writeBytesWithLengthSync(outputFile, signatureBytes); _writeBytesWithLengthSync(outputFile, signatureBytes);
_writeBytesWithLengthSync(outputFile, manifestBytes); _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'; ...@@ -12,6 +12,8 @@ import 'package:bignum/bignum.dart';
import 'package:cipher/cipher.dart'; import 'package:cipher/cipher.dart';
import 'package:cipher/impl/client.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 // The ECDSA algorithm parameters we're using. These match the parameters used
// by the Flutter updater package. // by the Flutter updater package.
class CipherParameters { class CipherParameters {
...@@ -143,26 +145,32 @@ ECPublicKey _publicKeyFromPrivateKey(ECPrivateKey privateKey) { ...@@ -143,26 +145,32 @@ ECPublicKey _publicKeyFromPrivateKey(ECPrivateKey privateKey) {
return new ECPublicKey(Q, privateKey.parameters); return new ECPublicKey(Q, privateKey.parameters);
} }
class KeyPair { AsymmetricKeyPair keyPairFromPrivateKeyFileSync(String privateKeyPath) {
KeyPair(this.publicKey, this.privateKey);
ECPublicKey publicKey;
ECPrivateKey privateKey;
static KeyPair readFromPrivateKeySync(String privateKeyPath) {
File file = new File(privateKeyPath); File file = new File(privateKeyPath);
if (!file.existsSync()) if (!file.existsSync())
return null; return null;
return fromPrivateKeyBytes(file.readAsBytesSync()); return keyPairFromPrivateKeyBytes(file.readAsBytesSync());
} }
static KeyPair fromPrivateKeyBytes(List<int> privateKeyBytes) { AsymmetricKeyPair keyPairFromPrivateKeyBytes(List<int> privateKeyBytes) {
ECPrivateKey privateKey = _asn1ParsePrivateKey( ECPrivateKey privateKey = _asn1ParsePrivateKey(
_params.domain, new Uint8List.fromList(privateKeyBytes)); _params.domain, new Uint8List.fromList(privateKeyBytes));
if (privateKey == null) if (privateKey == null)
return null; return null;
ECPublicKey publicKey = _publicKeyFromPrivateKey(privateKey); ECPublicKey publicKey = _publicKeyFromPrivateKey(privateKey);
return new KeyPair(publicKey, 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) {
AsymmetricKeyPair pair = keyPairFromPrivateKeyFileSync(privateKeyPath);
if (pair == null)
return null;
return new KeyPair(pair.publicKey, pair.privateKey);
} }
} }
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() { ...@@ -32,7 +32,7 @@ void main() {
final int kTestHash = 0x039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81; final int kTestHash = 0x039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81;
test('can read openssl key pair', () { test('can read openssl key pair', () {
KeyPair keyPair = KeyPair.fromPrivateKeyBytes(kPrivateKeyDER); AsymmetricKeyPair keyPair = keyPairFromPrivateKeyBytes(kPrivateKeyDER);
expect(keyPair != null, equals(true)); expect(keyPair != null, equals(true));
expect(keyPair.privateKey.d.intValue(), equals(kPrivateKeyD)); expect(keyPair.privateKey.d.intValue(), equals(kPrivateKeyD));
expect(keyPair.publicKey.Q.x.toBigInteger().intValue(), equals(kPublicKeyQx)); expect(keyPair.publicKey.Q.x.toBigInteger().intValue(), equals(kPublicKeyQx));
...@@ -40,7 +40,7 @@ void main() { ...@@ -40,7 +40,7 @@ void main() {
}); });
test('serializeManifest adds key and content-hash', () { test('serializeManifest adds key and content-hash', () {
KeyPair keyPair = KeyPair.fromPrivateKeyBytes(kPrivateKeyDER); AsymmetricKeyPair keyPair = keyPairFromPrivateKeyBytes(kPrivateKeyDER);
Uint8List manifestBytes = serializeManifest(kManifest, keyPair.publicKey, kTestBytes); Uint8List manifestBytes = serializeManifest(kManifest, keyPair.publicKey, kTestBytes);
Map<String, dynamic> decodedManifest = JSON.decode(UTF8.decode(manifestBytes)); Map<String, dynamic> decodedManifest = JSON.decode(UTF8.decode(manifestBytes));
String expectedKey = BASE64.encode(keyPair.publicKey.Q.getEncoded()); String expectedKey = BASE64.encode(keyPair.publicKey.Q.getEncoded());
...@@ -52,7 +52,7 @@ void main() { ...@@ -52,7 +52,7 @@ void main() {
}); });
test('signManifest and verifyManifestSignature work', () { test('signManifest and verifyManifestSignature work', () {
KeyPair keyPair = KeyPair.fromPrivateKeyBytes(kPrivateKeyDER); AsymmetricKeyPair keyPair = keyPairFromPrivateKeyBytes(kPrivateKeyDER);
Map<String, dynamic> manifest = JSON.decode(UTF8.decode( Map<String, dynamic> manifest = JSON.decode(UTF8.decode(
serializeManifest(kManifest, keyPair.publicKey, kTestBytes))); serializeManifest(kManifest, keyPair.publicKey, kTestBytes)));
Uint8List signatureBytes = signManifest(kTestBytes, keyPair.privateKey); Uint8List signatureBytes = signManifest(kTestBytes, keyPair.privateKey);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment