Module: BetterAuth::Crypto

Defined in:
lib/better_auth/crypto.rb,
lib/better_auth/crypto/jwe.rb

Defined Under Namespace

Modules: JWE

Constant Summary collapse

URL_SAFE_ALPHABET =
[*"a".."z", *"A".."Z", *"0".."9", "-", "_"].freeze
MASK_64 =
(1 << 64) - 1
KECCAK_ROUND_CONSTANTS =
[
  0x0000000000000001, 0x0000000000008082, 0x800000000000808a, 0x8000000080008000,
  0x000000000000808b, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009,
  0x000000000000008a, 0x0000000000000088, 0x0000000080008009, 0x000000008000000a,
  0x000000008000808b, 0x800000000000008b, 0x8000000000008089, 0x8000000000008003,
  0x8000000000008002, 0x8000000000000080, 0x000000000000800a, 0x800000008000000a,
  0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008
].freeze
KECCAK_ROTATION_OFFSETS =
[
  [0, 36, 3, 41, 18],
  [1, 44, 10, 45, 2],
  [62, 6, 43, 15, 61],
  [28, 55, 25, 21, 56],
  [27, 20, 39, 8, 14]
].freeze

Class Method Summary collapse

Class Method Details

.base64url_decode(value) ⇒ Object



129
130
131
# File 'lib/better_auth/crypto.rb', line 129

def base64url_decode(value)
  Base64.urlsafe_decode64(value.to_s)
end

.base64url_encode(value) ⇒ Object



125
126
127
# File 'lib/better_auth/crypto.rb', line 125

def base64url_encode(value)
  Base64.urlsafe_encode64(value.to_s, padding: false)
end

.constant_time_compare(left, right) ⇒ Object



69
70
71
72
73
# File 'lib/better_auth/crypto.rb', line 69

def constant_time_compare(left, right)
  return false unless left.bytesize == right.bytesize

  OpenSSL.fixed_length_secure_compare(left, right)
end

.hmac_signature(value, secret, encoding: :base64) ⇒ Object



59
60
61
62
# File 'lib/better_auth/crypto.rb', line 59

def hmac_signature(value, secret, encoding: :base64)
  digest = OpenSSL::HMAC.digest("SHA256", secret.to_s, value.to_s)
  (encoding == :base64url) ? base64url_encode(digest) : Base64.strict_encode64(digest)
end

.keccak256(value, encoding: :hex) ⇒ Object



45
46
47
48
# File 'lib/better_auth/crypto.rb', line 45

def keccak256(value, encoding: :hex)
  digest = keccak256_bytes(value.to_s.b)
  (encoding == :bytes) ? digest : digest.unpack1("H*")
end

.keccak256_bytes(input) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/better_auth/crypto.rb', line 140

def keccak256_bytes(input)
  rate = 136
  state = Array.new(25, 0)
  padded = input.bytes
  padded << 0x01
  padded << 0 while (padded.length % rate) != rate - 1
  padded << 0x80

  padded.each_slice(rate) do |block|
    block.each_with_index do |byte, index|
      state[index / 8] ^= byte << (8 * (index % 8))
    end
    keccak_permute!(state)
  end

  state.pack("Q<*").byteslice(0, 32)
end

.keccak_permute!(state) ⇒ Object



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/better_auth/crypto.rb', line 158

def keccak_permute!(state)
  KECCAK_ROUND_CONSTANTS.each do |round_constant|
    columns = Array.new(5) { |x| state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20] }
    deltas = Array.new(5) { |x| columns[(x - 1) % 5] ^ rotate_left_64(columns[(x + 1) % 5], 1) }
    5.times do |x|
      5.times { |y| state[x + (5 * y)] = (state[x + (5 * y)] ^ deltas[x]) & MASK_64 }
    end

    rotated = Array.new(25, 0)
    5.times do |x|
      5.times do |y|
        rotated[y + (5 * ((2 * x + 3 * y) % 5))] =
          rotate_left_64(state[x + (5 * y)], KECCAK_ROTATION_OFFSETS[x][y])
      end
    end

    5.times do |y|
      5.times do |x|
        state[x + (5 * y)] =
          (rotated[x + (5 * y)] ^ ((~rotated[((x + 1) % 5) + (5 * y)]) & rotated[((x + 2) % 5) + (5 * y)])) & MASK_64
      end
    end
    state[0] = (state[0] ^ round_constant) & MASK_64
  end
end

.random_string(length = 32) ⇒ Object



32
33
34
# File 'lib/better_auth/crypto.rb', line 32

def random_string(length = 32)
  Array.new(length) { URL_SAFE_ALPHABET[SecureRandom.random_number(URL_SAFE_ALPHABET.length)] }.join
end

.rotate_left_64(value, shift) ⇒ Object



