Skip to content

Commit

Permalink
feat(sha3): Major version 2.0.0 with breaking API changes
Browse files Browse the repository at this point in the history
- Rename digest symbols to use explicit SHA3 prefix (e.g., :sha3_256)
- Refactor C extension to use TypedData for better memory management
  • Loading branch information
johanns committed Feb 27, 2025
1 parent 67182a9 commit f8a62e2
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 51 deletions.
50 changes: 39 additions & 11 deletions ext/sha3/digest.c
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
/* Copyright (c) 2012 - 2013 Johanns Gregorian <io+sha3@jsani.com> */

#include <string.h>
#include <ruby.h>
#include <ruby/encoding.h>

#include "sha3.h"
#include "digest.h"

VALUE cSHA3Digest;
VALUE eSHA3DigestError;

// Forward declaration
static void free_mdx(MDX* mdx);

/*
* == Notes
*
Expand All @@ -22,7 +28,9 @@ VALUE eSHA3DigestError;
*
*/

static void free_mdx(MDX* mdx) {
// TypedData functions for MDX struct
static void mdx_free(void* ptr) {
MDX* mdx = (MDX*)ptr;
if (mdx) {
if (mdx->state) {
free(mdx->state);
Expand All @@ -31,6 +39,26 @@ static void free_mdx(MDX* mdx) {
}
}

// Implementation of free_mdx that calls mdx_free
static void free_mdx(MDX* mdx) {
mdx_free(mdx);
}

static size_t mdx_memsize(const void* ptr) {
const MDX* mdx = (const MDX*)ptr;
size_t size = sizeof(MDX);
if (mdx && mdx->state) {
size += sizeof(Keccak_HashInstance);
}
return size;
}

const rb_data_type_t mdx_type = {
"SHA3::Digest",
{NULL, mdx_free, mdx_memsize,},
NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY
};

static VALUE c_digest_alloc(VALUE klass) {
MDX* mdx = (MDX*)malloc(sizeof(MDX));
if (!mdx) {
Expand All @@ -39,11 +67,11 @@ static VALUE c_digest_alloc(VALUE klass) {

mdx->state = (Keccak_HashInstance*)calloc(1, sizeof(Keccak_HashInstance));
if (!mdx->state) {
free_mdx(mdx);
mdx_free(mdx);
rb_raise(eSHA3DigestError, "failed to allocate state memory");
}

VALUE obj = Data_Wrap_Struct(klass, 0, free_mdx, mdx);
VALUE obj = TypedData_Wrap_Struct(klass, &mdx_type, mdx);
mdx->hashbitlen = 0;

return obj;
Expand Down Expand Up @@ -74,7 +102,7 @@ static VALUE c_digest_init(int argc, VALUE* argv, VALUE self) {
VALUE hlen, data;

rb_scan_args(argc, argv, "02", &hlen, &data);
GETMDX(self, mdx);
get_mdx(self, &mdx);

mdx->hashbitlen = NIL_P(hlen) ? 256 : get_hlen(hlen);

Expand All @@ -95,7 +123,7 @@ static VALUE c_digest_update(VALUE self, VALUE data) {
BitLength dlen;

StringValue(data);
GETMDX(self, mdx);
get_mdx(self, &mdx);

// Check for empty data
if (RSTRING_LEN(data) == 0) {
Expand All @@ -119,7 +147,7 @@ static VALUE c_digest_update(VALUE self, VALUE data) {
// SHA3::Digest.reset() -> self
static VALUE c_digest_reset(VALUE self) {
MDX* mdx;
GETMDX(self, mdx);
get_mdx(self, &mdx);

memset(mdx->state, 0, sizeof(Keccak_HashInstance));

Expand Down Expand Up @@ -168,8 +196,8 @@ static VALUE c_digest_copy(VALUE self, VALUE obj) {
return self;
}

GETMDX(self, mdx1);
SAFEGETMDX(obj, mdx2);
get_mdx(self, &mdx1);
safe_get_mdx(obj, &mdx2);

memcpy(mdx1->state, mdx2->state, sizeof(Keccak_HashInstance));
mdx1->hashbitlen = mdx2->hashbitlen;
Expand All @@ -184,15 +212,15 @@ static VALUE c_digest_copy(VALUE self, VALUE obj) {
// SHA3::Digest.digest_length -> Integer
static VALUE c_digest_length(VALUE self) {
MDX* mdx;
GETMDX(self, mdx);
get_mdx(self, &mdx);

return ULL2NUM(mdx->hashbitlen / 8);
}

// SHA3::Digest.block_length -> Integer
static VALUE c_digest_block_length(VALUE self) {
MDX* mdx;
GETMDX(self, mdx);
get_mdx(self, &mdx);

return ULL2NUM(200 - (2 * (mdx->hashbitlen / 8)));
}
Expand All @@ -209,7 +237,7 @@ static VALUE c_digest_finish(int argc, VALUE* argv, VALUE self) {
int digest_bytes;

rb_scan_args(argc, argv, "01", &str);
GETMDX(self, mdx);
get_mdx(self, &mdx);

digest_bytes = mdx->hashbitlen / 8;

Expand Down
43 changes: 22 additions & 21 deletions ext/sha3/digest.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,33 @@
extern "C" {
#endif

// From ruby/ext/openssl/ossl_digest.c
#define GETMDX(obj, mdx) \
do { \
Data_Get_Struct((obj), MDX, (mdx)); \
if (!(mdx)) { \
rb_raise(rb_eRuntimeError, "Digest data not initialized!"); \
} \
} while (0)

#define SAFEGETMDX(obj, mdx) \
do { \
if (!rb_obj_is_kind_of(obj, cSHA3Digest)) { \
rb_raise(rb_eTypeError, "wrong argument (%s)! (expected %s)", \
rb_obj_classname(obj), rb_class2name(cSHA3Digest)); \
} \
GETMDX(obj, mdx); \
} while (0)

extern VALUE cSHA3Digest;
extern VALUE eSHA3DigestError;

typedef struct {
Keccak_HashInstance *state;
int hashbitlen;
} MDX;

// TypedData functions
extern const rb_data_type_t mdx_type;

extern VALUE cSHA3Digest;
extern VALUE eSHA3DigestError;

// Static inline functions replacing macros
static inline void get_mdx(VALUE obj, MDX **mdx) {
TypedData_Get_Struct((obj), MDX, &mdx_type, (*mdx));
if (!(*mdx)) {
rb_raise(rb_eRuntimeError, "Digest data not initialized!");
}
}

static inline void safe_get_mdx(VALUE obj, MDX **mdx) {
if (!rb_obj_is_kind_of(obj, cSHA3Digest)) {
rb_raise(rb_eTypeError, "wrong argument (%s)! (expected %s)",
rb_obj_classname(obj), rb_class2name(cSHA3Digest));
}
get_mdx(obj, mdx);
}

void Init_sha3_n_digest(void);

#ifdef __cplusplus
Expand Down
2 changes: 1 addition & 1 deletion lib/sha3/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module SHA3
# sha3 release version
VERSION = '1.0.5'
VERSION = '2.0.0'

# keccak version number
KECCAK_VERSION = '4.0'
Expand Down
36 changes: 18 additions & 18 deletions spec/sha3_core_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

# rubocop:disable Metrics/BlockLength(RuboCop)
RSpec.describe SHA3::Digest do
it 'passes Digest.new() (default: :sha256) usage test' do
it 'passes Digest.new() (default: :sha3_256) usage test' do
sha = described_class.new

expect(sha.hexdigest).to eq('a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a')
Expand All @@ -33,8 +33,8 @@
expect(sha.block_length).to eq(136)
end

it 'passes Digest.new(:sha224) usage test' do
sha = described_class.new(:sha224)
it 'passes Digest.new(:sha3_224) usage test' do
sha = described_class.new(:sha3_224)

expect(sha.hexdigest).to eq('6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7')
expect(sha.update(['cc'].pack('H*'))).to eq('df70adc49b2e76eee3a6931b93fa41841c3af2cdf5b32a18b5478c39')
Expand All @@ -47,8 +47,8 @@
expect(sha.block_length).to eq(144)
end

it 'passes Digest.new(:sha256) usage test' do
sha = described_class.new(:sha256)
it 'passes Digest.new(:sha3_256) usage test' do
sha = described_class.new(:sha3_256)

expect(sha.hexdigest).to eq('a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a')
expect(sha.update(['cc'].pack('H*'))).to eq('677035391cd3701293d385f037ba32796252bb7ce180b00b582dd9b20aaad7f0')
Expand All @@ -61,8 +61,8 @@
expect(sha.block_length).to eq(136)
end

it 'passes Digest.new(:sha384) usage test' do
sha = described_class.new(:sha384)
it 'passes Digest.new(:sha3_384) usage test' do
sha = described_class.new(:sha3_384)

expect(sha.hexdigest).to eq('0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004')

Expand All @@ -76,8 +76,8 @@
expect(sha.block_length).to eq(104)
end

it 'passes Digest.new(:sha512) usage test' do
sha = described_class.new(:sha512)
it 'passes Digest.new(:sha3_512) usage test' do
sha = described_class.new(:sha3_512)

expect(sha.hexdigest).to eq('a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26')
expect(sha.update(['cc'].pack('H*'))).to eq('3939fcc8b57b63612542da31a834e5dcc36e2ee0f652ac72e02624fa2e5adeecc7dd6bb3580224b4d6138706fc6e80597b528051230b00621cc2b22999eaa205')
Expand All @@ -91,7 +91,7 @@
end

it 'does not Segfault due to buffer overflow vulnerability' do
sha = described_class.new(:sha224)
sha = described_class.new(:sha3_224)

sha.update("\x00" * 1)
sha.update("\x00" * 4294967295)
Expand All @@ -101,8 +101,8 @@
end

RSpec.describe 'SHA3::Digest::SHAxyz' do
it 'passes Digest.SHA224() usage test' do
sha = SHA3::Digest::SHA224.new
it 'passes Digest.SHA3_224() usage test' do
sha = SHA3::Digest::SHA3_224.new

expect(sha.hexdigest).to eq('6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7')
expect(sha.update(['cc'].pack('H*'))).to eq('df70adc49b2e76eee3a6931b93fa41841c3af2cdf5b32a18b5478c39')
Expand All @@ -115,8 +115,8 @@
expect(sha.block_length).to eq(144)
end

it 'passes Digest.SHA256() usage test' do
sha = SHA3::Digest::SHA256.new
it 'passes Digest.SHA3_256() usage test' do
sha = SHA3::Digest::SHA3_256.new

expect(sha.hexdigest).to eq('a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a')
expect(sha.update(['cc'].pack('H*'))).to eq('677035391cd3701293d385f037ba32796252bb7ce180b00b582dd9b20aaad7f0')
Expand All @@ -129,8 +129,8 @@
expect(sha.block_length).to eq(136)
end

it 'passes Digest.SHA384() usage test' do
sha = SHA3::Digest::SHA384.new
it 'passes Digest.SHA3_384() usage test' do
sha = SHA3::Digest::SHA3_384.new

expect(sha.hexdigest).to eq('0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004')

Expand All @@ -144,8 +144,8 @@
expect(sha.block_length).to eq(104)
end

it 'passes Digest.SHA512() usage test' do
sha = SHA3::Digest::SHA512.new
it 'passes Digest.SHA3_512() usage test' do
sha = SHA3::Digest::SHA3_512.new

expect(sha.hexdigest).to eq('a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26')
expect(sha.update(['cc'].pack('H*'))).to eq('3939fcc8b57b63612542da31a834e5dcc36e2ee0f652ac72e02624fa2e5adeecc7dd6bb3580224b4d6138706fc6e80597b528051230b00621cc2b22999eaa205')
Expand Down

0 comments on commit f8a62e2

Please sign in to comment.