Change Hash subclasses to be Converters.

To make it easier to migrate packages to the new API, I plan to release
one more 0.9.2 release with a bunch of new APIs but no old functionality
removed. A bunch of stuff will be deprecated, though, and it will be
immediately followed by a 1.0.0 release that removes all the deprecated
APIs.

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//1355223002 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ccbe390..8efbd20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,24 @@
-# Changelog
+## 0.9.2
 
-## next
+* `Hash`, `MD5`, `SHA1`, and `SHA256` now implement `Converter`. They convert
+  between `List<int>`s and the new `Digest` class, which represents a hash
+  digest. The `Converter` APIs—`Hash.convert()` and
+  `Hash.startChunkedConversion`—should be used in preference to the old APIs,
+  which are now deprecated.
 
-* Hashing (md5, sha1, sha256) now works correctly for input sizes
-  up to 64 bits.
-* Small example added to generate message digests for files.
+* Top-level `sha1`, `sha256`, and `md5` fields have been added to make it easier
+  to use those hash algorithms without having to instantiate new instances.
+
+* Hashing now works correctly for input sizes up to 2^64 bytes.
+
+### Deprecations
+
+* `Hash.add`, `Hash.close`, and `Hash.newInstance` are deprecated.
+  `Hash.convert` should be used for hashing single values, and
+  `Hash.startChunkedConversion` should be used for hashing streamed values.
+  
+* `new SHA1()`, `new SHA256()`, and `new MD5()` are deprecated. Use the
+  top-level `sha1`, `sha256`, and `md5` fields instead.
 
 ## 0.9.1
 
diff --git a/lib/src/digest.dart b/lib/src/digest.dart
index 02af614..8dafced 100644
--- a/lib/src/digest.dart
+++ b/lib/src/digest.dart
@@ -4,8 +4,6 @@
 
 library crypto.digest;
 
-import 'dart:typed_data';
-
 import 'crypto_utils.dart';
 
 /// A message digest as computed by a [Hash] or [HMAC] function.
@@ -13,8 +11,7 @@
   /// The message digest as an array of bytes.
   final List<int> bytes;
 
-  Digest(List<int> bytes)
-      : bytes = new Uint8List.fromList(bytes);
+  Digest(this.bytes);
 
   /// Returns whether this is equal to another digest.
   ///
@@ -22,11 +19,13 @@
   /// information via timing.
   bool operator ==(Object other) {
     if (other is! Digest) return false;
-    if (other.bytes.length != bytes.length) return false;
+
+    var digest = other as Digest;
+    if (digest.bytes.length != bytes.length) return false;
 
     var result = 0;
     for (var i = 0; i < bytes.length; i++) {
-      result |= bytes[i] ^ other.bytes[i];
+      result |= bytes[i] ^ digest.bytes[i];
     }
     return result == 0;
   }
diff --git a/lib/src/digest_sink.dart b/lib/src/digest_sink.dart
new file mode 100644
index 0000000..bb5c85f
--- /dev/null
+++ b/lib/src/digest_sink.dart
@@ -0,0 +1,29 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library crypto.digest_sink;
+
+import 'digest.dart';
+
+/// A sink used to get a digest value out of [Hash.startChunkedConversion].
+class DigestSink extends Sink<Digest> {
+  /// The value added to the sink, if any.
+  Digest get value {
+    assert(_value != null);
+    return _value;
+  }
+  Digest _value;
+
+  /// Adds [value] to the sink.
+  ///
+  /// Unlike most sinks, this may only be called once.
+  void add(Digest value) {
+    assert(_value == null);
+    _value = value;
+  }
+
+  void close() {
+    assert(_value != null);
+  }
+}
diff --git a/lib/src/hash.dart b/lib/src/hash.dart
index bb228fa..6f9b33b 100644
--- a/lib/src/hash.dart
+++ b/lib/src/hash.dart
@@ -4,30 +4,61 @@
 
 library crypto.hash;
 
+import 'dart:convert';
+
+import 'digest.dart';
+import 'digest_sink.dart';
+
 /// An interface for cryptographic hash functions.
 ///
