1 module tagion.crypto.Cipher;
2 
3 import std.exception : assumeUnique, ifThrown;
4 import tagion.basic.Types : Buffer;
5 import tagion.crypto.Types : Pubkey;
6 import tagion.crypto.random.random;
7 import tagion.hibon.Document;
8 import tagion.hibon.HiBONRecord;
9 
10 @safe
11 struct Cipher {
12     import tagion.crypto.secp256k1.NativeSecp256k1;
13     import std.digest.crc : crc64ECMAOf;
14     import tagion.basic.ConsensusExceptions : ConsensusException, ConsensusFailCode, SecurityConsensusException;
15     import tagion.crypto.SecureInterfaceNet : SecureNet;
16     import tagion.crypto.SecureNet : check;
17     import tagion.crypto.random.random;
18     import tagion.crypto.aes.AESCrypto : AESCrypto;
19 
20     alias AES = AESCrypto!256;
21     enum CRC_SIZE = crc64ECMAOf.length;
22 
23     @recordType("TCD")
24     struct CipherDocument {
25         @label("$m") Buffer ciphermsg;
26         @label("$n") Buffer nonce;
27         @label("$a") Buffer authTag;
28         @label("$k") Pubkey cipherPubkey;
29         mixin HiBONRecord;
30     }
31 
32     static const(CipherDocument) encrypt(const(SecureNet) net, const(Pubkey) pubkey, const(Document) msg) {
33 
34         scope ubyte[32] secret_key_alloc;
35         scope ubyte[] secret_key = secret_key_alloc;
36         scope (exit) {
37             secret_key[] = 0;
38         }
39         getRandom(secret_key);
40         CipherDocument result;
41         result.cipherPubkey = net.getPubkey(secret_key);
42         scope ubyte[AES.BLOCK_SIZE] nonce_alloc;
43         scope ubyte[] nonce = nonce_alloc;
44         getRandom(nonce);
45         result.nonce = nonce.idup;
46         // Appand CRC
47         auto ciphermsg = new ubyte[AES.enclength(msg.data.length + CRC_SIZE)];
48 
49         // Put random padding to in the last block
50         auto last_block = ciphermsg[$ - AES.BLOCK_SIZE + CRC_SIZE .. $];
51         getRandom(last_block);
52         const crc = msg.data.crc64ECMAOf;
53         ciphermsg[0 .. msg.data.length] = msg.data;
54         ciphermsg[msg.data.length .. msg.data.length + CRC_SIZE] = crc;
55 
56         scope sharedECCKey = net.ECDHSecret(secret_key, pubkey);
57         AES.encrypt(sharedECCKey, result.nonce, ciphermsg, ciphermsg);
58         Buffer get_ciphermsg() @trusted {
59             return assumeUnique(ciphermsg);
60         }
61 
62         result.ciphermsg = get_ciphermsg;
63         return result;
64     }
65 
66     static const(CipherDocument) encrypt(const(SecureNet) net, const(Document) msg) {
67         return encrypt(net, net.pubkey, msg);
68     }
69 
70     static const(Document) decrypt(const(SecureNet) net, const(CipherDocument) cipher_doc) {
71         scope sharedECCKey = net.ECDHSecret(cipher_doc.cipherPubkey);
72         auto clearmsg = new ubyte[cipher_doc.ciphermsg.length];
73         AES.decrypt(sharedECCKey, cipher_doc.nonce, cipher_doc.ciphermsg, clearmsg);
74         Buffer data = (() @trusted => assumeUnique(clearmsg))();
75         const result = Document(data);
76         immutable full_size = result.full_size;
77         check(full_size + CRC_SIZE <= data.length && full_size !is 0,
78                 ConsensusFailCode.CIPHER_DECRYPT_ERROR);
79         const crc = data[full_size .. full_size + CRC_SIZE];
80         check(data[0 .. full_size].crc64ECMAOf == crc, 
81                 ConsensusFailCode.CIPHER_DECRYPT_CRC_ERROR);
82         return result;
83     }
84 
85     unittest {
86         import std.algorithm.searching : all, any;
87         import tagion.basic.Types : FileExtension;
88         import tagion.basic.basic : fileId;
89         import tagion.crypto.SecureNet;
90         import tagion.hibon.Document : Document;
91         import tagion.hibon.HiBON : HiBON;
92         import tagion.utils.Miscellaneous : decode;
93 
94         immutable passphrase = "Secret pass word";
95         auto net = new StdSecureNet; /// Only works with ECDSA for now 
96         net.generateKeyPair(passphrase);
97 
98         immutable some_secret_message = "Text to be encrypted by ECC public key and " ~
99             "decrypted by its corresponding ECC private key";
100         auto hibon = new HiBON;
101         hibon["text"] = some_secret_message;
102         const secret_doc = Document(hibon);
103 
104         { // Encrypt and Decrypt secrte message
105             auto dummy_net = new StdSecureNet;
106             const secret_cipher_doc = Cipher.encrypt(dummy_net, net.pubkey, secret_doc);
107             const encrypted_doc = Cipher.decrypt(net, secret_cipher_doc);
108             assert(encrypted_doc["text"].get!string == some_secret_message);
109             assert(secret_doc.data == encrypted_doc.data);
110         }
111 
112         { // Use of the wrong privat-key
113             auto dummy_net = new StdSecureNet;
114             auto wrong_net = new StdSecureNet;
115             immutable wrong_passphrase = "wrong word";
116             wrong_net.generateKeyPair(wrong_passphrase);
117             bool cipher_decrypt_error;
118             bool cipher_decrypt_crc_error;
119             while (!cipher_decrypt_error || !cipher_decrypt_crc_error) {
120                 const secret_cipher_doc = Cipher.encrypt(dummy_net, wrong_net.pubkey, secret_doc);
121                 try {
122                     const encrypted_doc = Cipher.decrypt(net, secret_cipher_doc);
123                     assert(secret_doc != encrypted_doc);
124                     if (!encrypted_doc.empty) {
125                         break; /// Run the loop until the decrypt does not fail
126                     }
127                 }
128                 catch (ConsensusException e) {
129                     cipher_decrypt_error |= (e.code == ConsensusFailCode.CIPHER_DECRYPT_ERROR);
130                     cipher_decrypt_crc_error |= (e.code == ConsensusFailCode.CIPHER_DECRYPT_CRC_ERROR);
131                 }
132             }
133         }
134 
135         { // Encrypt and Decrypt secrte message with owner privat-key
136             const secret_cipher_doc = Cipher.encrypt(net, secret_doc);
137             const encrypted_doc = Cipher.decrypt(net, secret_cipher_doc);
138             assert(encrypted_doc["text"].get!string == some_secret_message);
139             assert(secret_doc.data == encrypted_doc.data);
140         }
141 
142     }
143 
144 }