xcrypt 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +4 -100
- data/ext/libxcrypt/build-aux/m4/libtool.m4 +8488 -0
- data/ext/libxcrypt/build-aux/m4/ltoptions.m4 +467 -0
- data/ext/libxcrypt/build-aux/m4/ltsugar.m4 +124 -0
- data/ext/libxcrypt/build-aux/m4/ltversion.m4 +24 -0
- data/ext/libxcrypt/build-aux/m4/lt~obsolete.m4 +99 -0
- data/lib/xcrypt/ffi.rb +12 -8
- data/lib/xcrypt/version.rb +1 -1
- data/lib/xcrypt/yescrypt.rb +219 -0
- data/lib/xcrypt.rb +223 -6
- metadata +16 -11
- data/ext/xcrypt/xcrypt.c +0 -9
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# lt~obsolete.m4 -- aclocal satisfying obsolete definitions. -*-Autoconf-*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2004-2005, 2007, 2009, 2011-2019, 2021-2024 Free
|
|
4
|
+
# Software Foundation, Inc.
|
|
5
|
+
# Written by Scott James Remnant, 2004.
|
|
6
|
+
#
|
|
7
|
+
# This file is free software; the Free Software Foundation gives
|
|
8
|
+
# unlimited permission to copy and/or distribute it, with or without
|
|
9
|
+
# modifications, as long as this notice is preserved.
|
|
10
|
+
|
|
11
|
+
# serial 5 lt~obsolete.m4
|
|
12
|
+
|
|
13
|
+
# These exist entirely to fool aclocal when bootstrapping libtool.
|
|
14
|
+
#
|
|
15
|
+
# In the past libtool.m4 has provided macros via AC_DEFUN (or AU_DEFUN),
|
|
16
|
+
# which have later been changed to m4_define as they aren't part of the
|
|
17
|
+
# exported API, or moved to Autoconf or Automake where they belong.
|
|
18
|
+
#
|
|
19
|
+
# The trouble is, aclocal is a bit thick. It'll see the old AC_DEFUN
|
|
20
|
+
# in /usr/share/aclocal/libtool.m4 and remember it, then when it sees us
|
|
21
|
+
# using a macro with the same name in our local m4/libtool.m4 it'll
|
|
22
|
+
# pull the old libtool.m4 in (it doesn't see our shiny new m4_define
|
|
23
|
+
# and doesn't know about Autoconf macros at all.)
|
|
24
|
+
#
|
|
25
|
+
# So we provide this file, which has a silly filename so it's always
|
|
26
|
+
# included after everything else. This provides aclocal with the
|
|
27
|
+
# AC_DEFUNs it wants, but when m4 processes it, it doesn't do anything
|
|
28
|
+
# because those macros already exist, or will be overwritten later.
|
|
29
|
+
# We use AC_DEFUN over AU_DEFUN for compatibility with aclocal-1.6.
|
|
30
|
+
#
|
|
31
|
+
# Anytime we withdraw an AC_DEFUN or AU_DEFUN, remember to add it here.
|
|
32
|
+
# Yes, that means every name once taken will need to remain here until
|
|
33
|
+
# we give up compatibility with versions before 1.7, at which point
|
|
34
|
+
# we need to keep only those names which we still refer to.
|
|
35
|
+
|
|
36
|
+
# This is to help aclocal find these macros, as it can't see m4_define.
|
|
37
|
+
AC_DEFUN([LTOBSOLETE_VERSION], [m4_if([1])])
|
|
38
|
+
|
|
39
|
+
m4_ifndef([AC_LIBTOOL_LINKER_OPTION], [AC_DEFUN([AC_LIBTOOL_LINKER_OPTION])])
|
|
40
|
+
m4_ifndef([AC_PROG_EGREP], [AC_DEFUN([AC_PROG_EGREP])])
|
|
41
|
+
m4_ifndef([_LT_AC_PROG_ECHO_BACKSLASH], [AC_DEFUN([_LT_AC_PROG_ECHO_BACKSLASH])])
|
|
42
|
+
m4_ifndef([_LT_AC_SHELL_INIT], [AC_DEFUN([_LT_AC_SHELL_INIT])])
|
|
43
|
+
m4_ifndef([_LT_AC_SYS_LIBPATH_AIX], [AC_DEFUN([_LT_AC_SYS_LIBPATH_AIX])])
|
|
44
|
+
m4_ifndef([_LT_PROG_LTMAIN], [AC_DEFUN([_LT_PROG_LTMAIN])])
|
|
45
|
+
m4_ifndef([_LT_AC_TAGVAR], [AC_DEFUN([_LT_AC_TAGVAR])])
|
|
46
|
+
m4_ifndef([AC_LTDL_ENABLE_INSTALL], [AC_DEFUN([AC_LTDL_ENABLE_INSTALL])])
|
|
47
|
+
m4_ifndef([AC_LTDL_PREOPEN], [AC_DEFUN([AC_LTDL_PREOPEN])])
|
|
48
|
+
m4_ifndef([_LT_AC_SYS_COMPILER], [AC_DEFUN([_LT_AC_SYS_COMPILER])])
|
|
49
|
+
m4_ifndef([_LT_AC_LOCK], [AC_DEFUN([_LT_AC_LOCK])])
|
|
50
|
+
m4_ifndef([AC_LIBTOOL_SYS_OLD_ARCHIVE], [AC_DEFUN([AC_LIBTOOL_SYS_OLD_ARCHIVE])])
|
|
51
|
+
m4_ifndef([_LT_AC_TRY_DLOPEN_SELF], [AC_DEFUN([_LT_AC_TRY_DLOPEN_SELF])])
|
|
52
|
+
m4_ifndef([AC_LIBTOOL_PROG_CC_C_O], [AC_DEFUN([AC_LIBTOOL_PROG_CC_C_O])])
|
|
53
|
+
m4_ifndef([AC_LIBTOOL_SYS_HARD_LINK_LOCKS], [AC_DEFUN([AC_LIBTOOL_SYS_HARD_LINK_LOCKS])])
|
|
54
|
+
m4_ifndef([AC_LIBTOOL_OBJDIR], [AC_DEFUN([AC_LIBTOOL_OBJDIR])])
|
|
55
|
+
m4_ifndef([AC_LTDL_OBJDIR], [AC_DEFUN([AC_LTDL_OBJDIR])])
|
|
56
|
+
m4_ifndef([AC_LIBTOOL_PROG_LD_HARDCODE_LIBPATH], [AC_DEFUN([AC_LIBTOOL_PROG_LD_HARDCODE_LIBPATH])])
|
|
57
|
+
m4_ifndef([AC_LIBTOOL_SYS_LIB_STRIP], [AC_DEFUN([AC_LIBTOOL_SYS_LIB_STRIP])])
|
|
58
|
+
m4_ifndef([AC_PATH_MAGIC], [AC_DEFUN([AC_PATH_MAGIC])])
|
|
59
|
+
m4_ifndef([AC_PROG_LD_GNU], [AC_DEFUN([AC_PROG_LD_GNU])])
|
|
60
|
+
m4_ifndef([AC_PROG_LD_RELOAD_FLAG], [AC_DEFUN([AC_PROG_LD_RELOAD_FLAG])])
|
|
61
|
+
m4_ifndef([AC_DEPLIBS_CHECK_METHOD], [AC_DEFUN([AC_DEPLIBS_CHECK_METHOD])])
|
|
62
|
+
m4_ifndef([AC_LIBTOOL_PROG_COMPILER_NO_RTTI], [AC_DEFUN([AC_LIBTOOL_PROG_COMPILER_NO_RTTI])])
|
|
63
|
+
m4_ifndef([AC_LIBTOOL_SYS_GLOBAL_SYMBOL_PIPE], [AC_DEFUN([AC_LIBTOOL_SYS_GLOBAL_SYMBOL_PIPE])])
|
|
64
|
+
m4_ifndef([AC_LIBTOOL_PROG_COMPILER_PIC], [AC_DEFUN([AC_LIBTOOL_PROG_COMPILER_PIC])])
|
|
65
|
+
m4_ifndef([AC_LIBTOOL_PROG_LD_SHLIBS], [AC_DEFUN([AC_LIBTOOL_PROG_LD_SHLIBS])])
|
|
66
|
+
m4_ifndef([AC_LIBTOOL_POSTDEP_PREDEP], [AC_DEFUN([AC_LIBTOOL_POSTDEP_PREDEP])])
|
|
67
|
+
m4_ifndef([LT_AC_PROG_EGREP], [AC_DEFUN([LT_AC_PROG_EGREP])])
|
|
68
|
+
m4_ifndef([LT_AC_PROG_SED], [AC_DEFUN([LT_AC_PROG_SED])])
|
|
69
|
+
m4_ifndef([_LT_CC_BASENAME], [AC_DEFUN([_LT_CC_BASENAME])])
|
|
70
|
+
m4_ifndef([_LT_COMPILER_BOILERPLATE], [AC_DEFUN([_LT_COMPILER_BOILERPLATE])])
|
|
71
|
+
m4_ifndef([_LT_LINKER_BOILERPLATE], [AC_DEFUN([_LT_LINKER_BOILERPLATE])])
|
|
72
|
+
m4_ifndef([_AC_PROG_LIBTOOL], [AC_DEFUN([_AC_PROG_LIBTOOL])])
|
|
73
|
+
m4_ifndef([AC_LIBTOOL_SETUP], [AC_DEFUN([AC_LIBTOOL_SETUP])])
|
|
74
|
+
m4_ifndef([_LT_AC_CHECK_DLFCN], [AC_DEFUN([_LT_AC_CHECK_DLFCN])])
|
|
75
|
+
m4_ifndef([AC_LIBTOOL_SYS_DYNAMIC_LINKER], [AC_DEFUN([AC_LIBTOOL_SYS_DYNAMIC_LINKER])])
|
|
76
|
+
m4_ifndef([_LT_AC_TAGCONFIG], [AC_DEFUN([_LT_AC_TAGCONFIG])])
|
|
77
|
+
m4_ifndef([AC_DISABLE_FAST_INSTALL], [AC_DEFUN([AC_DISABLE_FAST_INSTALL])])
|
|
78
|
+
m4_ifndef([_LT_AC_LANG_CXX], [AC_DEFUN([_LT_AC_LANG_CXX])])
|
|
79
|
+
m4_ifndef([_LT_AC_LANG_F77], [AC_DEFUN([_LT_AC_LANG_F77])])
|
|
80
|
+
m4_ifndef([_LT_AC_LANG_GCJ], [AC_DEFUN([_LT_AC_LANG_GCJ])])
|
|
81
|
+
m4_ifndef([AC_LIBTOOL_LANG_C_CONFIG], [AC_DEFUN([AC_LIBTOOL_LANG_C_CONFIG])])
|
|
82
|
+
m4_ifndef([_LT_AC_LANG_C_CONFIG], [AC_DEFUN([_LT_AC_LANG_C_CONFIG])])
|
|
83
|
+
m4_ifndef([AC_LIBTOOL_LANG_CXX_CONFIG], [AC_DEFUN([AC_LIBTOOL_LANG_CXX_CONFIG])])
|
|
84
|
+
m4_ifndef([_LT_AC_LANG_CXX_CONFIG], [AC_DEFUN([_LT_AC_LANG_CXX_CONFIG])])
|
|
85
|
+
m4_ifndef([AC_LIBTOOL_LANG_F77_CONFIG], [AC_DEFUN([AC_LIBTOOL_LANG_F77_CONFIG])])
|
|
86
|
+
m4_ifndef([_LT_AC_LANG_F77_CONFIG], [AC_DEFUN([_LT_AC_LANG_F77_CONFIG])])
|
|
87
|
+
m4_ifndef([AC_LIBTOOL_LANG_GCJ_CONFIG], [AC_DEFUN([AC_LIBTOOL_LANG_GCJ_CONFIG])])
|
|
88
|
+
m4_ifndef([_LT_AC_LANG_GCJ_CONFIG], [AC_DEFUN([_LT_AC_LANG_GCJ_CONFIG])])
|
|
89
|
+
m4_ifndef([AC_LIBTOOL_LANG_RC_CONFIG], [AC_DEFUN([AC_LIBTOOL_LANG_RC_CONFIG])])
|
|
90
|
+
m4_ifndef([_LT_AC_LANG_RC_CONFIG], [AC_DEFUN([_LT_AC_LANG_RC_CONFIG])])
|
|
91
|
+
m4_ifndef([AC_LIBTOOL_CONFIG], [AC_DEFUN([AC_LIBTOOL_CONFIG])])
|
|
92
|
+
m4_ifndef([_LT_AC_FILE_LTDLL_C], [AC_DEFUN([_LT_AC_FILE_LTDLL_C])])
|
|
93
|
+
m4_ifndef([_LT_REQUIRED_DARWIN_CHECKS], [AC_DEFUN([_LT_REQUIRED_DARWIN_CHECKS])])
|
|
94
|
+
m4_ifndef([_LT_AC_PROG_CXXCPP], [AC_DEFUN([_LT_AC_PROG_CXXCPP])])
|
|
95
|
+
m4_ifndef([_LT_PREPARE_SED_QUOTE_VARS], [AC_DEFUN([_LT_PREPARE_SED_QUOTE_VARS])])
|
|
96
|
+
m4_ifndef([_LT_PROG_ECHO_BACKSLASH], [AC_DEFUN([_LT_PROG_ECHO_BACKSLASH])])
|
|
97
|
+
m4_ifndef([_LT_PROG_F77], [AC_DEFUN([_LT_PROG_F77])])
|
|
98
|
+
m4_ifndef([_LT_PROG_FC], [AC_DEFUN([_LT_PROG_FC])])
|
|
99
|
+
m4_ifndef([_LT_PROG_CXX], [AC_DEFUN([_LT_PROG_CXX])])
|
data/lib/xcrypt/ffi.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "ffi"
|
|
4
|
-
require "ffi-compiler/loader"
|
|
5
4
|
|
|
6
5
|
module XCrypt
|
|
7
6
|
# Low-level FFI bindings for libxcrypt.
|
|
@@ -9,9 +8,9 @@ module XCrypt
|
|
|
9
8
|
# Consumers should use the high-level {XCrypt} module methods instead of
|
|
10
9
|
# calling into this module directly.
|
|
11
10
|
#
|
|
12
|
-
# The shared library loaded here is
|
|
13
|
-
# (ext/libxcrypt)
|
|
14
|
-
#
|
|
11
|
+
# The shared library loaded here is built from the libxcrypt submodule
|
|
12
|
+
# (ext/libxcrypt). Run <tt>bundle exec rake compile</tt> to build it
|
|
13
|
+
# before loading this gem.
|
|
15
14
|
module FFI
|
|
16
15
|
extend ::FFI::Library
|
|
17
16
|
|
|
@@ -32,11 +31,15 @@ module XCrypt
|
|
|
32
31
|
CRYPT_SALT_METHOD_LEGACY = 3
|
|
33
32
|
CRYPT_SALT_TOO_CHEAP = 4
|
|
34
33
|
|
|
35
|
-
# Load the
|
|
36
|
-
#
|
|
37
|
-
# relative to this file's location.
|
|
34
|
+
# Load the shared library built from the libxcrypt submodule.
|
|
35
|
+
# It lives in a platform-specific subdirectory next to this file.
|
|
38
36
|
begin
|
|
39
|
-
|
|
37
|
+
_ext = ::FFI::Platform.mac? ? "bundle" : "so"
|
|
38
|
+
_lib = File.expand_path(
|
|
39
|
+
"../#{::FFI::Platform::ARCH}-#{::FFI::Platform::OS}/libxcrypt.#{_ext}",
|
|
40
|
+
__FILE__
|
|
41
|
+
)
|
|
42
|
+
ffi_lib _lib
|
|
40
43
|
rescue LoadError
|
|
41
44
|
raise LoadError,
|
|
42
45
|
"XCrypt native extension not found. " \
|
|
@@ -70,6 +73,7 @@ module XCrypt
|
|
|
70
73
|
# Returns the prefix string of the library's preferred (strongest)
|
|
71
74
|
# hashing method.
|
|
72
75
|
attach_function :crypt_preferred_method, [], :string
|
|
76
|
+
|
|
73
77
|
end
|
|
74
78
|
|
|
75
79
|
private_constant :FFI
|
data/lib/xcrypt/version.rb
CHANGED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module XCrypt
|
|
6
|
+
# Setting-string generation for the yescrypt and scrypt algorithms.
|
|
7
|
+
#
|
|
8
|
+
# Both algorithms share a base-64 alphabet and encoding scheme taken from
|
|
9
|
+
# libxcrypt's +alg-yescrypt-common.c+. The public methods produce setting
|
|
10
|
+
# strings that can be passed directly to {XCrypt.crypt}.
|
|
11
|
+
#
|
|
12
|
+
# @example Generate a $y$ yescrypt setting
|
|
13
|
+
# setting = XCrypt::Yescrypt.generate_setting(n: 16384, r: 8, p: 1)
|
|
14
|
+
# hash = XCrypt.crypt("hunter2", setting)
|
|
15
|
+
#
|
|
16
|
+
# @example Generate a $7$ scrypt setting
|
|
17
|
+
# setting = XCrypt::Yescrypt.generate_scrypt_setting(n: 16384, r: 32, p: 1)
|
|
18
|
+
# hash = XCrypt.crypt("hunter2", setting)
|
|
19
|
+
module Yescrypt
|
|
20
|
+
extend self
|
|
21
|
+
|
|
22
|
+
# -------------------------------------------------------------------------
|
|
23
|
+
# Flag constants — mirrors alg-yescrypt.h
|
|
24
|
+
#
|
|
25
|
+
# These may be OR'd together to form the +flags:+ argument of
|
|
26
|
+
# {generate_setting}, except that {WORM} stands alone (do not combine
|
|
27
|
+
# with {RW}).
|
|
28
|
+
# -------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
# Classic scrypt with minimal extensions (t parameter support only).
|
|
31
|
+
WORM = 0x001
|
|
32
|
+
|
|
33
|
+
# Full yescrypt mode — time-memory tradeoff resistant.
|
|
34
|
+
RW = 0x002
|
|
35
|
+
|
|
36
|
+
# Number of inner rounds.
|
|
37
|
+
ROUNDS_3 = 0x000
|
|
38
|
+
ROUNDS_6 = 0x004
|
|
39
|
+
|
|
40
|
+
# Memory-access gather width.
|
|
41
|
+
GATHER_1 = 0x000
|
|
42
|
+
GATHER_2 = 0x008
|
|
43
|
+
GATHER_4 = 0x010
|
|
44
|
+
GATHER_8 = 0x018
|
|
45
|
+
|
|
46
|
+
# Simple mix factor.
|
|
47
|
+
SIMPLE_1 = 0x000
|
|
48
|
+
SIMPLE_2 = 0x020
|
|
49
|
+
SIMPLE_4 = 0x040
|
|
50
|
+
SIMPLE_8 = 0x060
|
|
51
|
+
|
|
52
|
+
# S-box size.
|
|
53
|
+
SBOX_6K = 0x000
|
|
54
|
+
SBOX_12K = 0x080
|
|
55
|
+
SBOX_24K = 0x100
|
|
56
|
+
SBOX_48K = 0x180
|
|
57
|
+
SBOX_96K = 0x200
|
|
58
|
+
SBOX_192K = 0x280
|
|
59
|
+
SBOX_384K = 0x300
|
|
60
|
+
SBOX_768K = 0x380
|
|
61
|
+
|
|
62
|
+
# Recommended defaults: RW mode with 6 rounds, 4-wide gather, 2x simple
|
|
63
|
+
# mix, and a 12 KiB S-box.
|
|
64
|
+
DEFAULTS = RW | ROUNDS_6 | GATHER_4 | SIMPLE_2 | SBOX_12K
|
|
65
|
+
|
|
66
|
+
# -------------------------------------------------------------------------
|
|
67
|
+
# Public interface
|
|
68
|
+
# -------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
# Generates a +$y$+ yescrypt setting string from explicit parameters.
|
|
71
|
+
#
|
|
72
|
+
# Implements the encoding of libxcrypt's +yescrypt_encode_params_r+
|
|
73
|
+
# (alg-yescrypt-common.c) in pure Ruby, because that function is not
|
|
74
|
+
# exported on Linux.
|
|
75
|
+
#
|
|
76
|
+
# @param n [Integer] memory/CPU cost; must be a power of 2 greater than 1
|
|
77
|
+
# @param r [Integer] block size (default: 8)
|
|
78
|
+
# @param p [Integer] parallelism (default: 1)
|
|
79
|
+
# @param t [Integer] additional time cost (default: 0)
|
|
80
|
+
# @param flags [Integer] yescrypt mode flags; see +WORM+/+RW+/+ROUNDS_*+
|
|
81
|
+
# etc.; defaults to {DEFAULTS}
|
|
82
|
+
# @return [String] a +$y$+ setting string
|
|
83
|
+
# @raise [ArgumentError] if +n+ is not a valid power of 2, or if +flags+
|
|
84
|
+
# is an unsupported combination
|
|
85
|
+
def generate_setting(n:, r: 8, p: 1, t: 0, flags: DEFAULTS)
|
|
86
|
+
n_log2 = log2_of_power_of_2(n)
|
|
87
|
+
|
|
88
|
+
# Compute the "flavor" field exactly as yescrypt_encode_params_r does:
|
|
89
|
+
# flags < RW → flavor = flags (WORM / pure-scrypt modes)
|
|
90
|
+
# flags is valid RW → flavor = RW + (flags >> 2)
|
|
91
|
+
flavor =
|
|
92
|
+
if flags < RW
|
|
93
|
+
flags
|
|
94
|
+
elsif (flags & 0x3) == RW && flags <= (RW | 0x3fc)
|
|
95
|
+
RW + (flags >> 2)
|
|
96
|
+
else
|
|
97
|
+
raise ArgumentError, "invalid yescrypt flags: 0x#{flags.to_s(16)}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# "have" bitmask indicates which optional fields follow r.
|
|
101
|
+
have = 0
|
|
102
|
+
have |= 1 if p != 1
|
|
103
|
+
have |= 2 if t != 0
|
|
104
|
+
|
|
105
|
+
setting = +"$y$"
|
|
106
|
+
setting << encode_varint(flavor, 0)
|
|
107
|
+
setting << encode_varint(n_log2, 1)
|
|
108
|
+
setting << encode_varint(r, 1)
|
|
109
|
+
if have != 0
|
|
110
|
+
setting << encode_varint(have, 1)
|
|
111
|
+
setting << encode_varint(p, 2) if p != 1
|
|
112
|
+
setting << encode_varint(t, 1) if t != 0
|
|
113
|
+
end
|
|
114
|
+
setting << "$"
|
|
115
|
+
setting << encode_bytes(SecureRandom.random_bytes(32))
|
|
116
|
+
setting
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Generates a +$7$+ scrypt setting string from explicit parameters.
|
|
120
|
+
#
|
|
121
|
+
# Encodes the setting using the same base-64 alphabet and field layout as
|
|
122
|
+
# libxcrypt's +gensalt_scrypt_rn+ (crypt-scrypt.c).
|
|
123
|
+
#
|
|
124
|
+
# @param n [Integer] memory/CPU cost; must be a power of 2 greater than 1
|
|
125
|
+
# @param r [Integer] block size (default: 32)
|
|
126
|
+
# @param p [Integer] parallelism (default: 1)
|
|
127
|
+
# @return [String] a +$7$+ setting string
|
|
128
|
+
# @raise [ArgumentError] if +n+ is not a valid power of 2
|
|
129
|
+
def generate_scrypt_setting(n:, r: 32, p: 1)
|
|
130
|
+
n_log2 = log2_of_power_of_2(n)
|
|
131
|
+
|
|
132
|
+
setting = +"$7$"
|
|
133
|
+
setting << B64[n_log2]
|
|
134
|
+
setting << encode_uint32(r, 30)
|
|
135
|
+
setting << encode_uint32(p, 30)
|
|
136
|
+
setting << encode_bytes(SecureRandom.random_bytes(32))
|
|
137
|
+
setting
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
# Base-64 alphabet shared by yescrypt and scrypt (crypt-style, not RFC 4648).
|
|
143
|
+
B64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
144
|
+
|
|
145
|
+
# Returns log2(n) after validating that n is a power of 2 greater than 1.
|
|
146
|
+
def log2_of_power_of_2(n)
|
|
147
|
+
n_log2 = Integer(Math.log2(n).round)
|
|
148
|
+
raise ArgumentError, "n must be a power of 2 greater than 1 (got #{n})" unless (1 << n_log2) == n && n > 1
|
|
149
|
+
n_log2
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Variable-length base-64 encoding for yescrypt parameter fields
|
|
153
|
+
# (encode64_uint32 in alg-yescrypt-common.c, last argument = min).
|
|
154
|
+
#
|
|
155
|
+
# The first character of the output encodes both the number of subsequent
|
|
156
|
+
# characters and the most-significant bits. The character ranges used are:
|
|
157
|
+
# 1 char : indices 0..47 (48 distinct values)
|
|
158
|
+
# 2 chars: indices 48..56 (9 × 64 = 576 additional values)
|
|
159
|
+
# 3 chars: indices 57..60 (4 × 64² = 16 384 additional values), …
|
|
160
|
+
def encode_varint(src, min)
|
|
161
|
+
raise ArgumentError, "value #{src} is below minimum #{min}" if src < min
|
|
162
|
+
|
|
163
|
+
src -= min
|
|
164
|
+
start = 0
|
|
165
|
+
endv = 47
|
|
166
|
+
chars = 1
|
|
167
|
+
bits = 0
|
|
168
|
+
|
|
169
|
+
loop do
|
|
170
|
+
count = (endv + 1 - start) << bits
|
|
171
|
+
break if src < count
|
|
172
|
+
raise ArgumentError, "value too large for yescrypt varint encoding" if start >= 63
|
|
173
|
+
|
|
174
|
+
start = endv + 1
|
|
175
|
+
endv = start + (62 - endv) / 2
|
|
176
|
+
src -= count
|
|
177
|
+
chars += 1
|
|
178
|
+
bits += 6
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
result = +B64[start + (src >> bits)]
|
|
182
|
+
(chars - 1).times { bits -= 6; result << B64[(src >> bits) & 0x3f] }
|
|
183
|
+
result
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Fixed-width base-64 encoding of a 32-bit value using +srcbits+ bits,
|
|
187
|
+
# LSB first (ceil(srcbits/6) output characters).
|
|
188
|
+
# Used for scrypt's r and p fields (encode64_uint32 in crypt-scrypt.c).
|
|
189
|
+
def encode_uint32(value, srcbits)
|
|
190
|
+
out = +""
|
|
191
|
+
bits = 0
|
|
192
|
+
while bits < srcbits
|
|
193
|
+
out << B64[value & 0x3f]
|
|
194
|
+
value >>= 6
|
|
195
|
+
bits += 6
|
|
196
|
+
end
|
|
197
|
+
out
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Encodes a binary string using the fixed-width base-64 scheme, processing
|
|
201
|
+
# 3 bytes (24 bits) into 4 characters at a time (encode64 in both
|
|
202
|
+
# alg-yescrypt-common.c and crypt-scrypt.c).
|
|
203
|
+
def encode_bytes(bytes)
|
|
204
|
+
out = +""
|
|
205
|
+
i = 0
|
|
206
|
+
while i < bytes.bytesize
|
|
207
|
+
value = 0
|
|
208
|
+
bits = 0
|
|
209
|
+
while bits < 24 && i < bytes.bytesize
|
|
210
|
+
value |= bytes.getbyte(i) << bits
|
|
211
|
+
bits += 8
|
|
212
|
+
i += 1
|
|
213
|
+
end
|
|
214
|
+
out << encode_uint32(value, bits)
|
|
215
|
+
end
|
|
216
|
+
out
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
data/lib/xcrypt.rb
CHANGED
|
@@ -1,11 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Top-level module providing a high-level Ruby interface to libxcrypt, a
|
|
4
|
+
# modern library for one-way hashing of passwords.
|
|
5
|
+
#
|
|
6
|
+
# All public methods are available directly on the module.
|
|
7
|
+
# The most common entry points are the algorithm-specific convenience methods
|
|
8
|
+
# ({yescrypt}, {bcrypt}, {sha512}, etc.) and {verify}.
|
|
9
|
+
#
|
|
10
|
+
# @example Hash a password with yescrypt (the strongest supported algorithm)
|
|
11
|
+
# hash = XCrypt.yescrypt("correct horse battery staple")
|
|
12
|
+
# XCrypt.verify("correct horse battery staple", hash) #=> true
|
|
13
|
+
#
|
|
14
|
+
# @example Hash with an explicit cost factor
|
|
15
|
+
# hash = XCrypt.bcrypt("hunter2", cost: 12)
|
|
16
|
+
#
|
|
17
|
+
# @example Use the generic interface
|
|
18
|
+
# hash = XCrypt.crypt("hunter2", algorithm: :sha512)
|
|
19
|
+
#
|
|
20
|
+
# @example Generate a yescrypt setting with explicit N, r, p, t, and flags
|
|
21
|
+
# setting = XCrypt.generate_setting(:yescrypt, n: 16384, r: 8, p: 1, t: 0,
|
|
22
|
+
# flags: XCrypt::YESCRYPT_DEFAULTS)
|
|
23
|
+
# hash = XCrypt.crypt("hunter2", setting)
|
|
24
|
+
#
|
|
25
|
+
# @example Generate a scrypt ($7$) setting with explicit N, r, p
|
|
26
|
+
# setting = XCrypt.generate_setting(:scrypt, n: 16384, r: 32, p: 1)
|
|
27
|
+
# hash = XCrypt.crypt("hunter2", setting)
|
|
3
28
|
module XCrypt
|
|
4
29
|
require "xcrypt/version"
|
|
5
30
|
require "xcrypt/ffi"
|
|
31
|
+
require "xcrypt/yescrypt"
|
|
6
32
|
|
|
33
|
+
# Raised when hashing or salt generation fails. Common causes include an
|
|
34
|
+
# unsupported algorithm, a malformed setting string, or a passphrase that
|
|
35
|
+
# exceeds {FFI::CRYPT_MAX_PASSPHRASE_SIZE} bytes.
|
|
7
36
|
Error ||= Class.new(StandardError)
|
|
8
37
|
|
|
38
|
+
# Maps each supported algorithm name to its setting-string prefix.
|
|
39
|
+
#
|
|
40
|
+
# The prefix is the leading characters of any hash produced by that
|
|
41
|
+
# algorithm and is used to identify the algorithm from an existing hash.
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash{Symbol => String}]
|
|
9
44
|
ALGORITHMS = {
|
|
10
45
|
yescrypt: "$y$",
|
|
11
46
|
gost_yescrypt: "$gy$",
|
|
@@ -25,25 +60,131 @@ module XCrypt
|
|
|
25
60
|
|
|
26
61
|
extend self
|
|
27
62
|
|
|
63
|
+
# @!method yescrypt(phrase, setting = nil, cost: nil)
|
|
64
|
+
# Hash +phrase+ using yescrypt, the strongest supported algorithm.
|
|
65
|
+
# @param phrase [String] the password to hash
|
|
66
|
+
# @param setting [String, nil] an existing hash or salt string to use as
|
|
67
|
+
# the setting; a new setting is generated automatically when +nil+
|
|
68
|
+
# @param cost [Integer, nil] work-factor override; uses the library
|
|
69
|
+
# default when +nil+
|
|
70
|
+
# @return [String] the hashed password
|
|
71
|
+
# @raise [ArgumentError] if +setting+ belongs to a different algorithm
|
|
72
|
+
# @raise [Error] if hashing fails
|
|
73
|
+
|
|
74
|
+
# @!method gost_yescrypt(phrase, setting = nil, cost: nil)
|
|
75
|
+
# Hash +phrase+ using GOST R 34.11-2012 combined with yescrypt.
|
|
76
|
+
# @param (see #yescrypt)
|
|
77
|
+
# @return (see #yescrypt)
|
|
78
|
+
# @raise (see #yescrypt)
|
|
79
|
+
|
|
80
|
+
# @!method scrypt(phrase, setting = nil, cost: nil)
|
|
81
|
+
# Hash +phrase+ using scrypt.
|
|
82
|
+
# @param (see #yescrypt)
|
|
83
|
+
# @return (see #yescrypt)
|
|
84
|
+
# @raise (see #yescrypt)
|
|
85
|
+
|
|
86
|
+
# @!method bcrypt(phrase, setting = nil, cost: nil)
|
|
87
|
+
# Hash +phrase+ using bcrypt (Blowfish-based password hashing).
|
|
88
|
+
# @param (see #yescrypt)
|
|
89
|
+
# @return (see #yescrypt)
|
|
90
|
+
# @raise (see #yescrypt)
|
|
91
|
+
|
|
92
|
+
# @!method sha512(phrase, setting = nil, cost: nil)
|
|
93
|
+
# Hash +phrase+ using SHA-512 crypt.
|
|
94
|
+
# @param (see #yescrypt)
|
|
95
|
+
# @return (see #yescrypt)
|
|
96
|
+
# @raise (see #yescrypt)
|
|
97
|
+
|
|
98
|
+
# @!method sha256(phrase, setting = nil, cost: nil)
|
|
99
|
+
# Hash +phrase+ using SHA-256 crypt.
|
|
100
|
+
# @param (see #yescrypt)
|
|
101
|
+
# @return (see #yescrypt)
|
|
102
|
+
# @raise (see #yescrypt)
|
|
103
|
+
|
|
104
|
+
# @!method sha1(phrase, setting = nil, cost: nil)
|
|
105
|
+
# Hash +phrase+ using HMAC-SHA1 NetBSD crypt.
|
|
106
|
+
# @param (see #yescrypt)
|
|
107
|
+
# @return (see #yescrypt)
|
|
108
|
+
# @raise (see #yescrypt)
|
|
109
|
+
|
|
110
|
+
# @!method sun_md5(phrase, setting = nil, cost: nil)
|
|
111
|
+
# Hash +phrase+ using SunMD5 (Solaris MD5 crypt).
|
|
112
|
+
# @param (see #yescrypt)
|
|
113
|
+
# @return (see #yescrypt)
|
|
114
|
+
# @raise (see #yescrypt)
|
|
115
|
+
|
|
116
|
+
# @!method md5(phrase, setting = nil, cost: nil)
|
|
117
|
+
# Hash +phrase+ using MD5 crypt.
|
|
118
|
+
# @param (see #yescrypt)
|
|
119
|
+
# @return (see #yescrypt)
|
|
120
|
+
# @raise (see #yescrypt)
|
|
121
|
+
|
|
122
|
+
# @!method bsdi_des(phrase, setting = nil, cost: nil)
|
|
123
|
+
# Hash +phrase+ using BSDi extended DES crypt.
|
|
124
|
+
# @param (see #yescrypt)
|
|
125
|
+
# @return (see #yescrypt)
|
|
126
|
+
# @raise (see #yescrypt)
|
|
127
|
+
|
|
128
|
+
# @!method des(phrase, setting = nil, cost: nil)
|
|
129
|
+
# Hash +phrase+ using traditional DES crypt.
|
|
130
|
+
# @param (see #yescrypt)
|
|
131
|
+
# @return (see #yescrypt)
|
|
132
|
+
# @raise (see #yescrypt)
|
|
133
|
+
|
|
28
134
|
ALGORITHMS.each_key do |algorithm|
|
|
29
|
-
define_method(algorithm) do |phrase, setting = nil, cost: nil|
|
|
135
|
+
define_method(algorithm) do |phrase, setting = nil, cost: nil, n: nil, r: nil, p: nil, t: nil, flags: nil|
|
|
30
136
|
if setting
|
|
31
137
|
setting_algorithm = detect_algorithm(setting)
|
|
32
138
|
if setting_algorithm != algorithm
|
|
33
139
|
raise ArgumentError, "setting algorithm #{setting_algorithm.inspect} does not match expected #{algorithm.inspect}"
|
|
34
140
|
end
|
|
35
141
|
end
|
|
36
|
-
crypt(phrase, setting, algorithm:, cost:)
|
|
142
|
+
crypt(phrase, setting, algorithm:, cost:, n:, r:, p:, t:, flags:)
|
|
37
143
|
end
|
|
38
144
|
end
|
|
39
145
|
|
|
146
|
+
# Returns the names of all supported algorithms.
|
|
147
|
+
#
|
|
148
|
+
# @return [Array<Symbol>] algorithm names in order from strongest to weakest
|
|
40
149
|
def algorithms = ALGORITHMS.keys
|
|
41
150
|
|
|
151
|
+
# Detects which algorithm produced a given setting or hash string by
|
|
152
|
+
# matching its leading prefix against {ALGORITHMS}.
|
|
153
|
+
#
|
|
154
|
+
# @param setting [String] a setting string or an existing password hash
|
|
155
|
+
# @return [Symbol, nil] the algorithm name, or +nil+ if the prefix is
|
|
156
|
+
# unrecognized
|
|
42
157
|
def detect_algorithm(setting) = PREFIXES[setting[/\A\$\w+\$?|_/].to_s]
|
|
43
158
|
|
|
44
|
-
|
|
159
|
+
# Hashes +phrase+ using libxcrypt's +crypt_rn+ function.
|
|
160
|
+
#
|
|
161
|
+
# When both +setting+ and +algorithm+ are omitted, a fresh setting is
|
|
162
|
+
# generated with the library's default algorithm. The result is always a
|
|
163
|
+
# self-describing string whose leading prefix identifies the algorithm and
|
|
164
|
+
# encodes the salt, making it safe to store directly.
|
|
165
|
+
#
|
|
166
|
+
# @param phrase [String] the password to hash
|
|
167
|
+
# @param setting [String, Symbol, nil] an existing hash or salt string, or
|
|
168
|
+
# an algorithm +Symbol+ as shorthand for passing only +algorithm:+;
|
|
169
|
+
# generates a fresh setting when +nil+
|
|
170
|
+
# @param algorithm [Symbol, nil] algorithm to use when generating a new
|
|
171
|
+
# setting; ignored when +setting+ is already a String
|
|
172
|
+
# @param cost [Integer, nil] work-factor override passed to
|
|
173
|
+
# {generate_setting}; uses the library default when +nil+
|
|
174
|
+
# @param n [Integer, nil] explicit N parameter for yescrypt/scrypt; passed
|
|
175
|
+
# to {generate_setting} when no +setting+ is provided
|
|
176
|
+
# @param r [Integer, nil] explicit r parameter; passed to {generate_setting}
|
|
177
|
+
# @param p [Integer, nil] explicit p parameter; passed to {generate_setting}
|
|
178
|
+
# @param t [Integer, nil] explicit t parameter (yescrypt only); passed to
|
|
179
|
+
# {generate_setting}
|
|
180
|
+
# @param flags [Integer, nil] explicit yescrypt flags; passed to
|
|
181
|
+
# {generate_setting}
|
|
182
|
+
# @return [String] the hashed password
|
|
183
|
+
# @raise [Error] if +crypt_rn+ returns +NULL+, indicating an invalid
|
|
184
|
+
# setting or an unsupported algorithm
|
|
185
|
+
def crypt(phrase, setting = nil, algorithm: nil, cost: nil, n: nil, r: nil, p: nil, t: nil, flags: nil)
|
|
45
186
|
setting, algorithm = nil, setting if setting.is_a? Symbol
|
|
46
|
-
setting ||= generate_setting(algorithm, cost:)
|
|
187
|
+
setting ||= generate_setting(algorithm, cost:, n:, r:, p:, t:, flags:)
|
|
47
188
|
data = ::FFI::MemoryPointer.new(:uint8, FFI::CRYPT_DATA_SIZE)
|
|
48
189
|
result_ptr = FFI.crypt_rn(phrase, setting, data, FFI::CRYPT_DATA_SIZE)
|
|
49
190
|
raise Error, "crypt failed: invalid setting or unsupported algorithm" if result_ptr.null?
|
|
@@ -52,6 +193,17 @@ module XCrypt
|
|
|
52
193
|
data&.clear
|
|
53
194
|
end
|
|
54
195
|
|
|
196
|
+
# Verifies that +phrase+ matches a previously computed +hash+.
|
|
197
|
+
#
|
|
198
|
+
# Returns +false+ immediately for any hash value that would cause
|
|
199
|
+
# libxcrypt to return a magic failure token (strings beginning with +"*"+),
|
|
200
|
+
# or for empty or +nil+ input, guarding against invalid-hash oracle attacks.
|
|
201
|
+
# The final comparison is performed in constant time to prevent timing
|
|
202
|
+
# attacks.
|
|
203
|
+
#
|
|
204
|
+
# @param phrase [String] the candidate password
|
|
205
|
+
# @param hash [String, nil] the stored password hash to verify against
|
|
206
|
+
# @return [Boolean] +true+ if +phrase+ matches +hash+, +false+ otherwise
|
|
55
207
|
def verify(phrase, hash)
|
|
56
208
|
return false if hash.nil? || hash.empty? || hash.start_with?("*")
|
|
57
209
|
result = crypt(phrase, hash)
|
|
@@ -60,8 +212,60 @@ module XCrypt
|
|
|
60
212
|
false
|
|
61
213
|
end
|
|
62
214
|
|
|
63
|
-
|
|
64
|
-
|
|
215
|
+
# Generates a fresh setting string suitable for passing to {crypt}.
|
|
216
|
+
#
|
|
217
|
+
# When only +algorithm+ and optionally +cost+ are given, delegates to
|
|
218
|
+
# libxcrypt's +crypt_gensalt_rn+, which draws entropy from the OS.
|
|
219
|
+
#
|
|
220
|
+
# When +n+, +r+, +p+, +t+, or +flags+ are supplied the method generates the
|
|
221
|
+
# setting directly from those parameters instead, delegating to
|
|
222
|
+
# {XCrypt::Yescrypt}:
|
|
223
|
+
#
|
|
224
|
+
# * For +:yescrypt+ (and +:gost_yescrypt+): delegates to
|
|
225
|
+
# {XCrypt::Yescrypt.generate_setting}, producing a +$y$+ setting.
|
|
226
|
+
# +n+ must be a power of 2 greater than 1; +r+, +p+, +t+, and +flags+
|
|
227
|
+
# default to 8, 1, 0, and {XCrypt::Yescrypt::DEFAULTS} respectively.
|
|
228
|
+
#
|
|
229
|
+
# * For +:scrypt+: delegates to {XCrypt::Yescrypt.generate_scrypt_setting},
|
|
230
|
+
# producing a +$7$+ setting. +n+ must be a power of 2 (2..2^63); +r+
|
|
231
|
+
# and +p+ default to 32 and 1. +t+ and +flags+ are not used for scrypt.
|
|
232
|
+
#
|
|
233
|
+
# When +algorithm+ is +nil+, the library selects its preferred algorithm.
|
|
234
|
+
#
|
|
235
|
+
# @param algorithm [Symbol, nil] the desired algorithm; uses the library
|
|
236
|
+
# default when +nil+
|
|
237
|
+
# @param cost [Integer, nil] work-factor for the generated setting; a value
|
|
238
|
+
# of +0+ selects the library's own default cost; ignored when +n:+ is set
|
|
239
|
+
# @param n [Integer, nil] explicit N (memory/CPU cost, must be a power of 2
|
|
240
|
+
# greater than 1); yescrypt and scrypt only
|
|
241
|
+
# @param r [Integer, nil] block size parameter; yescrypt and scrypt only
|
|
242
|
+
# @param p [Integer, nil] parallelism parameter; yescrypt and scrypt only
|
|
243
|
+
# @param t [Integer, nil] additional time cost; yescrypt only
|
|
244
|
+
# @param flags [Integer, nil] yescrypt mode flags; see {XCrypt::Yescrypt}
|
|
245
|
+
# constants; yescrypt only; defaults to {XCrypt::Yescrypt::DEFAULTS}
|
|
246
|
+
# @return [String] a setting string beginning with the algorithm prefix
|
|
247
|
+
# @raise [ArgumentError] if +algorithm+ is not a key in {ALGORITHMS}, or if
|
|
248
|
+
# +n+ is not a power of 2 greater than 1
|
|
249
|
+
# @raise [Error] if the underlying C call returns +NULL+
|
|
250
|
+
def generate_setting(algorithm = nil, cost: nil, n: nil, r: nil, p: nil, t: nil, flags: nil)
|
|
251
|
+
if algorithm
|
|
252
|
+
ALGORITHMS.key?(algorithm) or raise ArgumentError, "unknown algorithm: #{algorithm.inspect}"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
if n || r || p || t || flags
|
|
256
|
+
case algorithm
|
|
257
|
+
when :yescrypt, :gost_yescrypt, nil
|
|
258
|
+
return Yescrypt.generate_setting(n: n || 4096, r: r || 8, p: p || 1,
|
|
259
|
+
t: t || 0, flags: flags || Yescrypt::DEFAULTS)
|
|
260
|
+
when :scrypt
|
|
261
|
+
return Yescrypt.generate_scrypt_setting(n: n || 16384, r: r || 32, p: p || 1)
|
|
262
|
+
else
|
|
263
|
+
raise ArgumentError,
|
|
264
|
+
"n/r/p/t/flags parameters are only supported for :yescrypt and :scrypt, got #{algorithm.inspect}"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
prefix = ALGORITHMS[algorithm]
|
|
65
269
|
cost ||= 0
|
|
66
270
|
|
|
67
271
|
output = ::FFI::MemoryPointer.new(:char, FFI::CRYPT_GENSALT_OUTPUT_SIZE)
|
|
@@ -73,6 +277,19 @@ module XCrypt
|
|
|
73
277
|
|
|
74
278
|
private
|
|
75
279
|
|
|
280
|
+
# Compares two strings in constant time to prevent timing attacks.
|
|
281
|
+
#
|
|
282
|
+
# Pads or truncates +trusted+ to match +untrusted+'s byte length before
|
|
283
|
+
# comparing so that the number of loop iterations is always the same
|
|
284
|
+
# regardless of content. A separate length check at the end ensures that a
|
|
285
|
+
# length-padded match is still rejected.
|
|
286
|
+
#
|
|
287
|
+
# Uses {OpenSSL.fixed_length_secure_compare} when available (Ruby >= 2.7
|
|
288
|
+
# with openssl >= 2.2); otherwise falls back to a pure-Ruby XOR loop.
|
|
289
|
+
#
|
|
290
|
+
# @param trusted [String] the known-good value (e.g., the output of {crypt})
|
|
291
|
+
# @param untrusted [String] the value supplied by the caller
|
|
292
|
+
# @return [Boolean] +true+ only when both strings are identical
|
|
76
293
|
def secure_compare(trusted, untrusted)
|
|
77
294
|
return false unless trusted.respond_to? :to_str and trusted = trusted.to_str.b
|
|
78
295
|
return false unless untrusted.respond_to? :to_str and untrusted = untrusted.to_str.b
|