Make HMAC a Converter.

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//1356403003 .
diff --git a/lib/src/hmac.dart b/lib/src/hmac.dart
index d9eae8c..a11c4b6 100644
--- a/lib/src/hmac.dart
+++ b/lib/src/hmac.dart
@@ -4,10 +4,12 @@
 
 library crypto.hmac;
 
+import 'dart:convert';
 import 'dart:typed_data';
 
 import 'package:typed_data/typed_data.dart';
 
+import 'digest.dart';
 import 'digest_sink.dart';
 import 'hash.dart';
 
@@ -17,26 +19,22 @@
 ///
 /// HMAC allows messages to be cryptographically authenticated using any
 /// iterated cryptographic hash function.
-///
-/// The message's data is added using [add]. Once it's been fully added, the
-/// [digest] and [close] methods can be used to extract the message
-/// authentication digest.
-///
-/// If an expected authentication digest is available, the [verify] method may
-/// also be used to ensure that the message actually corresponds to that digest.
-// TODO(floitsch): make HMAC implement Sink, EventSink or similar.
-class HMAC {
-  /// The bytes from the message so far.
-  final _message = new Uint8Buffer();
-
+class HMAC extends Converter<List<int>, Digest> {
   /// The hash function used to compute the authentication digest.
   final Hash _hash;
 
   /// The secret key shared by the sender and the receiver.
   final Uint8List _key;
 
-  /// Whether [close] has been called.
-  bool _isClosed = false;
+  /// The bytes from the message so far.
+  final _message = new Uint8Buffer();
+
+  /// The sink for implementing the deprecated APIs that involved adding data
+  /// directly to the [HMAC] instance.
+  _HmacSink _sink;
+
+  /// The sink that [_sink] sends the [Digest] to once it finishes hashing.
+  DigestSink _innerSink;
 
   /// Create an [HMAC] object from a [Hash] and a binary key.
   ///
@@ -51,52 +49,60 @@
     // If [key] is shorter than the block size, the rest of [_key] will be
     // 0-padded.
     _key.setRange(0, key.length, key);
+
+    _innerSink = new DigestSink();
+    _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) =>
+      new _HmacSink(sink, _hash, _key);
+
   /// Adds a list of bytes to the message.
   ///
   /// If [this] has already been closed, throws a [StateError].
+  @Deprecated("Expires in 1.0.0. Use HMAC.convert() or "
+      "HMAC.startChunkedConversion() instead.")
   void add(List<int> data) {
-    if (_isClosed) throw new StateError("HMAC is closed");
     _message.addAll(data);
-  }
-
-  /// Returns the digest of the message so far, as a list of bytes.
-  List<int> get digest {
-    // Compute inner padding.
-    var padding = new Uint8List(_key.length);
-    for (var i = 0; i < padding.length; i++) {
-      padding[i] = 0x36 ^ _key[i];
-    }
-
-    // Inner hash computation.
-    var innerDigest = _hashWithPadding(padding, _message);
-
-    // Compute outer padding.
-    for (var i = 0; i < padding.length; i++) {
-      padding[i] = 0x5c ^ _key[i];
-    }
-
-    // Outer hash computation which is the result.
-    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;
+    _sink.add(data);
   }
 
   /// Closes [this] and returns the digest of the message as a list of bytes.
   ///
   /// Once closed, [add] may no longer be called.
+  @Deprecated("Expires in 1.0.0. Use HMAC.convert() or "
+      "HMAC.startChunkedConversion() instead.")
   List<int> close() {
-    _isClosed = true;
-    return digest;
+    _sink.close();
+    return _innerSink.value.bytes;
+  }
+
+  /// Returns the digest of the message so far, as a list of bytes.
+  @Deprecated("Expires in 1.0.0. Use HMAC.convert() or "
+      "HMAC.startChunkedConversion() instead.")
+  List<int> get digest {
+    if (_sink._isClosed) return _innerSink.value.bytes;
+
+    // This may be called at any point while the message is being hashed, but
+    // the [_HmacSink] only supports getting the value once. To make this work,
+    // we just re-hash everything after we get the digest. It's redundant, but
+    // this API is deprecated anyway.
+    _sink.close();
+    var bytes = _innerSink.value.bytes;
+
+    _innerSink = new DigestSink();
+    _sink = _hash.startChunkedConversion(_innerSink);
+    _sink.add(_message);
+
+    return bytes;
   }
 
   /// Returns whether the digest computed for the data so far matches the given
@@ -107,6 +113,7 @@
   ///
   /// Throws an [ArgumentError] if the given digest does not have the same size
   /// as the digest computed by [this].
+  @Deprecated("Expires in 1.0.0. Use Digest.==() instead.")
   bool verify(List<int> digest) {
     var computedDigest = this.digest;
     if (digest.length != computedDigest.length) {
@@ -122,3 +129,55 @@
     return result == 0;
   }
 }
+
+/// The concrete implementation of the HMAC algorithm.
+class _HmacSink extends ByteConversionSink {
+  /// The sink for the outer hash computation.
+  final ByteConversionSink _outerSink;
+
+  /// The sink that [_innerSink]'s result will be added to when it's available.
+  final _innerResultSink = new DigestSink();
+
+  /// The sink for the inner hash computation.
+  ByteConversionSink _innerSink;
+
+  /// Whether [close] has been called.
+  bool _isClosed = false;
+
+  _HmacSink(Sink<Digest> sink, Hash hash, List<int> key)
+      : _outerSink = hash.startChunkedConversion(sink) {
+    _innerSink = hash.startChunkedConversion(_innerResultSink);
+
+    // Compute outer padding.
+    var padding = new Uint8List(key.length);
+    for (var i = 0; i < padding.length; i++) {
+      padding[i] = 0x5c ^ key[i];
+    }
+    _outerSink.add(padding);
+
+    // Compute inner padding.
+    for (var i = 0; i < padding.length; i++) {
+      padding[i] = 0x36 ^ key[i];
+    }
+    _innerSink.add(padding);
+  }
+
+  void add(List<int> data) {
+    if (_isClosed) throw new StateError("HMAC is closed");
+    _innerSink.add(data);
+  }
+
+  void addSlice(List<int> data, int start, int end, bool isLast) {
+    if (_isClosed) throw new StateError("HMAC is closed");
+    _innerSink.addSlice(data, start, end, isLast);
+  }
+
+  void close() {
+    if (_isClosed) return;
+    _isClosed = true;
+
+    _innerSink.close();
+    _outerSink.add(_innerResultSink.value.bytes);
+    _outerSink.close();
+  }
+}
diff --git a/test/utils.dart b/test/utils.dart
index d65f0eb..82b9f8a 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -11,6 +11,5 @@
 /// Asserts that an HMAC using [hash] returns [mac] for [input] and [key].
 void expectHmacEquals(Hash hash, List<int> input, List<int> key, String mac) {
   var hmac = new HMAC(hash, key);
-  hmac.add(input);
-  expect(CryptoUtils.bytesToHex(hmac.close()), startsWith(mac));
+  expect(hmac.convert(input).toString(), startsWith(mac));
 }