1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// 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: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 ';
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,
contentBytes,
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;
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('#!mojo mojo:sky_viewer\n');
_writeBytesWithLengthSync(outputFile, signatureBytes);
_writeBytesWithLengthSync(outputFile, manifestBytes);
outputFile.writeFromSync(_contentBytes);
outputFile.close();
}
}