% Copyright (C) 2013-2019 Olivier Boudeville
%
% This file is part of the Ceylan-Myriad library.
%
% This library is free software: you can redistribute it and/or modify
% it under the terms of the GNU Lesser General Public License or
% the GNU General Public License, as they are published by the Free Software
% Foundation, either version 3 of these Licenses, or (at your option)
% any later version.
% You can also redistribute it and/or modify it under the terms of the
% Mozilla Public License, version 1.1 or later.
%
% This library is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU Lesser General Public License and the GNU General Public License
% for more details.
%
% You should have received a copy of the GNU Lesser General Public
% License, of the GNU General Public License and of the Mozilla Public License
% along with this library.
% If not, see and
% .
%
% Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com]
% Creation date: Friday, November 1, 2013.
% Gathering of various cipher-related facilities.
%
% We focus on symmetric ciphering here.
%
% See cipher_utils_test.erl for testing.
%
-module(cipher_utils).
-export([ generate_key/2, key_to_string/1,
encrypt/3, decrypt/3,
generate_mealy_table/1, compute_inverse_mealy_table/1,
mealy_table_to_string/1 ]).
% This pseudo-export allows to avoid a spurious Dialyzer warning about the final
% catch-all clause (user-provided arguments, hence they can have any type).
%
-export( [ apply_cipher/3 ]).
% Implementation notes.
%
% To encrypt a file, one shall use a key file, whose extension is by convention
% 'cipher' (ex: "my-key-file.cipher").
%
% The same file can be used to perform the reverse operation.
%
% The mode of operation is to chain a series of elementary transformations that
% can be reversed. These operations are listed and described in the
% aforementioned file, which contains Erlang terms for that.
% When ciphering or deciphering a file of size N, as much as possible:
%
% - the whole file is streamed, hence it will never be loaded fully in memory
%
% - we expect that the free storage capacity is at least 2.N bytes
%
% - we could apply all transformations in-memory once (instead of writing as
% many intermediate files as there are transformations), however it would be
% difficult to implement (as such, and because of streaming, and because, from a
% transformation to another, the access patterns are usually different)
% Available transformations are:
%
% - id: identity (content not changed)
%
% - offset: the specified value is added to all bytes of the file
%
% - compress: the file content is replaced by a compressed version thereof,
% using one of the supported formats (see compress_format/0)
%
% - insert_random: based on the specified seed and on the specified range R, a
% series of strictly positive values is uniformly drawn in [1,R]; these values
% are offsets relative to the last random insertion (initial one is 0); at each
% position determined thanks to offsets, a random value in [0,255] is inserted
%
% - delta_combine: a byte Bk+1 is replaced by its difference with the previous
% byte, with B0=128; hence Bk+1 is replaced by Bk+1 - Bk (Bk having obeyed the
% same rule)
%
% - shuffle: based on the specified seed and length L, each series of up to L
% bytes is uniformly shuffled
%
% - xor: based on the specified list of bytes, the content of the file is XOR'ed
%
% - mealy: based on specified state-transition data, the content of the file is
% modified accordingly; the input and output alphabet are the same, B, the set
% of all bytes (i.e. integers in [0,255]), while the states are strictly
% positive integers; the transition and output function are coalesced into a
% single function: f( { CurrentState, InputByte } ) -> { NewState, OutputByte };
% for more information: https://en.wikipedia.org/wiki/Mealy_machine
% Just for testing:
-type id_transform() :: 'id'.
-type offset_transform() :: { 'offset', integer() }.
-type compress_transform() :: { 'compress', file_utils:compression_format() }.
-type decompress_transform() :: { 'decompress',
file_utils:compression_format() }.
-type insert_random_transform() :: { 'insert_random', random_utils:seed(),
basic_utils:count() }.
-type extract_random_transform() :: { 'extract_random', random_utils:seed(),
basic_utils:count() }.
-type delta_combine_transform() :: 'delta_combine'.
-type delta_combine_reverse_transform() :: 'delta_combine_reverse'.
-type shuffle_transform() :: { 'shuffle', random_utils:seed(),
basic_utils:count() }.
-type reciprocal_shuffle_transform() :: { 'reciprocal_shuffle',
random_utils:seed(), basic_utils:count() }.
-type xor_transform() :: { 'xor', [ integer() ] }.
% Mealy transform section.
% Designates a state of the Mealy machine, starting from 1:
-type state() :: integer().
% A letter (of both alphabets, i.e. input and output):
-type letter() :: byte().
% A cell of any inner array, each of these arrays being relative to a given
% input letter and being indexed by the possible machine states:
%
-type cell() :: { state(), letter() }.
% Each array of this type is relative to an input letter, and its cell are
% indexed by states:
%
-type inner_array() :: array:array( cell() ).
% A Mealy table can be seen as a two-dimensional array, whose first dimension is
% the states of the Machine (in [1,StateCount]) and second is the input alphabet
% (here letters are bytes, in [0,255]).
%
% Each cell is a { NextState, OutputLetter } pair.
%
% The Mealy table is implemented (easier to build) as a fixed-size array, one
% element per possible state (corresponding to a column of the 2D array).
%
% Each element of this table is itself an array, having as many elements as the
% size of the input alphabet, hence 256 of them, in [0,255].
%
% Each element of these inner arrays corresponds to the aforementioned cell.
%
-type mealy_table() :: array:array( inner_array() ).
% A state is defined by a strictly positive integer:
-type mealy_state() :: integer().
-type mealy_transform() :: { 'mealy', mealy_state(), mealy_table() }.
-type cipher_transform() ::
id_transform()
| offset_transform()
| compress_transform()
| insert_random_transform()
| delta_combine_transform()
| shuffle_transform()
| xor_transform()
| mealy_transform().
% For ciphers which require specific reverse transformations:
-type decipher_transform() ::
decompress_transform()
| extract_random_transform()
| delta_combine_reverse_transform()
| reciprocal_shuffle_transform().
-type any_transform() :: cipher_transform() | decipher_transform().
% User may specify either some licit transform, or possibly mistakes:
-type user_specified_transform() :: any().
-type key() :: [ any_transform() ].
-export_type([ cipher_transform/0, decipher_transform/0 ]).
-spec generate_key( file_utils:file_name(), [ cipher_transform() ] ) -> void().
generate_key( KeyFilename, Transforms ) ->
case file_utils:exists( KeyFilename ) of
true ->
throw( { already_existing_key_file, KeyFilename } );
false ->
ok
end,
KeyFile = file_utils:open( KeyFilename, _Opts=[ write, raw ] ),
Header = text_utils:format( "% Key generated on ~s, by ~s, on ~s.~n",
[ time_utils:get_textual_timestamp(),
system_utils:get_user_name(),
net_utils:localhost() ] ),
file_utils:write( KeyFile, Header ),
file_utils:write( KeyFile, "~n~w.~n~n", [ Transforms ] ),
file_utils:write( KeyFile, "% End of key file.~n", [] ),
file_utils:close( KeyFile ).
% Returns a description of the specified key.
%
-spec key_to_string( key() ) -> string().
key_to_string( Key ) ->
text_utils:format( "Key composed of following ~B cipher(s): ~s",
[ length( Key ),
text_utils:strings_to_string( key_to_string(
lists:reverse( Key ), _Acc=[] ) ) ] ).
key_to_string( [], Acc ) ->
Acc;
key_to_string( [ _Cipher={ mealy, InitialState, Table } | T ], Acc ) ->
% Much info:
%CipherString = text_utils:format(
% "Mealy cipher with initial state S~B and a ~s",
% [ InitialState, mealy_table_to_string( Table ) ] ),
% Shorter:
StateCount = array:size( Table ),
AlphabetSize = array:size( array:get( 0, Table ) ),
CipherString = text_utils:format(
"Mealy cipher with initial state S~B for a table of "
"~B states and an alphabet of ~B letters",
[ InitialState, StateCount, AlphabetSize ] ),
key_to_string( T, [ CipherString | Acc ] );
key_to_string( [ Cipher | T ], Acc ) ->
CipherString = text_utils:format( "~p", [ Cipher ] ),
key_to_string( T, [ CipherString | Acc ] ).
% Encrypts specified source file using specified key file, and writes the result
% in specified target file.
%
% The original file is kept as is.
%
-spec encrypt( file_utils:file_name(), file_utils:file_name(),
file_utils:file_name() ) -> void().
encrypt( SourceFilename, TargetFilename, KeyFilename ) ->
_SourceFile = case file_utils:is_existing_file_or_link( SourceFilename ) of
true ->
file_utils:open( SourceFilename, _Opts=[ read, raw, read_ahead ] );
false ->
throw( { non_existing_source_file, SourceFilename } )
end,
_TargetFile = case file_utils:exists( TargetFilename ) of
true ->
throw( { already_existing_target_file, TargetFilename } );
false ->
ok
end,
KeyInfos = read_key( KeyFilename ),
io:format( "Encrypting source file '~s' with key file '~s', "
"storing the result in '~s'.~n~s~n",
[ SourceFilename, KeyFilename, TargetFilename,
key_to_string( KeyInfos ) ] ),
% We may use randomised ciphers:
random_utils:start_random_source( default_seed ),
TempFilename = apply_key( KeyInfos, SourceFilename ),
file_utils:rename( TempFilename, TargetFilename ).
% Decrypts specified source file using specified key file, and writes the result
% in specified target file.
%
% The ciphered file is kept as is.
%
-spec decrypt( file_utils:file_name(), file_utils:file_name(),
file_utils:file_name() ) -> void().
decrypt( SourceFilename, TargetFilename, KeyFilename ) ->
_SourceFile = case file_utils:is_existing_file_or_link( SourceFilename ) of
true ->
file_utils:open( SourceFilename, _Opts=[ read, raw, read_ahead ] );
false ->
throw( { non_existing_source_file, SourceFilename } )
end,
_TargetFile = case file_utils:exists( TargetFilename ) of
true ->
throw( { already_existing_target_file, TargetFilename } );
false ->
ok
end,
KeyInfos = read_key( KeyFilename ),
io:format( "Decrypting source file '~s' with key file '~s', "
"storing the result in '~s'.~n~s~n",
[ SourceFilename, KeyFilename, TargetFilename,
key_to_string( KeyInfos ) ] ),
% We may use randomised ciphers:
random_utils:start_random_source( default_seed ),
ReverseKey = get_reverse_key_from( KeyInfos ),
io:format( "Determined reverse key:~n~s~n",
[ key_to_string( ReverseKey ) ] ),
TempFilename = apply_key( ReverseKey, SourceFilename ),
file_utils:rename( TempFilename, TargetFilename ).
% Helper section.
% Reads key from specified filename, and returns it.
%
% (helper)
%
-spec read_key( file_utils:file_name() ) -> [ user_specified_transform() ].
read_key( KeyFilename ) ->
case file_utils:is_existing_file_or_link( KeyFilename ) of
true ->
case file_utils:read_terms( KeyFilename ) of
[] ->
throw( { empty_key, KeyFilename } );
[ Key ] when is_list( Key ) ->
Key;
Invalid ->
throw( { invalid_multiline_key, Invalid } )
end;
false ->
throw( { non_existing_key_file, KeyFilename } )
end.
% Returns the reverse key of specified one.
%
% (helper)
%
-spec get_reverse_key_from( [ user_specified_transform() ] ) ->
[ any_transform() ].
get_reverse_key_from( KeyInfos ) ->
get_reverse_key_from( KeyInfos, _Acc=[] ).
get_reverse_key_from( _KeyInfos=[], Acc ) ->
% Order already reversed by design:
Acc;
get_reverse_key_from( _KeyInfos=[ K | H ], Acc ) ->
ReversedK = reverse_cipher( K ),
get_reverse_key_from( H, [ ReversedK | Acc ] ).
% Applies specified key to specified file.
%
% Returns the filename of the resulting file.
%
%
apply_key( KeyInfos, SourceFilename ) ->
apply_key( KeyInfos, SourceFilename, _CipherCount=1 ).
apply_key( _KeyInfos=[], SourceFilename, _CipherCount ) ->
SourceFilename;
apply_key( _KeyInfos=[ C | H ], SourceFilename, CipherCount ) ->
io:format( " - applying cipher #~B: '~p'~n",
[ CipherCount, get_cipher_description( C ) ] ),
% Filename of the ciphered version:
CipheredFilename = generate_filename(),
apply_cipher( C, SourceFilename, CipheredFilename ),
case CipherCount of
1 ->
ok;
_ ->
% Not wanting to saturate the storage space with intermediate files:
file_utils:remove_file( SourceFilename )
end,
apply_key( H, CipheredFilename, CipherCount + 1 ).
% Table way too big to be displayed:
get_cipher_description( { mealy, InitialState, _Table } ) ->
text_utils:format( "Mealy transform, with initial state ~p",
[ InitialState ] );
get_cipher_description( OtherCipher ) ->
OtherCipher.
% Applies specified cipher to specified file.
%
% Some ciphers are better managed if special-cased, whereas others can rely on
% base (yet generic) mechanisms.
%
-spec apply_cipher( any(), file_utils:file_name(), file_utils:file_name() ) ->
void().
apply_cipher( id, SourceFilename, CipheredFilename ) ->
id_cipher( SourceFilename, CipheredFilename );
apply_cipher( { offset, Offset }, SourceFilename, CipheredFilename ) ->
% CypherState is simply the constant offset used:
OffsetFun = fun( InputByte, CypherState ) ->
OutputByte = InputByte + Offset,
{ OutputByte, CypherState }
end,
apply_byte_level_cipher( SourceFilename, CipheredFilename,
_Transform=OffsetFun, _InitialCipherState=Offset );
apply_cipher( { compress, CompressFormat }, SourceFilename,
CipheredFilename ) ->
compress_cipher( SourceFilename, CipheredFilename, CompressFormat );
apply_cipher( { decompress, CompressFormat }, SourceFilename,
CipheredFilename ) ->
decompress_cipher( SourceFilename, CipheredFilename, CompressFormat );
apply_cipher( { insert_random, Seed, Range }, SourceFilename,
CipheredFilename ) ->
random_utils:reset_random_source( Seed ),
insert_random_cipher( SourceFilename, CipheredFilename, Range );
apply_cipher( { extract_random, Seed, Range }, SourceFilename,
CipheredFilename ) ->
random_utils:reset_random_source( Seed ),
extract_random_cipher( SourceFilename, CipheredFilename, Range );
apply_cipher( delta_combine, SourceFilename, CipheredFilename ) ->
% CypherState is simply the last value read:
DeltaFun = fun( InputByte, CypherState ) ->
OutputByte = InputByte - CypherState,
{ OutputByte, InputByte }
end,
apply_byte_level_cipher( SourceFilename, CipheredFilename,
_Transform=DeltaFun, _InitialCipherState=100 );
apply_cipher( delta_combine_reverse, SourceFilename, CipheredFilename ) ->
% CypherState is simply the last value read:
ReverseDeltaFun = fun( InputByte, CypherState ) ->
OutputByte = InputByte + CypherState,
{ OutputByte, OutputByte }
end,
apply_byte_level_cipher( SourceFilename, CipheredFilename,
_Transform=ReverseDeltaFun, _InitialCipherState=100 );
apply_cipher( { shuffle, Seed, Length }, SourceFilename, CipheredFilename ) ->
random_utils:reset_random_source( Seed ),
shuffle_cipher( SourceFilename, CipheredFilename, Length, direct );
apply_cipher( { reciprocal_shuffle, Seed, Length }, SourceFilename,
CipheredFilename ) ->
random_utils:reset_random_source( Seed ),
shuffle_cipher( SourceFilename, CipheredFilename, Length, reciprocal );
apply_cipher( { 'xor', XORList }, SourceFilename, CipheredFilename ) ->
xor_cipher( SourceFilename, CipheredFilename, XORList );
apply_cipher( { mealy, InitialMealyState, MealyTable }, SourceFilename,
CipheredFilename ) ->
mealy_cipher( SourceFilename, CipheredFilename, InitialMealyState,
MealyTable );
apply_cipher( C, _SourceFilename, _CipheredFilename ) ->
throw( { unknown_cipher_to_apply, C } ).
% Returns the reverse cipher of the specified one.
%
reverse_cipher( C=id ) ->
C;
reverse_cipher( { offset, Offset } ) ->
{ offset, 256 - Offset };
reverse_cipher( { compress, CompressFormat } ) ->
{ decompress, CompressFormat };
reverse_cipher( { insert_random, Seed, Range } ) ->
{ extract_random, Seed, Range };
reverse_cipher( delta_combine ) ->
delta_combine_reverse;
reverse_cipher( delta_combine_reverse ) ->
delta_reverse;
reverse_cipher( { shuffle, _Seed, _Length } ) ->
{ reciprocal_shuffle, _Seed, _Length };
reverse_cipher( C={ 'xor', _XORList } ) ->
C;
reverse_cipher( { mealy, InitialState, MealyMachine } ) ->
{ mealy, InitialState, compute_inverse_mealy_table( MealyMachine ) };
reverse_cipher( C ) ->
throw( { unknown_cipher_to_reverse, C } ).
% Cipher section.
% For all ciphers that can be expressed by a byte-level, stateful transformation
% fun.
%
apply_byte_level_cipher( SourceFilename, CipheredFilename, CipherFun,
CipherInitialState ) ->
% No need for intermediate buffering, thanks to read_ahead and
% delayed_write;
SourceFile = file_utils:open( SourceFilename,
_ReadOpts=[ read, raw, binary, read_ahead ] ),
TargetFile = file_utils:open( CipheredFilename,
_WriteOpts=[ write, raw, delayed_write ] ),
apply_byte_level_helper( SourceFile, TargetFile, CipherFun,
CipherInitialState ).
% Actual application of a byte-level transform.
%
apply_byte_level_helper( SourceFile, TargetFile, CipherFun,
CipherInitialState ) ->
Count = 1024 * 8,
case file_utils:read( SourceFile, Count ) of
eof ->
file_utils:close( SourceFile ),
file_utils:close( TargetFile );
{ ok, DataBin } ->
{ NewDataBin, NewCipherState } = transform_bytes( DataBin,
CipherFun, CipherInitialState ),
file_utils:write( TargetFile, NewDataBin ),
apply_byte_level_helper( SourceFile, TargetFile, CipherFun,
NewCipherState )
end.
% There must be a way of folding onto binaries (bitstring comprehensions):
transform_bytes( DataBin, CipherFun, CipherInitialState ) ->
transform_bytes( DataBin, CipherFun, CipherInitialState, _AccBin = <<>> ).
transform_bytes( <<>>, _CipherFun, CipherState, AccBin ) ->
{ AccBin, CipherState };
transform_bytes( _A = << InputByte:8, T/binary >>, CipherFun,
CipherState, AccBin ) ->
{ OutputByte, NewCipherState } = CipherFun( InputByte, CipherState ),
transform_bytes( T, CipherFun, NewCipherState,
<< AccBin/binary, OutputByte >> ).
% We must though create a new file, as the semantics is to create an additional
% file in all cases.
%
id_cipher( SourceFilename, CipheredFilename ) ->
file_utils:copy_file( SourceFilename, CipheredFilename ).
compress_cipher( SourceFilename, CipheredFilename, CompressFormat ) ->
CompressedFilename = file_utils:compress( SourceFilename, CompressFormat ),
% Preserves the caller-naming convention:
file_utils:rename( CompressedFilename, CipheredFilename ).
decompress_cipher( CipheredFilename, TargetFilename, CompressFormat ) ->
% The decompressing function will check for the relevant extension:
% We must avoid, to decompress X, to rename it to X.bzip2 and then to
% decompress it, as this would produce a new decompressed file named X,
% overwriting the initial one.
%NewCipheredFilename = CipheredFilename
NewCipheredFilename = generate_filename()
++ file_utils:get_extension_for( CompressFormat ),
file_utils:rename( CipheredFilename, NewCipheredFilename ),
DecompressedFilename = file_utils:decompress( NewCipheredFilename,
CompressFormat ),
file_utils:rename( NewCipheredFilename, CipheredFilename ),
% Preserves the caller-naming convention:
file_utils:rename( DecompressedFilename, TargetFilename ).
insert_random_cipher( SourceFilename, CipheredFilename, Range )
when Range > 1 ->
SourceFile = file_utils:open( SourceFilename,
_ReadOpts=[ read, raw, binary, read_ahead ] ),
TargetFile = file_utils:open( CipheredFilename,
_WriteOpts=[ write, raw, delayed_write ] ),
_InsertedCount = insert_helper( SourceFile, TargetFile, Range, _Count=0 ).
%trace_utils:debug_fmt( "insert_random_cipher: inserted ~B bytes.",
% [ InsertedCount ] ).
% We insert at random places random values in the content:
insert_helper( SourceFile, TargetFile, Range, Count ) ->
NextInsertionOffset = random_utils:get_random_value( Range ),
case file_utils:read( SourceFile, NextInsertionOffset ) of
eof ->
file_utils:close( SourceFile ),
file_utils:close( TargetFile ),
Count;
{ ok, DataBin } when size( DataBin ) =:= NextInsertionOffset ->
RandomByte = random_utils:get_random_value( 255 ),
NewDataBin = << DataBin/binary, RandomByte:8 >>,
file_utils:write( TargetFile, NewDataBin ),
insert_helper( SourceFile, TargetFile, Range, Count + 1 );
{ ok, PartialDataBin } ->
% Drawn offset not reachable, just finished then:
file_utils:write( TargetFile, PartialDataBin ),
file_utils:close( SourceFile ),
file_utils:close( TargetFile ),
Count
end.
extract_random_cipher( CipheredFilename, TargetFilename, Range )
when Range > 1 ->
CipheredFile = file_utils:open( CipheredFilename,
_ReadOpts=[ read, raw, binary, read_ahead ] ),
TargetFile = file_utils:open( TargetFilename,
_WriteOpts=[ write, raw, delayed_write ] ),
_ExtractedCount = extract_helper( CipheredFile, TargetFile, Range,
_Count=0 ).
%io:format( "extract_random_cipher: extracted ~B bytes.~n",
% [ ExtractedCount ] ).
% We extract at random places the bytes found in the content:
extract_helper( CipheredFile, TargetFile, Range, Count ) ->
NextExtractionOffset = random_utils:get_random_value( Range ),
case file_utils:read( CipheredFile, NextExtractionOffset ) of
eof ->
file_utils:close( CipheredFile ),
file_utils:close( TargetFile ),
Count;
{ ok, DataBin } when size( DataBin ) =:= NextExtractionOffset ->
% We drop on the floor the previously inserted byte:
case file_utils:read( CipheredFile, 1 ) of
eof ->
file_utils:close( CipheredFile ),
file_utils:close( TargetFile ),
Count;
{ ok, <<_ExtractedByte:8>> } ->
% Dummy operation, needed to reproduce the insertion random
% state:
_RandomByte = random_utils:get_random_value( 255 ),
file_utils:write( TargetFile, DataBin ),
extract_helper( CipheredFile, TargetFile, Range, Count + 1 )
end;
{ ok, PartialDataBin } ->
% Finished:
file_utils:write( TargetFile, PartialDataBin ),
file_utils:close( CipheredFile ),
file_utils:close( TargetFile ),
Count
end.
% Direction is either 'direct' or 'reciprocal':
shuffle_cipher( SourceFilename, CipheredFilename, Length, Direction ) ->
% Wanting to read lists, not binaries:
SourceFile = file_utils:open( SourceFilename,
_ReadOpts=[ read, raw, read_ahead ] ),
TargetFile = file_utils:open( CipheredFilename,
_WriteOpts=[ write, raw, delayed_write ] ),
shuffle_helper( SourceFile, TargetFile, Length, Direction ).
shuffle_helper( SourceFile, TargetFile, Length, Direction ) ->
case file_utils:read( SourceFile, Length ) of
eof ->
file_utils:close( SourceFile ),
file_utils:close( TargetFile );
% When will hit the end of file, may perform shuffle on a smaller chunk:
{ ok, ByteList } ->
ShuffledByteList = case Direction of
direct ->
list_utils:random_permute( ByteList );
reciprocal ->
list_utils:random_permute_reciprocal( ByteList )
end,
file_utils:write( TargetFile, ShuffledByteList ),
shuffle_helper( SourceFile, TargetFile, Length, Direction )
end.
xor_cipher( SourceFilename, CipheredFilename, XORList ) ->
% Wanting to read lists, not binaries:
SourceFile = file_utils:open( SourceFilename,
_ReadOpts=[ read, raw, read_ahead ] ),
TargetFile = file_utils:open( CipheredFilename,
_WriteOpts=[ write, raw, delayed_write ] ),
XORRing = ring_utils:from_list( XORList ),
xor_helper( SourceFile, TargetFile, XORRing ).
xor_helper( SourceFile, TargetFile, XORRing ) ->
case file_utils:read( SourceFile, 1 ) of
eof ->
file_utils:close( SourceFile ),
file_utils:close( TargetFile );
{ ok, [ Byte ] } ->
{ H, NewXORRing } = ring_utils:head( XORRing ),
NewByte = Byte bxor H,
% In a list, as an integer is not licit, io_list() needed:
file_utils:write( TargetFile, [ NewByte ] ),
xor_helper( SourceFile, TargetFile, NewXORRing )
end.
mealy_cipher( SourceFilename, CipheredFilename, InitialMealyState,
MealyTable ) ->
% Wanting to read lists, not binaries:
SourceFile = file_utils:open( SourceFilename,
_ReadOpts=[ read, raw, read_ahead ] ),
TargetFile = file_utils:open( CipheredFilename,
_WriteOpts=[ write, raw, delayed_write ] ),
mealy_helper( SourceFile, TargetFile, InitialMealyState, MealyTable ).
mealy_helper( SourceFile, TargetFile, CurrentMealyState, MealyTable ) ->
case file_utils:read( SourceFile, 1 ) of
eof ->
file_utils:close( SourceFile ),
file_utils:close( TargetFile );
{ ok, [ InputByte ] } ->
{ NextMealyState, OutputByte } =
apply_mealy( InputByte, CurrentMealyState, MealyTable ),
file_utils:write( TargetFile, [ OutputByte ] ),
mealy_helper( SourceFile, TargetFile, NextMealyState, MealyTable )
end.
% Applies the Mealy machine to new input, while in specified state.
%
% Returns { NextMealyState, OutputByte }.
%
apply_mealy( InputByte, CurrentMealyState, MealyTable ) ->
% Zero-indexed:
InnerArray = array:get( CurrentMealyState - 1, MealyTable ),
% Returns the cell:
array:get( InputByte, InnerArray ).
generate_filename() ->
Filename = ".cipher-" ++ id_utils:generate_uuid(),
case file_utils:is_existing_file( Filename ) of
% Rather unlikely:
true ->
generate_filename();
false ->
Filename
end.
% Mealy section.
% We can represent a Mealy table that way:
% Alphabet \ States:
%
% S1 S2 ... Sstate_count
% 0 { S44, 135 }
% 1 { S2, 11 }
% . .
% . .
% . .
% 255
% Each inner array corresponds to a column, in charge of the behaviour of the
% machine when it is in the corresponding state Sn.
% Generates a Mealy table for the specified number of states.
%
% Relies on the current random state.
%
generate_mealy_table( StateCount ) ->
% Default alphabet is all byte values:
generate_mealy_table( StateCount, _AlphabetSize=256 ).
% Generates a Mealy table for the specified number of states and alphabet size.
%
% We manage index to designate symbols of the alphabet; for example, if the
% alphabet is [ alpha, beta, gamma ], then the alpha symbol is coded by 1, the
% beta one by 2, etc.
%
% Relies on the current random state.
%
generate_mealy_table( StateCount, AlphabetSize ) ->
io:format( "Generating a Mealy table for ~B states and "
"an alphabet of ~B symbols.~n", [ StateCount, AlphabetSize ] ),
Table = array:new( StateCount ),
% Prebuilt for easier permutations in [0;255]:
Alphabet = lists:seq( 0, AlphabetSize - 1 ),
% Arrays are zero-indexed:
fill_table( Table, _Index=0, _FinalIndex=StateCount, Alphabet,
AlphabetSize ).
% Adds the inner arrays:
fill_table( Table, _Index=FinalIndex, FinalIndex, _Alphabet, _AlphabetSize ) ->
Table;
fill_table( Table, Index, FinalIndex, Alphabet, AlphabetSize ) ->
% FinalIndex is StateCount:
InnerArray = create_inner_array( Alphabet, AlphabetSize, FinalIndex ),
NewTable = array:set( Index, InnerArray, Table ),
fill_table( NewTable, Index + 1, FinalIndex, Alphabet, AlphabetSize ).
% Will have as many elements as there are input letters:
create_inner_array( Alphabet, AlphabetSize, StateCount ) ->
Array = array:new( AlphabetSize ),
% To fill this inner array (corresponding to a given state), we must, for
% each of the possible input letter, specify the corresponding cell.
%
% The corresponding pair is made of a new state (uniformly chosen at random
% among the possible states - some states can appear multiple times, other
% none) and an output letter; in each inner array, these output letters must
% form an exact permutation of the alphabet, for reversibility purpose.
% Updates also the random state:
Letters = list_utils:random_permute( Alphabet ),
% No, a fold would not be clearer:
fill_inner_array( Array, _Index=0, _FinalIndex=AlphabetSize, Letters,
StateCount ).
% We iterate through the permuted letters:
fill_inner_array( Array, _Index=FinalIndex, FinalIndex, _Letters=[],
_StateCount ) ->
Array;
fill_inner_array( Array, Index, FinalIndex, _Letters=[ L | T ], StateCount ) ->
% Returns a value in [1,StateCount]:
NextState = random_utils:get_random_value( StateCount ),
Cell = { NextState, L },
NewArray = array:set( Index, Cell, Array ),
fill_inner_array( NewArray, Index + 1, FinalIndex, T, StateCount ).
% Returns the inverse Mealy table of the specified one.
%
-spec compute_inverse_mealy_table( mealy_table() ) -> mealy_table().
compute_inverse_mealy_table( Table ) ->
StateCount = array:size( Table ),
% At least one state defined:
AlphabetSize = array:size( array:get( 0, Table ) ),
% To inverse a Mealy table: when we read the first encrypted byte while in
% initial state S, we look up in the original table to which input byte it
% corresponded for state S, and write that byte. The state in that cell is
% the next state. Then we iterate.
% So the inverse table can be computed simply by finding, for each read
% byte, what is the input byte which corresponded.
InverseTable = array:new( StateCount ),
% Iterate first on inner arrays:
InnerArrays = array:to_list( Table ),
fill_reverse_table( InverseTable, InnerArrays, _Index=0,
_FinalIndex=StateCount, AlphabetSize ).
fill_reverse_table( InverseTable, _InnerArrays=[], _Index=FinalIndex,
FinalIndex, _AlphabetSize ) ->
InverseTable;
fill_reverse_table( InverseTable, _InnerArrays=[ A | T ], Index, FinalIndex,
AlphabetSize ) ->
ReversedInnerArray = inverse_inner_array( A, AlphabetSize ),
NewInverseTable = array:set( Index, ReversedInnerArray, InverseTable ),
fill_reverse_table( NewInverseTable, T, Index + 1, FinalIndex,
AlphabetSize ).
inverse_inner_array( InnerArray, AlphabetSize ) ->
Cells = array:to_list( InnerArray ),
NewInnerArray = array:new( AlphabetSize ),
inverse_cells( Cells, _Index=0, NewInnerArray ).
inverse_cells( _Cells=[], _Index, AccArray ) ->
AccArray;
% We will branch to the same next state, but we output what the direct machine
% must have read for that output letter:
%
inverse_cells( _Cells=[ { NextState, OutputLetter } | T ], Index, AccArray ) ->
NewAccArray = array:set( OutputLetter, { NextState, Index }, AccArray ),
inverse_cells( T, Index + 1, NewAccArray ).
% Returns a textual representation of this mealy table.
%
-spec mealy_table_to_string( mealy_table() ) -> string().
mealy_table_to_string( Table ) ->
StateCount = array:size( Table ),
StateStrings = get_inner_info( Table, _Index=0, _FinalIndex=StateCount,
_Acc=[] ),
AlphabetSize = array:size( array:get( 0, Table ) ),
text_utils:format( "Mealy table with ~B states and an alphabet of "
"~B letters: ~s",
[ StateCount, AlphabetSize,
text_utils:strings_to_string( StateStrings ) ] ).
get_inner_info( _Table, _Index=FinalIndex, FinalIndex, Acc ) ->
lists:reverse( Acc );
get_inner_info( Table, Index, FinalIndex, Acc ) ->
% List of cells:
InnerList = array:to_list( array:get( Index, Table ) ),
S = text_utils:format( "for state S~B:~n~s",
[ Index + 1, get_cells_info( InnerList ) ] ),
get_inner_info( Table, Index + 1, FinalIndex, [ S | Acc ] ).
get_cells_info( InnerList ) ->
% To avoid many ineffective concatenations:
get_cells_info( lists:reverse( InnerList ), _StringAcc=[] ).
get_cells_info( _InnerList=[], StringAcc ) ->
StringAcc;
get_cells_info( _InnerList=[ _C={ NewState, OutputByte } | T ], StringAcc ) ->
NewAcc = text_utils:format( "{~p,~B} ", [ NewState, OutputByte ] )
++ StringAcc,
get_cells_info( T, NewAcc ).