libsodium, from xTalk
SodiumXT brings libsodium, the cryptography library that thousands of programs trust, to xTalk. Its handlers begin with sx, and each does one job the safe way, with modern methods chosen for you: you never pick an algorithm, a mode or a padding, which is where most home-made cryptography goes wrong. Hashes, randomness and encryption met two of them; this lesson takes the whole tour.
Everything here works on bytes, not text. textEncode(tText, "UTF-8") turns text into bytes before it is locked, and textDecode turns them back after (Unicode). Keys, fingerprints and locked boxes are bytes too, which look like gibberish when you put them, so the examples show them as hex or base64:
put textEncode("Hello, world", "UTF-8") into tBytes
put the number of bytes in tBytes
put sxBin2Hex(tBytes)
put sxBin2Base64(tBytes)
put textDecode(sxHex2Bin(sxBin2Hex(tBytes)), "UTF-8")
12 48656c6c6f2c20776f726c64 SGVsbG8sIHdvcmxk Hello, world
Hex takes two letters a byte and is easy to read; base64 takes four letters for three bytes, so it is shorter. Both survive an email, a field or a web form, which raw bytes may not.
Random bytes nobody can predict
Every key starts as random bytes, and they must come from the computer's secure source, never from random, whose numbers follow a pattern. sxRandomBytes gives as many as you ask for, and sxRandomUniform(n) a fair whole number from 0 to n - 1, a die with any number of sides:
put sxBin2Hex(sxRandomBytes(16))
put sxRandomUniform(6) + 1
put the number of bytes in sxRandomBytes(32)
9f1c0b7e52d4a8e36c01f5a2b4d39e70 4 32
The first two lines differ every run, which is the point.
Fingerprints and seals
A hash is a fingerprint of data: always the same size, always the same for the same data, completely different for the smallest change, and impossible to work back from. sxHash makes a BLAKE2b fingerprint of the length you ask for (32 bytes is usual), and sxSha3_256 a SHA-3 one:
put sxBin2Hex(sxHash(textEncode("cat", "UTF-8"), 32))
put sxBin2Hex(sxHash(textEncode("bat", "UTF-8"), 32))
put the length of sxBin2Hex(sxSha3_256(textEncode("cat", "UTF-8")))
6a7178d382cfb186b49fd344db9231f636975548d72c4d2541d1f5655b8983e1 f58660315cf2f229520ab4f3a762c76ebbebeb8e14d71097fa919426b54f004e 64
Anyone can make a plain fingerprint, so a changed message can come with a changed fingerprint. A seal (a keyed hash) needs a secret key as well: only someone with the key can make one that matches. sxHashKeyed makes a BLAKE2b seal and sxHmacSha256 the HMAC-SHA256 seal that many web services ask for. Compare seals with sxMemEqual, never with is: it takes as long however much of them matches, so its timing gives nothing away:
put sxRandomBytes(32) into tKey
put sxHashKeyed(textEncode("Pay Sam 10 dollars", "UTF-8"), tKey, 32) into tSeal
put sxHashKeyed(textEncode("Pay Sam 10 dollars", "UTF-8"), tKey, 32) into tCheck
put sxMemEqual(tSeal, tCheck)
put sxHashKeyed(textEncode("Pay Sam 99 dollars", "UTF-8"), tKey, 32) into tCheck
put sxMemEqual(tSeal, tCheck)
put the number of bytes in sxHmacSha256(tKey, textEncode("Pay Sam 10 dollars", "UTF-8"))
true false 32
Secret boxes
A secret box locks a message with a key of 32 random bytes. With the key, sxSecretBoxOpen opens it; without it, the box is noise. It also notices tampering: change one byte, and it refuses to open, with an error you catch with try (Errors), rather than hand back nonsense:
put sxRandomBytes(32) into tKey
put sxSecretBox(textEncode("Meet at noon", "UTF-8"), tKey) into tBox
put the number of bytes in tBox
put textDecode(sxSecretBoxOpen(tBox, tKey), "UTF-8")
-- change one byte of the box
put numToByte((byteToNum(byte 30 of tBox) + 1) mod 256) into byte 30 of tBox
try
get sxSecretBoxOpen(tBox, tKey)
put "opened?!"
catch tError
put "a changed box will not open"
end try
try
get sxSecretBoxOpen(sxSecretBox(textEncode("hi", "UTF-8"), tKey), sxRandomBytes(32))
catch tError
put "the wrong key will not open it"
end try
52 Meet at noon a changed box will not open the wrong key will not open it
The box is 40 bytes longer than the message: a random nonce, so that the same message locked twice looks different each time, and a tag that proves it is untouched. SodiumXT makes the nonce and keeps it in the box, so there is nothing for you to get wrong.
Sometimes part of a message must stay readable, like the address on an envelope, but nobody may swap it. sxAeadEncrypt locks a message and ties a readable label to it; sxAeadDecrypt opens it only with the very same label, so nobody can move a locked box to another record:
put sxRandomBytes(32) into tKey
put textEncode("to: Sam, note 7", "UTF-8") into tLabel
put sxAeadEncrypt(textEncode("Gate code: 4417", "UTF-8"), tLabel, tKey) into tBox
put textDecode(sxAeadDecrypt(tBox, tLabel, tKey), "UTF-8")
try
get sxAeadDecrypt(tBox, textEncode("to: Max, note 7", "UTF-8"), tKey)
catch tError
put "another label: it will not open"
end try
Gate code: 4417 another label: it will not open
Public keys
With a secret box, both people need the same key, and handing it over safely is the hard part. With public-key cryptography each person makes a keypair: a public key to give to anyone, and a secret key to keep. sxBoxKeypair makes one, putting the two keys into the variables you give it, which must be declared first with local (Scope): a name that is not yet a variable is passed as the text of its name. sxBox locks a message for one person, with your secret key and their public key, and sxBoxOpen opens it and proves who sent it. sxSeal needs only the reader's public key: like a postbox, anyone can post and only the owner can open:
local tAnnPublic, tAnnSecret, tBobPublic, tBobSecret
sxBoxKeypair tAnnPublic, tAnnSecret
sxBoxKeypair tBobPublic, tBobSecret
put the number of bytes in tAnnPublic && the number of bytes in tAnnSecret
-- Ann writes to Bob; Bob opens it with Ann's public key
put sxBox(textEncode("Hi Bob, it is Ann", "UTF-8"), tBobPublic, tAnnSecret) into tBox
put textDecode(sxBoxOpen(tBox, tAnnPublic, tBobSecret), "UTF-8")
-- anyone can seal a note that only Bob can open
put sxSeal(textEncode("A secret tip", "UTF-8"), tBobPublic) into tBox
put textDecode(sxSealOpen(tBox, tBobPublic, tBobSecret), "UTF-8")
32 32 Hi Bob, it is Ann A secret tip
Signatures
A signature hides nothing: it proves who wrote something, and that nobody has changed it since. You sign with your secret key, and anyone with your public key can check. Software updates, licences and Nostr posts are signed this way:
local tPublic, tSecret
sxSignKeypair tPublic, tSecret
put textEncode("Version 2.0 is out", "UTF-8") into tMessage
put sxSignDetached(tMessage, tSecret) into tSignature
put the number of bytes in tSignature
put sxSignVerifyDetached(tSignature, tMessage, tPublic)
put sxSignVerifyDetached(tSignature, textEncode("Version 6.6 is out", "UTF-8"), tPublic)
64 true false
Agreeing on keys in the open
Public-key boxes are slow for a long conversation. With a key exchange, two programs each mix their own keypair with the other's public key, and both get the same pair of fast secret keys without ever sending them: one to send with and one to receive with. One side plays the client and the other the server, and the client's send key is the server's receive key:
local tClientPublic, tClientSecret, tServerPublic, tServerSecret
local tClientRx, tClientTx, tServerRx, tServerTx
sxKeyExchangeKeypair tClientPublic, tClientSecret
sxKeyExchangeKeypair tServerPublic, tServerSecret
-- each side needs only the other's public key
sxKeyExchangeClient tClientPublic, tClientSecret, tServerPublic, tClientRx, tClientTx
sxKeyExchangeServer tServerPublic, tServerSecret, tClientPublic, tServerRx, tServerTx
put sxMemEqual(tClientTx, tServerRx)
put sxMemEqual(tServerTx, tClientRx)
put sxSecretBox(textEncode("hello, server", "UTF-8"), tClientTx) into tBox
put textDecode(sxSecretBoxOpen(tBox, tServerRx), "UTF-8")
true true hello, server
Files and streams
A big file need not fit in memory. sxEncryptFile and sxDecryptFile lock and unlock a file of any size a piece at a time, so its bytes never pass through your variables, and the locked file notices tampering as a box does. sxHashFile fingerprints a whole file, to check a copy or a download:
put sxRandomBytes(32) into tKey
put specialFolderPath("temporary") & "/oxt-lesson-diary" into tFile
put "Dear diary: crypto is fun." into URL ("file:" & tFile)
sxEncryptFile tFile, tFile & ".locked", tKey
sxDecryptFile tFile & ".locked", tFile & ".back", tKey
put URL ("file:" & tFile & ".locked") contains "diary"
put URL ("file:" & tFile & ".back")
put sxMemEqual(sxHashFile(tFile, 32), sxHashFile(tFile & ".back", 32))
delete file tFile
delete file (tFile & ".locked")
delete file (tFile & ".back")
false Dear diary: crypto is fun. true
Data from a network or a recording arrives in pieces. A secret stream locks each piece with one key, in order, and marks the last; the reader opens them in the same order and notices a piece that is missing, swapped, repeated or cut short. sxSecretStreamInitPush starts one and gives a header to send first, sxSecretStreamPush locks a piece (true for the last), sxSecretStreamPull opens one, and sxFreeStream lets the stream go:
local tHeader, tTag
put sxRandomBytes(32) into tKey
put sxSecretStreamInitPush(tKey, tHeader) into tStream
put sxSecretStreamPush(tStream, textEncode("Part one", "UTF-8"), empty, false) into tOne
put sxSecretStreamPush(tStream, textEncode("part two", "UTF-8"), empty, true) into tTwo
sxFreeStream tStream
put sxSecretStreamInitPull(tKey, tHeader) into tStream
put textDecode(sxSecretStreamPull(tStream, tOne, empty, tTag), "UTF-8") & ":" && sxIsFinalTag(tTag)
put textDecode(sxSecretStreamPull(tStream, tTwo, empty, tTag), "UTF-8") & ":" && sxIsFinalTag(tTag)
sxFreeStream tStream
Part one: false part two: true
Many keys from one, and lengths that tell less
sxKdfDerive makes many keys from one master key: give it the master, a number and a label of exactly 8 bytes, and each number gives an unrelated key, none of which gives the master away. One key for files, another for messages, and only the master to keep safe. A locked message still shows its length, and "yes" and "no thanks" are easy to tell apart by size alone, so sxPad grows data to a whole number of blocks and sxUnpad takes the padding off:
put sxRandomBytes(32) into tMaster
put textEncode("MyApp-v1", "UTF-8") into tLabel
put sxKdfDerive(tMaster, "1", tLabel, 32) into tFilesKey
put sxKdfDerive(tMaster, "2", tLabel, 32) into tMessagesKey
put sxMemEqual(tFilesKey, tMessagesKey)
put sxMemEqual(tFilesKey, sxKdfDerive(tMaster, "1", tLabel, 32))
repeat for each item tWord in "yes,no thanks"
put tWord & ":" && the number of bytes in sxPad(textEncode(tWord, "UTF-8"), 16) && "bytes"
end repeat
put textDecode(sxUnpad(sxPad(textEncode("no thanks", "UTF-8"), 16), 16), "UTF-8")
false true yes: 16 bytes no thanks: 16 bytes no thanks
Try it yourself
Write encryptText and unencryptText: the first locks text with a key and returns it as base64, safe to keep in a field or a file; the second takes that base64 back to the text, or returns empty when the key is wrong or the text was changed.
Show a solution
function encryptText pText, pKey
return sxBin2Base64(sxSecretBox(textEncode(pText, "UTF-8"), pKey))
end encryptText
function unencryptText pBase64, pKey
try
return textDecode(sxSecretBoxOpen(sxBase642Bin(pBase64), pKey), "UTF-8")
catch tError
return empty
end try
end unencryptText
put sxRandomBytes(32) into tKey
put encryptText("The treasure is under the oak", tKey) into tLocked
put unencryptText(tLocked, tKey)
put unencryptText(tLocked, sxRandomBytes(32)) is empty
put tLocked is encryptText("The treasure is under the oak", tKey)
The treasure is under the oak true false
The last line is false because each box has its own random nonce: the same text locked twice never looks the same.
Words in this lesson
sxRandomBytessxRandomUniformsxBin2HexsxBin2Base64sxHashsxHashKeyedsxHmacSha256sxMemEqualsxSecretBoxsxSecretBoxOpensxAeadEncryptsxAeadDecryptsxBoxKeypairsxBoxsxBoxOpensxSealsxSealOpensxSignKeypairsxSignDetachedsxSignVerifyDetachedsxKeyExchangeKeypairsxEncryptFilesxDecryptFilesxHashFilesxSecretStreamInitPushsxSecretStreamPushsxSecretStreamPullsxKdfDerivesxPadEach one finds every lesson that uses it. The Dictionary has the whole story of each, with its syntax and more examples.