-/// The [add] method adds data to the hash. The [close] method extracts the
-/// message digest.
-///
-/// If multiple instances of a given Hash is needed, the [newInstance] method
-/// can provide a new instance.
-// TODO(floitsch): make Hash implement Sink, EventSink or similar.
-abstract class Hash {
-  /// Add a list of bytes to the hash computation.
-  ///
-  /// If [this] has already been closed, throws a [StateError].
-  void add(List<int> data);
-
-  /// Finish the hash computation and extract the message digest as a list of
-  /// bytes.
-  List<int> close();
-
-  /// Returns a new instance of this hash function.
-  Hash newInstance();
-
+/// Every hash is a converter that takes a list of ints and returns a single
+/// digest. When used in chunked mode, it will only ever add one digest to the
+/// inner [Sink].
+abstract class Hash extends Converter<List<int>, Digest> {
   /// The internal block size of the hash in bytes.
   ///
   /// This is exposed for use by the [HMAC] class, which needs to know the block
   /// size for the [Hash] it uses.
   int get blockSize;
+
+  /// The sink for implementing the deprecated APIs that involved adding data
+  /// directly to the [Hash] instance.
+  ByteConversionSink _sink;
+
+  /// The sink that [_sink] sends the [Digest] to once it finishes hashing.
+  final DigestSink _innerSink = new DigestSink();
+
+  Hash() {
+    _sink = startChunkedConversion(_innerSink);
+  }
+
+  Digest convert(List<int> data) {
+    var innerSink = new DigestSink();
+    var outerSink = startChunkedConversion(innerSink);
+    outerSink.add(data);
+    outerSink.close();
+    return innerSink.value;
+  }
+
+  ByteConversionSink startChunkedConversion(Sink<Digest> sink);
+
+  /// Returns a new instance of this hash function.
+  @Deprecated("Expires in 1.0.0. Use Hash.startChunkedConversion() instead.")
+  Hash newInstance();
+
+  /// Add a list of bytes to the hash computation.
+  ///
+  /// If [this] has already been closed, throws a [StateError].
+  @Deprecated("Expires in 1.0.0. Use Hash.convert() or "
+      "Hash.startChunkedConversion() instead.")
+  void add(List<int> data) => _sink.add(data);
+
+  /// Finish the hash computation and extract the message digest as a list of
+  /// bytes.
+  @Deprecated("Expires in 1.0.0. Use Hash.convert() or "
+      "Hash.startChunkedConversion() instead.")
+  List<int> close() {
+    _sink.close();
+    return _innerSink.value.bytes;
+  }
 }
diff --git a/lib/src/hash_base.dart b/lib/src/hash_sink.dart
similarity index 91%
rename from lib/src/hash_base.dart
rename to lib/src/hash_sink.dart
index 8cd2ad5..c5986e5 100644
--- a/lib/src/hash_base.dart
+++ b/lib/src/hash_sink.dart
@@ -8,13 +8,17 @@
 
 import 'package:typed_data/typed_data.dart';
 
+import 'digest.dart';
 import 'hash.dart';
 import 'utils.dart';
 
-/// A base class for [Hash] implementations.
+/// A base class for [Sink] implementations for hash algorithms.
 ///
 /// Subclasses should override [updateHash] and [digest].
-abstract class HashBase implements Hash {
+abstract class HashSink implements Sink<List<int>> {
+  /// The inner sink that this should forward to.
+  final Sink<Digest> _sink;
+
   /// Whether the hash function operates on big-endian words.
   final Endianness _endian;
 
@@ -38,13 +42,12 @@
   /// This should be updated each time [updateHash] is called.
   Uint32List get digest;
 
-  int get blockSize => _currentChunk.lengthInBytes;
-
   /// Creates a new hash.
   ///
   /// [chunkSizeInWords] represents the size of the input chunks processed by
   /// the algorithm, in terms of 32-bit words.
-  HashBase(int chunkSizeInWords, {Endianness endian: Endianness.BIG_ENDIAN})
+  HashSink(this._sink, int chunkSizeInWords,
+          {Endianness endian: Endianness.BIG_ENDIAN})
       : _endian = endian,
         _currentChunk = new Uint32List(chunkSizeInWords);
 
@@ -62,14 +65,14 @@
     _iterate();
   }
 
-  List<int> close() {
-    if (_isClosed) return _byteDigest();
+  void close() {
+    if (_isClosed) return;
     _isClosed = true;
 
     _finalizeData();
     _iterate();
     assert(_pendingData.isEmpty);
-    return _byteDigest();
+    _sink.add(new Digest(_byteDigest()));
   }
 
   Uint8List _byteDigest() {
diff --git a/lib/src/hmac.dart b/lib/src/hmac.dart
index 169e1fe..d9eae8c 100644
--- a/lib/src/hmac.dart
+++ b/lib/src/hmac.dart
@@ -8,6 +8,7 @@
 
 import 'package:typed_data/typed_data.dart';
 
+import 'digest_sink.dart';
 import 'hash.dart';
 
 /// An implementation of [keyed-hash method authentication codes][rfc].
@@ -29,7 +30,7 @@
   final _message = new Uint8Buffer();
 
   /// The hash function used to compute the authentication digest.
-  Hash _hash;
+  final Hash _hash;
 
   /// The secret key shared by the sender and the receiver.
   final Uint8List _key;
@@ -45,11 +46,7 @@
       : _hash = hash,
         _key = new Uint8List(hash.blockSize) {
     // Hash the key if it's longer than the block size of the hash.
-    if (key.length > _hash.blockSize) {
-      _hash = _hash.newInstance();
-      _hash.add(key);
-      key = _hash.close();
-    }
+    if (key.length > _hash.blockSize) key = _hash.convert(key).bytes;
 
     // If [key] is shorter than the block size, the rest of [_key] will be
     // 0-padded.
@@ -73,10 +70,7 @@
     }
 
     // Inner hash computation.
-    _hash = _hash.newInstance();
-    _hash.add(padding);
-    _hash.add(_message);
-    var innerDigest = _hash.close();
+    var innerDigest = _hashWithPadding(padding, _message);
 
     // Compute outer padding.
     for (var i = 0; i < padding.length; i++) {
@@ -84,10 +78,17 @@
     }
 
     // Outer hash computation which is the result.
-    _hash = _hash.newInstance();
-    _hash.add(padding);
-    _hash.add(innerDigest);
-    return _hash.close();
+    return _hashWithPadding(padding, innerDigest);
+  }
+
+  /// Returns the digest of [padding] followed by [data].
+  List<int> _hashWithPadding(List<int> padding, List<int> data) {
+    var innerSink = new DigestSink();
+    _hash.startChunkedConversion(innerSink)
+      ..add(padding)
+      ..add(data)
+      ..close();
+    return innerSink.value.bytes;
   }
 
   /// Closes [this] and returns the digest of the message as a list of bytes.
diff --git a/lib/src/md5.dart b/lib/src/md5.dart
index 3cae7fc..1964bc2 100644
--- a/lib/src/md5.dart
+++ b/lib/src/md5.dart
@@ -4,22 +4,43 @@
 
 library crypto.md5;
 
+import 'dart:convert';
 import 'dart:typed_data';
 
+import 'digest.dart';
 import 'hash.dart';
-import 'hash_base.dart';
+import 'hash_sink.dart';
 import 'utils.dart';
 
+/// An instance of [MD5].
+///
+/// This instance provides convenient access to the [MD5][rfc] hash function.
+///
+/// [rfc]: https://tools.ietf.org/html/rfc1321
+///
+/// **Warning**: MD5 has known collisions and should only be used when required
+/// for backwards compatibility.
+final md5 = new MD5._();
+
 /// An implementation of the [MD5][rfc] hash function.
 ///
 /// [rfc]: https://tools.ietf.org/html/rfc1321
 ///
 /// **Warning**: MD5 has known collisions and should only be used when required
 /// for backwards compatibility.
-abstract class MD5 implements Hash {
-  factory MD5() = _MD5;
+///
+/// Note that it's almost always easier to use [md5] rather than creating a new
+/// instance.
+class MD5 extends Hash {
+  final int blockSize = 16 * bytesPerWord;
 
-  MD5 newInstance();
+  @Deprecated("Use the md5 field instead.")
+  MD5();
+
+  MD5 newInstance() => new MD5._();
+
+  ByteConversionSink startChunkedConversion(Sink<Digest> sink) =>
+      new ByteConversionSink.from(new _MD5Sink(sink));
 }
 
 /// Data from a non-linear mathematical function that functions as
@@ -50,18 +71,17 @@
 ///
 /// This is separate so that it can extend [HashBase] without leaking additional
 /// public memebers.
-class _MD5 extends HashBase implements MD5 {
+class _MD5Sink extends HashSink {
   final digest = new Uint32List(4);
 
-  _MD5() : super(16, endian: Endianness.LITTLE_ENDIAN) {
+  _MD5Sink(Sink<Digest> sink)
+      : super(sink, 16, endian: Endianness.LITTLE_ENDIAN) {
     digest[0] = 0x67452301;
     digest[1] = 0xefcdab89;
     digest[2] = 0x98badcfe;
     digest[3] = 0x10325476;
   }
 
-  MD5 newInstance() => new _MD5();
-
   void updateHash(Uint32List chunk) {
     assert(chunk.length == 16);
 
diff --git a/lib/src/sha1.dart b/lib/src/sha1.dart
index 098b0a8..300147f 100644
--- a/lib/src/sha1.dart
+++ b/lib/src/sha1.dart
@@ -4,26 +4,44 @@
 
 library crypto.sha1;
 
+import 'dart:convert';
 import 'dart:typed_data';
 
+import 'digest.dart';
 import 'hash.dart';
-import 'hash_base.dart';
+import 'hash_sink.dart';
 import 'utils.dart';
 
+/// An instance of [SHA1].
+///
+/// This instance provides convenient access to the [SHA1][rfc] hash function.
+///
+/// [rfc]: http://tools.ietf.org/html/rfc3174
+final sha1 = new SHA1();
+
 /// An implementation of the [SHA-1][rfc] hash function.
 ///
 /// [rfc]: http://tools.ietf.org/html/rfc3174
-abstract class SHA1 implements Hash {
-  factory SHA1() = _SHA1;
+///
+/// Note that it's almost always easier to use [sha1] rather than creating a new
+/// instance.
+class SHA1 extends Hash {
+  final int blockSize = 16 * bytesPerWord;
 
-  SHA1 newInstance();
+  @Deprecated("Use the sha1 field instead.")
+  SHA1();
+
+  SHA1 newInstance() => new SHA1();
+
+  ByteConversionSink startChunkedConversion(Sink<Digest> sink) =>
+      new ByteConversionSink.from(new _SHA1Sink(sink));
 }
 
 /// The concrete implementation of [SHA1].
 ///
 /// This is separate so that it can extend [HashBase] without leaking additional
 /// public memebers.
-class _SHA1 extends HashBase implements SHA1 {
+class _SHA1Sink extends HashSink {
   final digest = new Uint32List(5);
 
   /// The sixteen words from the original chunk, extended to 80 words.
@@ -32,9 +50,9 @@
   /// used across invocations of [updateHash].
   final Uint32List _extended;
 
-  _SHA1()
+  _SHA1Sink(Sink<Digest> sink)
       : _extended = new Uint32List(80),
-        super(16) {
+        super(sink, 16) {
     digest[0] = 0x67452301;
     digest[1] = 0xEFCDAB89;
     digest[2] = 0x98BADCFE;
@@ -42,8 +60,6 @@
     digest[4] = 0xC3D2E1F0;
   }
 
-  SHA1 newInstance() => new _SHA1();
-
   void updateHash(Uint32List chunk) {
     assert(chunk.length == 16);
 
diff --git a/lib/src/sha256.dart b/lib/src/sha256.dart
index ca2f337..09c3c69 100644
--- a/lib/src/sha256.dart
+++ b/lib/src/sha256.dart
@@ -4,19 +4,37 @@
 
 library crypto.sha256;
 
+import 'dart:convert';
 import 'dart:typed_data';
 
+import 'digest.dart';
 import 'hash.dart';
-import 'hash_base.dart';
+import 'hash_sink.dart';
 import 'utils.dart';
 
+/// An instance of [SHA256].
+///
+/// This instance provides convenient access to the [SHA256][rfc] hash function.
+///
+/// [rfc]: http://tools.ietf.org/html/rfc6234
+final sha256 = new SHA256();
+
 /// An implementation of the [SHA-256][rfc] hash function.
 ///
 /// [rfc]: http://tools.ietf.org/html/rfc6234
-abstract class SHA256 implements Hash {
-  factory SHA256() = _SHA256;
+///
+/// Note that it's almost always easier to use [sha256] rather than creating a
+/// new instance.
+class SHA256 extends Hash {
+  final int blockSize = 16 * bytesPerWord;
 
-  SHA256 newInstance();
+  @Deprecated("Use the sha256 field instead.")
+  SHA256();
+
+  SHA256 newInstance() => new SHA256();
+
+  ByteConversionSink startChunkedConversion(Sink<Digest> sink) =>
+      new ByteConversionSink.from(new _SHA256Sink(sink));
 }
 
 /// Data from a non-linear function that functions as reproducible noise.
@@ -38,7 +56,7 @@
 ///
 /// This is separate so that it can extend [HashBase] without leaking additional
 /// public memebers.
-class _SHA256 extends HashBase implements SHA256 {
+class _SHA256Sink extends HashSink {
   final digest = new Uint32List(8);
 
   /// The sixteen words from the original chunk, extended to 64 words.
@@ -47,9 +65,9 @@
   /// used across invocations of [updateHash].
   final Uint32List _extended;
 
-  _SHA256()
+  _SHA256Sink(Sink<Digest> sink)
       : _extended = new Uint32List(64),
-        super(16) {
+        super(sink, 16) {
     // Initial value of the hash parts. First 32 bits of the fractional parts
     // of the square roots of the first 8 prime numbers.
     digest[0] = 0x6a09e667;
@@ -62,8 +80,6 @@
     digest[7] = 0x5be0cd19;
   }
 
-  SHA256 newInstance() => new _SHA256();
-
   // The following helper functions are taken directly from
   // http://tools.ietf.org/html/rfc6234.
 
diff --git a/test/sha1_test.dart b/test/sha1_test.dart
index e76c9ae..e3471c3 100644
--- a/test/sha1_test.dart
+++ b/test/sha1_test.dart
@@ -2,33 +2,48 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-// Library tag to allow dartium to run the test.
-library sha1_test;
+import "dart:async";
 
 import "package:crypto/crypto.dart";
 import "package:test/test.dart";
 
 void main() {
-  test('add may not be called after close', () {
-    var sha = new SHA1();
-    sha.close();
-    expect(() => sha.add([0]), throwsStateError);
+  group("with the old API", () {
+    test('add may not be called after close', () {
+      var sha = new SHA1();
+      sha.close();
+      expect(() => sha.add([0]), throwsStateError);
+    });
+
+    test('close returns the same digest repeatedly', () {
+      var sha = new SHA1();
+      var digest = sha.close();
+      expect(sha.close(), equals(digest));
+      expect(sha.close(), equals(digest));
+      expect(sha.close(), equals(digest));
+    });
   });
 
-  test('close returns the same digest repeatedly', () {
-    var sha = new SHA1();
-    var digest = sha.close();
-    expect(sha.close(), equals(digest));
-    expect(sha.close(), equals(digest));
-    expect(sha.close(), equals(digest));
+  group("with a chunked converter", () {
+    test('add may not be called after close', () {
+      var sink = sha1.startChunkedConversion(new StreamController().sink);
+      sink.close();
+      expect(() => sink.add([0]), throwsStateError);
+    });
+
+    test('close may be called multiple times', () {
+      var sink = sha1.startChunkedConversion(new StreamController().sink);
+      sink.close();
+      sink.close();
+      sink.close();
+      sink.close();
+    });
   });
 
   group("standard vector", () {
     for (var i = 0; i < _inputs.length; i++) {
       test(_digests[i], () {
-        var hash = new SHA1();
-        hash.add(_inputs[i]);
-        expect(CryptoUtils.bytesToHex(hash.close()), equals(_digests[i]));
+        expect(sha1.convert(_inputs[i]).toString(), equals(_digests[i]));
       });
     }
   });
diff --git a/test/sha256_test.dart b/test/sha256_test.dart
index dc52d10..c2b6d3e 100644
--- a/test/sha256_test.dart
+++ b/test/sha256_test.dart
@@ -2,8 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-// Library tag to allow Dartium to run the tests.
-library sha256_test;
+import "dart:async";
 
 import "package:test/test.dart";
 import "package:crypto/crypto.dart";
@@ -11,26 +10,42 @@
 import "utils.dart";
 
 void main() {
-  test('add may not be called after close', () {
-    var sha = new SHA256();
-    sha.close();
-    expect(() => sha.add([0]), throwsStateError);
+  group("with the old API", () {
+    test('add may not be called after close', () {
+      var sha = new SHA256();
+      sha.close();
+      expect(() => sha.add([0]), throwsStateError);
+    });
+
+    test('close returns the same digest repeatedly', () {
+      var sha = new SHA256();
+      var digest = sha.close();
+      expect(sha.close(), equals(digest));
+      expect(sha.close(), equals(digest));
+      expect(sha.close(), equals(digest));
+    });
   });
 
-  test('close returns the same digest repeatedly', () {
-    var sha = new SHA256();
-    var digest = sha.close();
-    expect(sha.close(), equals(digest));
-    expect(sha.close(), equals(digest));
-    expect(sha.close(), equals(digest));
+  group("with a chunked converter", () {
+    test('add may not be called after close', () {
+      var sink = sha256.startChunkedConversion(new StreamController().sink);
+      sink.close();
+      expect(() => sink.add([0]), throwsStateError);
+    });
+
+    test('close may be called multiple times', () {
+      var sink = sha256.startChunkedConversion(new StreamController().sink);
+      sink.close();
+      sink.close();
+      sink.close();
+      sink.close();
+    });
   });
 
   group("standard vector", () {
     for (var i = 0; i < _inputs.length; i++) {
       test(_digests[i], () {
-        var hash = new SHA256();
-        hash.add(_inputs[i]);
-        expect(CryptoUtils.bytesToHex(hash.close()), equals(_digests[i]));
+        expect(sha256.convert(_inputs[i]).toString(), equals(_digests[i]));
       });
     }
   });