Skip to content
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
1 change: 1 addition & 0 deletions dist/esm/index.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export declare const EDWARDS25519SHA512BATCH = "edwards25519sha512batch";
export declare const CURVE25519XSALSA20POLY1305 = "curve25519xsalsa20poly1305";
export declare const XSALSA20POLY1305 = "xsalsa20poly1305";
export declare const EXTERNALKM32 = "externalkm32";
export declare const HMACSHA512 = "hmacsha512";
export type Key = {
type: string;
secretKey?: unknown;
Expand Down
77 changes: 60 additions & 17 deletions dist/esm/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const CURVE25519XSALSA20POLY1305 = 'curve25519xsalsa20poly1305';
export const XSALSA20POLY1305 = 'xsalsa20poly1305';
// 32 bytes of keying material, used for external keys (such as files)
export const EXTERNALKM32 = 'externalkm32';
export const HMACSHA512 = 'hmacsha512';
if (process.env.NODE_ENV === 'production' && process.env.ENABLE_UNSAFE_NULL_CRYPTO === 'true') {
throw new Error('ENABLE_UNSAFE_NULL_CRYPTO cannot be enabled in production mode');
}
Expand Down Expand Up @@ -87,6 +88,13 @@ export const keygen = (type) => {
Object.defineProperty(res, 'secretKey', { value: nacl.randomBytes(32) });
return res;
}
else if (type === HMACSHA512) {
const res = {
type
};
Object.defineProperty(res, 'secretKey', { value: nacl.randomBytes(128) });
return res;
}
throw new Error('Unsupported key type');
};
export const generateSalt = () => {
Expand Down Expand Up @@ -169,7 +177,7 @@ export const serializeKey = (key, saveSecretKey) => {
bytesOrObjectToB64(key.secretKey)
], undefined, 0);
}
else if (key.type === XSALSA20POLY1305) {
else if (key.type === XSALSA20POLY1305 || key.type === HMACSHA512) {
if (!saveSecretKey) {
throw new Error('Unsupported operation: no public key to export');
}
Expand Down Expand Up @@ -238,7 +246,7 @@ export const deserializeKey = (data) => {
}
throw new Error('Missing secret or public key');
}
else if (keyData[0] === XSALSA20POLY1305) {
else if (keyData[0] === XSALSA20POLY1305 || keyData[0] === HMACSHA512) {
if (!keyData[2]) {
throw new Error('Secret key missing');
}
Expand Down Expand Up @@ -267,16 +275,39 @@ export const sign = (inKey, data) => {
}
return key.secretKey + ';' + blake32Hash(data);
}
if (key.type !== EDWARDS25519SHA512BATCH) {
throw new Error('Unsupported algorithm');
}
if (!key.secretKey) {
throw new Error('Secret key missing');
}
const messageUint8 = strToBuf(data);
const signature = nacl.sign.detached(messageUint8, key.secretKey);
const base64Signature = bytesOrObjectToB64(signature);
return base64Signature;
if (key.type === EDWARDS25519SHA512BATCH) {
const signature = nacl.sign.detached(messageUint8, key.secretKey);
const base64Signature = bytesOrObjectToB64(signature);
return base64Signature;
}
else if (key.type === HMACSHA512) {
const secretKey = key.secretKey;
const blockSize = 128; // SHA-512
const kOpad = new Uint8Array(blockSize);
const kIpad = new Uint8Array(blockSize + messageUint8.byteLength);
const hmacKey = secretKey.byteLength > blockSize ? nacl.hash(secretKey) : secretKey;
const keyBlock = new Uint8Array(blockSize);
keyBlock.set(hmacKey);
for (let i = 0; i < blockSize; i++) {
kOpad[i] = keyBlock[i] ^ 0x5c;
kIpad[i] = keyBlock[i] ^ 0x36;
}
kIpad.set(messageUint8, blockSize);
const inner = nacl.hash(kIpad);
const dataBuf = new Uint8Array(kOpad.byteLength + inner.byteLength);
dataBuf.set(kOpad, 0);
dataBuf.set(inner, kOpad.byteLength);
const signature = nacl.hash(dataBuf);
const base64Signature = bytesOrObjectToB64(signature);
return base64Signature;
}
else {
throw new Error('Unsupported algorithm');
}
};
export const verifySignature = (inKey, data, signature) => {
const key = typeof inKey === 'string' ? deserializeKey(inKey) : inKey;
Expand All @@ -289,17 +320,29 @@ export const verifySignature = (inKey, data, signature) => {
}
return;
}
if (key.type !== EDWARDS25519SHA512BATCH) {
throw new Error('Unsupported algorithm');
if (key.type === EDWARDS25519SHA512BATCH) {
if (!key.publicKey) {
throw new Error('Public key missing');
}
const decodedSignature = b64ToBuf(signature);
const messageUint8 = strToBuf(data);
const result = nacl.sign.detached.verify(messageUint8, decodedSignature, key.publicKey);
if (!result) {
throw new Error('Invalid signature');
}
}
if (!key.publicKey) {
throw new Error('Public key missing');
else if (key.type === HMACSHA512) {
const mac = sign(key, data);
let r = mac.length ^ signature.length;
for (let i = 0; i < mac.length; i++) {
r |= signature.charCodeAt(i) ^ mac.charCodeAt(i);
}
if (r !== 0) {
throw new Error('Invalid signature');
}
}
const decodedSignature = b64ToBuf(signature);
const messageUint8 = strToBuf(data);
const result = nacl.sign.detached.verify(messageUint8, decodedSignature, key.publicKey);
if (!result) {
throw new Error('Invalid signature');
else {
throw new Error('Unsupported algorithm');
}
};
/**
Expand Down
79 changes: 61 additions & 18 deletions dist/umd/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.decrypt = exports.encrypt = exports.verifySignature = exports.sign = exports.keyId = exports.keygenOfSameType = exports.deserializeKey = exports.serializeKey = exports.deriveKeyFromPassword = exports.generateSalt = exports.keygen = exports.EXTERNALKM32 = exports.XSALSA20POLY1305 = exports.CURVE25519XSALSA20POLY1305 = exports.EDWARDS25519SHA512BATCH = exports.SNULL = exports.ENULL = void 0;
exports.decrypt = exports.encrypt = exports.verifySignature = exports.sign = exports.keyId = exports.keygenOfSameType = exports.deserializeKey = exports.serializeKey = exports.deriveKeyFromPassword = exports.generateSalt = exports.keygen = exports.HMACSHA512 = exports.EXTERNALKM32 = exports.XSALSA20POLY1305 = exports.CURVE25519XSALSA20POLY1305 = exports.EDWARDS25519SHA512BATCH = exports.SNULL = exports.ENULL = void 0;
const multiformats_1 = require("@chelonia/multiformats");
const scrypt_async_1 = __importDefault(require("scrypt-async"));
const tweetnacl_1 = __importDefault(require("tweetnacl"));
Expand Down Expand Up @@ -49,6 +49,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
exports.XSALSA20POLY1305 = 'xsalsa20poly1305';
// 32 bytes of keying material, used for external keys (such as files)
exports.EXTERNALKM32 = 'externalkm32';
exports.HMACSHA512 = 'hmacsha512';
if (process.env.NODE_ENV === 'production' && process.env.ENABLE_UNSAFE_NULL_CRYPTO === 'true') {
throw new Error('ENABLE_UNSAFE_NULL_CRYPTO cannot be enabled in production mode');
}
Expand Down Expand Up @@ -102,6 +103,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(res, 'secretKey', { value: tweetnacl_1.default.randomBytes(32) });
return res;
}
else if (type === exports.HMACSHA512) {
const res = {
type
};
Object.defineProperty(res, 'secretKey', { value: tweetnacl_1.default.randomBytes(128) });
return res;
}
throw new Error('Unsupported key type');
};
exports.keygen = keygen;
Expand Down Expand Up @@ -187,7 +195,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
bytesOrObjectToB64(key.secretKey)
], undefined, 0);
}
else if (key.type === exports.XSALSA20POLY1305) {
else if (key.type === exports.XSALSA20POLY1305 || key.type === exports.HMACSHA512) {
if (!saveSecretKey) {
throw new Error('Unsupported operation: no public key to export');
}
Expand Down Expand Up @@ -257,7 +265,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
}
throw new Error('Missing secret or public key');
}
else if (keyData[0] === exports.XSALSA20POLY1305) {
else if (keyData[0] === exports.XSALSA20POLY1305 || keyData[0] === exports.HMACSHA512) {
if (!keyData[2]) {
throw new Error('Secret key missing');
}
Expand Down Expand Up @@ -289,16 +297,39 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
}
return key.secretKey + ';' + blake32Hash(data);
}
if (key.type !== exports.EDWARDS25519SHA512BATCH) {
throw new Error('Unsupported algorithm');
}
if (!key.secretKey) {
throw new Error('Secret key missing');
}
const messageUint8 = strToBuf(data);
const signature = tweetnacl_1.default.sign.detached(messageUint8, key.secretKey);
const base64Signature = bytesOrObjectToB64(signature);
return base64Signature;
if (key.type === exports.EDWARDS25519SHA512BATCH) {
const signature = tweetnacl_1.default.sign.detached(messageUint8, key.secretKey);
const base64Signature = bytesOrObjectToB64(signature);
return base64Signature;
}
else if (key.type === exports.HMACSHA512) {
const secretKey = key.secretKey;
const blockSize = 128; // SHA-512
const kOpad = new Uint8Array(blockSize);
const kIpad = new Uint8Array(blockSize + messageUint8.byteLength);
const hmacKey = secretKey.byteLength > blockSize ? tweetnacl_1.default.hash(secretKey) : secretKey;
const keyBlock = new Uint8Array(blockSize);
keyBlock.set(hmacKey);
for (let i = 0; i < blockSize; i++) {
kOpad[i] = keyBlock[i] ^ 0x5c;
kIpad[i] = keyBlock[i] ^ 0x36;
}
kIpad.set(messageUint8, blockSize);
const inner = tweetnacl_1.default.hash(kIpad);
const dataBuf = new Uint8Array(kOpad.byteLength + inner.byteLength);
dataBuf.set(kOpad, 0);
dataBuf.set(inner, kOpad.byteLength);
const signature = tweetnacl_1.default.hash(dataBuf);
const base64Signature = bytesOrObjectToB64(signature);
return base64Signature;
}
else {
throw new Error('Unsupported algorithm');
}
};
exports.sign = sign;
const verifySignature = (inKey, data, signature) => {
Expand All @@ -312,17 +343,29 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
}
return;
}
if (key.type !== exports.EDWARDS25519SHA512BATCH) {
throw new Error('Unsupported algorithm');
if (key.type === exports.EDWARDS25519SHA512BATCH) {
if (!key.publicKey) {
throw new Error('Public key missing');
}
const decodedSignature = b64ToBuf(signature);
const messageUint8 = strToBuf(data);
const result = tweetnacl_1.default.sign.detached.verify(messageUint8, decodedSignature, key.publicKey);
if (!result) {
throw new Error('Invalid signature');
}
}
if (!key.publicKey) {
throw new Error('Public key missing');
else if (key.type === exports.HMACSHA512) {
const mac = (0, exports.sign)(key, data);
let r = mac.length ^ signature.length;
for (let i = 0; i < mac.length; i++) {
r |= signature.charCodeAt(i) ^ mac.charCodeAt(i);
}
if (r !== 0) {
throw new Error('Invalid signature');
}
}
const decodedSignature = b64ToBuf(signature);
const messageUint8 = strToBuf(data);
const result = tweetnacl_1.default.sign.detached.verify(messageUint8, decodedSignature, key.publicKey);
if (!result) {
throw new Error('Invalid signature');
else {
throw new Error('Unsupported algorithm');
}
};
exports.verifySignature = verifySignature;
Expand Down
1 change: 1 addition & 0 deletions dist/umd/index.d.cts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export declare const EDWARDS25519SHA512BATCH = "edwards25519sha512batch";
export declare const CURVE25519XSALSA20POLY1305 = "curve25519xsalsa20poly1305";
export declare const XSALSA20POLY1305 = "xsalsa20poly1305";
export declare const EXTERNALKM32 = "externalkm32";
export declare const HMACSHA512 = "hmacsha512";
export type Key = {
type: string;
secretKey?: unknown;
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chelonia/crypto",
"version": "1.0.1",
"version": "1.0.2",
"description": "Contains various utilities for performing cryptographic operations.",
"main": "dist/umd/index.cjs",
"module": "dist/esm/index.mjs",
Expand All @@ -19,7 +19,7 @@
}
},
"scripts": {
"test": "npm run lint && node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));' src/index.test.ts",
"test": "node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\")); process.on(\"uncaughtException\", (e) => { console.error(\"ERROR\", e); throw e });' src/index.test.ts",
"build:esm": "tsc --project tsconfig.json --declaration && mv ./dist/esm/index.js ./dist/esm/index.mjs && mv ./dist/esm/index.d.ts ./dist/esm/index.d.mts",
"build:umd": "tsc --project tsconfig.umd.json --declaration && mv ./dist/umd/index.js ./dist/umd/index.cjs && mv ./dist/umd/index.d.ts ./dist/umd/index.d.cts",
"build": "npm run build:esm && npm run build:umd",
Expand Down
65 changes: 64 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as assert from 'node:assert/strict'
import { describe, it } from 'node:test'

import { EDWARDS25519SHA512BATCH, CURVE25519XSALSA20POLY1305, XSALSA20POLY1305, keygen, deriveKeyFromPassword, generateSalt, serializeKey, deserializeKey, encrypt, decrypt, sign, verifySignature } from './index.js'
import { EDWARDS25519SHA512BATCH, CURVE25519XSALSA20POLY1305, XSALSA20POLY1305, HMACSHA512, keygen, deriveKeyFromPassword, generateSalt, serializeKey, deserializeKey, encrypt, decrypt, sign, verifySignature } from './index.js'

describe('Crypto suite', () => {
it('should deserialize to the same contents as when serializing', () => {
Expand Down Expand Up @@ -122,4 +122,67 @@ describe('Crypto suite', () => {
assert.throws(() => decrypt(key, 'Invalid message'))
}
})

it('can compute and verify HMACSHA512 signatures', () => {
const key = keygen(HMACSHA512)
const data1 = 'test msg 1'
const data2 = 'test msg 2'
const result1 = sign(key, data1)
const result2 = sign(key, data2)
verifySignature(key, data1, result1)
verifySignature(key, data2, result2)
assert.throws(() => verifySignature(key, data1, result2))
assert.throws(() => verifySignature(key, data2, result1))
})

it('Passes RFC 4231 test vectors for HMACSHA512', () => {
// From <https://datatracker.ietf.org/doc/html/rfc4231>
const testVectors = [
{
key: '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b',
message: 'Hi There',
result: '87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854'
},
{
key: '4a656665',
message: 'what do ya want for nothing?',
result: '164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737'
},
/*
// The following two messages will fail due to UTF-8 encoding
{
key: 'aa'.repeat(20),
message: '\xdd'.repeat(50),
result: 'fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb'
},
{
key: '0102030405060708090a0b0c0d0e0f10111213141516171819',
message: '\xcd'.repeat(50),
result: 'b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd'
},
*/
{
key: 'aa'.repeat(131),
message: 'Test Using Larger Than Block-Size Key - Hash Key First',
result: '80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598'
},
{
key: 'aa'.repeat(131),
message: 'This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm.',
result: 'e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58'
}
]

for (const vec of testVectors) {
const key = {
type: HMACSHA512,
secretKey: Buffer.from(vec.key, 'hex')
}
const result = sign(key, vec.message)
const expected = Buffer.from(vec.result, 'hex').toString('base64')
assert.equal(result, expected)
verifySignature(key, vec.message, expected)
assert.throws(() => verifySignature(key, vec.message, 'AAA' + expected.slice(3)))
}
})
})
Loading