Class: MemoriClient::Engine::HMACHelper

Inherits:
Object
  • Object
show all
Defined in:
lib/memori_client/engine/hmac_helper.rb

Constant Summary collapse

IPAD =
0x36
OPAD =
0x5c

Instance Method Summary collapse

Constructor Details

#initialize(app_id, key, max_delta_secs = 30) ⇒ HMACHelper

Returns a new instance of HMACHelper.

Raises:

  • (ArgumentError)


9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/memori_client/engine/hmac_helper.rb', line 9

def initialize(app_id, key, max_delta_secs = 30)
  # Check arguments
  raise ArgumentError, "HMAC: invalid appID" if app_id.nil? || app_id <= 0 || app_id == ''
  raise ArgumentError, "HMAC: invalid key" if key.nil? || key.to_s == "00000000-0000-0000-0000-000000000000"

  # Initialization
  @app_id = app_id
  @max_delta_secs = max_delta_secs

  @message_cache = {}
  @cache_mutex = Mutex.new

  @@next_nonce ||= 0
  @nonce_mutex = Mutex.new

  # Build inner and outer keys
  key_bytes = guid_to_bytes(key)

  inner_key_bytes = key_bytes.map { |byte| byte ^ IPAD }
  @inner_key = inner_key_bytes.map { |byte| byte.to_s(16).rjust(2, '0') }.join

  outer_key_bytes = key_bytes.map { |byte| byte ^ OPAD }
  @outer_key = outer_key_bytes.map { |byte| byte.to_s(16).rjust(2, '0') }.join

  # Start a thread for cache expiration
  Thread.new do
    loop do
      clean_cache
      sleep 1
    end
  end
end

Instance Method Details

#build_hmac(nonce:, epoch:) ⇒ Hash

Not meant to be invoked directly, but could be used for testing

Returns:

  • (Hash)

    the message and the signed HMAC



44
45
46
47
48
49
50
# File 'lib/memori_client/engine/hmac_helper.rb', line 44

def build_hmac(nonce:, epoch:)
  message = "#{@app_id}:#{nonce}:#{epoch}"

  # Sign the message
  signature = sign(message, @inner_key, @outer_key)
  [message, signature]
end

#next_hmacObject



52
53
54
55
56
57
58
# File 'lib/memori_client/engine/hmac_helper.rb', line 52

def next_hmac
  nonce = @nonce_mutex.synchronize { @@next_nonce += 1 }
  epoch = Time.now.utc.to_i  # Use UTC time like C#
  message, signature = build_hmac(nonce: nonce, epoch: epoch)

  "#{message}:#{signature}"
end

#verify_hmac(hmac) ⇒ Object

Raises:

  • (ArgumentError)


60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/memori_client/engine/hmac_helper.rb', line 60

def verify_hmac(hmac)
  # Check arguments
  raise ArgumentError, "HMAC: invalid message" if hmac.nil? || hmac.empty?

  # String split HMAC message
  parts = hmac.split(':')
  raise ArgumentError, "HMAC: invalid message format" if parts.length != 4

  # Check the app ID
  begin
    app_id = Integer(parts[0])
  rescue
    raise ArgumentError, "HMAC: invalid app ID"
  end

  raise ArgumentError, "HMAC: unrecognized app ID" if app_id != @app_id

  # Check the nonce
  begin
    nonce = Integer(parts[1])
  rescue
    raise ArgumentError, "HMAC: invalid nonce"
  end

  raise ArgumentError, "HMAC: invalid nonce" if nonce < 1

  # Check the timestamp
  begin
    timestamp = Integer(parts[2])
  rescue
    raise ArgumentError, "HMAC: invalid timestamp"
  end

  epoch = Time.now.utc.to_i  # Use UTC time like C#
  time_diff = epoch - timestamp
  raise ArgumentError, "HMAC: timestamp out of range" if time_diff <= -@max_delta_secs || time_diff >= @max_delta_secs

  # Check the signature
  message = "#{app_id}:#{nonce}:#{timestamp}"
  expected_signature = sign(message, @inner_key, @outer_key)
  actual_signature = parts[3]

  # Use a constant-time comparison to prevent timing attacks
  signature_valid = secure_compare(expected_signature, actual_signature)
  raise ArgumentError, "HMAC: wrong signature" unless signature_valid

  # Check if the message has already been seen
  is_new = false

  @cache_mutex.synchronize do
    unless @message_cache.key?(message)
      @message_cache[message] = Time.now.to_i + 30
      is_new = true
    end
  end

  raise ArgumentError, "HMAC: message already seen" unless is_new

  true
end