416 lines
15 KiB
Go
416 lines
15 KiB
Go
|
// Copyright (c) 2024 The Decred developers
|
||
|
// Use of this source code is governed by an ISC
|
||
|
// license that can be found in the LICENSE file.
|
||
|
//
|
||
|
// Main Go code originally written and optimized by Dave Collins May 2020.
|
||
|
// Additional cleanup and comments added July 2024.
|
||
|
|
||
|
// Package blake256 implements BLAKE-256 and BLAKE-224 with SSE2, SSE4.1, and
|
||
|
// AVX acceleration and zero allocations.
|
||
|
package blake256
|
||
|
|
||
|
import (
|
||
|
"encoding/binary"
|
||
|
"fmt"
|
||
|
|
||
|
"github.com/decred/dcrd/crypto/blake256/internal/compress"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
// BlockSize is the block size of the hash algorithm in bytes.
|
||
|
BlockSize = 64
|
||
|
|
||
|
// Size is the size of a BLAKE-256 hash in bytes.
|
||
|
Size = 32
|
||
|
|
||
|
// Size224 is the size of a BLAKE-224 hash in bytes.
|
||
|
Size224 = 28
|
||
|
|
||
|
// SavedStateSize is the number of bytes of a serialized intermediate state.
|
||
|
SavedStateSize = 128
|
||
|
)
|
||
|
|
||
|
// pad provides an efficient means to pad a message.
|
||
|
var pad = [64]byte{0x80}
|
||
|
|
||
|
// hasher implements a zero-allocation rolling BLAKE checksum. It can safely be
|
||
|
// copied at any point to save its internal state for use in additional
|
||
|
// processing later, without having to write the previously written data again.
|
||
|
//
|
||
|
// It contains the common logic between BLAKE-224 and BLAKE-256.
|
||
|
type hasher struct {
|
||
|
state compress.State // the current chain value and salt
|
||
|
count uint64 // running total of message bits hashed
|
||
|
buf [BlockSize]byte // partial block data buffer
|
||
|
nbuf uint32 // number of bytes written to data buffer
|
||
|
}
|
||
|
|
||
|
// makeHasher returns an instance of a rolling hasher initialized with the
|
||
|
// provided chain value.
|
||
|
func makeHasher(cv [8]uint32) hasher {
|
||
|
return hasher{state: compress.State{CV: cv}}
|
||
|
}
|
||
|
|
||
|
// reset resets the state of the rolling hash.
|
||
|
func (h *hasher) reset(iv [8]uint32) {
|
||
|
h.state.CV = iv
|
||
|
h.count = 0
|
||
|
h.nbuf = 0
|
||
|
}
|
||
|
|
||
|
// initializeSalt initialize the hasher state with the provided salt. Note that
|
||
|
// this must only be done when first creating the hasher state for correct
|
||
|
// results.
|
||
|
//
|
||
|
// It will panic if the provided salt is not 16 bytes.
|
||
|
func (h *hasher) initializeSalt(salt []byte) {
|
||
|
if len(salt) != 16 {
|
||
|
panic("salt length must be 16 bytes")
|
||
|
}
|
||
|
h.state.S[0] = binary.BigEndian.Uint32(salt)
|
||
|
h.state.S[1] = binary.BigEndian.Uint32(salt[4:])
|
||
|
h.state.S[2] = binary.BigEndian.Uint32(salt[8:])
|
||
|
h.state.S[3] = binary.BigEndian.Uint32(salt[12:])
|
||
|
}
|
||
|
|
||
|
// write adds the given bytes to the rolling hash.
|
||
|
//
|
||
|
// NOTE: This method only returns an error in order to satisfy the [io.Writer]
|
||
|
// and [hash.Hash] interfaces. However, it will never error, meaning the error
|
||
|
// will always be nil, so it is safe to ignore.
|
||
|
func (h *hasher) write(b []byte) (int, error) {
|
||
|
// All bytes will be written.
|
||
|
totalWritten := len(b)
|
||
|
|
||
|
// When a partial block exists and adding the new data would meet or exceed
|
||
|
// the size of a block, fill up the partial block and compress it.
|
||
|
if h.nbuf > 0 && h.nbuf+uint32(len(b)) >= BlockSize {
|
||
|
written := uint32(copy(h.buf[h.nbuf:], b))
|
||
|
h.count += BlockSize << 3
|
||
|
compress.Blocks(&h.state, h.buf[:], h.count)
|
||
|
b = b[written:]
|
||
|
h.nbuf = 0
|
||
|
}
|
||
|
|
||
|
// The previous section ensures there is no partial block data remaining.
|
||
|
//
|
||
|
// Use that fact to compress full blocks directly when the remaining number
|
||
|
// of bytes to write will completely fill one or more additional blocks.
|
||
|
//
|
||
|
// It is perhaps also worth noting that this approach is used over having a
|
||
|
// compression function that only accepts a single block because it provides
|
||
|
// a rather significant speed advantage on inputs that are larger than the
|
||
|
// size of a couple of blocks while only having a negligible impact on small
|
||
|
// inputs.
|
||
|
if len(b) >= BlockSize {
|
||
|
h.count += BlockSize << 3
|
||
|
compress.Blocks(&h.state, b, h.count)
|
||
|
|
||
|
// Update the count of message bits hashed and slice of remaining
|
||
|
// unwritten bytes to account for the total number of blocks compressed.
|
||
|
bytesHashed := uint64(len(b) &^ (BlockSize - 1))
|
||
|
h.count += (bytesHashed - BlockSize) << 3
|
||
|
b = b[bytesHashed:]
|
||
|
}
|
||
|
|
||
|
// Write any remaining bytes to the next partial block. Note the number of
|
||
|
// remaining bytes is guaranteed to be less than the size of a full block
|
||
|
// due to the previous sections.
|
||
|
if len(b) > 0 {
|
||
|
h.nbuf += uint32(copy(h.buf[h.nbuf:], b))
|
||
|
}
|
||
|
|
||
|
return totalWritten, nil
|
||
|
}
|
||
|
|
||
|
// writeByte adds the given byte to the rolling hash.
|
||
|
func (h *hasher) writeByte(b byte) {
|
||
|
var buf [1]byte
|
||
|
buf[0] = b
|
||
|
h.write(buf[:])
|
||
|
}
|
||
|
|
||
|
// writeString adds the given string to the rolling hash.
|
||
|
func (h *hasher) writeString(s string) {
|
||
|
h.write([]byte(s))
|
||
|
}
|
||
|
|
||
|
// writeUint16LE encodes the given unsigned 16-bit integer as a 2-byte
|
||
|
// little-endian byte sequence and adds it to the rolling hash.
|
||
|
func (h *hasher) writeUint16LE(v uint16) {
|
||
|
var buf [2]byte
|
||
|
binary.LittleEndian.PutUint16(buf[:], v)
|
||
|
h.write(buf[:])
|
||
|
}
|
||
|
|
||
|
// writeUint16BE encodes the given unsigned 16-bit integer as a 2-byte
|
||
|
// big-endian byte sequence and adds it to the rolling hash.
|
||
|
func (h *hasher) writeUint16BE(v uint16) {
|
||
|
var buf [2]byte
|
||
|
binary.BigEndian.PutUint16(buf[:], v)
|
||
|
h.write(buf[:])
|
||
|
}
|
||
|
|
||
|
// writeUint32LE encodes the given unsigned 32-bit integer as a 4-byte
|
||
|
// little-endian byte sequence and adds it to the rolling hash.
|
||
|
func (h *hasher) writeUint32LE(v uint32) {
|
||
|
var buf [4]byte
|
||
|
binary.LittleEndian.PutUint32(buf[:], v)
|
||
|
h.write(buf[:])
|
||
|
}
|
||
|
|
||
|
// writeUint32BE encodes the given unsigned 32-bit integer as a 4-byte
|
||
|
// big-endian byte sequence and adds it to the rolling hash.
|
||
|
func (h *hasher) writeUint32BE(v uint32) {
|
||
|
var buf [4]byte
|
||
|
binary.BigEndian.PutUint32(buf[:], v)
|
||
|
h.write(buf[:])
|
||
|
}
|
||
|
|
||
|
// writeUint64LE encodes the given unsigned 64-bit integer as an 8-byte
|
||
|
// little-endian byte sequence and adds it to the rolling hash.
|
||
|
func (h *hasher) writeUint64LE(v uint64) {
|
||
|
var buf [8]byte
|
||
|
binary.LittleEndian.PutUint64(buf[:], v)
|
||
|
h.write(buf[:])
|
||
|
}
|
||
|
|
||
|
// writeUint64BE encodes the given unsigned 64-bit integer as an 8-byte
|
||
|
// big-endian byte sequence and adds it to the rolling hash.
|
||
|
func (h *hasher) writeUint64BE(v uint64) {
|
||
|
var buf [8]byte
|
||
|
binary.BigEndian.PutUint64(buf[:], v)
|
||
|
h.write(buf[:])
|
||
|
}
|
||
|
|
||
|
// finalize finalizes of the rolling hash by writing any remaining partial block
|
||
|
// data and appending the necessary padding.
|
||
|
//
|
||
|
// The hasher may no longer be used after invoking this method. Callers always
|
||
|
// run finalize on a copy of the hasher so the original hasher state is not
|
||
|
// modified.
|
||
|
//
|
||
|
// The length preamble bit MUST be 0 (for BLAKE-224) or 1 (for BLAKE-256).
|
||
|
func (h *hasher) finalize(lenPreambleBit uint8) {
|
||
|
// Hashing a message consists of padding the message to a multiple of the
|
||
|
// block size and processing it block per block by the compression function.
|
||
|
//
|
||
|
// Padding the message consists of first extending the message so that its
|
||
|
// bit length is congruent to 447 modulo 512 by appending a 1 bit followed
|
||
|
// by enough 0s to reach the required congruence. Then a length preamble
|
||
|
// bit is added (1 for BLAKE-256, 0 for BLAKE-224) followed by the length
|
||
|
// of original message encoded as a 64-bit unsigned big-endian integer.
|
||
|
// This ensures the message length is a multiple of the block size since
|
||
|
// 447+1+64 = 512.
|
||
|
//
|
||
|
// Note that a special case occurs when the final block contains no original
|
||
|
// message bit. In that case, the message bit counter provided to the
|
||
|
// compression function is set to zero for that final block. This
|
||
|
// guarantees unique blocks.
|
||
|
//
|
||
|
// This implementation performs iterated hashing by compressing full blocks
|
||
|
// as data is written and storing the resulting chain value, total number of
|
||
|
// message bits compressed, and any remaining partial block data in the
|
||
|
// state.
|
||
|
//
|
||
|
// Thus, finalization consists of writing any remaining partial block data
|
||
|
// that hasn't already been compressed and padding the message out per the
|
||
|
// above.
|
||
|
//
|
||
|
// Since this implementation only allows writing full 8-bit bytes at a time,
|
||
|
// the following is optimized to only consider message bit lengths that are
|
||
|
// multiples of 8. Concretely, note that floor(447/8) = 55. Therefore, as
|
||
|
// long as the remaining partial block data is <= 55, only one compression
|
||
|
// is needed. Otherwise a second compression is needed.
|
||
|
msgBitLen := h.count + uint64(h.nbuf)<<3
|
||
|
switch {
|
||
|
// Exactly one padding byte is needed.
|
||
|
case h.nbuf == 55:
|
||
|
h.buf[55] = 0x80 | lenPreambleBit
|
||
|
binary.BigEndian.PutUint64(h.buf[56:], msgBitLen)
|
||
|
compress.Blocks(&h.state, h.buf[:], msgBitLen)
|
||
|
return
|
||
|
|
||
|
// Appending the padding to the remaining partial block data will fit
|
||
|
// without needing another block.
|
||
|
case h.nbuf < 55:
|
||
|
copy(h.buf[h.nbuf:55], pad[:])
|
||
|
h.buf[55] = lenPreambleBit
|
||
|
binary.BigEndian.PutUint64(h.buf[56:], msgBitLen)
|
||
|
|
||
|
// Per the specification, the counter is set to zero for the final
|
||
|
// compression when the final block contains no bits from the original
|
||
|
// message.
|
||
|
if h.nbuf == 0 {
|
||
|
msgBitLen = 0
|
||
|
}
|
||
|
compress.Blocks(&h.state, h.buf[:], msgBitLen)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// The partial block data plus the padding and message bit length exceed the
|
||
|
// size of a block, so two compressions are needed where the second one is
|
||
|
// a padding block (all zeros except for the final 8 bytes which house the
|
||
|
// original message length encoded as a 64-bit unsigned big-endian integer).
|
||
|
|
||
|
// Pad the remaining partial block data and compress it.
|
||
|
copy(h.buf[h.nbuf:], pad[:])
|
||
|
compress.Blocks(&h.state, h.buf[:], msgBitLen)
|
||
|
|
||
|
// Create the final padding block and compress it.
|
||
|
//
|
||
|
// Note that since the padding block does not contain any bits from the
|
||
|
// original message, the counter is set to zero when performing compression
|
||
|
// per the specification.
|
||
|
copy(h.buf[:], pad[1:56])
|
||
|
h.buf[55] = lenPreambleBit
|
||
|
binary.BigEndian.PutUint64(h.buf[56:], msgBitLen)
|
||
|
compress.Blocks(&h.state, h.buf[:], 0)
|
||
|
}
|
||
|
|
||
|
// wordsToBytes224 converts an array of 8 32-bit unsigned big-endian words to an
|
||
|
// array of 28 bytes. The final word is truncated.
|
||
|
func wordsToBytes224(cv [8]uint32) (out [28]byte) {
|
||
|
binary.BigEndian.PutUint32(out[24:], cv[6])
|
||
|
binary.BigEndian.PutUint32(out[20:], cv[5])
|
||
|
binary.BigEndian.PutUint32(out[16:], cv[4])
|
||
|
binary.BigEndian.PutUint32(out[12:], cv[3])
|
||
|
binary.BigEndian.PutUint32(out[8:], cv[2])
|
||
|
binary.BigEndian.PutUint32(out[4:], cv[1])
|
||
|
binary.BigEndian.PutUint32(out[0:], cv[0])
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
// finalize224 finalizes of the rolling hash by writing any remaining partial
|
||
|
// block data and appending the necessary padding for BLAKE-224.
|
||
|
//
|
||
|
// The hasher may no longer be used after invoking this method. Callers always
|
||
|
// run finalize on a copy of the hasher so the original hasher state is not
|
||
|
// modified.
|
||
|
func (h *hasher) finalize224() [Size224]byte {
|
||
|
const lenPreambleBit = 0x00
|
||
|
h.finalize(lenPreambleBit)
|
||
|
return wordsToBytes224(h.state.CV)
|
||
|
}
|
||
|
|
||
|
// wordsToBytes256 converts an array of 8 32-bit unsigned big-endian words to an
|
||
|
// array of 32 bytes.
|
||
|
func wordsToBytes256(cv [8]uint32) (out [32]byte) {
|
||
|
binary.BigEndian.PutUint32(out[28:], cv[7])
|
||
|
binary.BigEndian.PutUint32(out[24:], cv[6])
|
||
|
binary.BigEndian.PutUint32(out[20:], cv[5])
|
||
|
binary.BigEndian.PutUint32(out[16:], cv[4])
|
||
|
binary.BigEndian.PutUint32(out[12:], cv[3])
|
||
|
binary.BigEndian.PutUint32(out[8:], cv[2])
|
||
|
binary.BigEndian.PutUint32(out[4:], cv[1])
|
||
|
binary.BigEndian.PutUint32(out[0:], cv[0])
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
// finalize256 finalizes of the rolling hash by writing any remaining partial
|
||
|
// block data and appending the necessary padding for BLAKE-256.
|
||
|
//
|
||
|
// The hasher may no longer be used after invoking this method. Callers always
|
||
|
// run finalize on a copy of the hasher so the original hasher state is not
|
||
|
// modified.
|
||
|
func (h *hasher) finalize256() [Size]byte {
|
||
|
const lenPreambleBit = 0x01
|
||
|
h.finalize(lenPreambleBit)
|
||
|
return wordsToBytes256(h.state.CV)
|
||
|
}
|
||
|
|
||
|
// putSavedState serializes the intermediate state directly into the passed byte
|
||
|
// slice. The target slice MUST have at least [SavedStateSize] bytes available
|
||
|
// or it will panic.
|
||
|
func (h *hasher) putSavedState(target []byte, prefix uint32) {
|
||
|
var offset uint32
|
||
|
binary.BigEndian.PutUint32(target[offset:], prefix)
|
||
|
offset += 4
|
||
|
for _, cv := range h.state.CV {
|
||
|
binary.BigEndian.PutUint32(target[offset:], cv)
|
||
|
offset += 4
|
||
|
}
|
||
|
for _, s := range h.state.S {
|
||
|
binary.BigEndian.PutUint32(target[offset:], s)
|
||
|
offset += 4
|
||
|
}
|
||
|
binary.BigEndian.PutUint64(target[offset:], h.count)
|
||
|
offset += 8
|
||
|
offset += uint32(copy(target[offset:], h.buf[:]))
|
||
|
binary.BigEndian.PutUint32(target[offset:], h.nbuf)
|
||
|
}
|
||
|
|
||
|
// saveState appends the current intermediate state of the rolling hash prefixed
|
||
|
// by the passed value to the provided slice and returns the resulting slice.
|
||
|
// It does not change the underlying hash state.
|
||
|
//
|
||
|
// The provided prefix is expected to either be [statePrefix224] or
|
||
|
// [statePrefix256] depending on which hash variant is being saved.
|
||
|
//
|
||
|
// As described by the [hasher] documentation, the hasher instance can simply be
|
||
|
// copied to achieve the same result much more efficiently when the caller is
|
||
|
// able to keep a copy. Therefore, that approach should be preferred when
|
||
|
// possible.
|
||
|
//
|
||
|
// However, the ability to serialize the state is also provided to enable
|
||
|
// sharing it across process boundaries.
|
||
|
func (h *hasher) saveState(target []byte, prefix uint32) []byte {
|
||
|
// Create a new array and append it to the target when there is not enough
|
||
|
// space remaining in the slice. Otherwise, write directly into it.
|
||
|
//
|
||
|
// Note that this could alternatively just grow the slice if needed and then
|
||
|
// write directly into it unconditionally, but this approach is faster for
|
||
|
// the two much more common cases of the caller providing a slice that is
|
||
|
// already big enough or a nil slice.
|
||
|
if needed := SavedStateSize - (cap(target) - len(target)); needed > 0 {
|
||
|
var state [SavedStateSize]byte
|
||
|
h.putSavedState(state[:], prefix)
|
||
|
return append(target, state[:]...)
|
||
|
}
|
||
|
h.putSavedState(target[len(target):len(target)+SavedStateSize], prefix)
|
||
|
return target[:len(target)+SavedStateSize]
|
||
|
}
|
||
|
|
||
|
// loadState restores the rolling hash to the provided serialized intermediate
|
||
|
// state. See [hasher.saveState] for more details.
|
||
|
//
|
||
|
// The provided prefix is expected to either be [statePrefix224] or
|
||
|
// [statePrefix256] depending on which hash variant is being loaded.
|
||
|
//
|
||
|
// [ErrMalformedState] will be returned when the provided serialized state is
|
||
|
// not at least the required [SavedStateSize] number of bytes.
|
||
|
//
|
||
|
// [ErrMismatchedState] will be returned if the prefix in the serialized state
|
||
|
// does not match the given required prefix.
|
||
|
func (h *hasher) loadState(state []byte, requiredPrefix uint32) error {
|
||
|
if len(state) < SavedStateSize {
|
||
|
str := fmt.Sprintf("malformed intermediate state - must be at least "+
|
||
|
"%d bytes", SavedStateSize)
|
||
|
return makeError(ErrMalformedState, str)
|
||
|
}
|
||
|
var offset uint32
|
||
|
if pre := binary.BigEndian.Uint32(state[offset:]); pre != requiredPrefix {
|
||
|
hashType := "BLAKE-256"
|
||
|
if requiredPrefix != statePrefix256 {
|
||
|
hashType = "BLAKE-224"
|
||
|
}
|
||
|
str := fmt.Sprintf("the provided intermediate state is not for %s",
|
||
|
hashType)
|
||
|
return makeError(ErrMismatchedState, str)
|
||
|
}
|
||
|
offset += 4
|
||
|
for i := range h.state.CV {
|
||
|
h.state.CV[i] = binary.BigEndian.Uint32(state[offset:])
|
||
|
offset += 4
|
||
|
}
|
||
|
for i := range h.state.S {
|
||
|
h.state.S[i] = binary.BigEndian.Uint32(state[offset:])
|
||
|
offset += 4
|
||
|
}
|
||
|
h.count = binary.BigEndian.Uint64(state[offset:])
|
||
|
offset += 8
|
||
|
offset += uint32(copy(h.buf[:], state[offset:]))
|
||
|
h.nbuf = binary.BigEndian.Uint32(state[offset:])
|
||
|
return nil
|
||
|
}
|