diff --git a/README.txt b/README.txt index 7ae5a08..e8fbbe0 100644 --- a/README.txt +++ b/README.txt @@ -1,16 +1,67 @@ -Simple Cipher Mod v1.0 +Simple Cipher Mod v1.1 By Leslie E. Krause -Simple Cipher is a lightweight, portable hashing algorithm providing a minimal degree -of security for non mission-critical data (e.g. unique, non-guessable URLs). +Simple Cipher is a lightweight and portable cryptography library providing a minimal +degree of security for non mission-critical data (e.g. unique, non-guessable URLs). +Also included is a pure Lua-adaptation of XTEA, a public domain block-cipher algorithm +designed by Needham and Wheeler in 1997. + +The following library functions are available: + + cipher.get_checksum( str, method ) + Calculates and returns the numeric hash of the string using one of these methods: + o "fletcher64" for the Fletcher-64 algorithm + o "fletcher32" for the Fletcher-32 algorithm + o "fletcher16" for the Fletcher-16 algorithm + o "adler32" for the Adler-32 algorithm (default) + + cipher.tokenize( hash ) + Given a numeric hash, this function generates and returns a token consisting only of + letters in the alphabet soup. This is primary useful for generating short-URLs, in + which the hash corresponds to a unique key within a database of site redirects. + + cipher.generate_key( username, password ) + Generates and returns a 128-bit public/private key pair for use with either of the + cryptography functions described below. This key is intended only for encrpytion and + decryption, and should NEVER be shared under any circumstances. If a password is not + provided, then the value of `cipher.password` will be used by default. + + cipher.encrypt( num, str, key ) + Encrypts the string using the XTEA block-cipher and returns the ciphertext. The key + should be generated by `cipher.generate_key`, and an appropriate number of rounds + chosen to mitigate against attacks (32 or more is recommended, 64 is maximum). + + cipher.decrypt( str, key ) + Decrypts the string that was previously encrypted by `cipher.encrypt()` with the same + key generated by `cipher.generate_key`. The enrypted string will be checked both for + proper length and valid header prior to decryption. + + cipher.encrypt_to_base64( str, username ) + This is a convenience function for encryption of a string with just a username. It + returns a Base-64 string representation of the ciphertext. + + cipher.decrypt_from_base64( str, username ) + This is the inverse of `cipher.encrypt_to_base64`, and therefore expects a Base-64 + string representation of the ciphertext as well as the original username. + +Before you begin, it is important that you customize the alphabet soup that will be used +by the tokenizer. Likewise, if you intend to use the XTEA block-cipher, then a password +needs to be set. Both variables can be found at the head of the `init.lua` file. + + +Dependencies +---------------------- + +Bitwise Operators Mod (required) + https://bitbucket.org/sorcerykid/bitwise Source Code License ---------------------- MIT License -Copyright (c) 2016-2019, Leslie E. Krause. +Copyright (c) 2016-2020, Leslie E. Krause. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software diff --git a/init.lua b/init.lua index 881b005..dd79f22 100644 --- a/init.lua +++ b/init.lua @@ -1,47 +1,290 @@ -------------------------------------------------------- --- Minetest :: Simple Cipher Mod v1.0 (cipher) +-- Minetest :: Simple Cipher Mod v1.1 (cipher) -- -- See README.txt for licensing and other information. --- Copyright (c) 2016-2019, Leslie E. Krause +-- Copyright (c) 2016-2020, Leslie E. Krause -- -- ./games/just_test_tribute/mods/cipher/init.lua -------------------------------------------------------- --- alphabet soup to be used by the tokenizer -local alphabet = "pdy3jbh7vms5zxrftnc9gqw" +dofile( "/home/minetest/games/minetest_game/mods/bitwise/init.lua" ) cipher = { } +cipher.alphabet = "7pdy3jbhvms5zxrftnc9gqw" +cipher.password = "password123" + +------------------------------- + +local LSHIFT = LSHIFT +local RSHIFT = RSHIFT +local XOR = XOR +local AND = AND + +local floor = math.floor +local ceil = math.ceil +local sub = string.sub +local to_char = string.char +local to_byte = string.byte + +------------------------------------------- + +local algorithms = { + -- https://gchq.github.io/CyberChef/#recipe=Adler-32_Checksum()&input=VGVzdA + -- "Minetest" -> 0x0e35034a + adler32 = function ( str ) + local a = 1 + local b = 0 + + for i = 1, #str do + a = ( a + to_byte( str, i ) ) % 65521 + b = ( a + b ) % 65521 + end + + return b * 65536 + a -- put first sum into lower 16 bits and second sum into upper 16 bits + end, + -- https://gchq.github.io/CyberChef/#recipe=Fletcher-16_Checksum()&input=VGVzdA + -- "Minetest" -> 0x3b4c + fletcher16 = function ( str ) + local a = 0 + local b = 0 + + for i = 1, #str do + a = ( a + to_byte( str, i ) ) % 255 + b = ( a + b ) % 255 + end + + return b * 256 + a -- put first sum into lower 8 bits and second sum into upper 8 bits + end, + -- https://gchq.github.io/CyberChef/#recipe=Fletcher-32_Checksum()&input=VGVzdA + -- "Minetest" -> 0x0e2d0349 + fletcher32 = function ( str ) + local a = 0 + local b = 0 + + for i = 1, #str do + a = ( a + to_byte( str, i ) ) % 65535 + b = ( a + b ) % 65535 + end + + return b * 65536 + a -- put first sum into lower 16 bits and second sum into upper 16 bits + end, + -- https://gchq.github.io/CyberChef/#recipe=Fletcher-64_Checksum()&input=VGVzdA + -- "Minetest" -> 0x00000e2d00000349 + fletcher64 = function ( str ) + local a = 0 + local b = 0 + + for i = 1, #str do + a = ( a + to_byte( str, i ) ) % 4294967295 + b = ( a + b ) % 4294967295 + end + + return b * 4294967296 + a -- put first sum into lower 32 bits and second sum into upper 32 bits + end, +} + +------------------------------- + +cipher.get_checksum = function ( str, method ) + return algorithms[ method or "adler32" ]( str ) +end + cipher.tokenize = function ( hash ) local base = #alphabet local str = "" - hash = hash + 4294836226 + hash = hash + cipher.get_checksum( password, "fletcher64" ) while hash > 0 do local idx = hash % base + 1 - str = str .. string.sub( alphabet, idx, idx ) - hash = math.floor( hash / base ) + str = str .. sub( alphabet, idx, idx ) + hash = floor( hash / base ) end return str end -cipher.get_checksum = function ( input ) - local a = 378551 - local b = 63689 - local hash = 0 - local i = 0 +------------------------------- - for i = 1, #input do - hash = ( hash * a + string.byte( input, i ) ) % 2147483648 - a = ( a * b ) % 65536 +local _ + +local function is_match( text, glob ) + -- use array for captures + _ = { string.match( text, glob ) } + return #_ > 0 and _ or nil +end + +local function to_byte_fill( str, idx ) + return to_byte( str, idx ) or 0x00 +end + +local function to_char_trim( num ) + return num == 0 and "" or to_char( num ) +end + +local function string_to_blocks( str, off, can_fill ) + local blocks = { } + local to_byte = can_fill and to_byte_fill or to_byte + + -- dissassemble string into pairs of DWORDs and pad with zeroes, if necessary + for idx = 1 + off, ceil( #str / 8 ) do + local b1 = to_byte( str, 8 * idx - 7 ) + local b2 = to_byte( str, 8 * idx - 6 ) + local b3 = to_byte( str, 8 * idx - 5 ) + local b4 = to_byte( str, 8 * idx - 4 ) + local b5 = to_byte( str, 8 * idx - 3 ) + local b6 = to_byte( str, 8 * idx - 2 ) + local b7 = to_byte( str, 8 * idx - 1 ) + local b8 = to_byte( str, 8 * idx ) + + -- block must be 64-bits (big endian) for XTEA algorithm to work + table.insert( blocks, { + upper = b1 * 16777216 + b2 * 65536 + b3 * 256 + b4, -- upper 32-bits as + lower = b5 * 16777216 + b6 * 65536 + b7 * 256 + b8, -- lower 32-bits as + } ) end - return 4294967295 - hash + return blocks end -if cipher.tokenize( cipher.get_checksum( "sorcerykid" ) ) ~= "gfwd9pmd" then - -- basic sanity check upon startup - error( "[cipher] Failed to generate correct token from hash!" ) +local function serialize( upper, lower, can_trim ) + local to_char = can_trim and to_char_trim or to_char + + return table.concat( { + to_char( AND( RSHIFT( upper, 24 ), 0xFF ) ), -- 0x??000000 (byte 1) + to_char( AND( RSHIFT( upper, 16 ), 0xFF ) ), -- 0x00??0000 (byte 2) + to_char( AND( RSHIFT( upper, 8 ), 0xFF ) ), -- 0x0000??00 (byte 3) + to_char( AND( upper, 0xFF ) ), -- 0x000000?? (byte 4) + to_char( AND( RSHIFT( lower, 24 ), 0xFF ) ), -- 0x??000000 (byte 5) + to_char( AND( RSHIFT( lower, 16 ), 0xFF ) ), -- 0x00??0000 (byte 6) + to_char( AND( RSHIFT( lower, 8 ), 0xFF ) ), -- 0x0000??00 (byte 7) + to_char( AND( lower, 0xFF ) ), -- 0x000000?? (byte 8) + }, "" ) +end + +cipher.generate_key = function ( username, password ) + local upper = cipher.get_checksum( password or cipher.password, "fletcher64" ) -- get password checksum for private key + local lower = cipher.get_checksum( username, "fletcher64" ) -- get username checksum for public key + + -- generate a 128-bit key from the two 64-bit hashes + + local key = { + AND( RSHIFT( upper, 32 ), 0xFFFFFFFF ), + AND( upper, 0xFFFFFFFF ), + AND( RSHIFT( lower, 32 ), 0xFFFFFFFF ), + AND( lower, 0xFFFFFFFF ), + } + +-- print( "key = ", key[ 1 ], key[ 2 ], key[ 3 ], key[ 4 ] ) + + return key +end + +cipher.decrypt_from_base64 = function ( str, username ) + local key = cipher.generate_key( username ) + local head = string.sub( str, 1, 8 ) + local data = minetest.decode_base64( string.sub( str, 9 ) ) + + return cipher.decrypt( head .. data, key ) +end + +cipher.encrypt_to_base64 = function ( str, username ) + local key = cipher.generate_key( username ) + local out = cipher.encrypt( 32, str, key ) + + return string.sub( out, 1, 8 ) .. minetest.encode_base64( string.sub( out, 9 ) ) +end + +cipher.decrypt = function ( str, key ) + local ver, num + + -- format of 8-byte header-block for encrypted strings: SC// + -- is the major revision of SimpleCipher in decimal (currently 01) + -- is the number of rounds minus one in octal (64 maximum rounds) + + if is_match( str, "^SC([0-9][0-9])/([0-7][0-7])/" ) then + ver = tonumber( _[ 1 ] ) + num = tonumber( _[ 2 ], 8 ) + 1 + + assert( ver == 1, "Unsupported version in stream header." ) + assert( #str % 8 == 0, "Invalid stream length." ) -- encrypted string must be a multiple of 8-bytes! + else + error( "Unable to parse stream header." ) + end + + local blocks = string_to_blocks( str, 1, false ) -- be sure to skip over header-block + local chunks = { } + + -- perform block-chain decryption by iterating the 64-bit blocks + -- based on XTEA algorithm: https://en.wikipedia.org/wiki/XTEA + + for _, val in ipairs( blocks ) do + local v1 = val.upper -- upper DWORD of block + local v2 = val.lower -- lower DWORD of block + local delta = 0x9E3779B9 + local sum = delta * num + + for idx = 1, num do + -- print( "block (in) = ", v1, v2 ) + + v2 = v2 - XOR( XOR( LSHIFT( v1, 4 ), RSHIFT( v1, 5 ) ) + v1, sum + key[ 1 + AND( RSHIFT( sum, 11 ), 3 ) ] ) + v2 = AND( v2 < 0 and NOT32( math.abs( v2 ) ) + 1 or v2, 0xFFFFFFFF ) -- avoid negatives and limit to 32-bits + sum = sum - delta + v1 = v1 - XOR( XOR( LSHIFT( v2, 4 ), RSHIFT( v2, 5 ) ) + v2, sum + key[ 1 + AND( sum, 3 ) ] ) + v1 = AND( v1 < 0 and NOT32( math.abs( v1 ) ) + 1 or v1, 0xFFFFFFFF ) -- avoid negatives and limit to 32-bits + + -- print( "block (out) = ", v1, v2 ) + end + + table.insert( chunks, serialize( v1, v2, true ) ) -- serialize block as a string of 8 bytes + end + + -- string concat is slow, so assemble string chunks via temporary table + return table.concat( chunks, "" ), ver, num +end + +cipher.encrypt = function ( num, str, key ) + local blocks = string_to_blocks( str, 0, true ) + local chunks = { + string.format( "SC%02d/%02o/", 1, num - 1 ) -- begin with 8-byte header-block + } + + -- perform block-chain encryption by iterating the 64-bit blocks + -- based on XTEA algorithm: https://en.wikipedia.org/wiki/XTEA + + for _, val in ipairs( blocks ) do + local v1 = val.upper -- upper DWORD of block + local v2 = val.lower -- lower DWORD of block + local delta = 0x9E3779B9 + local sum = 0 + + for idx = 1, num do + -- print( "block (in) = ", v1, v2 ) + + v1 = v1 + XOR( XOR( LSHIFT( v2, 4 ), RSHIFT( v2, 5 ) ) + v2, sum + key[ 1 + AND( sum, 3 ) ] ) + v1 = AND( v1, 0xFFFFFFFF ) -- limit to 32 bits + sum = AND( sum + delta, 0xFFFFFFFF ) + v2 = v2 + XOR( XOR( LSHIFT( v1, 4 ), RSHIFT( v1, 5 ) ) + v1, sum + key[ 1 + AND( RSHIFT( sum, 11 ), 3 ) ] ) + v2 = AND( v2, 0xFFFFFFFF ) -- limit to 32 bits + + -- print( "block (out) = ", v1, v2 ) + end + + table.insert( chunks, serialize( v1, v2, false ) ) -- serialize block as a string of 8 bytes + end + + return table.concat( chunks, "" ) +end + +------------------------------- + +if cipher.get_checksum( "Minetest" ) ~= 0x0e35034a then + error( "[cipher] Failed to generate correct checksum from Adler32!" ) +elseif cipher.get_checksum( "Minetest", "fletcher16" ) ~= 0x3b4c then + error( "[cipher] Failed to generate correct checksum from Fletcher16!" ) +elseif cipher.get_checksum( "Minetest", "fletcher32" ) ~= 0x0e2d0349 then + error( "[cipher] Failed to generate correct checksum from Fletcher32!" ) +elseif cipher.get_checksum( "Minetest", "fletcher64" ) ~= 0x00000e2d00000349 then + error( "[cipher] Failed to generate correct checksum from Fletcher64!" ) end