A Lua module that wraps a Barracuda IO with transparent AES-GCM file encryption so applications can keep normal file I/O APIs while storing data encrypted at rest.
Module CryptoIO (www/.lua/CryptoIO.lua) is a Lua module that wraps an existing IO instance and exposes a new encrypted IO via ba.create.luaio. It encrypts file content with ba.crypto.symmetric("GCM", ...) and keeps directory operations delegated to the wrapped io.
The www/.preload script includes two examples showing how to use CryptoIO. Run the examples with the Mako Server:
cd CryptoIO
mako -l::wwwThe first section in .preload verifies that CryptoIO.lua can round-trip file data correctly.
High-level flow:
- Create a base I/O (
hio) for the home directory. - Wrap it with
CryptoIOto get an encrypted I/O (cio). - Read plaintext from
README.mdusinghio. - Write that plaintext to
README.encryptedusingcio(this writes encrypted bytes on disk). - Compare file sizes:
hio:stat("README.encrypted").size= encrypted size on diskcio:stat("README.encrypted").size= original plaintext size decoded from CryptoIO trailer
- Open
README.encryptedthroughcio, read decrypted content, and compare with original plaintext.
If the comparison matches, encryption + decryption are working as expected.
The second section in .preload sets up an encrypted file service mounted at /fs/. The example is similar to the WebDAV and Web File Server example, but it uses CryptoIO to encrypt all files stored on the file system. It creates an encrypted sub-directory and uses it as the base for all encrypted resources.
After starting the Mako Server, open a browser and go to http://localhost:portno/fs/, where portno is the HTTP port printed by the server.
Login credentials:
- Username:
admin - Password:
admin
High-level flow:
- Ensure an
encrypteddirectory exists in the base storage. - Create a sub-IO for that directory and wrap it with
CryptoIO(wdio). - Create a lock directory (
/.LOCK) for WebDAV locks. - Create and mount a Web File Server (
ba.create.wfs) usingwdio. - Add simple auth (
admin/admin) and attach it to the mounted directory. - Register
onunload()to unmount the file server cleanly. - Print a directory listing by iterating
wdio:files("/", true).
The module does not return a Lua table, but a factory function:
local CryptoIO = require "CryptoIO"
local eio = CryptoIO(io, keyname, op) -- Create encrypted IOFactory arguments:
io(userdata): existing BAS io instance to wrapkeyname(string): TPM key name used to derive the symmetric key. Key derivation:ba.tpm.uniquekey(keyname, 32)is hashed with SHA-256 and used as GCM key material.- See keyname security note
op(table, optional)op.size(default1024): encryption block size (>= 16, divisible by16, and<= 0xFFF0). Note: the block size must be the same for encryption and decryption; it cannot be changed after a file has been written.op.auth(optional): Additional Authenticated Data (AAD) for GCM passed to s:setauth(op.auth)
Returned encrypted io supports:
open(name, mode)where mode must be"r"or"w"files(name)stat(name)(returns decrypted size for encrypted files)mkdir(name),rmdir(name),remove(name)(delegated)
Opened file handle methods:
read(size): reads decrypted bytes; default size is512; returnsnilon EOF. On decryption error: returns nil, "enoent".write(data): encrypts and writes dataflush(): forwards to underlyingfp:flush()in write mode; read mode flush is not allowedclose(): write mode writes trailer and closes; read mode closesseek(): not implemented (raises error)
local CryptoIO = require "CryptoIO"
-- baseIo is your existing BAS io instance
local encryptedIo = CryptoIO(baseIo, "my-device-key", {
size = 1024,
auth = "my-aad-v1"
})
-- Write encrypted file
do
local fp, err = encryptedIo:open("data.bin", "w")
assert(fp, err)
assert(fp:write("hello "))
assert(fp:write("world"))
assert(fp:flush())
assert(fp:close())
end
-- Read decrypted file
do
local fp, err = encryptedIo:open("data.bin", "r")
assert(fp, err)
local data = {} -- Begin
while true do
local chunk, rerr = fp:read(256)
if not chunk then
assert(not rerr, rerr) -- nil means EOF
break
end
data[#data + 1] = chunk
end
local plaintext = table.concat(data) -- End
-- Alternative to above Begin - End: local plaintext,err="fp:read"a"
assert(fp:close())
-- plaintext == "hello world"
endFile layout:
+-------------------+-------------------------------+-----------------------+
| IV (12 bytes) | (TAG + CIPHERTEXT) repeated | END-DATA (8 bytes) |
+-------------------+-------------------------------+-----------------------+
Details:
-
IV: First 12 bytes in file and used to initialize GCM for the file stream. -
Repeated encrypted payload blocks: Each block is written as
TAG(16 bytes) followed byCIPHERTEXT(up toop.sizebytes for full blocks; last block may be shorter). Multiple(TAG + CIPHERTEXT)pairs may exist. -
END-DATAtrailer (8 bytes total):u32(size)+u32(-size)(both big-endian 4-byte values).sizeis the original plaintext file size.-sizeis encoded as 32-bit two's complement and used as a consistency check.
stat behavior for encrypted files:
- Reads trailer and validates the two size values.
- On success returns table
stwithst.size = plaintext_size - On failure (not encrypted/corrupt trailer): returns
nil, "enoent".
CryptoIO derives its AES key from ba.tpm.uniquekey(keyname, 32). This means the keyname alone is not sufficient to decrypt files on another device, because the derived key is also bound to the local softTPM state.
In deployments where the Barracuda App Server is configured to run only manufacturer-signed ZIP applications, storing keyname as plaintext is normally acceptable in practice. Since untrusted Lua code cannot be installed or executed, an attacker cannot simply supply the same keyname to ba.tpm.uniquekey() and recover the encryption key.
If the product allows end users or third parties to add their own Lua code, then protecting keyname becomes more important, because code running on the same device may be able to request the same TPM-derived key. In that type of deployment, you should combine signed application control with any additional protections appropriate for your design.
Keeping keyname inside an AES-encrypted ZIP file is therefore optional defense in depth, not a strict requirement for normal CryptoIO deployments based on signed ZIP applications. For more information, see the tutorial Signed and Encrypted ZIP files.