Module: Familia::VerifiableIdentifier
- Extended by:
- SecureIdentifier
- Defined in:
- lib/familia/verifiable_identifier.rb
Overview
Creates and verifies identifiers that contain an embedded HMAC signature, allowing for stateless verification of an identifier's authenticity.
Constant Summary collapse
- SECRET_KEY =
Note:
Security Considerations:
- Secrecy: This key MUST be kept secret and secure, just like a database password or API key. Do not commit it to version control.
- Consistency: All running instances of your application must use the exact same key, otherwise verification will fail across different servers.
- Rotation: If this key is ever compromised, it must be rotated. Be aware that rotating the key will invalidate all previously generated verifiable identifiers.
The secret key for HMAC generation, loaded from an environment variable.
This key is the root of trust for verifying identifier authenticity. It must be a long, random, and cryptographically strong string.
ENV.fetch('VERIFIABLE_ID_HMAC_SECRET', 'cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d')
- RANDOM_HEX_LENGTH =
The length of the random part of the ID in hex characters (256 bits).
64- TAG_HEX_LENGTH =
The length of the HMAC tag in hex characters (64 bits). 64 bits is strong enough to prevent forgery (1 in 18 quintillion chance).
16
Instance Attribute Summary collapse
-
#SECRET_KEY ⇒ String
readonly
The secret key.
Class Method Summary collapse
-
.generate_verifiable_id(base_or_scope = nil, scope: nil, base: 36) ⇒ String
Generates a verifiable, base-36 encoded identifier.
-
.plausible_identifier?(identifier_str, base = 36) ⇒ Boolean
Checks if an identifier is plausible (correct format and length) without performing cryptographic verification.
-
.verified_identifier?(verifiable_id, base_or_scope = nil, scope: nil, base: 36) ⇒ Boolean
Verifies the authenticity of a given identifier using a timing-safe comparison.
Methods included from SecureIdentifier
generate_id, generate_lite_id, generate_trace_id, shorten_to_trace_id, truncate_hex
Instance Attribute Details
#SECRET_KEY ⇒ String (readonly)
Returns The secret key.
41 |
# File 'lib/familia/verifiable_identifier.rb', line 41 SECRET_KEY = ENV.fetch('VERIFIABLE_ID_HMAC_SECRET', 'cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d') |
Class Method Details
.generate_verifiable_id(base_or_scope = nil, scope: nil, base: 36) ⇒ String
Generates a verifiable, base-36 encoded identifier.
The final identifier contains a 256-bit random component and a 64-bit authentication tag.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/familia/verifiable_identifier.rb', line 56 def self.generate_verifiable_id(base_or_scope = nil, scope: nil, base: 36) # Handle backward compatibility with positional base argument if base_or_scope.is_a?(Integer) base = base_or_scope # scope remains as passed in keyword argument elsif base_or_scope.is_a?(String) || base_or_scope.nil? scope = base_or_scope if scope.nil? # base remains as passed in keyword argument or default end # Re-use generate_id from the SecureIdentifier module. random_hex = generate_id(16) tag_hex = generate_tag(random_hex, scope: scope) combined_hex = random_hex + tag_hex # Re-use the min_length_for_bits helper from the SecureIdentifier module. total_bits = (RANDOM_HEX_LENGTH + TAG_HEX_LENGTH) * 4 target_length = Familia::SecureIdentifier.min_length_for_bits(total_bits, base) combined_hex.to_i(16).to_s(base).rjust(target_length, '0') end |
.plausible_identifier?(identifier_str, base = 36) ⇒ Boolean
Checks if an identifier is plausible (correct format and length) without performing cryptographic verification.
This can be used as a fast pre-flight check to reject obviously malformed identifiers.
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/familia/verifiable_identifier.rb', line 115 def self.plausible_identifier?(identifier_str, base = 36) return false unless identifier_str.is_a?(::String) # 1. Check length total_bits = (RANDOM_HEX_LENGTH + TAG_HEX_LENGTH) * 4 expected_length = Familia::SecureIdentifier.min_length_for_bits(total_bits, base) return false unless identifier_str.length == expected_length # 2. Check character set # The most efficient way to check for invalid characters is to attempt # conversion and rescue the error. Integer(identifier_str, base) true rescue ArgumentError false end |
.verified_identifier?(verifiable_id, base_or_scope = nil, scope: nil, base: 36) ⇒ Boolean
Verifies the authenticity of a given identifier using a timing-safe comparison.
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/familia/verifiable_identifier.rb', line 84 def self.verified_identifier?(verifiable_id, base_or_scope = nil, scope: nil, base: 36) # Handle backward compatibility with positional base argument if base_or_scope.is_a?(Integer) base = base_or_scope # scope remains as passed in keyword argument elsif base_or_scope.is_a?(String) || base_or_scope.nil? scope = base_or_scope if scope.nil? # base remains as passed in keyword argument or default end return false unless plausible_identifier?(verifiable_id, base) expected_hex_length = (RANDOM_HEX_LENGTH + TAG_HEX_LENGTH) combined_hex = verifiable_id.to_i(base).to_s(16).rjust(expected_hex_length, '0') random_part = combined_hex[0...RANDOM_HEX_LENGTH] tag_part = combined_hex[RANDOM_HEX_LENGTH..] expected_tag = generate_tag(random_part, scope: scope) OpenSSL.secure_compare(expected_tag, tag_part) end |