bundle.dart 4.72 KB
Newer Older
1 2 3 4 5 6 7 8 9
// 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';

10 11 12 13 14
import 'package:bignum/bignum.dart';

import 'signing.dart';

// Magic string we put at the top of all bundle files.
15
const String kBundleMagic = '#!mojo mojo:flutter\n';
16 17 18 19

// Prefix of the above, used when reading bundle files. This allows us to be
// more flexbile about what we accept.
const String kBundleMagicPrefix = '#!mojo ';
20

Matt Perry's avatar
Matt Perry committed
21 22
typedef Stream<List<int>> StreamOpener();

23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
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;
}

43
// Writes a 32-bit length followed by the content of [bytes].
Matt Perry's avatar
Matt Perry committed
44
void _writeBytesWithLengthSync(RandomAccessFile outputFile, List<int> bytes) {
45 46 47 48
  if (bytes == null)
    bytes = new Uint8List(0);
  assert(bytes.length < 0xffffffff);
  ByteData length = new ByteData(4)..setUint32(0, bytes.length, Endianness.LITTLE_ENDIAN);
Matt Perry's avatar
Matt Perry committed
49 50
  outputFile.writeFromSync(length.buffer.asUint8List());
  outputFile.writeFromSync(bytes);
51 52
}

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
// 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 {
70 71 72 73 74
  Bundle._fromFile(this.path);
  Bundle.fromContent({
    this.path,
    this.manifest,
    contentBytes,
Matt Perry's avatar
Matt Perry committed
75
    AsymmetricKeyPair keyPair: null
76 77 78 79 80 81
  }) : _contentBytes = contentBytes {
    assert(path != null);
    assert(manifest != null);
    assert(_contentBytes != null);
    manifestBytes = serializeManifest(manifest, keyPair?.publicKey, _contentBytes);
    signatureBytes = signManifest(manifestBytes, keyPair?.privateKey);
Matt Perry's avatar
Matt Perry committed
82
    _openContentStream = () => new Stream.fromIterable(<List<int>>[_contentBytes]);
83
  }
84 85 86 87 88

  final String path;
  List<int> signatureBytes;
  List<int> manifestBytes;
  Map<String, dynamic> manifest;
89

Matt Perry's avatar
Matt Perry committed
90 91
  // Callback to open a Stream containing the bundle content data.
  StreamOpener _openContentStream;
92 93 94

  // Zip content bytes. Only valid when created in memory.
  List<int> _contentBytes;
95 96

  Future<bool> _readHeader() async {
97 98 99 100
    RandomAccessFile file = await new File(path).open();
    String magic = await _readLine(file);
    if (!magic.startsWith(kBundleMagicPrefix)) {
      file.close();
101
      return false;
102 103 104
    }
    signatureBytes = await _readBytesWithLength(file);
    manifestBytes = await _readBytesWithLength(file);
Matt Perry's avatar
Matt Perry committed
105 106
    int contentOffset = await file.position();
    _openContentStream = () => new File(path).openRead(contentOffset);
107 108
    file.close();

109 110 111 112 113 114
    String manifestString = UTF8.decode(manifestBytes);
    manifest = JSON.decode(manifestString);
    return true;
  }

  static Future<Bundle> readHeader(String path) async {
115
    Bundle bundle = new Bundle._fromFile(path);
116 117 118 119
    if (!await bundle._readHeader())
      return null;
    return bundle;
  }
120

Matt Perry's avatar
Matt Perry committed
121
  // Verifies that the package has a valid signature and content.
122 123 124 125
  Future<bool> verifyContent() async {
    if (!verifyManifestSignature(manifest, manifestBytes, signatureBytes))
      return false;

Matt Perry's avatar
Matt Perry committed
126
    Stream<List<int>> content = _openContentStream();
127 128 129 130 131 132 133 134 135 136
    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);
Matt Perry's avatar
Matt Perry committed
137
    RandomAccessFile outputFile = new File(path).openSync(mode: FileMode.WRITE);
138
    outputFile.writeStringSync(kBundleMagic);
139 140
    _writeBytesWithLengthSync(outputFile, signatureBytes);
    _writeBytesWithLengthSync(outputFile, manifestBytes);
Matt Perry's avatar
Matt Perry committed
141 142
    outputFile.writeFromSync(_contentBytes);
    outputFile.close();
143
  }
144
}