Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added SHA3 and Keccak-256 #3

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/HMAC.mo
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ module HMAC {

public func size() : Nat { outer.size() };

public func checkSum() : [Nat8] { [0] };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be removed.


public func sum(bs : [Nat8]) : [Nat8] {
let l = bs.size();
let p = inner.sum(bs);
Expand Down
3 changes: 3 additions & 0 deletions src/Hash.mo
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ module Hash {
reset() : ();
// Returns the number of bytes that sum will return.
size() : Nat;
// Return the hash.
// TODO: Should probably rename this
checkSum() : [Nat8];
Comment on lines +9 to +11
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It there are reason this was added to the Hash interface?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it to avoid repeating myself in the SHA3_* by reusing New. The checkSum function in SHA2.mo seemed to be the main part of the hashing too. Thought it'd be good to add. I can define the parameters like H256 instead though.

// Adds the current hash to the resulting slice.
// The underlying hash is not modified.
sum(bs : [Nat8]) : [Nat8];
Expand Down
242 changes: 242 additions & 0 deletions src/SHA/Keccak.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import Array "mo:base-0.7.3/Array";
import Nat "mo:base-0.7.3/Nat";
import Nat8 "mo:base-0.7.3/Nat8";
import Nat64 "mo:base-0.7.3/Nat64";
import Int "mo:base-0.7.3/Int";
import Iter "mo:base-0.7.3/Iter";

import Hash "../Hash";

import Hex "mo:encoding/Hex";
import Debug "mo:base-0.7.3/Debug";

module {

// Keccak constants
private let keccakf_rndc : [Nat64] = [
0x0000000000000001, 0x0000000000008082, 0x800000000000808a,
0x8000000080008000, 0x000000000000808b, 0x0000000080000001,
0x8000000080008081, 0x8000000000008009, 0x000000000000008a,
0x0000000000000088, 0x0000000080008009, 0x000000008000000a,
0x000000008000808b, 0x800000000000008b, 0x8000000000008089,
0x8000000000008003, 0x8000000000008002, 0x8000000000000080,
0x000000000000800a, 0x800000008000000a, 0x8000000080008081,
0x8000000000008080, 0x0000000080000001, 0x8000000080008008
];

// Keccak constants
private let keccakf_rotc : [Nat64] = [
1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14,
27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44
];

// Keccak constants
private let keccakf_piln : [Nat] = [
10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4,
15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1
];

public class Keccak(
initialState : [Nat64], // unused for now
hashSize : Nat,
delimitedSuffix: Nat8,
) : Hash.Hash = {
// Keccak l-parameter selects "bus size". For SHA3, l = 6 and
// this is the only value supported at this time.
private let keccakf_l : Nat = 6;

// Number of rounds
private let n_rounds: Nat = 12 + 2*keccakf_l;

// Bus sizes in bits
private let bsize: Nat = 25 * 2**keccakf_l;

// Capactiy in bits
private let cap: Nat = hashSize * 2;

// Block size in bytes
assert(bsize > cap+8);
private let rsize: Nat = (bsize - cap)/8;

// Internal state
private let state: [var Nat64] = Array.init<Nat64>(bsize/64, 0);

// Current write state index
private var pt: Nat = 0;

// blockSize in bytes
public func blockSize() : Nat { rsize; };

// Function to initialize
public func reset() : () {
for (i in Iter.range(0, state.size()-1)) {
state[i] := 0;
};
pt := 0;
};

// Return size of hash in bits
public func size() : Nat { hashSize; };

// Turn array of Nat8s into Nat64, LSB first
private func pack64(data : [Nat8]) : Nat64 {
let dsize = data.size();
assert(dsize <= 8 and dsize > 0);

var q: Nat64 = 0;
// Avoid reverse() here to optimize? Maybe unroll?
for (i in Iter.range(0, dsize-1:Nat)) {
q |= Nat64.fromNat(Nat8.toNat(data[dsize-1:Nat-i]));
if (i < (dsize-1:Nat)) {
q <<= 8;
};
};
return q;
};

// Unpack Nat64 into array of Nat8s, LSB first
private func unpack64(q : Nat64) : [Nat8] {
var t = q;
let qbuf = Array.init<Nat8>(8, 0);
for (i in Iter.range(0, qbuf.size()-1)) {
qbuf[i] := Nat8.fromNat(Nat64.toNat(t & 0xff));
t >>= 8;
};
return Array.freeze<Nat8>(qbuf);
};

private func dump() {
Debug.print("State");
for (i in Iter.range(0, state.size()-1)) {
let b = Array.reverse<Nat8>(unpack64(state[i]));
Debug.print(Hex.encode(b));
};
};
Comment on lines +108 to +114
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be removed.

Copy link
Author

@ray-react0r ray-react0r Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought I'd leave it, in case more testing (and debugging) need to be done. Easier to have it and ask to remove IMHO. Can remove it no problem.


// Repeated from SHA2. Candidate for DRY principle refactor?
public func sum(bs : [Nat8]) : [Nat8] {
let cs = checkSum();
let size = bs.size();
Array.tabulate<Nat8>(
size + cs.size(),
func (x : Nat) {
if (x < size) return bs[x];
cs[x - size];
}
);
};

// Function to finalize hashing
public func checkSum() : [Nat8] {
state[pt/8] ^= Nat64.fromNat(Nat8.toNat(delimitedSuffix)) << Nat64.fromNat((pt%8)*8);
state[(rsize-1:Nat)/8] ^= 0x80 : Nat64 << Nat64.fromNat(((rsize-1):Nat%8)*8);

//dump();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idem.

keccak_f(state);

let md = Array.init<Nat8>(hashSize/8, 0);

var i = 0;
label done for (q in state.vals()) {
for (b in unpack64(q).vals()) {
if (i >= md.size()) {
break done;
};
md[i] := b;
i += 1;
};
};
return Array.freeze<Nat8>(md);
};

// Function to update internal state with content
// TODO: should data be a blob? Sticking with Array from Hash type
Copy link
Member

@q-uint q-uint Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once you can get individual bytes out of a blob, the whole package will migrate to it.
Currently blobs do not allow you to do much.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know. I'll drop the comment.

public func write(data : [Nat8]) : () {
if (data.size() == 0) {
return;
};

// Creating qbuf to apply against the entire Nat64 to
// avoid repacking because we don't have unions in
// Motoko. First time might require leading zeros, so it's
// initialized to zero
let qbuf = Array.init<Nat8>(8, 0);
let dlast = data.size()-1:Nat;

for (i in Iter.range(0, dlast)) {
qbuf[pt % 8] := data[i];

// Set zeros at the end of our quad-word before submitting
if (i >= dlast and (pt+1) % 8 != 0) {
for (j in Iter.range((pt+1) % 8, 7)) {
qbuf[j] := 0;
};
};

// Finished 8 bytes (64-bits) chunk to apply to state
if (i >= dlast or (pt+1) % 8 == 0) {
// Debug.print(Nat.toText(pt/8));
// Debug.print(Hex.encode(Array.freeze<Nat8>(qbuf)));
// let b = Array.reverse<Nat8>(unpack64(state[pt/8]));
// Debug.print(Hex.encode(b));
state[pt/8] ^= pack64(Array.freeze<Nat8>(qbuf));
};
pt += 1;

if (pt >= rsize) {
keccak_f(state);
pt := 0;
};
};
//dump();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idem.


// Sanitize buffer for sensitive information
for (j in Iter.range(0, 7)) {
qbuf[j] := 0;
};
};

// Main Keccak hashing function. See:
// https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf
func keccak_f(st : [var Nat64]) {
var t : Nat64 = 0;
let bc = Array.init<Nat64>(5, 0);
var j: Nat = 0;

for (r in Iter.range(0, n_rounds - 1)) {
// Theta
for (i in Iter.range(0, 4)) {
bc[i] := st[i] ^ st[i + 5] ^ st[i + 10] ^ st[i + 15] ^ st[i + 20];
};
for (i in Iter.range(0, 4)) {
t := bc[(i + 4) % 5] ^ Nat64.bitrotLeft(bc[(i + 1) % 5], 1);
for (j in Iter.range(0, 4)) {
st[j*5 + i] ^= t;
};
};

// Rho Pi
t := st[1];
for (i in Iter.range(0, keccakf_piln.size()-1)) {
j := keccakf_piln[i];
bc[0] := st[j];
st[j] := Nat64.bitrotLeft(t, keccakf_rotc[i]);
t := bc[0];
};

// Chi
for (j in Iter.range(0, 4)) {
for (i in Iter.range(0, 4)) {
bc[i] := st[j*5 + i];
};
for (i in Iter.range(0, 4)) {
st[j*5 + i] ^= (^bc[(i + 1) % 5]) & bc[(i + 2) % 5];
};
};

// Iota
st[0] ^= keccakf_rndc[r];
};
};
};
};
16 changes: 16 additions & 0 deletions src/SHA/Keccak_256.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Keccak "Keccak";

import Hash "../Hash";

module Keccak_256 {

public func New() : Hash.Hash { Keccak.Keccak([], 256, 0x01); };

/// Returns the Keccak-256 checksum of the data.
public func sum(bs : [Nat8]) : [Nat8] {
let h = New();
h.write(bs);
h.checkSum();
};

};
16 changes: 16 additions & 0 deletions src/SHA/SHA3_224.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Keccak "Keccak";

import Hash "../Hash";

module SHA3_224 {

public func New() : Hash.Hash { Keccak.Keccak([], 224, 0x06); };

/// Returns the SHA3-224 checksum of the data.
public func sum(bs : [Nat8]) : [Nat8] {
let h = New();
h.write(bs);
h.checkSum();
};

};
16 changes: 16 additions & 0 deletions src/SHA/SHA3_256.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Keccak "Keccak";

import Hash "../Hash";

module SHA3_256 {

public func New() : Hash.Hash { Keccak.Keccak([], 256, 0x06); };

/// Returns the SHA3-256 checksum of the data.
public func sum(bs : [Nat8]) : [Nat8] {
let h = New();
h.write(bs);
h.checkSum();
};

};
16 changes: 16 additions & 0 deletions src/SHA/SHA3_384.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Keccak "Keccak";

import Hash "../Hash";

module SHA3_384 {

public func New() : Hash.Hash { Keccak.Keccak([], 384, 0x06); };

/// Returns the SHA3-384 checksum of the data.
public func sum(bs : [Nat8]) : [Nat8] {
let h = New();
h.write(bs);
h.checkSum();
};

};
16 changes: 16 additions & 0 deletions src/SHA/SHA3_512.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Keccak "Keccak";

import Hash "../Hash";

module SHA3_512 {

public func New() : Hash.Hash { Keccak.Keccak([], 512, 0x06); };

/// Returns the SHA3-512 checksum of the data.
public func sum(bs : [Nat8]) : [Nat8] {
let h = New();
h.write(bs);
h.checkSum();
};

};
45 changes: 45 additions & 0 deletions test/Keccak_256.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Blob "mo:base-0.7.3/Blob";
import Hex "mo:encoding/Hex";
import Text "mo:base-0.7.3/Text";

import Debug "mo:base-0.7.3/Debug";

import Keccak_256 "../src/SHA/Keccak_256";

//Debug.print("Testing Keccak_256");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should either be removed or uncommented (same for other print statements).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, these served more as comments about describing the tests and to break them up. Safe to assume you want them removed as the other tests don't emit any messages?

let shasum = Keccak_256.sum(Blob.toArray(Text.encodeUtf8("hello world\n")));
//Debug.print(Hex.encode(shasum));
assert(Hex.encode(shasum) == "70e3788906c57c18999ba6b0389a768ff3333e3d6136fdf85743e66a03bc29f9");

//Debug.print("Testing Keccak_256 sum");
let h = Keccak_256.New();
h.write(Blob.toArray(Text.encodeUtf8("hello world\n")));
assert(Hex.encode(shasum) == Hex.encode(h.sum([])));

//Debug.print("Testing Keccak_256 vectors");
for (v in [
("", "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"),
("a", "3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb"),
("ab", "67fad3bfa1e0321bd021ca805ce14876e50acac8ca8532eda8cbf924da565160"),
("abc", "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45"),
("abcd", "48bed44d1bcd124a28c27f343a817e5f5243190d3c52bf347daf876de1dbbf77"),
("abcde", "6377c7e66081cb65e473c1b95db5195a27d04a7108b468890224bedbe1a8a6eb"),
("abcdef", "acd0c377fe36d5b209125185bc3ac41155ed1bf7103ef9f0c2aff4320460b6df"),
("abcdefg", "a82aec019867b7307551dc397acde18b541e742fa1a4e53df4ce3b02d462f524"),
("abcdefgh", "48624fa43c68d5c552855a4e2919e74645f683f5384f72b5b051b71ea41d4f2d"),
("abcdefghi", "34fb2702da7001bf4dbf26a1e4cf31044bd95b85e1017596ee2d23aedc90498b"),
("123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", "9f475334b0dafde0fdbe358fbe2b65b7129e0b52e242e2e1317479f9b590f581"),
].vals()) {
let hv = Hex.encode(Keccak_256.sum(Blob.toArray(Text.encodeUtf8(v.0))));
//Debug.print(hv);
assert(hv == v.1);
};

//Debug.print("Testing Keccak_256 write pieces");
do {
let h = Keccak_256.New();
h.write(Blob.toArray(Text.encodeUtf8("hello")));
h.write(Blob.toArray(Text.encodeUtf8(" ")));
h.write(Blob.toArray(Text.encodeUtf8("world\n")));
assert(Hex.encode(h.sum([])) == "70e3788906c57c18999ba6b0389a768ff3333e3d6136fdf85743e66a03bc29f9");
};
Loading