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]));
});
}
});