Skip to content

Commit ffb7166

Browse files
jtmcdolelrhn
authored andcommitted
SHA 512/384 padding is 128 bit (flutter#77)
The SHA algorithms were fine; but the padding in HashSink was hardcoded to 64-bit signatures. While we still only generate a 64-bit signature, the signature space is 128-bit. Fixes flutter#69. Special thanks to @Nico04 for providing the test cases that lead to this discovery.
1 parent 11aff37 commit ffb7166

File tree

6 files changed

+60
-15
lines changed

6 files changed

+60
-15
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 2.1.4
2+
* BugFix: padding was incorrect for some SHA-512/328.
3+
14
## 2.1.3
25
* **Security vulnerability**: Fixed constant-time comparison in `Digest`.
36

lib/src/hash_sink.dart

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,23 @@ abstract class HashSink implements Sink<List<int>> {
4444
/// This should be updated each time [updateHash] is called.
4545
Uint32List get digest;
4646

47+
/// The number of signature bytes emitted at the end of the message.
48+
///
49+
/// An encrypted message is followed by a signature which depends
50+
/// on the encryption algorithm used. This value specifies the
51+
/// number of bytes used by this signature. It must always be
52+
/// a power of 2 and no less than 8.
53+
final int _signatureBytes;
54+
4755
/// Creates a new hash.
4856
///
4957
/// [chunkSizeInWords] represents the size of the input chunks processed by
5058
/// the algorithm, in terms of 32-bit words.
51-
HashSink(this._sink, int chunkSizeInWords, {Endian endian = Endian.big})
59+
HashSink(this._sink, int chunkSizeInWords,
60+
{Endian endian = Endian.big, int signatureBytes = 8})
5261
: _endian = endian,
62+
assert(signatureBytes >= 8),
63+
_signatureBytes = signatureBytes,
5364
_currentChunk = Uint32List(chunkSizeInWords);
5465

5566
/// Runs a single iteration of the hash computation, updating [digest] with
@@ -82,10 +93,12 @@ abstract class HashSink implements Sink<List<int>> {
8293
Uint8List _byteDigest() {
8394
if (_endian == Endian.host) return digest.buffer.asUint8List();
8495

85-
var byteDigest = Uint8List(digest.lengthInBytes);
86-
var byteData = byteDigest.buffer.asByteData();
87-
for (var i = 0; i < digest.length; i++) {
88-
byteData.setUint32(i * bytesPerWord, digest[i]);
96+
// Cache the digest locally as `get` could be expensive.
97+
final cachedDigest = digest;
98+
final byteDigest = Uint8List(cachedDigest.lengthInBytes);
99+
final byteData = byteDigest.buffer.asByteData();
100+
for (var i = 0; i < cachedDigest.length; i++) {
101+
byteData.setUint32(i * bytesPerWord, cachedDigest[i]);
89102
}
90103
return byteDigest;
91104
}
@@ -116,11 +129,14 @@ abstract class HashSink implements Sink<List<int>> {
116129
/// This adds a 1 bit to the end of the message, and expands it with 0 bits to
117130
/// pad it out.
118131
void _finalizeData() {
119-
// Pad out the data with 0x80, eight 0s, and as many more 0s as we need to
120-
// land cleanly on a chunk boundary.
132+
// Pad out the data with 0x80, eight or sixteen 0s, and as many more 0s
133+
// as we need to land cleanly on a chunk boundary.
121134
_pendingData.add(0x80);
122-
var contentsLength = _lengthInBytes + 9;
123-
var finalizedLength = _roundUp(contentsLength, _currentChunk.lengthInBytes);
135+
136+
final contentsLength = _lengthInBytes + 1 /* 0x80 */ + _signatureBytes;
137+
final finalizedLength =
138+
_roundUp(contentsLength, _currentChunk.lengthInBytes);
139+
124140
for (var i = 0; i < finalizedLength - contentsLength; i++) {
125141
_pendingData.add(0);
126142
}
@@ -133,9 +149,11 @@ abstract class HashSink implements Sink<List<int>> {
133149
var lengthInBits = _lengthInBytes * bitsPerByte;
134150

135151
// Add the full length of the input data as a 64-bit value at the end of the
136-
// hash.
137-
var offset = _pendingData.length;
138-
_pendingData.addAll(Uint8List(8));
152+
// hash. Note: we're only writing out 64 bits, so skip ahead 8 if the
153+
// signature is 128-bit.
154+
final offset = _pendingData.length + (_signatureBytes - 8);
155+
156+
_pendingData.addAll(Uint8List(_signatureBytes));
139157
var byteData = _pendingData.buffer.asByteData();
140158

141159
// We're essentially doing byteData.setUint64(offset, lengthInBits, _endian)

lib/src/sha512_fastsinks.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ abstract class _Sha64BitSink extends HashSink {
3030
/// used across invocations of [updateHash].
3131
final _extended = Uint64List(80);
3232

33-
_Sha64BitSink(Sink<Digest> sink, this._digest) : super(sink, 32);
33+
_Sha64BitSink(Sink<Digest> sink, this._digest)
34+
: super(sink, 32, signatureBytes: 16);
3435
// The following helper functions are taken directly from
3536
// http://tools.ietf.org/html/rfc6234.
3637

lib/src/sha512_slowsinks.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ abstract class _Sha64BitSink extends HashSink {
7171
/// used across invocations of [updateHash].
7272
final _extended = Uint32List(160);
7373

74-
_Sha64BitSink(Sink<Digest> sink, this._digest) : super(sink, 32);
74+
_Sha64BitSink(Sink<Digest> sink, this._digest)
75+
: super(sink, 32, signatureBytes: 16);
7576
// The following helper functions are taken directly from
7677
// http://tools.ietf.org/html/rfc6234.
7778

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: crypto
2-
version: 2.1.3
2+
version: 2.1.4
33
author: Dart Team <misc@dartlang.org>
44
description: Library of cryptographic functions.
55
homepage: https://www.github.com/dart-lang/crypto

test/sha512_test.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,27 @@ void main() {
7575
outer.close();
7676
});
7777
});
78+
79+
test('128 bit padding', () {
80+
final salts = [
81+
'AAAA{3FXhiiyc5gGWlRrVQ2RlJ.6xj.DKvf6l0bJxqh0BzA}'.codeUnits,
82+
'AAAA{3FXhiiyc5gGWlRrVQ.2RlJ6xj.DKvf6l0bJxqh0BzA}'.codeUnits,
83+
'AAAA{rFXhiiyc5gGWlVQ.2RlJ6xj.DKvf6lFXhiiyc5gGWl0}'.codeUnits,
84+
];
85+
86+
const results = [
87+
'nYg7eEsF/P7/l1AO0w8JFNNomS1gC76VE7Eg7Dpet+Dh6XiScDntYEU4tVItXp67evaLFvtMpW2uVJBZVKrBPw==',
88+
'TXNM4uk1Iwr2cYisWSdFifXdjfNiJTGEmNaMtqYrwJoS3JXpL1rebPKPfKudbFQGpcgJkLLhhpfnLzULBqq8KA==',
89+
'ckPYMDuPJjc73qHXQZiJgCskNG8mj9cPqFNsqYqxcBbQESgkWChoibAN7ssJrnoMFIpz9HwsBwMtt3z/KDUh9w==',
90+
];
91+
92+
for (int i = 0; i < salts.length; i++) {
93+
var digest = <int>[];
94+
for (int run = 0; run < 2000; run++) {
95+
digest = sha512.convert([]..addAll(digest)..addAll(salts[i])).bytes;
96+
}
97+
expect(base64.encode(digest), results[i]);
98+
}
99+
});
78100
});
79101
}

0 commit comments

Comments
 (0)