// 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'; import 'package:bignum/bignum.dart'; import 'signing.dart'; // Magic string we put at the top of all bundle files. const String kBundleMagic = '#!mojo mojo:flutter\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 '; typedef Stream<List<int>> StreamOpener(); 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; } // Writes a 32-bit length followed by the content of [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.writeFromSync(length.buffer.asUint8List()); outputFile.writeFromSync(bytes); } // 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._fromFile(this.path); Bundle.fromContent({ this.path, this.manifest, List<int> contentBytes, AsymmetricKeyPair<PublicKey, PrivateKey> keyPair }) : _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<List<int>>.fromIterable(<List<int>>[_contentBytes]); } final String path; List<int> signatureBytes; List<int> manifestBytes; Map<String, dynamic> manifest; // Callback to open a Stream containing the bundle content data. StreamOpener _openContentStream; // Zip content bytes. Only valid when created in memory. List<int> _contentBytes; Future<bool> _readHeader() async { RandomAccessFile file = await new File(path).open(); String magic = await _readLine(file); if (!magic.startsWith(kBundleMagicPrefix)) { file.close(); return false; } signatureBytes = await _readBytesWithLength(file); manifestBytes = await _readBytesWithLength(file); int contentOffset = await file.position(); _openContentStream = () => new File(path).openRead(contentOffset); file.close(); String manifestString = UTF8.decode(manifestBytes); manifest = JSON.decode(manifestString); return true; } static Future<Bundle> readHeader(String path) async { Bundle bundle = new Bundle._fromFile(path); if (!await bundle._readHeader()) return null; return bundle; } // Verifies that the package has a valid signature and content. Future<bool> verifyContent() async { if (!verifyManifestSignature(manifest, manifestBytes, signatureBytes)) return false; Stream<List<int>> content = _openContentStream(); 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); RandomAccessFile outputFile = new File(path).openSync(mode: FileMode.WRITE); outputFile.writeStringSync(kBundleMagic); _writeBytesWithLengthSync(outputFile, signatureBytes); _writeBytesWithLengthSync(outputFile, manifestBytes); outputFile.writeFromSync(_contentBytes); outputFile.close(); } }