LessonsExpert: BeyondLesson 53

Hashes, randomness and encryption

Fingerprint data with SHA-256, sign messages with HMAC, store passwords safely with Argon2, make secure random keys and IDs, and encrypt with libsodium and AES.

  • Expert
  • Lesson 53 of 83
  • About 12 minutes
  • 7 examples

Fingerprints: hashes

A hash turns any amount of data into a short fingerprint of fixed size. The same data always gives the same hash, and the smallest change gives a completely different one, so a hash tells you whether a file arrived intact or has changed since you last saw it. messageDigest makes one with the algorithm you name; the result is bytes, best shown in hexadecimal (Binary data):

ExampleRun it in the playground
function sha256Hex pText
   local tHex
   get binaryDecode("H*", messageDigest(textEncode(pText, "UTF-8"), "SHA-256"), tHex)
   return tHex
end sha256Hex

put sha256Hex("hello")
put sha256Hex("hello!")
put the length of sha256Hex("a much, much longer piece of text than the others")
Output
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
ce06092fb948d9ffac7d1a376e404b26b7575bcc11ee05a4615fef4fec3a308b
64

Hash text by its UTF-8 bytes, as here, so the same text gives the same hash on every system. "SHA-256", "SHA-512" and "SHA3-256" are good choices today.

Signing a message: HMAC

A hash shows that data changed, but anyone can compute one, so an attacker who changes a message can change its hash too. An HMAC mixes a secret key into the hash: only someone with the key can make the right code, so a message that arrives with a matching code came from someone who knows the key, unchanged. Many web services sign their messages this way:

ExampleRun it in the playground
function signHex pMessage, pKey
   local tHex
   get binaryDecode("H*", messageAuthenticationCode(pMessage, pKey, "HMAC-SHA-256"), tHex)
   return tHex
end signHex

put signHex("pay Ada 10", "our shared secret") into tCode
put the length of tCode
put signHex("pay Ada 10", "our shared secret") is tCode
put signHex("pay Ada 1000", "our shared secret") is tCode
put signHex("pay Ada 10", "a guessed secret") is tCode
Output
64
true
false
false

Random enough to be secret

random is fine for games, but its numbers can be predicted, which makes them useless for anything secret. randomBytes takes bytes from the system's secure source, the right way to make a key, a token or a salt. uuid makes a unique ID, 36 characters, safe to use as the name of a record or a file without asking a server for one:

ExampleRun it in the playground
get binaryDecode("H*", randomBytes(16), tToken)
put "a token:" && tToken
put "an ID:" && uuid()
Output (yours will differ: it changes from run to run)
a token: 3f9c0e51a2b7d48c6e01f5a9b3d27c84
an ID: 1b4e28ba-2fa1-4d3b-a3f5-ef19b5a7633b

Both lines differ every run, which is the point.

Storing passwords

Never store a password, and never store a plain hash of one: computers can try billions of guesses at a SHA-256 hash every second. A password needs a hash made to be slow and to use a lot of memory, with a random salt so that two people with the same password get different hashes. OXT-Beyond comes with libsodium, through the SodiumXT extension, whose sxPwHashStr does all of this with Argon2id and gives back one line of text to store. sxPwHashStrVerify checks a password typed at login against it:

ExampleRun it in the playground
function hashPassword pPassword
   return textDecode(sxPwHashStr(textEncode(pPassword, "UTF-8"), "2", sxPwMemInteractive()), "UTF-8")
end hashPassword

function passwordMatches pPassword, pStored
   return sxPwHashStrVerify(pStored, textEncode(pPassword, "UTF-8"))
end passwordMatches

put hashPassword("correct horse battery") into tStored
put word 1 of tStored begins with "$argon2id$"
put hashPassword("correct horse battery") is tStored
put passwordMatches("correct horse battery", tStored)
put passwordMatches("correct horse batter", tStored)
Output
true
false
true
false

Hashing the same password twice gives two different lines, because each has its own salt, and both still match. The line records the salt and the settings, so it is all you store.

Encrypting

To keep data secret but get it back later, encrypt it with a key. libsodium's sxSecretBox encrypts and also seals the data, so any change to it, or the wrong key, is refused rather than giving nonsense:

ExampleRun it in the playground
put sxRandomBytes(32) into tKey
put sxSecretBox(textEncode("meet at noon", "UTF-8"), tKey) into tBox
put the number of bytes of tBox && "bytes, not readable"
put textDecode(sxSecretBoxOpen(tBox, tKey), "UTF-8")

try
   get sxSecretBoxOpen(tBox, sxRandomBytes(32))
   put "opened with the wrong key?"
catch tError
   put "the wrong key is refused"
end try
Output
52 bytes, not readable
meet at noon
the wrong key is refused

The key is 32 random bytes. Keep it as safe as the data: anyone who has it can open the box. To make a key from a password instead, see sxPwHash in the Dictionary.

The engine's own encrypt and decrypt commands use OpenSSL, with any cipher of the cipherNames and a password or key. They suit data that another program will decrypt with the same cipher:

ExampleRun it in the playground
encrypt "meet at noon" using "aes-256-cbc" with password "a long passphrase"
put it into tSecret
put tSecret is not "meet at noon"
decrypt tSecret using "aes-256-cbc" with password "a long passphrase"
put it
decrypt tSecret using "aes-256-cbc" with password "a wrong guess"
put "with the wrong password:" && word 1 to -1 of the result
Output
true
meet at noon
with the wrong password: (SSL error: bad decrypt)

Try it yourself

Write a function that says whether a file has changed: it keeps the SHA-256 hash of each file it has seen in a script local, and returns new, same or changed. Write a file, check it, check it again, change it, and check it once more.

Show a solution
ExampleRun it in the playground
local sSeen

function fileStatus pPath
   local tHash, tStatus
   put messageDigest(URL ("binfile:" & pPath), "SHA-256") into tHash
   if sSeen[pPath] is empty then
      put "new" into tStatus
   else if sSeen[pPath] is tHash then
      put "same" into tStatus
   else
      put "changed" into tStatus
   end if
   put tHash into sSeen[pPath]
   return tStatus
end fileStatus

put specialFolderPath("temporary") & "/oxt-lesson-watch.txt" into tPath
put "version 1" into URL ("file:" & tPath)
put fileStatus(tPath)
put fileStatus(tPath)
put "version 2" into URL ("file:" & tPath)
put fileStatus(tPath)
delete file tPath
Output
new
same
changed

The file is read with binfile:, so the hash is of its exact bytes.

Words in this lesson

Each one finds every lesson that uses it. The Dictionary has the whole story of each, with its syntax and more examples.