Build 02
- 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:
parent
9e6160daff
commit
18359e7920
59
README.txt
59
README.txt
|
@ -1,16 +1,67 @@
|
||||||
Simple Cipher Mod v1.0
|
Simple Cipher Mod v1.1
|
||||||
By Leslie E. Krause
|
By Leslie E. Krause
|
||||||
|
|
||||||
Simple Cipher is a lightweight, portable hashing algorithm providing a minimal degree
|
Simple Cipher is a lightweight and portable cryptography library providing a minimal
|
||||||
of security for non mission-critical data (e.g. unique, non-guessable URLs).
|
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
|
Source Code License
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
MIT 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
|
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
|
software and associated documentation files (the "Software"), to deal in the Software
|
||||||
|
|
281
init.lua
281
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.
|
-- 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
|
-- ./games/just_test_tribute/mods/cipher/init.lua
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
|
|
||||||
-- alphabet soup to be used by the tokenizer
|
dofile( "/home/minetest/games/minetest_game/mods/bitwise/init.lua" )
|
||||||
local alphabet = "pdy3jbh7vms5zxrftnc9gqw"
|
|
||||||
|
|
||||||
cipher = { }
|
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 )
|
cipher.tokenize = function ( hash )
|
||||||
local base = #alphabet
|
local base = #alphabet
|
||||||
local str = ""
|
local str = ""
|
||||||
|
|
||||||
hash = hash + 4294836226
|
hash = hash + cipher.get_checksum( password, "fletcher64" )
|
||||||
|
|
||||||
while hash > 0 do
|
while hash > 0 do
|
||||||
local idx = hash % base + 1
|
local idx = hash % base + 1
|
||||||
str = str .. string.sub( alphabet, idx, idx )
|
str = str .. sub( alphabet, idx, idx )
|
||||||
hash = math.floor( hash / base )
|
hash = floor( hash / base )
|
||||||
end
|
end
|
||||||
|
|
||||||
return str
|
return str
|
||||||
end
|
end
|
||||||
|
|
||||||
cipher.get_checksum = function ( input )
|
-------------------------------
|
||||||
local a = 378551
|
|
||||||
local b = 63689
|
|
||||||
local hash = 0
|
|
||||||
local i = 0
|
|
||||||
|
|
||||||
for i = 1, #input do
|
local _
|
||||||
hash = ( hash * a + string.byte( input, i ) ) % 2147483648
|
|
||||||
a = ( a * b ) % 65536
|
local function is_match( text, glob )
|
||||||
|
-- use array for captures
|
||||||
|
_ = { string.match( text, glob ) }
|
||||||
|
return #_ > 0 and _ or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
return 4294967295 - hash
|
local function to_byte_fill( str, idx )
|
||||||
|
return to_byte( str, idx ) or 0x00
|
||||||
end
|
end
|
||||||
|
|
||||||
if cipher.tokenize( cipher.get_checksum( "sorcerykid" ) ) ~= "gfwd9pmd" then
|
local function to_char_trim( num )
|
||||||
-- basic sanity check upon startup
|
return num == 0 and "" or to_char( num )
|
||||||
error( "[cipher] Failed to generate correct token from hash!" )
|
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 blocks
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
end
|
||||||
|
|
Loading…
Reference in New Issue