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';
// 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;
static KeyPair readFromPrivateKeySync(String privateKeyPath) {
File file = new File(privateKeyPath);
if (!file.existsSync())
return null;
return fromPrivateKeyBytes(file.readAsBytesSync());
}
ECPublicKey publicKey = _publicKeyFromPrivateKey(privateKey);
return new AsymmetricKeyPair(publicKey, privateKey);
}
static KeyPair fromPrivateKeyBytes(List<int> privateKeyBytes) {
ECPrivateKey privateKey = _asn1ParsePrivateKey(
_params.domain, new Uint8List.fromList(privateKeyBytes));
if (privateKey == null)
return null;
// TODO(mpcomplete): remove this class when flutter_tools is updated.
class KeyPair extends AsymmetricKeyPair {
KeyPair(PublicKey publicKey, PrivateKey privateKey)
: super(publicKey, privateKey);
ECPublicKey publicKey = _publicKeyFromPrivateKey(privateKey);
return new KeyPair(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() {
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);
......
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