Commit 80667ded authored by Matt Perry's avatar Matt Perry

Change updater to use shared flx package.

This changes flutter's updater package to depend on and use the new flx
package. I also did a little cleanup of the Bundle interface.
parent 3154d55e
...@@ -7,7 +7,16 @@ import 'dart:convert'; ...@@ -7,7 +7,16 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
const String kBundleMagic = '#!mojo '; import 'package:bignum/bignum.dart';
import 'signing.dart';
// Magic string we put at the top of all bundle files.
const String kBundleMagic = '#!mojo mojo:sky_viewer\n';
// Prefix of the above, used when reading bundle files. This allows us to be
// more flexbile about what we accept.
const String kBundleMagicPrefix = '#!mojo ';
Future<List<int>> _readBytesWithLength(RandomAccessFile file) async { Future<List<int>> _readBytesWithLength(RandomAccessFile file) async {
ByteData buffer = new ByteData(4); ByteData buffer = new ByteData(4);
...@@ -29,6 +38,16 @@ Future<String> _readLine(RandomAccessFile file) async { ...@@ -29,6 +38,16 @@ Future<String> _readLine(RandomAccessFile file) async {
return line; return line;
} }
// Writes a 32-bit length followed by the content of [bytes].
void _writeBytesWithLengthSync(File 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);
}
// Represents a parsed .flx Bundle. Contains information from the bundle's // Represents a parsed .flx Bundle. Contains information from the bundle's
// header, as well as an open File handle positioned where the zip content // header, as well as an open File handle positioned where the zip content
// begins. // begins.
...@@ -46,30 +65,78 @@ Future<String> _readLine(RandomAccessFile file) async { ...@@ -46,30 +65,78 @@ Future<String> _readLine(RandomAccessFile file) async {
// ECDSA public key that was used to sign this manifest. // ECDSA public key that was used to sign this manifest.
// content-hash: an integer SHA-256 hash value of the <zip content>. // content-hash: an integer SHA-256 hash value of the <zip content>.
class Bundle { class Bundle {
Bundle(this.path); Bundle._fromFile(this.path);
Bundle.fromContent({
this.path,
this.manifest,
contentBytes,
KeyPair keyPair: null
}) : _contentBytes = contentBytes {
assert(path != null);
assert(manifest != null);
assert(_contentBytes != null);
manifestBytes = serializeManifest(manifest, keyPair?.publicKey, _contentBytes);
signatureBytes = signManifest(manifestBytes, keyPair?.privateKey);
}
final String path; final String path;
List<int> signatureBytes; List<int> signatureBytes;
List<int> manifestBytes; List<int> manifestBytes;
Map<String, dynamic> manifest; Map<String, dynamic> manifest;
RandomAccessFile content;
// File byte offset of the start of the zip content. Only valid when opened
// from a file.
int _contentOffset;
// Zip content bytes. Only valid when created in memory.
List<int> _contentBytes;
Future<bool> _readHeader() async { Future<bool> _readHeader() async {
content = await new File(path).open(); RandomAccessFile file = await new File(path).open();
String magic = await _readLine(content); String magic = await _readLine(file);
if (!magic.startsWith(kBundleMagic)) if (!magic.startsWith(kBundleMagicPrefix)) {
file.close();
return false; return false;
signatureBytes = await _readBytesWithLength(content); }
manifestBytes = await _readBytesWithLength(content); signatureBytes = await _readBytesWithLength(file);
manifestBytes = await _readBytesWithLength(file);
_contentOffset = await file.position();
file.close();
String manifestString = UTF8.decode(manifestBytes); String manifestString = UTF8.decode(manifestBytes);
manifest = JSON.decode(manifestString); manifest = JSON.decode(manifestString);
return true; return true;
} }
static Future<Bundle> readHeader(String path) async { static Future<Bundle> readHeader(String path) async {
Bundle bundle = new Bundle(path); Bundle bundle = new Bundle._fromFile(path);
if (!await bundle._readHeader()) if (!await bundle._readHeader())
return null; return null;
return bundle; return bundle;
} }
// When opened from a file, 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);
BigInteger expectedHash = new BigInteger(manifest['content-hash'], 10);
if (!await verifyContentHash(expectedHash, content))
return false;
return true;
}
// Writes the in-memory representation to disk.
void writeSync() {
assert(_contentBytes != null);
File outputFile = new File(path);
outputFile.writeAsStringSync('#!mojo mojo:sky_viewer\n');
_writeBytesWithLengthSync(outputFile, signatureBytes);
_writeBytesWithLengthSync(outputFile, manifestBytes);
outputFile.writeAsBytesSync(_contentBytes, mode: FileMode.APPEND, flush: true);
}
} }
...@@ -4,20 +4,23 @@ ...@@ -4,20 +4,23 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:asn1lib/asn1lib.dart'; import 'package:asn1lib/asn1lib.dart';
import 'package:bignum/bignum.dart'; import 'package:bignum/bignum.dart';
import 'package:cipher/cipher.dart'; import 'package:cipher/cipher.dart';
import 'package:cipher/impl/client.dart';
// 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.
final ECDomainParameters _ecDomain = new ECDomainParameters('prime256v1'); class CipherParameters {
final String kSignerAlgorithm = 'SHA-256/ECDSA'; final String signerAlgorithm = 'SHA-256/ECDSA';
final String kHashAlgorithm = 'SHA-256'; final String hashAlgorithm = 'SHA-256';
final ECDomainParameters domain = new ECDomainParameters('prime256v1');
final SecureRandom _random = _initRandom(); final SecureRandom random = _initRandom();
}
SecureRandom _initRandom() { SecureRandom _initRandom() {
// TODO(mpcomplete): Provide a better seed here. External entropy source? // TODO(mpcomplete): Provide a better seed here. External entropy source?
...@@ -29,6 +32,13 @@ SecureRandom _initRandom() { ...@@ -29,6 +32,13 @@ SecureRandom _initRandom() {
return random; return random;
} }
CipherParameters _initParams() {
initCipher();
return new CipherParameters();
}
final CipherParameters _params = _initParams();
// Returns a serialized manifest, with the public key and hash of the content // Returns a serialized manifest, with the public key and hash of the content
// included. // included.
Uint8List serializeManifest(Map manifestDescriptor, ECPublicKey publicKey, Uint8List zipBytes) { Uint8List serializeManifest(Map manifestDescriptor, ECPublicKey publicKey, Uint8List zipBytes) {
...@@ -48,7 +58,7 @@ Uint8List serializeManifest(Map manifestDescriptor, ECPublicKey publicKey, Uint8 ...@@ -48,7 +58,7 @@ Uint8List serializeManifest(Map manifestDescriptor, ECPublicKey publicKey, Uint8
if (publicKey != null) if (publicKey != null)
outputManifest['key'] = BASE64.encode(publicKey.Q.getEncoded()); outputManifest['key'] = BASE64.encode(publicKey.Q.getEncoded());
Uint8List zipHash = new Digest(kHashAlgorithm).process(zipBytes); Uint8List zipHash = new Digest(_params.hashAlgorithm).process(zipBytes);
BigInteger zipHashInt = new BigInteger.fromBytes(1, zipHash); BigInteger zipHashInt = new BigInteger.fromBytes(1, zipHash);
outputManifest['content-hash'] = zipHashInt.intValue(); outputManifest['content-hash'] = zipHashInt.intValue();
...@@ -59,9 +69,9 @@ Uint8List serializeManifest(Map manifestDescriptor, ECPublicKey publicKey, Uint8 ...@@ -59,9 +69,9 @@ Uint8List serializeManifest(Map manifestDescriptor, ECPublicKey publicKey, Uint8
List<int> signManifest(Uint8List manifestBytes, ECPrivateKey privateKey) { List<int> signManifest(Uint8List manifestBytes, ECPrivateKey privateKey) {
if (manifestBytes == null || privateKey == null) if (manifestBytes == null || privateKey == null)
return []; return [];
Signer signer = new Signer(kSignerAlgorithm); Signer signer = new Signer(_params.signerAlgorithm);
PrivateKeyParameter params = new PrivateKeyParameter(privateKey); PrivateKeyParameter params = new PrivateKeyParameter(privateKey);
signer.init(true, new ParametersWithRandom(params, _random)); signer.init(true, new ParametersWithRandom(params, _params.random));
ECSignature signature = signer.generateSignature(manifestBytes); ECSignature signature = signer.generateSignature(manifestBytes);
ASN1Sequence asn1 = new ASN1Sequence() ASN1Sequence asn1 = new ASN1Sequence()
..add(new ASN1Integer(signature.r)) ..add(new ASN1Integer(signature.r))
...@@ -69,6 +79,37 @@ List<int> signManifest(Uint8List manifestBytes, ECPrivateKey privateKey) { ...@@ -69,6 +79,37 @@ List<int> signManifest(Uint8List manifestBytes, ECPrivateKey privateKey) {
return asn1.encodedBytes; return asn1.encodedBytes;
} }
bool verifyManifestSignature(Map<String, dynamic> manifest,
Uint8List manifestBytes,
Uint8List signatureBytes) {
ECSignature signature = _asn1ParseSignature(signatureBytes);
if (signature == null)
return false;
List<int> keyBytes = BASE64.decode(manifest['key']);
ECPoint q = _params.domain.curve.decodePoint(keyBytes);
ECPublicKey publicKey = new ECPublicKey(q, _params.domain);
Signer signer = new Signer(_params.signerAlgorithm);
signer.init(false, new PublicKeyParameter(publicKey));
return signer.verifySignature(manifestBytes, signature);
}
Future<bool> verifyContentHash(BigInteger expectedHash, Stream<List<int>> content) async {
// Hash the file incrementally.
Digest hasher = new Digest(_params.hashAlgorithm);
await content.forEach((List<int> chunk) {
hasher.update(chunk, 0, chunk.length);
});
Uint8List hashBytes = new Uint8List(hasher.digestSize);
int len = hasher.doFinal(hashBytes, 0);
hashBytes = hashBytes.sublist(0, len);
BigInteger actualHash = new BigInteger.fromBytes(1, hashBytes);
return expectedHash == actualHash;
}
// Parses a DER-encoded ASN.1 ECDSA private key block.
ECPrivateKey _asn1ParsePrivateKey(ECDomainParameters ecDomain, Uint8List privateKey) { ECPrivateKey _asn1ParsePrivateKey(ECDomainParameters ecDomain, Uint8List privateKey) {
ASN1Parser parser = new ASN1Parser(privateKey); ASN1Parser parser = new ASN1Parser(privateKey);
ASN1Sequence seq = parser.nextObject(); ASN1Sequence seq = parser.nextObject();
...@@ -78,17 +119,47 @@ ECPrivateKey _asn1ParsePrivateKey(ECDomainParameters ecDomain, Uint8List private ...@@ -78,17 +119,47 @@ ECPrivateKey _asn1ParsePrivateKey(ECDomainParameters ecDomain, Uint8List private
return new ECPrivateKey(d, ecDomain); return new ECPrivateKey(d, ecDomain);
} }
Future<ECPrivateKey> loadPrivateKey(String privateKeyPath) async { // Parses a DER-encoded ASN.1 ECDSA signature block.
ECSignature _asn1ParseSignature(Uint8List signature) {
ASN1Parser parser = new ASN1Parser(signature);
ASN1Object object = parser.nextObject();
if (object is! ASN1Sequence)
return null;
ASN1Sequence sequence = object;
if (!(sequence.elements.length == 2 &&
sequence.elements[0] is ASN1Integer &&
sequence.elements[1] is ASN1Integer))
return null;
ASN1Integer r = sequence.elements[0];
ASN1Integer s = sequence.elements[1];
return new ECSignature(r.valueAsPositiveBigInteger, s.valueAsPositiveBigInteger);
}
ECPrivateKey _readPrivateKeySync(String privateKeyPath) {
File file = new File(privateKeyPath); File file = new File(privateKeyPath);
if (!file.existsSync()) if (!file.existsSync())
return null; return null;
List<int> bytes = file.readAsBytesSync(); List<int> bytes = file.readAsBytesSync();
return _asn1ParsePrivateKey(_ecDomain, new Uint8List.fromList(bytes)); return _asn1ParsePrivateKey(_params.domain, new Uint8List.fromList(bytes));
} }
ECPublicKey publicKeyFromPrivateKey(ECPrivateKey privateKey) { ECPublicKey _publicKeyFromPrivateKey(ECPrivateKey privateKey) {
if (privateKey == null)
return null;
ECPoint Q = privateKey.parameters.G * privateKey.d; ECPoint Q = privateKey.parameters.G * privateKey.d;
return new ECPublicKey(Q, privateKey.parameters); return new ECPublicKey(Q, privateKey.parameters);
} }
class KeyPair {
KeyPair(this.publicKey, this.privateKey);
ECPublicKey publicKey;
ECPrivateKey privateKey;
static KeyPair readFromPrivateKeySync(String path) {
ECPrivateKey privateKey = _readPrivateKeySync(path);
if (privateKey == null)
return null;
ECPublicKey publicKey = _publicKeyFromPrivateKey(privateKey);
return new KeyPair(publicKey, privateKey);
}
}
...@@ -4,7 +4,7 @@ author: Flutter Authors <flutter-dev@googlegroups.com> ...@@ -4,7 +4,7 @@ author: Flutter Authors <flutter-dev@googlegroups.com>
description: Library for dealing with Flutter bundle (.flx) files description: Library for dealing with Flutter bundle (.flx) files
homepage: http://flutter.io homepage: http://flutter.io
dependencies: dependencies:
sky_services: 0.0.38 sky_services: ^0.0.40
yaml: ^2.1.3 yaml: ^2.1.3
asn1lib: ^0.4.1 asn1lib: ^0.4.1
cipher: ^0.7.1 cipher: ^0.7.1
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
const String kBundleMagic = '#!mojo ';
Future<List<int>> _readBytesWithLength(RandomAccessFile file) async {
ByteData buffer = new ByteData(4);
await file.readInto(buffer.buffer.asUint8List());
int length = buffer.getUint32(0, Endianness.LITTLE_ENDIAN);
return await file.read(length);
}
const int kMaxLineLen = 10*1024;
const int kNewline = 0x0A;
Future<String> _readLine(RandomAccessFile file) async {
String line = '';
while (line.length < kMaxLineLen) {
int byte = await file.readByte();
if (byte == -1 || byte == kNewline)
break;
line += new String.fromCharCode(byte);
}
return line;
}
// Represents a parsed .flx Bundle. Contains information from the bundle's
// header, as well as an open File handle positioned where the zip content
// begins.
// The bundle format is:
// #!mojo <any string>\n
// <32-bit length><signature of the manifest data>
// <32-bit length><manifest data>
// <zip content>
//
// The manifest is a JSON string containing the following keys:
// (optional) name: the name of the package.
// version: the package version.
// update-url: the base URL to download a new manifest and bundle.
// key: a BASE-64 encoded DER-encoded ASN.1 representation of the Q point of the
// ECDSA public key that was used to sign this manifest.
// content-hash: an integer SHA-256 hash value of the <zip content>.
class Bundle {
Bundle(this.path);
final String path;
List<int> signatureBytes;
List<int> manifestBytes;
Map<String, dynamic> manifest;
RandomAccessFile content;
Future<bool> _readHeader() async {
content = await new File(path).open();
String magic = await _readLine(content);
if (!magic.startsWith(kBundleMagic))
return false;
signatureBytes = await _readBytesWithLength(content);
manifestBytes = await _readBytesWithLength(content);
String manifestString = UTF8.decode(manifestBytes);
manifest = JSON.decode(manifestString);
return true;
}
static Future<Bundle> readHeader(String path) async {
Bundle bundle = new Bundle(path);
if (!await bundle._readHeader())
return null;
return bundle;
}
}
...@@ -9,33 +9,18 @@ import 'dart:io'; ...@@ -9,33 +9,18 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:mojo/core.dart'; import 'package:mojo/core.dart';
// TODO(mpcomplete): Remove this 'hide' when we remove the conflicting import 'package:flutter/services.dart';
// UpdateService from activity.mojom. import 'package:flx/bundle.dart';
import 'package:flutter/services.dart' hide UpdateServiceProxy;
import 'package:sky_services/updater/update_service.mojom.dart'; import 'package:sky_services/updater/update_service.mojom.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart' as yaml; import 'package:yaml/yaml.dart' as yaml;
import 'package:asn1lib/asn1lib.dart';
import 'package:bignum/bignum.dart';
import 'package:cipher/cipher.dart';
import 'package:cipher/impl/client.dart';
import 'bundle.dart';
import 'pipe_to_file.dart'; import 'pipe_to_file.dart';
import 'version.dart'; import 'version.dart';
const String kManifestFile = 'sky.yaml'; const String kManifestFile = 'sky.yaml';
const String kBundleFile = 'app.flx'; const String kBundleFile = 'app.flx';
// Number of bytes to read at a time from a file.
const int kReadBlockSize = 32*1024;
// The ECDSA algorithm parameters we're using. These match the parameters used
// by the signing tool in flutter_tools.
final ECDomainParameters _ecDomain = new ECDomainParameters('prime256v1');
final String kSignerAlgorithm = 'SHA-256/ECDSA';
final String kHashAlgorithm = 'SHA-256';
UpdateServiceProxy _initUpdateService() { UpdateServiceProxy _initUpdateService() {
UpdateServiceProxy updateService = new UpdateServiceProxy.unbound(); UpdateServiceProxy updateService = new UpdateServiceProxy.unbound();
shell.requestService(null, updateService); shell.requestService(null, updateService);
...@@ -51,22 +36,6 @@ Future<String> getDataDir() async { ...@@ -51,22 +36,6 @@ Future<String> getDataDir() async {
return cachedDataDir; return cachedDataDir;
} }
// Parses a DER-encoded ASN.1 ECDSA signature block.
ECSignature _asn1ParseSignature(Uint8List signature) {
ASN1Parser parser = new ASN1Parser(signature);
ASN1Object object = parser.nextObject();
if (object is! ASN1Sequence)
return null;
ASN1Sequence sequence = object;
if (!(sequence.elements.length == 2 &&
sequence.elements[0] is ASN1Integer &&
sequence.elements[1] is ASN1Integer))
return null;
ASN1Integer r = sequence.elements[0];
ASN1Integer s = sequence.elements[1];
return new ECSignature(r.valueAsPositiveBigInteger, s.valueAsPositiveBigInteger);
}
class UpdateFailure extends Error { class UpdateFailure extends Error {
UpdateFailure(this._message); UpdateFailure(this._message);
String _message; String _message;
...@@ -112,7 +81,6 @@ class UpdateTask { ...@@ -112,7 +81,6 @@ class UpdateTask {
String bundlePath = path.join(_dataDir, kBundleFile); String bundlePath = path.join(_dataDir, kBundleFile);
Bundle bundle = await Bundle.readHeader(bundlePath); Bundle bundle = await Bundle.readHeader(bundlePath);
_currentManifest = bundle.manifest; _currentManifest = bundle.manifest;
bundle.content.close();
} }
Future<yaml.YamlMap> _fetchManifest() async { Future<yaml.YamlMap> _fetchManifest() async {
...@@ -144,47 +112,8 @@ class UpdateTask { ...@@ -144,47 +112,8 @@ class UpdateTask {
throw new UpdateFailure('Remote package not a valid FLX file.'); throw new UpdateFailure('Remote package not a valid FLX file.');
if (bundle.manifest['key'] != _currentManifest['key']) if (bundle.manifest['key'] != _currentManifest['key'])
throw new UpdateFailure('Remote package key does not match.'); throw new UpdateFailure('Remote package key does not match.');
if (!await bundle.verifyContent())
await _verifyManifestSignature(bundle); throw new UpdateFailure('Invalid package signature or hash. This package has been tampered with.');
await _verifyContentHash(bundle);
bundle.content.close();
}
Future _verifyManifestSignature(Bundle bundle) async {
ECSignature ecSignature = _asn1ParseSignature(bundle.signatureBytes);
if (ecSignature == null)
throw new UpdateFailure('Corrupt package signature.');
List keyBytes = BASE64.decode(_currentManifest['key']);
ECPoint q = _ecDomain.curve.decodePoint(keyBytes);
ECPublicKey ecPublicKey = new ECPublicKey(q, _ecDomain);
Signer signer = new Signer(kSignerAlgorithm);
signer.init(false, new PublicKeyParameter(ecPublicKey));
if (!signer.verifySignature(bundle.manifestBytes, ecSignature))
throw new UpdateFailure('Invalid package signature. This package has been tampered with.');
}
Future _verifyContentHash(Bundle bundle) async {
// Hash the bundle contents.
Digest hasher = new Digest(kHashAlgorithm);
RandomAccessFile content = bundle.content;
int remainingLen = await content.length() - await content.position();
while (remainingLen > 0) {
List<int> chunk = await content.read(min(remainingLen, kReadBlockSize));
hasher.update(chunk, 0, chunk.length);
remainingLen -= chunk.length;
}
Uint8List hashBytes = new Uint8List(hasher.digestSize);
int len = hasher.doFinal(hashBytes, 0);
hashBytes = hashBytes.sublist(0, len);
BigInteger actualHash = new BigInteger.fromBytes(1, hashBytes);
// Compare to our expected hash from the manifest.
BigInteger expectedHash = new BigInteger(bundle.manifest['content-hash'], 10);
if (expectedHash != actualHash)
throw new UpdateFailure('Invalid package content hash. This package has been tampered with.');
} }
Future _replaceBundle() async { Future _replaceBundle() async {
...@@ -194,7 +123,6 @@ class UpdateTask { ...@@ -194,7 +123,6 @@ class UpdateTask {
} }
void main() { void main() {
initCipher();
UpdateTask task = new UpdateTask(); UpdateTask task = new UpdateTask();
task.run(); task.run();
} }
...@@ -6,13 +6,14 @@ homepage: http://flutter.io ...@@ -6,13 +6,14 @@ homepage: http://flutter.io
dependencies: dependencies:
mojo: 0.3.0 mojo: 0.3.0
flutter: ">=0.0.3 <0.1.0" flutter: ">=0.0.3 <0.1.0"
sky_services: any sky_services: ^0.0.40
path: any yaml: ^2.1.3
yaml: any path: ^1.3.0
cipher: any flx: 0.0.1
asn1lib: any
dependency_overrides: dependency_overrides:
flutter: flutter:
path: ../sky path: ../sky
flx:
path: ../flx
environment: environment:
sdk: '>=1.12.0 <2.0.0' sdk: '>=1.12.0 <2.0.0'
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