luajitos

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

init.lua (21493B)


      1 -- Crypto CLI Application
      2 -- Supports: hashing, signing, key exchange, key derivation, random generation
      3 
      4 -- Base64 encoding/decoding implementation
      5 local base64 = {}
      6 
      7 -- Base64 encoding table
      8 local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
      9 
     10 -- Base64 encode
     11 function base64.encode(data)
     12     if not data or #data == 0 then return "" end
     13 
     14     local result = {}
     15     local padding = (3 - #data % 3) % 3
     16 
     17     -- Process 3 bytes at a time
     18     for i = 1, #data, 3 do
     19         local b1 = string.byte(data, i)
     20         local b2 = string.byte(data, i + 1) or 0
     21         local b3 = string.byte(data, i + 2) or 0
     22 
     23         local n = b1 * 65536 + b2 * 256 + b3
     24 
     25         local c1 = math.floor(n / 262144) % 64 + 1
     26         local c2 = math.floor(n / 4096) % 64 + 1
     27         local c3 = math.floor(n / 64) % 64 + 1
     28         local c4 = n % 64 + 1
     29 
     30         table.insert(result, b64chars:sub(c1, c1))
     31         table.insert(result, b64chars:sub(c2, c2))
     32 
     33         if i + 1 <= #data then
     34             table.insert(result, b64chars:sub(c3, c3))
     35         else
     36             table.insert(result, '=')
     37         end
     38 
     39         if i + 2 <= #data then
     40             table.insert(result, b64chars:sub(c4, c4))
     41         else
     42             table.insert(result, '=')
     43         end
     44     end
     45 
     46     return table.concat(result)
     47 end
     48 
     49 -- Base64 decode
     50 function base64.decode(data)
     51     if not data then return nil end
     52 
     53     -- Remove whitespace
     54     data = data:gsub("%s+", "")
     55 
     56     if #data == 0 then return "" end
     57     if #data % 4 ~= 0 then return nil, "Invalid base64 string length" end
     58 
     59     -- Build decode table
     60     local decode_table = {}
     61     for i = 1, 64 do
     62         decode_table[b64chars:sub(i, i)] = i - 1
     63     end
     64     decode_table['='] = 0
     65 
     66     local result = {}
     67 
     68     -- Process 4 characters at a time
     69     for i = 1, #data, 4 do
     70         local c1 = decode_table[data:sub(i, i)]
     71         local c2 = decode_table[data:sub(i + 1, i + 1)]
     72         local c3 = decode_table[data:sub(i + 2, i + 2)]
     73         local c4 = decode_table[data:sub(i + 3, i + 3)]
     74 
     75         if not c1 or not c2 or not c3 or not c4 then
     76             return nil, "Invalid base64 character"
     77         end
     78 
     79         local n = c1 * 262144 + c2 * 4096 + c3 * 64 + c4
     80 
     81         local b1 = math.floor(n / 65536) % 256
     82         local b2 = math.floor(n / 256) % 256
     83         local b3 = n % 256
     84 
     85         table.insert(result, string.char(b1))
     86 
     87         if data:sub(i + 2, i + 2) ~= '=' then
     88             table.insert(result, string.char(b2))
     89         end
     90 
     91         if data:sub(i + 3, i + 3) ~= '=' then
     92             table.insert(result, string.char(b3))
     93         end
     94     end
     95 
     96     return table.concat(result)
     97 end
     98 
     99 -- Helper function to convert binary data to hex string
    100 local function toHex(data)
    101     if not data then return "nil" end
    102     local hex = ""
    103     for i = 1, #data do
    104         hex = hex .. string.format("%02x", string.byte(data, i))
    105     end
    106     return hex
    107 end
    108 
    109 -- Helper function to convert hex string to binary data
    110 local function fromHex(hex)
    111     if not hex then return nil end
    112     hex = hex:gsub("%s+", "")  -- Remove whitespace
    113     if #hex % 2 ~= 0 then
    114         return nil, "Hex string must have even length"
    115     end
    116     local binary = ""
    117     for i = 1, #hex, 2 do
    118         local byte = tonumber(hex:sub(i, i+1), 16)
    119         if not byte then
    120             return nil, "Invalid hex character at position " .. i
    121         end
    122         binary = binary .. string.char(byte)
    123     end
    124     return binary
    125 end
    126 
    127 -- Helper function to detect and convert key from hex or base64 to binary
    128 local function parseKey(keyStr)
    129     if not keyStr then return nil, "No key provided" end
    130 
    131     -- Check if it looks like hex (only hex chars, even length)
    132     local hexClean = keyStr:gsub("%s+", "")
    133     if hexClean:match("^[0-9a-fA-F]+$") and #hexClean % 2 == 0 then
    134         -- It's hex
    135         return fromHex(hexClean)
    136     end
    137 
    138     -- Check if it looks like base64
    139     local b64Clean = keyStr:gsub("%s+", "")
    140     if b64Clean:match("^[A-Za-z0-9+/]+=*$") and #b64Clean % 4 == 0 then
    141         -- It's base64
    142         local decoded = base64.decode(b64Clean)
    143         if decoded then
    144             return decoded
    145         end
    146     end
    147 
    148     -- Assume it's raw binary/text
    149     return keyStr
    150 end
    151 
    152 -- Helper function to format output based on options
    153 local function formatOutput(data, options)
    154     if not data then return "nil" end
    155 
    156     if options.base64 then
    157         return base64.encode(data)
    158     else
    159         -- Default to hex
    160         return toHex(data)
    161     end
    162 end
    163 
    164 -- Parse command-line arguments (args is a table)
    165 local function parseArgs(args)
    166     local options = {}
    167     local positional = {}
    168     local i = 1
    169 
    170     while i <= #args do
    171         local arg = args[i]
    172 
    173         if arg:sub(1, 1) == "-" then
    174             -- Option flag
    175             if arg == "-key" or arg == "-k" then
    176                 i = i + 1
    177                 if i <= #args then
    178                     options.key = args[i]
    179                 else
    180                     return nil, "Option " .. arg .. " requires a value"
    181                 end
    182             elseif arg == "-salt" or arg == "-s" then
    183                 i = i + 1
    184                 if i <= #args then
    185                     options.salt = args[i]
    186                 else
    187                     return nil, "Option " .. arg .. " requires a value"
    188                 end
    189             elseif arg == "-iterations" or arg == "-i" then
    190                 i = i + 1
    191                 if i <= #args then
    192                     options.iterations = tonumber(args[i])
    193                     if not options.iterations then
    194                         return nil, "Iterations must be a number"
    195                     end
    196                 else
    197                     return nil, "Option " .. arg .. " requires a value"
    198                 end
    199             elseif arg == "-hex" then
    200                 options.hex = true
    201             elseif arg == "-b64" or arg == "-base64" then
    202                 options.base64 = true
    203             else
    204                 return nil, "Unknown option: " .. arg
    205             end
    206         else
    207             -- Positional argument
    208             table.insert(positional, arg)
    209         end
    210 
    211         i = i + 1
    212     end
    213 
    214     return positional, options
    215 end
    216 
    217 -- Check if crypto library is available
    218 if not crypto then
    219     print("Error: crypto library not available")
    220     return
    221 end
    222 
    223 -- Get command-line arguments from the sandbox environment
    224 -- The args table is provided by the run module after parsing _G.args.argStr
    225 local args = args or {}
    226 
    227 if #args == 0 then
    228     print("LuajitOS Crypto Utility v1.0.0")
    229     print("")
    230     print("Usage: crypto <command> [options] <data>")
    231     print("")
    232     print("Hash Commands:")
    233     print("  MD5 <data>                    - MD5 hash")
    234     print("  SHA1 <data>                   - SHA-1 hash")
    235     print("  SHA256 <data>                 - SHA-256 hash")
    236     print("  SHA512 <data>                 - SHA-512 hash")
    237     print("  SHA3-256 <data>               - SHA3-256 hash")
    238     print("  SHA3-512 <data>               - SHA3-512 hash")
    239     print("  BLAKE2b-256 <data>            - BLAKE2b-256 hash")
    240     print("  BLAKE2b-512 <data>            - BLAKE2b-512 hash")
    241     print("  CRC32 <data>                  - CRC32 checksum")
    242     print("")
    243     print("Key Derivation:")
    244     print("  PBKDF2 <password> -salt <salt> [-i <iter>]")
    245     print("  Argon2id <password> -salt <salt>")
    246     print("  generateSalt                  - Generate random salt")
    247     print("")
    248     print("Digital Signatures (Ed25519):")
    249     print("  Ed25519.keypair               - Generate keypair")
    250     print("  Ed25519.sign <data> -key <privkey_hex>")
    251     print("  Ed25519.verify <data> <sig_hex> -key <pubkey_hex>")
    252     print("")
    253     print("Key Exchange (X25519):")
    254     print("  X25519.keypair                - Generate keypair")
    255     print("  X25519.publicKey <privkey_hex> - Derive public key")
    256     print("  X25519.shared <privkey_hex> <pubkey_hex> - Compute shared secret")
    257     print("")
    258     print("Encryption (key auto-detected as hex, base64, or raw):")
    259     print("  encrypt <cipher> <key> <data>")
    260     print("  decrypt <cipher> <key> <ciphertext>")
    261     print("  Ciphers: AES-256-GCM, ChaCha20-Poly1305, Serpent-256-GCM,")
    262     print("           Twofish-256-GCM, Salsa20-Poly1305")
    263     print("")
    264     print("Random:")
    265     print("  random <bytes>                - Generate random bytes")
    266     print("  newKey <bytes>                - Generate cryptographic key")
    267     print("")
    268     print("Options:")
    269     print("  -key/-k <value>               - Specify key (hex, base64, or raw)")
    270     print("  -salt/-s <value>              - Specify salt")
    271     print("  -iterations/-i <num>          - Number of iterations (default: 100000)")
    272     print("  -b64/-base64                  - Output in base64 format (default: hex)")
    273     print("")
    274     print("Examples:")
    275     print("  crypto MD5 \"hello world\"")
    276     print("  crypto SHA256 \"test123\" -b64")
    277     print("  crypto PBKDF2 \"mypassword\" -salt \"randomsalt\" -i 100000")
    278     print("  crypto Ed25519.keypair -b64")
    279     print("  crypto random 32 -b64")
    280     print("  crypto encrypt AES-256-GCM <key-hex-or-b64> \"secret data\"")
    281     print("  crypto decrypt AES-256-GCM <key-hex-or-b64> <ciphertext>")
    282     return
    283 end
    284 
    285 -- Parse arguments
    286 local positional, options = parseArgs(args)
    287 
    288 if not positional then
    289     print("Error: " .. (options or "Failed to parse arguments"))
    290     return
    291 end
    292 
    293 if #positional == 0 then
    294     print("Error: No command specified. Use 'crypto' without arguments for help.")
    295     return
    296 end
    297 
    298 -- Normalize command - uppercase for hash/kdf commands, preserve case for others
    299 local rawCommand = positional[1]
    300 local command = rawCommand:upper()
    301 
    302 -- Map uppercase to proper mixed-case for specific commands
    303 local commandMap = {
    304     ["ED25519.KEYPAIR"] = "Ed25519.keypair",
    305     ["ED25519.SIGN"] = "Ed25519.sign",
    306     ["ED25519.VERIFY"] = "Ed25519.verify",
    307     ["X25519.KEYPAIR"] = "X25519.keypair",
    308     ["X25519.PUBLICKEY"] = "X25519.publicKey",
    309     ["X25519.SHARED"] = "X25519.shared",
    310     ["GENERATESALT"] = "generateSalt",
    311     ["NEWKEY"] = "newKey",
    312     ["ENCRYPT"] = "encrypt",
    313     ["DECRYPT"] = "decrypt",
    314     ["RANDOM"] = "random",
    315 }
    316 if commandMap[command] then
    317     command = commandMap[command]
    318 end
    319 
    320 -- Hash commands
    321 if command == "MD5" then
    322     if #positional < 2 then
    323         print("Error: MD5 requires data argument")
    324         return
    325     end
    326     local data = positional[2]
    327     local hash = crypto.MD5(data)
    328     print(formatOutput(hash, options))
    329 
    330 elseif command == "SHA1" then
    331     if #positional < 2 then
    332         print("Error: SHA1 requires data argument")
    333         return
    334     end
    335     local data = positional[2]
    336     local hash = crypto.SHA1(data)
    337     print(formatOutput(hash, options))
    338 
    339 elseif command == "SHA256" then
    340     if #positional < 2 then
    341         print("Error: SHA256 requires data argument")
    342         return
    343     end
    344     local data = positional[2]
    345     local hash = crypto.SHA256(data)
    346     print(formatOutput(hash, options))
    347 
    348 elseif command == "SHA512" then
    349     if #positional < 2 then
    350         print("Error: SHA512 requires data argument")
    351         return
    352     end
    353     local data = positional[2]
    354     local hash = crypto.SHA512(data)
    355     print(formatOutput(hash, options))
    356 
    357 elseif command == "SHA3-256" then
    358     if #positional < 2 then
    359         print("Error: SHA3-256 requires data argument")
    360         return
    361     end
    362     local data = positional[2]
    363     local hash = crypto["SHA3-256"](data)
    364     print(formatOutput(hash, options))
    365 
    366 elseif command == "SHA3-512" then
    367     if #positional < 2 then
    368         print("Error: SHA3-512 requires data argument")
    369         return
    370     end
    371     local data = positional[2]
    372     local hash = crypto["SHA3-512"](data)
    373     print(formatOutput(hash, options))
    374 
    375 elseif command == "BLAKE2b-256" then
    376     if #positional < 2 then
    377         print("Error: BLAKE2b-256 requires data argument")
    378         return
    379     end
    380     local data = positional[2]
    381     local hash = crypto["BLAKE2b-256"](data)
    382     print(formatOutput(hash, options))
    383 
    384 elseif command == "BLAKE2b-512" then
    385     if #positional < 2 then
    386         print("Error: BLAKE2b-512 requires data argument")
    387         return
    388     end
    389     local data = positional[2]
    390     local hash = crypto["BLAKE2b-512"](data)
    391     print(formatOutput(hash, options))
    392 
    393 elseif command == "CRC32" then
    394     if #positional < 2 then
    395         print("Error: CRC32 requires data argument")
    396         return
    397     end
    398     local data = positional[2]
    399     local hash = crypto.CRC32(data)
    400     print(formatOutput(hash, options))
    401 
    402 -- Key Derivation
    403 elseif command == "PBKDF2" then
    404     if #positional < 2 then
    405         print("Error: PBKDF2 requires password argument")
    406         return
    407     end
    408     local password = positional[2]
    409     local salt = options.salt
    410     if not salt then
    411         print("Error: PBKDF2 requires -salt option")
    412         return
    413     end
    414     local iterations = options.iterations or 100000
    415     local key = crypto.PBKDF2(password, salt, iterations, 32)
    416     if key then
    417         print(formatOutput(key, options))
    418     else
    419         print("Error: PBKDF2 failed")
    420     end
    421 
    422 elseif command == "Argon2id" then
    423     if #positional < 2 then
    424         print("Error: Argon2id requires password argument")
    425         return
    426     end
    427     local password = positional[2]
    428     local salt = options.salt
    429     if not salt then
    430         print("Error: Argon2id requires -salt option")
    431         return
    432     end
    433     local key = crypto.Argon2id(password, salt)
    434     if key then
    435         print(formatOutput(key, options))
    436     else
    437         print("Error: Argon2id failed")
    438     end
    439 
    440 elseif command == "generateSalt" then
    441     local salt = crypto.generateSalt()
    442     if salt then
    443         print(formatOutput(salt, options))
    444     else
    445         print("Error: Failed to generate salt")
    446     end
    447 
    448 -- Ed25519 Digital Signatures
    449 elseif command == "Ed25519.keypair" then
    450     local pubkey, privkey = crypto.sign.Ed25519.keypair()
    451     if pubkey and privkey then
    452         print("Public Key:  " .. formatOutput(pubkey, options))
    453         print("Private Key: " .. formatOutput(privkey, options))
    454     else
    455         print("Error: Failed to generate Ed25519 keypair")
    456     end
    457 
    458 elseif command == "Ed25519.sign" then
    459     if #positional < 2 then
    460         print("Error: Ed25519.sign requires data argument")
    461         return
    462     end
    463     local data = positional[2]
    464     local privkey_hex = options.key
    465     if not privkey_hex then
    466         print("Error: Ed25519.sign requires -key option with private key (hex)")
    467         return
    468     end
    469     local privkey, err = fromHex(privkey_hex)
    470     if not privkey then
    471         print("Error: Invalid private key hex: " .. err)
    472         return
    473     end
    474     local signature = crypto.sign.Ed25519.sign(data, privkey)
    475     if signature then
    476         print(formatOutput(signature, options))
    477     else
    478         print("Error: Failed to sign data")
    479     end
    480 
    481 elseif command == "Ed25519.verify" then
    482     if #positional < 3 then
    483         print("Error: Ed25519.verify requires data and signature arguments")
    484         return
    485     end
    486     local data = positional[2]
    487     local sig_hex = positional[3]
    488     local pubkey_hex = options.key
    489     if not pubkey_hex then
    490         print("Error: Ed25519.verify requires -key option with public key (hex)")
    491         return
    492     end
    493     local signature, err1 = fromHex(sig_hex)
    494     if not signature then
    495         print("Error: Invalid signature hex: " .. err1)
    496         return
    497     end
    498     local pubkey, err2 = fromHex(pubkey_hex)
    499     if not pubkey then
    500         print("Error: Invalid public key hex: " .. err2)
    501         return
    502     end
    503     local valid = crypto.sign.Ed25519.verify(data, signature, pubkey)
    504     if valid then
    505         print("Signature is VALID")
    506     else
    507         print("Signature is INVALID")
    508     end
    509 
    510 -- X25519 Key Exchange
    511 elseif command == "X25519.keypair" then
    512     local pubkey, privkey = crypto.keyExchange.X25519.keypair()
    513     if pubkey and privkey then
    514         print("Public Key:  " .. formatOutput(pubkey, options))
    515         print("Private Key: " .. formatOutput(privkey, options))
    516     else
    517         print("Error: Failed to generate X25519 keypair")
    518     end
    519 
    520 elseif command == "X25519.publicKey" then
    521     if #positional < 2 then
    522         print("Error: X25519.publicKey requires private key (hex) argument")
    523         return
    524     end
    525     local privkey_hex = positional[2]
    526     local privkey, err = fromHex(privkey_hex)
    527     if not privkey then
    528         print("Error: Invalid private key hex: " .. err)
    529         return
    530     end
    531     local pubkey = crypto.keyExchange.X25519.publicKey(privkey)
    532     if pubkey then
    533         print(formatOutput(pubkey, options))
    534     else
    535         print("Error: Failed to derive public key")
    536     end
    537 
    538 elseif command == "X25519.shared" then
    539     if #positional < 3 then
    540         print("Error: X25519.shared requires private key and public key (hex) arguments")
    541         return
    542     end
    543     local privkey_hex = positional[2]
    544     local pubkey_hex = positional[3]
    545     local privkey, err1 = fromHex(privkey_hex)
    546     if not privkey then
    547         print("Error: Invalid private key hex: " .. err1)
    548         return
    549     end
    550     local pubkey, err2 = fromHex(pubkey_hex)
    551     if not pubkey then
    552         print("Error: Invalid public key hex: " .. err2)
    553         return
    554     end
    555     local shared = crypto.keyExchange.X25519.sharedSecret(privkey, pubkey)
    556     if shared then
    557         print(formatOutput(shared, options))
    558     else
    559         print("Error: Failed to compute shared secret")
    560     end
    561 
    562 -- Random Generation
    563 elseif command == "random" then
    564     if #positional < 2 then
    565         print("Error: random requires number of bytes argument")
    566         return
    567     end
    568     local num_bytes = tonumber(positional[2])
    569     if not num_bytes or num_bytes <= 0 then
    570         print("Error: Number of bytes must be a positive integer")
    571         return
    572     end
    573     local random_data = crypto.random(num_bytes)
    574     if random_data then
    575         print(formatOutput(random_data, options))
    576     else
    577         print("Error: Failed to generate random data")
    578     end
    579 
    580 elseif command == "newKey" then
    581     if #positional < 2 then
    582         print("Error: newKey requires number of bytes argument")
    583         return
    584     end
    585     local num_bytes = tonumber(positional[2])
    586     if not num_bytes or num_bytes <= 0 then
    587         print("Error: Number of bytes must be a positive integer")
    588         return
    589     end
    590     local key = crypto.newKey(num_bytes)
    591     if key then
    592         print(formatOutput(key, options))
    593     else
    594         print("Error: Failed to generate key")
    595     end
    596 
    597 -- Encryption
    598 elseif command == "encrypt" then
    599     if #positional < 4 then
    600         print("Error: encrypt requires cipher, key, and data arguments")
    601         print("Usage: crypto encrypt <cipher> <key> <data>")
    602         return
    603     end
    604     local cipher = positional[2]
    605     local keyStr = positional[3]
    606     local plaintext = positional[4]
    607 
    608     -- Auto-convert key from hex/base64 to binary
    609     local key, err = parseKey(keyStr)
    610     if not key then
    611         print("Error: Invalid key: " .. (err or "unknown error"))
    612         return
    613     end
    614 
    615     -- Map cipher name to crypto function
    616     local cipherFuncs = {
    617         ["AES-256-GCM"] = crypto["AES-256-GCM"],
    618         ["ChaCha20-Poly1305"] = crypto["ChaCha20-Poly1305"],
    619         ["Serpent-256-GCM"] = crypto["Serpent-256-GCM"],
    620         ["Twofish-256-GCM"] = crypto["Twofish-256-GCM"],
    621         ["Salsa20-Poly1305"] = crypto["Salsa20-Poly1305"]
    622     }
    623 
    624     local cipherMod = cipherFuncs[cipher]
    625     if not cipherMod then
    626         print("Error: Unknown cipher '" .. cipher .. "'")
    627         print("Valid ciphers: AES-256-GCM, ChaCha20-Poly1305, Serpent-256-GCM, Twofish-256-GCM, Salsa20-Poly1305")
    628         return
    629     end
    630 
    631     local ciphertext, encErr = cipherMod.encryptAsBase64(plaintext, key)
    632     if ciphertext then
    633         print(ciphertext)
    634     else
    635         print("Error: Encryption failed: " .. (encErr or "unknown error"))
    636     end
    637 
    638 -- Decryption
    639 elseif command == "decrypt" then
    640     if #positional < 4 then
    641         print("Error: decrypt requires cipher, key, and ciphertext arguments")
    642         print("Usage: crypto decrypt <cipher> <key> <ciphertext>")
    643         return
    644     end
    645     local cipher = positional[2]
    646     local keyStr = positional[3]
    647     local ciphertext = positional[4]
    648 
    649     -- Auto-convert key from hex/base64 to binary
    650     local key, err = parseKey(keyStr)
    651     if not key then
    652         print("Error: Invalid key: " .. (err or "unknown error"))
    653         return
    654     end
    655 
    656     -- Map cipher name to crypto function
    657     local cipherFuncs = {
    658         ["AES-256-GCM"] = crypto["AES-256-GCM"],
    659         ["ChaCha20-Poly1305"] = crypto["ChaCha20-Poly1305"],
    660         ["Serpent-256-GCM"] = crypto["Serpent-256-GCM"],
    661         ["Twofish-256-GCM"] = crypto["Twofish-256-GCM"],
    662         ["Salsa20-Poly1305"] = crypto["Salsa20-Poly1305"]
    663     }
    664 
    665     local cipherMod = cipherFuncs[cipher]
    666     if not cipherMod then
    667         print("Error: Unknown cipher '" .. cipher .. "'")
    668         print("Valid ciphers: AES-256-GCM, ChaCha20-Poly1305, Serpent-256-GCM, Twofish-256-GCM, Salsa20-Poly1305")
    669         return
    670     end
    671 
    672     local plaintext, decErr = cipherMod.decryptFromBase64(ciphertext, key)
    673     if plaintext then
    674         print(plaintext)
    675     else
    676         print("Error: Decryption failed: " .. (decErr or "unknown error"))
    677     end
    678 
    679 else
    680     print("Error: Unknown command '" .. command .. "'")
    681     print("Use 'crypto' without arguments for help")
    682 end
    683 
    684 -- Export crypto library for other apps to import
    685 -- This allows other apps to use: local crypto = apps["com.luajitos.crypto"]:call("crypto")
    686 if app and app.export then
    687     app:export({
    688         name = "crypto",
    689         func = function()
    690             return crypto
    691         end,
    692         description = "Returns the global crypto library",
    693         returns = "table"
    694     })
    695 end