- expanded the README.txt file with usage instructions.
- implemented a Lua adaptation of the XTEA block-cipher.
- added support for commonly used checksum algorithms.
- major code reorganization and better comments.
This commit is contained in:
Leslie Krause 2020-02-05 23:21:03 -05:00
parent 9e6160daff
commit 18359e7920
2 changed files with 317 additions and 23 deletions

View File

@ -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

281
init.lua
View File

@ -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 <b1><b2><b3><b4>
lower = b5 * 16777216 + b6 * 65536 + b7 * 256 + b8, -- lower 32-bits as <b5><b6><b7><b8>
} )
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<VER>/<NUM>/
-- <VER> is the major revision of SimpleCipher in decimal (currently 01)
-- <NUM> 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