From: Markus Triska Date: Sat, 16 May 2020 17:51:41 +0000 (+0200) Subject: Authenticated encryption in library(crypto), new encoding/1 option for hashes (#515) X-Git-Tag: v0.8.123~28^2~1 X-Git-Url: https://git.sagredo.dev/?a=commitdiff_plain;h=862ae26631694f62d1e330fdb353a5b47853ce68;p=scryer-prolog.git Authenticated encryption in library(crypto), new encoding/1 option for hashes (#515) * shorten n_newlines//1 * remove unneeded variable * ADDED: authenticated encryption and decryption with ChaCha20-Poly1305 * ADDED: encoding/1 option for crypto_data_hash/3 and crypto_data_hkdf/4 --- diff --git a/README.md b/README.md index 013327c6..ed9161f5 100644 --- a/README.md +++ b/README.md @@ -380,7 +380,7 @@ The modules that ship with Scryer Prolog are also called * [`crypto`](src/prolog/lib/crypto.pl) Cryptographically secure random numbers and hashes, HMAC-based key derivation (HKDF), password-based key derivation (PBKDF2), - and reasoning about elliptic curves. + authenticated encryption, and reasoning about elliptic curves. To read contents of external files, use `phrase_from_file/2` from [`library(pio)`](src/prolog/lib/pio.pl) to apply a DCG to diff --git a/src/prolog/clause_types.rs b/src/prolog/clause_types.rs index fc4d1c94..cf4e1aa6 100644 --- a/src/prolog/clause_types.rs +++ b/src/prolog/clause_types.rs @@ -289,7 +289,9 @@ pub enum SystemClauseType { CryptoRandomByte, CryptoDataHash, CryptoDataHKDF, - CryptoPasswordHash + CryptoPasswordHash, + CryptoDataEncrypt, + CryptoDataDecrypt } impl SystemClauseType { @@ -476,6 +478,8 @@ impl SystemClauseType { &SystemClauseType::CryptoDataHash => clause_name!("$crypto_data_hash"), &SystemClauseType::CryptoDataHKDF => clause_name!("$crypto_data_hkdf"), &SystemClauseType::CryptoPasswordHash => clause_name!("$crypto_password_hash"), + &SystemClauseType::CryptoDataEncrypt => clause_name!("$crypto_data_encrypt"), + &SystemClauseType::CryptoDataDecrypt => clause_name!("$crypto_data_decrypt"), } } @@ -642,6 +646,8 @@ impl SystemClauseType { ("$crypto_data_hash", 3) => Some(SystemClauseType::CryptoDataHash), ("$crypto_data_hkdf", 6) => Some(SystemClauseType::CryptoDataHKDF), ("$crypto_password_hash", 4) => Some(SystemClauseType::CryptoPasswordHash), + ("$crypto_data_encrypt", 5) => Some(SystemClauseType::CryptoDataEncrypt), + ("$crypto_data_decrypt", 5) => Some(SystemClauseType::CryptoDataDecrypt), _ => None, } } diff --git a/src/prolog/lib/crypto.pl b/src/prolog/lib/crypto.pl index 0bb9d2a0..b93a9cff 100644 --- a/src/prolog/lib/crypto.pl +++ b/src/prolog/lib/crypto.pl @@ -12,17 +12,20 @@ using strings leaves little trace of what was processed in the system, - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -:- module(crypto, [hex_bytes/2, % ?Hex, ?Bytes - crypto_n_random_bytes/2, % +N, -Bytes - crypto_data_hash/3, % +Data, -Hash, +Options - crypto_data_hkdf/4, % +Data, +Length, -Bytes, +Options - crypto_name_curve/2, % +Name, -Curve - crypto_curve_order/2, % +Curve, -Order - crypto_curve_generator/2, % +Curve, -Generator - crypto_curve_scalar_mult/4, % +Curve, +Scalar, +Point, -Result - crypto_password_hash/2, % +Password, ?Hash - crypto_password_hash/3 % +Password, -Hash, +Options - ]). +:- module(crypto, + [hex_bytes/2, % ?Hex, ?Bytes + crypto_n_random_bytes/2, % +N, -Bytes + crypto_data_hash/3, % +Data, -Hash, +Options + crypto_data_hkdf/4, % +Data, +Length, -Bytes, +Options + crypto_password_hash/2, % +Password, ?Hash + crypto_password_hash/3, % +Password, -Hash, +Options + crypto_data_encrypt/6, % +PlainText, +Algorithm, +Key, +IV, -CipherText, +Options + crypto_data_decrypt/6, % +CipherText, +Algorithm, +Key, +IV, -PlainText, +Options + crypto_name_curve/2, % +Name, -Curve + crypto_curve_order/2, % +Curve, -Order + crypto_curve_generator/2, % +Curve, -Generator + crypto_curve_scalar_mult/4 % +Curve, +Scalar, +Point, -Result + ]). :- use_module(library(error)). :- use_module(library(lists)). @@ -152,16 +155,17 @@ crypto_random_byte(B) :- '$crypto_random_byte'(B). characters, and Hash is the computed hash as a list of hexadecimal characters. - The single supported option is: + Options is a list of: - algorithm(A) - - where A is one of ripemd160, sha256, sha384, sha512, sha512_256, - or a variable. - - If A is a variable, then it is unified with the default algorithm, - which is an algorithm that is considered cryptographically secure - at the time of this writing. + - algorithm(+A) + where A is one of ripemd160, sha256, sha384, sha512, + sha512_256, or a variable. If A is a variable, then it is + unified with the default algorithm, which is an algorithm that + is considered cryptographically secure at the time of this + writing. + - encoding(+Encoding) + The default encoding is utf8. The alternative is octet, + to treat the input as a list of raw bytes. Example: @@ -179,8 +183,9 @@ crypto_random_byte(B) :- '$crypto_random_byte'(B). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ crypto_data_hash(Data0, Hash, Options0) :- - chars_bytes_(Data0, Data, crypto_data_hash/3), must_be(list, Options0), + option(encoding(Encoding), Options0, utf8), + encoding_bytes(Encoding, Data0, Data), functor_hash_options(algorithm, A, Options0, _), ( hash_algorithm(A) -> true ; domain_error(hash_algorithm, A, crypto_data_hash/3) @@ -232,6 +237,9 @@ hash_algorithm(sha512_256). - salt(+List) Optionally, a list of bytes that are used as salt. The default is all zeroes. + - encoding(+Encoding) + The default encoding is utf8. The alternative is octet, + to treat the input as a list of raw bytes. The `info/1` option can be used to generate multiple keys from a single master key, using for example values such as "key" and @@ -242,7 +250,8 @@ hash_algorithm(sha512_256). crypto_data_hkdf(Data0, L, Bytes, Options0) :- functor_hash_options(algorithm, Algorithm, Options0, Options), - chars_bytes_(Data0, Data, crypto_data_hkdf/4), + option(encoding(Encoding), Options, utf8), + encoding_bytes(Encoding, Data0, Data), option(salt(SaltBytes), Options, []), must_be_bytes(SaltBytes, crypto_data_hkdf/4), option(info(Info0), Options, []), @@ -336,7 +345,7 @@ dollar_segments(Ls, Segments) :- - algorithm(+Algorithm) The algorithm to use. Currently, the only available algorithm - is =|pbkdf2-sha512|=, which is therefore also the default. + is 'pbkdf2-sha512', which is therefore also the default. - cost(+C) C is an integer, denoting the binary logarithm of the number of _iterations_ used for the derivation of the hash. This @@ -435,6 +444,165 @@ bytes_base64_([A,B,C|Ls]) --> [W,X,Y,Z], bytes_base64_(Ls). +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + crypto_data_encrypt(+PlainText, + +Algorithm, + +Key, + +IV, + -CipherText, + +Options). + + Encrypt the given PlainText, using the symmetric algorithm + Algorithm, key Key, and initialization vector (or nonce) IV, to + give CipherText. + + PlainText must be a list of codes or characters, Key and IV must be + lists of bytes, and CipherText is created as a list of characters. + + Keys and IVs can be chosen at random (using for example + crypto_n_random_bytes/2) or derived from input keying material (IKM) + using for example crypto_data_hkdf/4. This input is often a shared + secret, such as a negotiated point on an elliptic curve, or the hash + that was computed from a password via crypto_password_hash/3 with a + freshly generated and specified _salt_. + + Reusing the same combination of Key and IV typically leaks at least + _some_ information about the plaintext. For example, identical + plaintexts will then correspond to identical ciphertexts. For some + algorithms, reusing an IV with the same Key has disastrous results + and can cause the loss of all properties that are otherwise + guaranteed. Especially in such cases, an IV is also called a + _nonce_ (number used once). + + It is safe to store and transfer the used initialization vector (or + nonce) in plain text, but the key _must be kept secret_. + + Currently, the only supported algorithm is 'chacha20-poly1305', a + powerful and efficient _authenticated_ encryption scheme, providing + secrecy and at the same time reliable protection against undetected + _modifications_ of the encrypted data. This is a very good choice + for virtually all use cases. It is a stream cipher and can encrypt + data of any length up to 256 GB. Further, the encrypted data has + exactly the same length as the original, and no padding is used. + + Options: + + - encoding(+Encoding) + Encoding to use for PlainText. Default is utf8. The alternative + is octet to treat PlainText as raw bytes. + + - tag(-List) + For authenticated encryption schemes, List is unified with a + list of _bytes_ holding the tag. This tag must be provided for + decryption. + + Here is an example encryption and decryption, using the ChaCha20 + stream cipher with the Poly1305 authenticator. This cipher uses a + 256-bit key and a 96-bit nonce, i.e., 32 and 12 _bytes_, + respectively: + + ?- Algorithm = 'chacha20-poly1305', + crypto_n_random_bytes(32, Key), + crypto_n_random_bytes(12, IV), + crypto_data_encrypt("this text is to be encrypted", Algorithm, + Key, IV, CipherText, [tag(Tag)]), + crypto_data_decrypt(CipherText, Algorithm, + Key, IV, RecoveredText, [tag(Tag)]). + + Yielding: + + Algorithm = 'chacha20-poly1305', + Key = [113,247,153,134,177,220,13,193,50,150|...], + IV = [135,20,149,153,63,35,68,114,247,171|...], + CipherText = "\x94\0Ej\x94\®Â\x95\óÑÆXÃn¾ð©b\x1c\ ...", + RecoveredText = "this text is to be ...", + Tag = [152,117,152,17,162,75,150,206,144,40|...] + + In this example, we use crypto_n_random_bytes/2 to generate a key + and nonce from cryptographically secure random numbers. For + repeated applications, you must ensure that a nonce is only used + _once_ together with the same key. Note that for _authenticated_ + encryption schemes, the _tag_ that was computed during encryption + is necessary for decryption. It is safe to store and transfer the + tag in plain text. + + See also crypto_data_decrypt/6, and hex_bytes/2 for conversion + between bytes and hex encoding. +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +crypto_data_encrypt(PlainText0, Algorithm, Key, IV, CipherText, Options) :- + option(encoding(Encoding), Options, utf8), + encoding_bytes(Encoding, PlainText0, PlainText), + option(tag(Tag), Options, _), + ( nonvar(Tag) -> + must_be_bytes(Tag, crypto_data_encrypt/6) + ; true + ), + must_be_bytes(Key, crypto_data_encrypt/6), + must_be_bytes(IV, crypto_data_encrypt/6), + must_be(atom, Algorithm), + ( Algorithm = 'chacha20-poly1305' -> true + ; domain_error('chacha20-poly1305', Algorithm, crypto_data_encrypt/6) + ), + '$crypto_data_encrypt'(PlainText, Key, IV, Tag, CipherText). + +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + crypto_data_decrypt(+CipherText, + +Algorithm, + +Key, + +IV, + -PlainText, + +Options). + + Decrypt the given CipherText, using the symmetric algorithm + Algorithm, key Key, and initialization vector IV, to give + PlainText. CipherText must be a list of bytes or characters, and + Key and IV must be lists of bytes. PlainText is created as a list + of characters. + + Currently, the only supported algorithm is 'chacha20-poly1305', + a very secure, fast and versatile authenticated encryption method. + + Options is a list of: + + - encoding(+Encoding) + Encoding to use for PlainText. The default is utf8. The + alternative is octet, which is used if the data are raw bytes. + + - tag(+Tag) + For authenticated encryption schemes, the tag must be specified as + a list of bytes exactly as they were generated upon encryption. +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +crypto_data_decrypt(CipherText0, Algorithm, Key, IV, PlainText, Options) :- + option(tag(Tag), Options, []), + must_be_bytes(Tag, crypto_data_decrypt/6), + must_be_bytes(Key, crypto_data_decrypt/6), + must_be_bytes(IV, crypto_data_decrypt/6), + must_be(atom, Algorithm), + option(encoding(Encoding), Options, utf8), + must_be(list, CipherText0), + encoding_bytes(octet, CipherText0, CipherText1), + append(CipherText1, Tag, CipherText), + ( Algorithm = 'chacha20-poly1305' -> true + ; domain_error('chacha20-poly1305', Algorithm, crypto_data_decrypt/6) + ), + '$crypto_data_decrypt'(CipherText, Key, IV, Encoding, PlainText). + +encoding_bytes(octet, Bs0, Bs) :- + ( maplist(integer, Bs0) -> + Bs0 = Bs + ; maplist(char_code, Bs0, Bs) + ), + must_be_bytes(Bs, crypto_encoding). +encoding_bytes(utf8, Cs, Bs) :- + ( maplist(atom, Cs) -> + chars_bytes_(Cs, Bs, crypto_encoding) + ; domain_error(encryption_encoding, Cs, crypto) + ). + +char_code(Char, Code) :- atom_codes(Char, [Code]). + /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Modular multiplicative inverse. diff --git a/src/prolog/lib/format.pl b/src/prolog/lib/format.pl index a4ffd0ae..17ab0bfc 100644 --- a/src/prolog/lib/format.pl +++ b/src/prolog/lib/format.pl @@ -275,8 +275,7 @@ cells(Fs0, Args, Tab, Es) --> cells(Fs, Args, Tab, [chars(Fs1)|Es]). n_newlines(0) --> !. -n_newlines(1) --> !, [newline]. -n_newlines(N0) --> { N0 > 1, N is N0 - 1 }, [newline], n_newlines(N). +n_newlines(N0) --> { N0 > 0, N is N0 - 1 }, [newline], n_newlines(N). /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ?- phrase(upto_what(Cs, ~), "abc~test", Rest). @@ -545,7 +544,7 @@ listing(PI) :- ; type_error(predicate_indicator, PI, listing/1) ), functor(Head, Name, Arity), - \+ \+ clause(Head, Body), % only true if there is at least one clause + \+ \+ clause(Head, _), % only true if there is at least one clause ( clause(Head, Body), ( Body == true -> portray_clause(Head) diff --git a/src/prolog/machine/system_calls.rs b/src/prolog/machine/system_calls.rs index 70ece805..4ccfb7f0 100644 --- a/src/prolog/machine/system_calls.rs +++ b/src/prolog/machine/system_calls.rs @@ -40,7 +40,7 @@ use crate::crossterm::event::{read, Event, KeyCode, KeyEvent, KeyModifiers}; use crate::crossterm::terminal::{enable_raw_mode, disable_raw_mode}; use ring::rand::{SecureRandom, SystemRandom}; -use ring::{digest,hkdf,pbkdf2}; +use ring::{digest,hkdf,pbkdf2,aead,error}; use ripemd160::{Ripemd160, Digest}; pub fn get_key() -> KeyEvent { @@ -5326,6 +5326,85 @@ impl MachineState { self.unify(self[temp_v!(4)], ints_list); } + &SystemClauseType::CryptoDataEncrypt => { + let stub1 = MachineError::functor_stub(clause_name!("crypto_data_encrypt"), 6); + let data = self.integers_to_bytevec(temp_v!(1), stub1); + let stub2 = MachineError::functor_stub(clause_name!("crypto_data_encrypt"), 6); + let key = self.integers_to_bytevec(temp_v!(2), stub2); + let stub3 = MachineError::functor_stub(clause_name!("crypto_data_encrypt"), 6); + let iv = self.integers_to_bytevec(temp_v!(3), stub3); + + let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap(); + let nonce_sequence = OneNonceSequence::new(aead::Nonce::try_assume_unique_for_key(&iv).unwrap()); + let mut key: aead::SealingKey = aead::BoundKey::new(unbound_key, nonce_sequence); + + let mut in_out = data.clone(); + let tag = + match key.seal_in_place_separate_tag(aead::Aad::empty(), &mut in_out) { + Ok(d) => { d } + _ => { self.fail = true; return Ok(()); } + }; + + let tag_list = + Addr::HeapCell(self.heap.to_list(tag.as_ref().iter().map(|b| HeapCellValue::Integer(Rc::new(Integer::from(*b)))))); + + let complete_string = { + let buffer = String::from_iter(in_out.iter().map(|b| *b as char)); + self.heap.put_complete_string(&buffer) + }; + + self.unify(self[temp_v!(4)], tag_list); + self.unify(self[temp_v!(5)], complete_string); + } + &SystemClauseType::CryptoDataDecrypt => { + let stub1 = MachineError::functor_stub(clause_name!("crypto_data_decrypt"), 6); + let data = self.integers_to_bytevec(temp_v!(1), stub1); + let stub2 = MachineError::functor_stub(clause_name!("crypto_data_decrypt"), 6); + let key = self.integers_to_bytevec(temp_v!(2), stub2); + let stub3 = MachineError::functor_stub(clause_name!("crypto_data_decrypt"), 6); + let iv = self.integers_to_bytevec(temp_v!(3), stub3); + + let encoding = match self.store(self.deref(self[temp_v!(4)])) { + Addr::Con(h) if self.heap.atom_at(h) => { + if let HeapCellValue::Atom(ref atom, _) = &self.heap[h] { + atom.as_str() + } else { + unreachable!() + } + } + _ => { + unreachable!() + } + }; + + let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap(); + let nonce_sequence = OneNonceSequence::new(aead::Nonce::try_assume_unique_for_key(&iv).unwrap()); + let mut key: aead::OpeningKey = aead::BoundKey::new(unbound_key, nonce_sequence); + + let mut in_out = data.clone(); + + let complete_string = { + let decrypted_data = + match key.open_in_place(aead::Aad::empty(), &mut in_out) { + Ok(d) => { d } + _ => { self.fail = true; return Ok(()); } + }; + + let buffer = match encoding { + "octet" => { String::from_iter(decrypted_data.iter().map(|b| *b as char)) } + "utf8" => { match String::from_utf8(decrypted_data.to_vec()) { + Ok(str) => { str } + _ => { self.fail = true; return Ok(()); } + } + } + _ => { unreachable!() } + }; + + self.heap.put_complete_string(&buffer) + }; + + self.unify(self[temp_v!(5)], complete_string); + } }; return_from_clause!(self.last_call, self) @@ -5350,3 +5429,17 @@ impl hkdf::KeyType for MyKey { self.0 } } + +struct OneNonceSequence(Option); + +impl OneNonceSequence { + fn new(nonce: aead::Nonce) -> Self { + Self(Some(nonce)) + } +} + +impl aead::NonceSequence for OneNonceSequence { + fn advance(&mut self) -> Result { + self.0.take().ok_or(error::Unspecified) + } +}