Module: Encryption

Defined in:
lib/encryption.rb

Overview

Two-way encryption helpers for short URL tokens (order/quote/locator IDs).

Two formats produced by encrypt_string:

  • length(plain) <= 8 → Blowfish ECB on a single 8-byte block.
    Hex-encoded. Output: 16 hex chars.
  • length(plain) > 8 → AES-256-CBC, hex-encoded (url_encrypt_string).

decrypt_string recognises three on-the-wire shapes:

  • length 16 → Blowfish path
  • length 17–N → AES path (url_decrypt_string); falls back to
    legacy_decrypt_string (Base64+CGI-escape from
    an even older format).

Blowfish notes

  • This module previously depended on the crypt gem (Crypt::Blowfish,
    last released in 2007). It now uses Ruby's stdlib OpenSSL bf-ecb
    cipher which produces byte-identical ciphertext for the same key +
    8-byte plaintext block. Verified end-to-end (encrypt+decrypt and
    cross-decrypt) against Crypt::Blowfish output before the switch.
  • OpenSSL 3.x moved Blowfish to the "legacy" provider; we load it on
    demand at the call site so this works on both OpenSSL 1.1.x (no
    legacy concept) and OpenSSL 3.x (legacy provider required).
  • Blowfish has a 64-bit block size and is considered legacy. We keep
    it only for backward-compatibility with URL tokens already in
    circulation (links inside customer emails, support tickets, etc.).
    New token issuance for short IDs continues to flow through this
    path so that wire format stays stable; a future migration to
    signed_id / generates_token_for is tracked separately.

Class Method Summary collapse

Class Method Details

.aes_decrypt(text, key) ⇒ String

Returns the decrypted text ("" on cipher error).

Returns:

  • (String)

    the decrypted text ("" on cipher error)



103
104
105
106
107
# File 'lib/encryption.rb', line 103

def self.aes_decrypt(text, key)
  aes(:decrypt, text, key)
rescue OpenSSL::Cipher::CipherError
  ''
end

.aes_encrypt(text, key) ⇒ String

Returns the AES ciphertext.

Returns:

  • (String)

    the AES ciphertext



98
99
100
# File 'lib/encryption.rb', line 98

def self.aes_encrypt(text, key)
  aes(:encrypt, text, key)
end

.blowfish_decrypt(text, key) ⇒ String

Returns the decrypted text ("" on cipher error).

Returns:

  • (String)

    the decrypted text ("" on cipher error)



70
71
72
73
74
75
# File 'lib/encryption.rb', line 70

def self.blowfish_decrypt(text, key)
  cipher = blowfish_cipher(:decrypt, key)
  cipher.update(text) + cipher.final
rescue OpenSSL::Cipher::CipherError
  ''
end

.blowfish_encrypt(text, key) ⇒ String

Returns the Blowfish ciphertext.

Returns:

  • (String)

    the Blowfish ciphertext



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

def self.blowfish_encrypt(text, key)
  cipher = blowfish_cipher(:encrypt, key)
  cipher.update(text) + cipher.final
end

.decrypt_string(string) ⇒ String?

Returns the decrypted string, or nil when undecryptable.

Returns:

  • (String, nil)

    the decrypted string, or nil when undecryptable



48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/encryption.rb', line 48

def self.decrypt_string(string)
  str = string.to_s
  res = nil
  if str.length > 16
    # AES path; fall through to the very old Base64+CGI format if needed.
    res = url_decrypt_string(string)
    res = legacy_decrypt_string(string) if res.blank?
  elsif str.length == 16
    enc_str = str.each_line.to_a.pack('H*')
    padded_string = blowfish_decrypt(enc_str, REST_AUTH_SITE_KEY)
    res = padded_string.delete("\000")
  end
  res
end

.encrypt_string(string) ⇒ String

Returns the encrypted string, hex-encoded.

Returns:

  • (String)

    the encrypted string, hex-encoded



34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/encryption.rb', line 34

def self.encrypt_string(string)
  if string.to_s.length > 8
    url_encrypt_string(string)
  else
    # Pad to an exact 8-byte block with NUL.
    len = string.length
    mod = len == 8 ? 0 : 8 - (len % 8)
    padded_string = string.ljust(len + mod, "\0")
    enc_str = blowfish_encrypt(padded_string, REST_AUTH_SITE_KEY)
    enc_str.unpack1('H*')
  end
end

.legacy_decrypt_string(encrypted_string) ⇒ String

Returns the decrypted string.

Returns:

  • (String)

    the decrypted string



93
94
95
# File 'lib/encryption.rb', line 93

def self.legacy_decrypt_string(encrypted_string)
  aes_decrypt(Base64.decode64(CGI.unescape(encrypted_string)), REST_AUTH_SITE_KEY[0..31])
end

.legacy_encrypt_string(string) ⇒ String

Returns the Base64+CGI-escaped encrypted string.

Returns:

  • (String)

    the Base64+CGI-escaped encrypted string



88
89
90
# File 'lib/encryption.rb', line 88

def self.legacy_encrypt_string(string)
  CGI.escape(Base64.encode64(aes_encrypt(string.to_s, REST_AUTH_SITE_KEY[0..31])))
end

.url_decrypt_string(encrypted_string) ⇒ String

Returns the decrypted string.

Returns:

  • (String)

    the decrypted string



83
84
85
# File 'lib/encryption.rb', line 83

def self.url_decrypt_string(encrypted_string)
  aes_decrypt([encrypted_string].pack('H*'), REST_AUTH_SITE_KEY[0..31])
end

.url_encrypt_string(string) ⇒ String

Returns the AES-encrypted string, hex-encoded.

Returns:

  • (String)

    the AES-encrypted string, hex-encoded



78
79
80
# File 'lib/encryption.rb', line 78

def self.url_encrypt_string(string)
  aes_encrypt(string.to_s, REST_AUTH_SITE_KEY[0..31]).unpack1('H*')
end