0

In one of our signup processes, in a Ruby on Rails app, I want to send someone an email with a 6-digit numerical code that they need to copy or type into a box on the page.

How I thought I might do it is to have a string on the page, in a hidden field, say, which, on generating the email, gets combined with some secret key from our codebase, then hashed or encrypted to a series of decimal numbers. I then take the last six of these and put them in the email.

When the form is submitted, with the original string, and the 6 digits the user has typed in, I can repeat the process on the string and test that it produces the same six digits as the user entered.

The question is: how do I hash/encrypt a string to get a series of decimal numbers?  The original string can be anything (including a different set of decimal numbers), it just needs to be something that is randomly generated really.

Max Williams
  • 32,435
  • 31
  • 130
  • 197
  • Possible duplicate of [Good Hash Function for Strings](https://stackoverflow.com/questions/2624192/good-hash-function-for-strings) – VirtualMichael Jun 05 '18 at 08:36
  • 1
    HMAC their email with your secret key and use the first 8 bytes to produce an unsigned 64-bit integer. Concatenate to 6 digits and add 111111 if less than 10000. – Luke Joshua Park Jun 05 '18 at 08:38
  • @VirtualMichael Sorry, I didn't specify this question is about Ruby. Your suggested duplicate is about Java. I know some of the logic will transfer but I don't think enough to make this a duplicate. Also, my question is more specific in terms of the end result - the other question is quite general. – Max Williams Jun 05 '18 at 08:39
  • @LukeJoshuaPark nice idea to use the email, thanks – Max Williams Jun 05 '18 at 08:40
  • @LukeJoshuaPark any chance you could expand your comment into an answer with Ruby code? – Max Williams Jun 05 '18 at 08:41
  • @LukeJoshuaPark thanks but I think this is it - I use the last 6 digits of the decimal number rather than the first. `Digest::SHA1.hexdigest('foofoo@example.com-SECRETKEY').to_i(16).to_s.split("")[-6..-1].join()` => `978099`. As you suggest if the resulting number starts with a zero I can add 111111 to it. – Max Williams Jun 05 '18 at 08:50
  • 2
    Yep that looks about right. You should really use an HMAC rather than directly hashing, though. Hashing with an appended key can lead to [security vulnerabilities](https://crypto.stackexchange.com/questions/6493/what-is-the-difference-between-a-hmac-and-a-hash-of-data?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa). – Luke Joshua Park Jun 05 '18 at 08:52
  • What's the problem you are trying to solve by sending a 6-digit number via email? – Stefan Jun 05 '18 at 09:55
  • @Stefan to validate the email address, basically, without creating any orders or other records on our system. It's like "You must be able to receive an email from us in order to progress any further through this multi-step process". The user gets the email and types the code into the form. – Max Williams Jun 05 '18 at 10:19
  • @Stefan the actual **problem** is that people (bored kids I think, we provide software to schools) are getting quotes for subscriptions from us with bogus email addresses, which creates an admin overhead. We want to stop them from even creating the quote request in the first place. The email validation will not 100% stop this but it will reduce it. – Max Williams Jun 05 '18 at 10:24
  • I'd store the confirmation code either inside the user record (if you have one) or in the user's session (using an [encrypted session storage](http://guides.rubyonrails.org/security.html#encrypted-session-storage)). – Stefan Jun 05 '18 at 10:41

1 Answers1

0

Note: Do not use this as a security measure. You've stated that you intend to use this only as a kind of diy captcha system, and this answer is provided in that context.

OpenSSL provides good hash functions. Once you have the hash in hex the to_i method takes a base argument, so converting to decimal is simple. Then back to a string because that makes it easier to get the last characters.

require 'openssl'
hash = OpenSSL::Digest::SHA256.hexdigest('user@example.com' + Random.new_seed.to_s)
message = hash.to_i(16).to_s.chars.last(6).join

You could then store any part of this along with an expiration time.

iCodeSometime
  • 1,444
  • 15
  • 30