Skip to content

Commit

Permalink
Merge pull request #81 from linux-on-ibm-z/big-endian-port
Browse files Browse the repository at this point in the history
Add support for big endian platforms
  • Loading branch information
paulmillr authored Feb 8, 2024
2 parents f209f44 + 4beb199 commit fa8a7c4
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 23 deletions.
10 changes: 8 additions & 2 deletions src/_blake.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { number, exists, output } from './_assert.js';
import { Hash, Input, toBytes, u32 } from './utils.js';
import { Hash, Input, toBytes, u32, isLE, byteSwap32, byteSwapIfBE } from './utils.js';

// Blake is based on ChaCha permutation.

Expand Down Expand Up @@ -74,18 +74,22 @@ export abstract class BLAKE<T extends BLAKE<T>> extends Hash<T> {
for (let pos = 0; pos < len; ) {
// If buffer is full and we still have input (don't process last block, same as blake2s)
if (this.pos === blockLen) {
if (!isLE) byteSwap32(buffer32);
this.compress(buffer32, 0, false);
if (!isLE) byteSwap32(buffer32);
this.pos = 0;
}
const take = Math.min(blockLen - this.pos, len - pos);
const dataOffset = offset + pos;
// full block && aligned to 4 bytes && not last in input
if (take === blockLen && !(dataOffset % 4) && pos + take < len) {
const data32 = new Uint32Array(buf, dataOffset, Math.floor((len - pos) / 4));
if (!isLE) byteSwap32(data32);
for (let pos32 = 0; pos + blockLen < len; pos32 += buffer32.length, pos += blockLen) {
this.length += blockLen;
this.compress(data32, pos32, false);
}
if (!isLE) byteSwap32(data32);
continue;
}
buffer.set(data.subarray(pos, pos + take), this.pos);
Expand All @@ -102,9 +106,11 @@ export abstract class BLAKE<T extends BLAKE<T>> extends Hash<T> {
this.finished = true;
// Padding
this.buffer.subarray(pos).fill(0);
if (!isLE) byteSwap32(buffer32);
this.compress(buffer32, 0, true);
if (!isLE) byteSwap32(buffer32);
const out32 = u32(out);
this.get().forEach((v, i) => (out32[i] = v));
this.get().forEach((v, i) => (out32[i] = byteSwapIfBE(v)));
}
digest() {
const { buffer, outputLen } = this;
Expand Down
18 changes: 9 additions & 9 deletions src/blake2b.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BLAKE, BlakeOpts, SIGMA } from './_blake.js';
import u64 from './_u64.js';
import { toBytes, u32, wrapConstructorWithOpts } from './utils.js';
import { toBytes, u32, wrapConstructorWithOpts, byteSwapIfBE } from './utils.js';

// Same as SHA-512 but LE
// prettier-ignore
Expand Down Expand Up @@ -87,17 +87,17 @@ class BLAKE2b extends BLAKE<BLAKE2b> {
this.v0l ^= this.outputLen | (keyLength << 8) | (0x01 << 16) | (0x01 << 24);
if (opts.salt) {
const salt = u32(toBytes(opts.salt));
this.v4l ^= salt[0];
this.v4h ^= salt[1];
this.v5l ^= salt[2];
this.v5h ^= salt[3];
this.v4l ^= byteSwapIfBE(salt[0]);
this.v4h ^= byteSwapIfBE(salt[1]);
this.v5l ^= byteSwapIfBE(salt[2]);
this.v5h ^= byteSwapIfBE(salt[3]);
}
if (opts.personalization) {
const pers = u32(toBytes(opts.personalization));
this.v6l ^= pers[0];
this.v6h ^= pers[1];
this.v7l ^= pers[2];
this.v7h ^= pers[3];
this.v6l ^= byteSwapIfBE(pers[0]);
this.v6h ^= byteSwapIfBE(pers[1]);
this.v7l ^= byteSwapIfBE(pers[2]);
this.v7h ^= byteSwapIfBE(pers[3]);
}
if (opts.key) {
// Pad to blockLen and update
Expand Down
10 changes: 5 additions & 5 deletions src/blake2s.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BLAKE, BlakeOpts, SIGMA } from './_blake.js';
import { fromBig } from './_u64.js';
import { rotr, toBytes, wrapConstructorWithOpts, u32 } from './utils.js';
import { rotr, toBytes, wrapConstructorWithOpts, u32, byteSwapIfBE } from './utils.js';

// Initial state: same as SHA256
// first 32 bits of the fractional parts of the square roots of the first 8 primes 2..19
Expand Down Expand Up @@ -71,13 +71,13 @@ class BLAKE2s extends BLAKE<BLAKE2s> {
this.v0 ^= this.outputLen | (keyLength << 8) | (0x01 << 16) | (0x01 << 24);
if (opts.salt) {
const salt = u32(toBytes(opts.salt));
this.v4 ^= salt[0];
this.v5 ^= salt[1];
this.v4 ^= byteSwapIfBE(salt[0]);
this.v5 ^= byteSwapIfBE(salt[1]);
}
if (opts.personalization) {
const pers = u32(toBytes(opts.personalization));
this.v6 ^= pers[0];
this.v7 ^= pers[1];
this.v6 ^= byteSwapIfBE(pers[0]);
this.v7 ^= byteSwapIfBE(pers[1]);
}
if (opts.key) {
// Pad to blockLen and update
Expand Down
20 changes: 19 additions & 1 deletion src/blake3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ import { bytes, exists, number, output } from './_assert.js';
import { fromBig } from './_u64.js';
import { BLAKE } from './_blake.js';
import { compress, B2S_IV } from './blake2s.js';
import { Input, u8, u32, toBytes, HashXOF, wrapXOFConstructorWithOpts } from './utils.js';
import {
Input,
u8,
u32,
toBytes,
HashXOF,
wrapXOFConstructorWithOpts,
isLE,
byteSwap32,
} from './utils.js';

// Blake3 is single-option Blake2 with reduced security (round count).

Expand Down Expand Up @@ -64,12 +73,14 @@ class BLAKE3 extends BLAKE<BLAKE3> implements HashXOF<BLAKE3> {
const key = toBytes(opts.key).slice();
if (key.length !== 32) throw new Error('Blake3: key should be 32 byte');
this.IV = u32(key);
if (!isLE) byteSwap32(this.IV);
this.flags = flags | B3_Flags.KEYED_HASH;
} else if (opts.context !== undefined) {
const context_key = new BLAKE3({ dkLen: 32 }, B3_Flags.DERIVE_KEY_CONTEXT)
.update(opts.context)
.digest();
this.IV = u32(context_key);
if (!isLE) byteSwap32(this.IV);
this.flags = flags | B3_Flags.DERIVE_KEY_MATERIAL;
} else {
this.IV = B2S_IV.slice();
Expand Down Expand Up @@ -162,6 +173,7 @@ class BLAKE3 extends BLAKE<BLAKE3> implements HashXOF<BLAKE3> {
private b2CompressOut() {
const { state: s, pos, flags, buffer32, bufferOut32: out32 } = this;
const { h, l } = fromBig(BigInt(this.chunkOut++));
if (!isLE) byteSwap32(buffer32);
// prettier-ignore
const { v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15 } =
compress(
Expand All @@ -185,6 +197,10 @@ class BLAKE3 extends BLAKE<BLAKE3> implements HashXOF<BLAKE3> {
out32[13] = s[5] ^ v13;
out32[14] = s[6] ^ v14;
out32[15] = s[7] ^ v15;
if (!isLE) {
byteSwap32(buffer32);
byteSwap32(out32);
}
this.posOut = 0;
}
protected finish() {
Expand All @@ -196,7 +212,9 @@ class BLAKE3 extends BLAKE<BLAKE3> implements HashXOF<BLAKE3> {
let flags = this.flags | B3_Flags.ROOT;
if (this.stack.length) {
flags |= B3_Flags.PARENT;
if (!isLE) byteSwap32(this.buffer32);
this.compress(this.buffer32, 0, true);
if (!isLE) byteSwap32(this.buffer32);
this.chunksDone = 0;
this.pos = this.blockLen;
} else {
Expand Down
6 changes: 5 additions & 1 deletion src/scrypt.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { number as assertNumber } from './_assert.js';
import { sha256 } from './sha256.js';
import { pbkdf2 } from './pbkdf2.js';
import { rotl, asyncLoop, checkOpts, Input, u32 } from './utils.js';
import { rotl, asyncLoop, checkOpts, Input, u32, isLE, byteSwap32 } from './utils.js';

// RFC 7914 Scrypt KDF

Expand Down Expand Up @@ -186,6 +186,7 @@ export function scrypt(password: Input, salt: Input, opts: ScryptOpts) {
salt,
opts
);
if (!isLE) byteSwap32(B32);
for (let pi = 0; pi < p; pi++) {
const Pi = blockSize32 * pi;
for (let i = 0; i < blockSize32; i++) V[i] = B32[Pi + i]; // V[0] = B[i]
Expand All @@ -203,6 +204,7 @@ export function scrypt(password: Input, salt: Input, opts: ScryptOpts) {
blockMixCb();
}
}
if (!isLE) byteSwap32(B32);
return scryptOutput(password, dkLen, B, V, tmp);
}

Expand All @@ -215,6 +217,7 @@ export async function scryptAsync(password: Input, salt: Input, opts: ScryptOpts
salt,
opts
);
if (!isLE) byteSwap32(B32);
for (let pi = 0; pi < p; pi++) {
const Pi = blockSize32 * pi;
for (let i = 0; i < blockSize32; i++) V[i] = B32[Pi + i]; // V[0] = B[i]
Expand All @@ -233,5 +236,6 @@ export async function scryptAsync(password: Input, salt: Input, opts: ScryptOpts
blockMixCb();
});
}
if (!isLE) byteSwap32(B32);
return scryptOutput(password, dkLen, B, V, tmp);
}
4 changes: 4 additions & 0 deletions src/sha3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
wrapConstructor,
wrapXOFConstructorWithOpts,
HashXOF,
isLE,
byteSwap32,
} from './utils.js';

// SHA3 (keccak) is based on a new design: basically, the internal state is bigger than output size.
Expand Down Expand Up @@ -112,7 +114,9 @@ export class Keccak extends Hash<Keccak> implements HashXOF<Keccak> {
this.state32 = u32(this.state);
}
protected keccak() {
if (!isLE) byteSwap32(this.state32);
keccakP(this.state32, this.rounds);
if (!isLE) byteSwap32(this.state32);
this.posOut = 0;
this.pos = 0;
}
Expand Down
20 changes: 15 additions & 5 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,22 @@ export const rotr = (word: number, shift: number) => (word << (32 - shift)) | (w
export const rotl = (word: number, shift: number) =>
(word << shift) | ((word >>> (32 - shift)) >>> 0);

// big-endian hardware is rare. Just in case someone still decides to run hashes:
// early-throw an error because we don't support BE yet.
// Other libraries would silently corrupt the data instead of throwing an error,
// when they don't support it.
export const isLE = new Uint8Array(new Uint32Array([0x11223344]).buffer)[0] === 0x44;
if (!isLE) throw new Error('Non little-endian hardware is not supported');
// The byte swap operation for uint32
export const byteSwap = (word: number) =>
((word << 24) & 0xff000000) |
((word << 8) & 0xff0000) |
((word >>> 8) & 0xff00) |
((word >>> 24) & 0xff);
// Conditionally byte swap if on a big-endian platform
export const byteSwapIfBE = isLE ? (n: number) => n : (n: number) => byteSwap(n);

// In place byte swap for Uint32Array
export function byteSwap32(arr: Uint32Array) {
for (let i = 0; i < arr.length; i++) {
arr[i] = byteSwap(arr[i]);
}
}

// Array where index 0xf0 (240) is mapped to string 'f0'
const hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) =>
Expand Down
31 changes: 31 additions & 0 deletions test/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const assert = require('assert');
const { should } = require('micro-should');
const { optional, integer, gen } = require('./generator');
const { byteSwap, byteSwapIfBE, byteSwap32, isLE } = require('../utils.js');

// Here goes test for tests...
should(`Test generator`, () => {
Expand All @@ -21,4 +22,34 @@ should(`Test generator`, () => {
);
});

// Byte swapping
const BYTESWAP_TEST_CASES = [
{ in: 0x11223344 | 0, out: 0x44332211 | 0 },
{ in: 0xffeeddcc | 0, out: 0xccddeeff | 0 },
{ in: 0xccddeeff | 0, out: 0xffeeddcc | 0 },
];

should('byteSwap', () => {
BYTESWAP_TEST_CASES.forEach((test) => {
assert.deepStrictEqual(test.out, byteSwap(test.in));
});
});

should('byteSwapIfBE', () => {
BYTESWAP_TEST_CASES.forEach((test) => {
if (isLE) {
assert.deepStrictEqual(test.in, byteSwapIfBE(test.in));
} else {
assert.deepStrictEqual(test.out, byteSwapIfBE(test.in));
}
});
});

should('byteSwap32', () => {
const input = Uint32Array.of([0x11223344, 0xffeeddcc, 0xccddeeff]);
const expected = Uint32Array.of([0x44332211, 0xccddeeff, 0xffeeddcc]);
byteSwap32(input);
assert.deepStrictEqual(expected, input);
});

if (require.main === module) should.run();

0 comments on commit fa8a7c4

Please sign in to comment.