From 7ee2fd796d024d88126cbb6fb6dd77ab1912231a Mon Sep 17 00:00:00 2001 From: arrtchiu Date: Tue, 29 Jul 2014 18:23:40 +0800 Subject: [PATCH] add timing safe string compare method --- configure.in | 2 ++ string.c | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ test/ruby/test_string.rb | 28 +++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/configure.in b/configure.in index 2cbe36f..8560d82 100644 --- a/configure.in +++ b/configure.in @@ -2066,6 +2066,8 @@ AC_CHECK_FUNCS(utimensat) AC_CHECK_FUNCS(utimes) AC_CHECK_FUNCS(wait4) AC_CHECK_FUNCS(waitpid) +AC_CHECK_FUNCS(consttime_memequal) +AC_CHECK_FUNCS(timingsafe_memcmp) AS_IF([test "$ac_cv_func_getcwd" = yes], [ AC_CACHE_CHECK(if getcwd allocates buffer if NULL is given, [rb_cv_getcwd_malloc], diff --git a/string.c b/string.c index 9f06837..011d727 100644 --- a/string.c +++ b/string.c @@ -2490,6 +2490,54 @@ rb_str_eql(VALUE str1, VALUE str2) return str_eql(str1, str2); } +static inline int +rb_consttime_memequal(const char *buf1, const char *buf2, long len) +{ +#if defined(HAVE_TIMINGSAFE_MEMCMP) + return (timingsafe_memcmp(buf1, buf2, len) == 0); +#elif defined(HAVE_CONSTTIME_MEMEQUAL) + return (consttime_memequal(buf1, buf2, len) != 0); +#else + VALUE result; + long idx; + + result = 0; + idx = 0; + if (UNALIGNED_WORD_ACCESS || !((VALUE)buf1 % sizeof(VALUE)) && !((VALUE)buf2 % sizeof(VALUE))) { + for (; idx < len; idx += sizeof(VALUE)) { + result |= *(const VALUE *)(buf1+idx) ^ *(const VALUE *)(buf2+idx); + } + } + + for (; idx < len; idx++) { + result |= buf1[idx] ^ buf2[idx]; + } + + return (result == 0); +#endif +} + +/* + * call-seq: + * str.consttime_bytes_eq?(other) -> true or false + * + * Ignoring encoding, compares each byte of +str+ against +other+ in constant time. + */ + +static VALUE +rb_str_consttime_bytes_eq(VALUE str1, VALUE str2) +{ + long len; + + str2 = StringValue(str2); + len = RSTRING_LEN(str1); + + if (RSTRING_LEN(str2) != len) return Qfalse; + if (rb_consttime_memequal(RSTRING_PTR(str1), RSTRING_PTR(str2), len) != 0) return Qtrue; + + return Qfalse; +} + /* * call-seq: * string <=> other_string -> -1, 0, +1 or nil @@ -8761,6 +8809,7 @@ Init_String(void) rb_define_method(rb_cString, "==", rb_str_equal, 1); rb_define_method(rb_cString, "===", rb_str_equal, 1); rb_define_method(rb_cString, "eql?", rb_str_eql, 1); + rb_define_method(rb_cString, "consttime_bytes_eq?", rb_str_consttime_bytes_eq, 1); rb_define_method(rb_cString, "hash", rb_str_hash_m, 0); rb_define_method(rb_cString, "casecmp", rb_str_casecmp, 1); rb_define_method(rb_cString, "+", rb_str_plus, 1); diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index e8decc0..f710c8f 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -1,5 +1,6 @@ require 'test/unit' require_relative 'envutil' +require 'benchmark' # use of $= is deprecated after 1.7.1 def pre_1_7_1 @@ -304,6 +305,33 @@ class TestString < Test::Unit::TestCase casetest(S("CaT"), S('cAt'), true) # find these in the case. end + def test_consttime_bytes_eq # 'consttime_bytes_eq?' + assert_equal(true, S("foo").consttime_bytes_eq?(S("foo"))) + assert_equal(false, S("foo").consttime_bytes_eq?(S("foO"))) + assert_equal(true, S("f\x00oo").consttime_bytes_eq?(S("f\x00oo"))) + assert_equal(false, S("f\x00oo").consttime_bytes_eq?(S("f\x00oO"))) + end + + def test_consttime_bytes_eq_timing + # ensure using consttime_bytes_eq? takes almost exactly the same amount of time to compare two + # different strings. + # NOTE: this test may be susceptible to noise if the system running the tests is otherwise under + # load. + a = "x"*1024_000 + b = a+"y" + c = "y"+a + a << "x" + + def measure(&block) + Benchmark.measure(&block).real + end + + n = 10_000 + a_b_time = measure { n.times { a.consttime_bytes_eq?(b) } } + a_c_time = measure { n.times { a.consttime_bytes_eq?(c) } } + assert_in_delta(a_b_time, a_c_time, 0.25, "consttime_bytes_eq? timing test failed") + end + def test_capitalize assert_equal(S("Hello"), S("hello").capitalize) assert_equal(S("Hello"), S("hELLO").capitalize) -- 1.8.5.2 (Apple Git-48)