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
|
||||
|
||||
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
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.
|
||||
-- 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
|
||||
|
||||
return 4294967295 - hash
|
||||
local function to_byte_fill( str, idx )
|
||||
return to_byte( str, idx ) or 0x00
|
||||
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 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 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
|
||||
|
|
Loading…
Reference in New Issue