184
185
186
187
188
189
# File 'lib/better_auth/crypto.rb', line 184

def rotate_left_64(value, shift)
  shift %= 64
  return value & MASK_64 if shift.zero?

  ((value << shift) | (value >> (64 - shift))) & MASK_64
end

.sha256(value, encoding: :hex) ⇒ Object



40
41
42
43
# File 'lib/better_auth/crypto.rb', line 40

def sha256(value, encoding: :hex)
  digest = OpenSSL::Digest.digest("SHA256", value.to_s)
  (encoding == :base64url) ? base64url_encode(digest) : digest.unpack1("H*")
end

.sign_jwt(payload, secret, expires_in: 3600) ⇒ Object



102
103
104
105
106
107
108
# File 'lib/better_auth/crypto.rb', line 102

def sign_jwt(payload, secret, expires_in: 3600)
  claims = stringify_keys(payload).merge(
    "iat" => Time.now.to_i,
    "exp" => Time.now.to_i + expires_in.to_i
  )
  JWT.encode(claims, secret.to_s, "HS256")
end

.stringify_keys(value) ⇒ Object



133
134
135
136
137
138
# File 'lib/better_auth/crypto.rb', line 133

def stringify_keys(value)
  return value.each_with_object({}) { |(key, object_value), result| result[key.to_s] = stringify_keys(object_value) } if value.is_a?(Hash)
  return value.map { |entry| stringify_keys(entry) } if value.is_a?(Array)

  value
end

.symmetric_decode_jwt(token, secret, salt) ⇒ Object



121
122
123
# File 'lib/better_auth/crypto.rb', line 121

def symmetric_decode_jwt(token, secret, salt)
  JWE.decode(token, secret, salt)
end

.symmetric_decrypt(key:, data:) ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
# File 'lib/better_auth/crypto.rb', line 90

def symmetric_decrypt(key:, data:)
  payload = JSON.parse(base64url_decode(data.to_s))
  cipher = OpenSSL::Cipher.new("aes-256-gcm")
  cipher.decrypt
  cipher.key = OpenSSL::Digest.digest("SHA256", key.to_s)
  cipher.iv = base64url_decode(payload.fetch("iv"))
  cipher.auth_tag = base64url_decode(payload.fetch("tag"))
  cipher.update(base64url_decode(payload.fetch("data"))) + cipher.final
rescue JSON::ParserError, KeyError, OpenSSL::Cipher::CipherError, ArgumentError
  nil
end

.symmetric_encode_jwt(payload, secret, salt, expires_in: 3600) ⇒ Object



117
118
119
# File 'lib/better_auth/crypto.rb', line 117

def symmetric_encode_jwt(payload, secret, salt, expires_in: 3600)
  JWE.encode(payload, secret, salt, expires_in: expires_in)
end

.symmetric_encrypt(key:, data:) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/better_auth/crypto.rb', line 75

def symmetric_encrypt(key:, data:)
  cipher = OpenSSL::Cipher.new("aes-256-gcm")
  cipher.encrypt
  cipher.key = OpenSSL::Digest.digest("SHA256", key.to_s)
  iv = SecureRandom.random_bytes(12)
  cipher.iv = iv
  ciphertext = cipher.update(data.to_s) + cipher.final
  payload = {
    "iv" => base64url_encode(iv),
    "data" => base64url_encode(ciphertext),
    "tag" => base64url_encode(cipher.auth_tag)
  }
  base64url_encode(JSON.generate(payload))
end

.to_checksum_address(address) ⇒ Object



50
51
52
53
54
55
56
57
# File 'lib/better_auth/crypto.rb', line 50

def to_checksum_address(address)
  normalized = address.to_s.downcase.delete_prefix("0x")
  hash = keccak256(normalized)

  "0x" + normalized.chars.each_with_index.map do |char, index|
    (hash[index].to_i(16) >= 8) ? char.upcase : char
  end.join
end

.uuidObject



36
37
38
# File 'lib/better_auth/crypto.rb', line 36

def uuid
  SecureRandom.uuid
end

.verify_hmac_signature(value, signature, secret, encoding: :base64) ⇒ Object



64
65
66
67
# File 'lib/better_auth/crypto.rb', line 64

def verify_hmac_signature(value, signature, secret, encoding: :base64)
  expected = hmac_signature(value, secret, encoding: encoding)
  constant_time_compare(expected, signature.to_s)
end

.verify_jwt(token, secret) ⇒ Object



110
111
112
113
114
115
# File 'lib/better_auth/crypto.rb', line 110

def verify_jwt(token, secret)
  decoded, = JWT.decode(token.to_s, secret.to_s, true, algorithm: "HS256")
  decoded
rescue JWT::DecodeError
  nil
end