From 9f11b256a86299b9c2b08876c6c266df1a8f5a64 Mon Sep 17 00:00:00 2001 From: Jack Lloyd Date: Mon, 30 Nov 2020 15:53:28 -0500 Subject: [PATCH] Expose AES-GCM-SIV to Java and Swift --- Cargo.lock | 2 + .../main/java/org/signal/internal/Native.java | 5 + .../signal/libsignal/crypto/Aes256GcmSiv.java | 33 ++++++ .../whispersystems/libsignal/util/Hex.java | 16 ++- .../libsignal/crypto/Aes256GcmSivTests.java | 103 ++++++++++++++++++ rust/aes-gcm-siv/Cargo.toml | 2 +- rust/aes-gcm-siv/src/aes.rs | 2 +- rust/aes-gcm-siv/src/aes/aarch64.rs | 2 +- rust/aes-gcm-siv/src/aes_gcm_siv.rs | 14 +++ rust/aes-gcm-siv/src/cpuid.rs | 14 ++- rust/aes-gcm-siv/src/error.rs | 17 ++- rust/bridge/ffi/Cargo.toml | 1 + rust/bridge/ffi/cbindgen.toml | 2 +- rust/bridge/ffi/src/lib.rs | 66 +++++++++++ rust/bridge/ffi/src/util.rs | 21 +++- rust/bridge/jni/Cargo.toml | 1 + rust/bridge/jni/bin/gen_java_decl.py | 2 +- rust/bridge/jni/src/lib.rs | 59 +++++++++- rust/bridge/jni/src/util.rs | 21 +++- swift/Sources/SignalClient/Aes256GcmSiv.swift | 77 +++++++++++++ .../SignalClientTests/PublicAPITests.swift | 21 ++++ 21 files changed, 463 insertions(+), 18 deletions(-) create mode 100644 java/java/src/main/java/org/signal/libsignal/crypto/Aes256GcmSiv.java create mode 100644 java/tests/src/test/java/org/signal/libsignal/crypto/Aes256GcmSivTests.java create mode 100644 swift/Sources/SignalClient/Aes256GcmSiv.swift diff --git a/Cargo.lock b/Cargo.lock index f4d2a1d4..d8f1f278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -704,6 +704,7 @@ checksum = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743" name = "libsignal-ffi" version = "0.1.0" dependencies = [ + "aes-gcm-siv", "async-trait", "cbindgen", "futures", @@ -717,6 +718,7 @@ dependencies = [ name = "libsignal-jni" version = "0.1.0" dependencies = [ + "aes-gcm-siv", "async-trait", "futures", "jni", diff --git a/java/java/src/main/java/org/signal/internal/Native.java b/java/java/src/main/java/org/signal/internal/Native.java index 6d08ef20..45733ad1 100644 --- a/java/java/src/main/java/org/signal/internal/Native.java +++ b/java/java/src/main/java/org/signal/internal/Native.java @@ -66,6 +66,11 @@ public final class Native { private Native() {} + public static native byte[] Aes256GcmSiv_Decrypt(long aesGcmSiv, byte[] msg, byte[] nonce, byte[] associatedData); + public static native void Aes256GcmSiv_Destroy(long handle); + public static native byte[] Aes256GcmSiv_Encrypt(long aesGcmSiv, byte[] msg, byte[] nonce, byte[] associatedData); + public static native long Aes256GcmSiv_New(byte[] key); + public static native String DisplayableFingerprint_Format(byte[] local, byte[] remote); public static native byte[] ECPrivateKey_Agree(long privateKeyHandle, long publicKeyHandle); diff --git a/java/java/src/main/java/org/signal/libsignal/crypto/Aes256GcmSiv.java b/java/java/src/main/java/org/signal/libsignal/crypto/Aes256GcmSiv.java new file mode 100644 index 00000000..f942217f --- /dev/null +++ b/java/java/src/main/java/org/signal/libsignal/crypto/Aes256GcmSiv.java @@ -0,0 +1,33 @@ +// +// Copyright 2020 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +package org.signal.libsignal.crypto; + +import org.signal.client.internal.Native; +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.InvalidKeyException; + +class Aes256GcmSiv { + private final long handle; + + public Aes256GcmSiv(byte[] key) throws InvalidKeyException { + this.handle = Native.Aes256GcmSiv_New(key); + } + + @Override + protected void finalize() { + Native.Aes256GcmSiv_Destroy(this.handle); + } + + byte[] encrypt(byte[] plaintext, byte[] nonce, byte[] associated_data) + throws InvalidMessageException, IllegalArgumentException { + return Native.Aes256GcmSiv_Encrypt(this.handle, plaintext, nonce, associated_data); + } + + byte[] decrypt(byte[] ciphertext, byte[] nonce, byte[] associated_data) + throws InvalidMessageException { + return Native.Aes256GcmSiv_Decrypt(this.handle, ciphertext, nonce, associated_data); + } +} diff --git a/java/java/src/main/java/org/whispersystems/libsignal/util/Hex.java b/java/java/src/main/java/org/whispersystems/libsignal/util/Hex.java index abcd8a6f..e94d7298 100644 --- a/java/java/src/main/java/org/whispersystems/libsignal/util/Hex.java +++ b/java/java/src/main/java/org/whispersystems/libsignal/util/Hex.java @@ -23,13 +23,21 @@ public class Hex { public static String toString(byte[] bytes, int offset, int length) { StringBuffer buf = new StringBuffer(); for (int i = 0; i < length; i++) { - appendHexChar(buf, bytes[offset + i]); + appendHexCharWithPrefix(buf, bytes[offset + i]); buf.append(", "); } return buf.toString(); } public static String toStringCondensed(byte[] bytes) { + StringBuffer buf = new StringBuffer(); + for (int i=0;i> 4) & 0xf]); buf.append(HEX_DIGITS[b & 0xf]); } diff --git a/java/tests/src/test/java/org/signal/libsignal/crypto/Aes256GcmSivTests.java b/java/tests/src/test/java/org/signal/libsignal/crypto/Aes256GcmSivTests.java new file mode 100644 index 00000000..baffc05a --- /dev/null +++ b/java/tests/src/test/java/org/signal/libsignal/crypto/Aes256GcmSivTests.java @@ -0,0 +1,103 @@ +// +// Copyright 2020 Signal Messenger, LLC. +// SPDX-License-Identifier: AGPL-3.0-only +// + +package org.signal.libsignal.crypto; + +import java.io.IOException; +import junit.framework.TestCase; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.util.Hex; + +public class Aes256GcmSivTests extends TestCase { + + public void testAesGcmSivInvalidInputs() throws Exception { + try { + byte[] invalid_key = new byte[16]; + Aes256GcmSiv gcm_siv = new Aes256GcmSiv(invalid_key); + throw new AssertionError("Invalid key length accepted"); + } catch (InvalidKeyException e) { + /* good */ + } + + byte[] key = new byte[32]; + Aes256GcmSiv gcm_siv = new Aes256GcmSiv(key); + + try { + byte[] ptext = new byte[5]; + byte[] ad = new byte[5]; + byte[] invalid_nonce = new byte[16]; + gcm_siv.encrypt(ptext, invalid_nonce, ad); + throw new AssertionError("Invalid nonce accepted"); + } catch (IllegalArgumentException e) { + /* good */ + } + } + + public void testAesGcmSivKats() throws Exception { + testAesGcmSivKat( + "bae8e37fc83441b16034566b7a806c46bb91c3c5aedb64a6c590bc84d1a5e269", + "671fdd4fbdc66f146545fc880c94a95198", + "9209cfae7372e0a3ec2e5d072d5e26b7b9f3acb73908e54cddf7be1864914e13cf", + "e4b47801afc0577e34699b9e", + "874296d5cc1fd16132"); + } + + private static void testAesGcmSivKat( + String hex_key, + String hex_plaintext, + String hex_ciphertext, + String hex_nonce, + String hex_associated_data) { + + try { + byte[] key = Hex.fromStringCondensed(hex_key); + byte[] plaintext = Hex.fromStringCondensed(hex_plaintext); + byte[] nonce = Hex.fromStringCondensed(hex_nonce); + byte[] ad = Hex.fromStringCondensed(hex_associated_data); + + Aes256GcmSiv gcm_siv = new Aes256GcmSiv(key); + + byte[] ciphertext = gcm_siv.encrypt(plaintext, nonce, ad); + assertEquals(Hex.toHexString(ciphertext), hex_ciphertext); + + byte[] recovered = gcm_siv.decrypt(ciphertext, nonce, ad); + assertEquals(Hex.toHexString(recovered), hex_plaintext); + + try { + ciphertext[0] ^= 1; + gcm_siv.decrypt(ciphertext, nonce, ad); + throw new AssertionError("Should not have decrypted"); + } catch (InvalidMessageException e) { + /* good */ + } + + try { + ciphertext[0] ^= 1; // restore ciphertext + nonce[0] ^= 1; + gcm_siv.decrypt(ciphertext, nonce, ad); + throw new AssertionError("Should not have decrypted"); + } catch (InvalidMessageException e) { + /* good */ + } + + try { + nonce[0] ^= 1; // restore nonce + ad[0] ^= 1; + gcm_siv.decrypt(ciphertext, nonce, ad); + throw new AssertionError("Should not have decrypted"); + } catch (InvalidMessageException e) { + /* good */ + } + + } catch (IOException e) { + throw new AssertionError(e); + } catch (InvalidKeyException e) { + throw new AssertionError(e); + } catch (InvalidMessageException e) { + throw new AssertionError(e); + } + } +} diff --git a/rust/aes-gcm-siv/Cargo.toml b/rust/aes-gcm-siv/Cargo.toml index b079e6db..27c63639 100644 --- a/rust/aes-gcm-siv/Cargo.toml +++ b/rust/aes-gcm-siv/Cargo.toml @@ -19,7 +19,7 @@ subtle = "2.3" cipher = "0.2" generic-array = "0.14" -[target.'cfg(all(target_arch = "aarch64", any(target_os = "linux", target_os = "android")))'.dependencies] +[target.'cfg(all(target_arch = "aarch64", any(target_os = "linux")))'.dependencies] libc = "0.2" # for getauxval [target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies] diff --git a/rust/aes-gcm-siv/src/aes.rs b/rust/aes-gcm-siv/src/aes.rs index 195ab15f..4dfa7d88 100644 --- a/rust/aes-gcm-siv/src/aes.rs +++ b/rust/aes-gcm-siv/src/aes.rs @@ -47,7 +47,7 @@ impl Aes256 { pub fn encrypt(&self, buf: &mut [u8]) -> Result<()> { if buf.len() % 16 != 0 { - return Err(Error::InvalidOutputBuffer); + return Err(Error::InvalidInputSize); } fn trait_encrypt(aes: &C, buf: &mut [u8]) -> Result<()> { diff --git a/rust/aes-gcm-siv/src/aes/aarch64.rs b/rust/aes-gcm-siv/src/aes/aarch64.rs index 973cc43f..515cdd28 100644 --- a/rust/aes-gcm-siv/src/aes/aarch64.rs +++ b/rust/aes-gcm-siv/src/aes/aarch64.rs @@ -65,7 +65,7 @@ impl Aes256Aarch64 { #[target_feature(enable = "crypto")] pub unsafe fn encrypt(&self, buf: &mut [u8]) -> Result<()> { if buf.len() % 16 != 0 { - return Err(Error::InvalidOutputBuffer); + return Err(Error::InvalidInputSize); } let kp = self.ek.as_ptr() as *const u8; diff --git a/rust/aes-gcm-siv/src/aes_gcm_siv.rs b/rust/aes-gcm-siv/src/aes_gcm_siv.rs index 4f598168..ac8fd3c9 100644 --- a/rust/aes-gcm-siv/src/aes_gcm_siv.rs +++ b/rust/aes-gcm-siv/src/aes_gcm_siv.rs @@ -185,4 +185,18 @@ impl Aes256GcmSiv { } Ok(()) } + + pub fn decrypt_with_appended_tag( + &self, + buffer: &mut Vec, + nonce: &[u8], + associated_data: &[u8]) -> Result<()> { + + if buffer.len() < TAG_SIZE { + return Err(Error::InvalidInputSize); + } + + let tag = buffer.split_off(buffer.len() - TAG_SIZE); + self.decrypt(buffer, nonce, associated_data, &tag) + } } diff --git a/rust/aes-gcm-siv/src/cpuid.rs b/rust/aes-gcm-siv/src/cpuid.rs index 9e1115a3..07095ecc 100644 --- a/rust/aes-gcm-siv/src/cpuid.rs +++ b/rust/aes-gcm-siv/src/cpuid.rs @@ -5,7 +5,7 @@ #[cfg(all( target_arch = "aarch64", - any(target_os = "linux", target_os = "android") + target_os = "linux" ))] pub fn has_armv8_crypto() -> bool { // Require NEON, AES and PMULL @@ -20,6 +20,18 @@ pub fn has_armv8_crypto() -> bool { true } +#[cfg(all(target_arch = "aarch64", not(any(target_os = "linux", target_os = "ios"))))] +pub fn has_armv8_crypto() -> bool { + // Detection not available for this platform + false +} + +#[cfg(all(target_arch = "aarch64", target_os = "ios"))] +pub fn has_armv8_crypto() -> bool { + // All 64-bit iOS devices have AES/PMUL support + true +} + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub fn has_intel_aesni() -> bool { is_x86_feature_detected!("aes") diff --git a/rust/aes-gcm-siv/src/error.rs b/rust/aes-gcm-siv/src/error.rs index c880e05e..2e5a91d2 100644 --- a/rust/aes-gcm-siv/src/error.rs +++ b/rust/aes-gcm-siv/src/error.rs @@ -3,14 +3,27 @@ // SPDX-License-Identifier: AGPL-3.0-only // +use std::fmt; + #[derive(Clone, Debug, Eq, PartialEq)] pub enum Error { InvalidKeySize, InvalidNonceSize, InvalidInputSize, - InvalidOutputBuffer, InvalidTag, - CpuidFailure, } pub type Result = std::result::Result; + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let err_msg = match self { + Error::InvalidKeySize => "invalid AES-GCM-SIV key size", + Error::InvalidNonceSize => "invalid AES-GCM-SIV nonce size", + Error::InvalidInputSize => "invalid AES-GCM-SIV input size", + Error::InvalidTag => "invalid AES-GCM-SIV tag", + }; + + write!(f, "{}", err_msg) + } +} diff --git a/rust/bridge/ffi/Cargo.toml b/rust/bridge/ffi/Cargo.toml index abdc7f3f..db5d2fb8 100644 --- a/rust/bridge/ffi/Cargo.toml +++ b/rust/bridge/ffi/Cargo.toml @@ -16,6 +16,7 @@ crate-type = ["staticlib"] [dependencies] libsignal-protocol-rust = { path = "../../protocol" } +aes-gcm-siv = { path = "../../aes-gcm-siv" } async-trait = "0.1.41" libc = "0.2" futures = "0.3.7" diff --git a/rust/bridge/ffi/cbindgen.toml b/rust/bridge/ffi/cbindgen.toml index c3974e89..9271e0e0 100644 --- a/rust/bridge/ffi/cbindgen.toml +++ b/rust/bridge/ffi/cbindgen.toml @@ -38,7 +38,7 @@ sort_by = "None" [parse] parse_deps = true -include = ["libsignal-protocol-rust"] +include = ["libsignal-protocol-rust", "aes-gcm-siv"] [parse.expand] crates = ["libsignal-ffi"] diff --git a/rust/bridge/ffi/src/lib.rs b/rust/bridge/ffi/src/lib.rs index 13535f8e..fb8347d2 100644 --- a/rust/bridge/ffi/src/lib.rs +++ b/rust/bridge/ffi/src/lib.rs @@ -13,6 +13,8 @@ use static_assertions::const_assert_eq; use std::convert::TryFrom; use std::ffi::{c_void, CString}; +use aes_gcm_siv::Aes256GcmSiv; + mod util; use crate::util::*; @@ -1845,3 +1847,67 @@ pub unsafe extern "C" fn signal_sealed_session_cipher_decrypt( write_bytearray_to(out, out_len, Ok(decrypted.message)) }) } + +#[no_mangle] +pub unsafe extern "C" fn signal_aes256_gcm_siv_new( + obj: *mut *mut Aes256GcmSiv, + key: *const c_uchar, + key_len: size_t) -> *mut SignalFfiError { + + run_ffi_safe(|| { + let key = as_slice(key, key_len)?; + let aes_gcm_siv = aes_gcm_siv::Aes256GcmSiv::new(&key)?; + box_object::(obj, Ok(aes_gcm_siv)) + }) +} + +ffi_fn_destroy!(signal_aes256_gcm_siv_destroy destroys Aes256GcmSiv); + +#[no_mangle] +pub unsafe extern "C" fn signal_aes256_gcm_siv_encrypt( + aes_gcm_siv: *const Aes256GcmSiv, + ctext: *mut *const c_uchar, + ctext_len: *mut size_t, + ptext: *const c_uchar, + ptext_len: size_t, + nonce: *const c_uchar, + nonce_len: size_t, + associated_data: *const c_uchar, + associated_data_len: size_t) -> *mut SignalFfiError { + run_ffi_safe(|| { + let aes_gcm_siv = native_handle_cast::(aes_gcm_siv)?; + let ptext = as_slice(ptext, ptext_len)?; + let nonce = as_slice(nonce, nonce_len)?; + let associated_data = as_slice(associated_data, associated_data_len)?; + + let mut buf = Vec::with_capacity(ptext.len() + 16); + buf.extend_from_slice(ptext); + + let gcm_tag = aes_gcm_siv.encrypt(&mut buf, &nonce, &associated_data)?; + buf.extend_from_slice(&gcm_tag); + + write_bytearray_to(ctext, ctext_len, Ok(buf)) + }) +} + +#[no_mangle] +pub unsafe extern "C" fn signal_aes256_gcm_siv_decrypt( + aes_gcm_siv: *const Aes256GcmSiv, + ptext: *mut *const c_uchar, + ptext_len: *mut size_t, + ctext: *const c_uchar, + ctext_len: size_t, + nonce: *const c_uchar, + nonce_len: size_t, + associated_data: *const c_uchar, + associated_data_len: size_t) -> *mut SignalFfiError { + + run_ffi_safe(|| { + let aes_gcm_siv = native_handle_cast::(aes_gcm_siv)?; + let mut buf = as_slice(ctext, ctext_len)?.to_vec(); + let nonce = as_slice(nonce, nonce_len)?; + let associated_data = as_slice(associated_data, associated_data_len)?; + aes_gcm_siv.decrypt_with_appended_tag(&mut buf, &nonce, &associated_data)?; + write_bytearray_to(ptext, ptext_len, Ok(buf)) + }) +} diff --git a/rust/bridge/ffi/src/util.rs b/rust/bridge/ffi/src/util.rs index 3ed57ae7..fa46cfea 100644 --- a/rust/bridge/ffi/src/util.rs +++ b/rust/bridge/ffi/src/util.rs @@ -12,9 +12,12 @@ use std::fmt; use std::future::Future; use std::task::{self, Poll}; +use aes_gcm_siv::Error as AesGcmSivError; + #[derive(Debug)] pub enum SignalFfiError { Signal(SignalProtocolError), + AesGcmSiv(AesGcmSivError), InsufficientOutputSize(usize, usize), NullPointer, InvalidUtf8String, @@ -92,7 +95,8 @@ impl From<&SignalFfiError> for SignalErrorCode { SignalFfiError::Signal(SignalProtocolError::NoKeyTypeIdentifier) | SignalFfiError::Signal(SignalProtocolError::BadKeyType(_)) - | SignalFfiError::Signal(SignalProtocolError::BadKeyLength(_, _)) => { + | SignalFfiError::Signal(SignalProtocolError::BadKeyLength(_, _)) + | SignalFfiError::AesGcmSiv(AesGcmSivError::InvalidKeySize) => { SignalErrorCode::InvalidKey } @@ -109,7 +113,8 @@ impl From<&SignalFfiError> for SignalErrorCode { } SignalFfiError::Signal(SignalProtocolError::CiphertextMessageTooShort(_)) - | SignalFfiError::Signal(SignalProtocolError::InvalidCiphertext) => { + | SignalFfiError::Signal(SignalProtocolError::InvalidCiphertext) + | SignalFfiError::AesGcmSiv(AesGcmSivError::InvalidTag) => { SignalErrorCode::InvalidCiphertext } @@ -142,7 +147,8 @@ impl From<&SignalFfiError> for SignalErrorCode { SignalErrorCode::InvalidState } - SignalFfiError::Signal(SignalProtocolError::InvalidArgument(_)) => { + SignalFfiError::Signal(SignalProtocolError::InvalidArgument(_)) + | SignalFfiError::AesGcmSiv(_) => { SignalErrorCode::InvalidArgument } @@ -158,6 +164,9 @@ impl fmt::Display for SignalFfiError { SignalFfiError::CallbackError(c) => { write!(f, "callback invocation returned error code {}", c) } + SignalFfiError::AesGcmSiv(c) => { + write!(f, "AES-GCM-SIV operation failed {}", c) + } SignalFfiError::NullPointer => write!(f, "null pointer"), SignalFfiError::InvalidType => write!(f, "invalid type"), SignalFfiError::InvalidUtf8String => write!(f, "invalid UTF8 string"), @@ -179,6 +188,12 @@ impl From for SignalFfiError { } } +impl From for SignalFfiError { + fn from(e: AesGcmSivError) -> SignalFfiError { + SignalFfiError::AesGcmSiv(e) + } +} + pub fn run_ffi_safe Result<(), SignalFfiError> + std::panic::UnwindSafe>( f: F, ) -> *mut SignalFfiError { diff --git a/rust/bridge/jni/Cargo.toml b/rust/bridge/jni/Cargo.toml index 56072a3d..73b62852 100644 --- a/rust/bridge/jni/Cargo.toml +++ b/rust/bridge/jni/Cargo.toml @@ -18,6 +18,7 @@ crate-type = ["cdylib"] [dependencies] libsignal-protocol-rust = { path = "../../protocol" } +aes-gcm-siv = { path = "../../aes-gcm-siv" } async-trait = "0.1.41" futures = "0.3.7" jni = "0.17" diff --git a/rust/bridge/jni/bin/gen_java_decl.py b/rust/bridge/jni/bin/gen_java_decl.py index 9fcc30dc..43136ea6 100755 --- a/rust/bridge/jni/bin/gen_java_decl.py +++ b/rust/bridge/jni/bin/gen_java_decl.py @@ -36,7 +36,7 @@ for l in stderr.split('\n'): if unknown_warning: sys.exit(1) -java_decl = re.compile(r'([a-zA-Z]+) Java_org_signal_client_internal_Native_([A-Z][a-zA-Z]+)_1([A-Za-z0-9]+)\(JNIEnv .?env, JClass class_(, .*)?\);') +java_decl = re.compile(r'([a-zA-Z]+) Java_org_signal_client_internal_Native_([A-Z][a-zA-Z0-9]+)_1([A-Za-z0-9]+)\(JNIEnv .?env, JClass class_(, .*)?\);') def translate_to_java(typ): diff --git a/rust/bridge/jni/src/lib.rs b/rust/bridge/jni/src/lib.rs index abc41f76..9f132146 100644 --- a/rust/bridge/jni/src/lib.rs +++ b/rust/bridge/jni/src/lib.rs @@ -10,9 +10,11 @@ use async_trait::async_trait; use jni::objects::{JClass, JObject, JString, JValue}; use jni::sys::{jboolean, jbyteArray, jint, jlong, jobject, jstring}; use jni::JNIEnv; -use libsignal_protocol_rust::*; use std::convert::TryFrom; +use libsignal_protocol_rust::*; +use aes_gcm_siv::Aes256GcmSiv; + mod util; use crate::util::*; @@ -1930,3 +1932,58 @@ pub unsafe extern "C" fn Java_org_signal_client_internal_Native_SealedSessionCip box_object::(Ok(usmc)) }) } + +#[no_mangle] +pub unsafe extern "C" fn Java_org_signal_client_internal_Native_Aes256GcmSiv_1New( + env: JNIEnv, + _class: JClass, + key: jbyteArray) -> ObjectHandle { + + run_ffi_safe(&env, || { + let key = env.convert_byte_array(key)?; + let aes_gcm_siv = aes_gcm_siv::Aes256GcmSiv::new(&key)?; + box_object::(Ok(aes_gcm_siv)) + }) +} + +jni_fn_destroy!(Java_org_signal_client_internal_Native_Aes256GcmSiv_1Destroy destroys Aes256GcmSiv); + +#[no_mangle] +pub unsafe extern "C" fn Java_org_signal_client_internal_Native_Aes256GcmSiv_1Encrypt( + env: JNIEnv, + _class: JClass, + aes_gcm_siv: ObjectHandle, + msg: jbyteArray, + nonce: jbyteArray, + associated_data: jbyteArray) -> jbyteArray { + + run_ffi_safe(&env, || { + let aes_gcm_siv = native_handle_cast::(aes_gcm_siv)?; + let mut msg = env.convert_byte_array(msg)?; + let nonce = env.convert_byte_array(nonce)?; + let associated_data = env.convert_byte_array(associated_data)?; + let tag = aes_gcm_siv.encrypt(&mut msg, &nonce, &associated_data)?; + msg.extend_from_slice(&tag); + to_jbytearray(&env, Ok(msg)) + }) +} + +#[no_mangle] +pub unsafe extern "C" fn Java_org_signal_client_internal_Native_Aes256GcmSiv_1Decrypt( + env: JNIEnv, + _class: JClass, + aes_gcm_siv: ObjectHandle, + msg: jbyteArray, + nonce: jbyteArray, + associated_data: jbyteArray) -> jbyteArray { + + run_ffi_safe(&env, || { + let aes_gcm_siv = native_handle_cast::(aes_gcm_siv)?; + let mut msg = env.convert_byte_array(msg)?; + let nonce = env.convert_byte_array(nonce)?; + let associated_data = env.convert_byte_array(associated_data)?; + + aes_gcm_siv.decrypt_with_appended_tag(&mut msg, &nonce, &associated_data)?; + to_jbytearray(&env, Ok(msg)) + }) +} diff --git a/rust/bridge/jni/src/util.rs b/rust/bridge/jni/src/util.rs index 2708fe3c..be067b47 100644 --- a/rust/bridge/jni/src/util.rs +++ b/rust/bridge/jni/src/util.rs @@ -8,15 +8,18 @@ use futures::task::noop_waker_ref; use jni::objects::{JObject, JString, JThrowable, JValue}; use jni::sys::{_jobject, jboolean, jbyteArray, jint, jlong, jobject, jstring}; use jni::JNIEnv; -use libsignal_protocol_rust::SignalProtocolError; use std::convert::TryFrom; use std::fmt; use std::future::Future; use std::task::{self, Poll}; +use libsignal_protocol_rust::SignalProtocolError; +use aes_gcm_siv::Error as AesGcmSivError; + #[derive(Debug)] pub enum SignalJniError { Signal(SignalProtocolError), + AesGcmSiv(AesGcmSivError), Jni(jni::errors::Error), BadJniParameter(&'static str), UnexpectedJniResultType(&'static str, &'static str), @@ -43,6 +46,7 @@ impl fmt::Display for SignalJniError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { SignalJniError::Signal(s) => write!(f, "{}", s), + SignalJniError::AesGcmSiv(s) => write!(f, "{}", s), SignalJniError::Jni(s) => write!(f, "JNI error {}", s), SignalJniError::ExceptionDuringCallback(s) => { write!(f, "exception recieved during callback {}", s) @@ -69,6 +73,12 @@ impl From for SignalJniError { } } +impl From for SignalJniError { + fn from(e: AesGcmSivError) -> SignalJniError { + SignalJniError::AesGcmSiv(e) + } +} + impl From for SignalJniError { fn from(e: jni::errors::Error) -> SignalJniError { SignalJniError::Jni(e) @@ -111,7 +121,8 @@ pub fn throw_error(env: &JNIEnv, error: SignalJniError) { SignalJniError::Signal(SignalProtocolError::NoKeyTypeIdentifier) | SignalJniError::Signal(SignalProtocolError::SignatureValidationFailed) | SignalJniError::Signal(SignalProtocolError::BadKeyType(_)) - | SignalJniError::Signal(SignalProtocolError::BadKeyLength(_, _)) => { + | SignalJniError::Signal(SignalProtocolError::BadKeyLength(_, _)) + | SignalJniError::AesGcmSiv(AesGcmSivError::InvalidKeySize) => { "org/whispersystems/libsignal/InvalidKeyException" } @@ -124,7 +135,8 @@ pub fn throw_error(env: &JNIEnv, error: SignalJniError) { | SignalJniError::Signal(SignalProtocolError::UnrecognizedCiphertextVersion(_)) | SignalJniError::Signal(SignalProtocolError::UnrecognizedMessageVersion(_)) | SignalJniError::Signal(SignalProtocolError::InvalidCiphertext) - | SignalJniError::Signal(SignalProtocolError::InvalidProtobufEncoding) => { + | SignalJniError::Signal(SignalProtocolError::InvalidProtobufEncoding) + | SignalJniError::AesGcmSiv(AesGcmSivError::InvalidTag) => { "org/whispersystems/libsignal/InvalidMessageException" } @@ -146,7 +158,8 @@ pub fn throw_error(env: &JNIEnv, error: SignalJniError) { "org/signal/libsignal/metadata/SelfSendException" } - SignalJniError::Signal(SignalProtocolError::InvalidArgument(_)) => { + SignalJniError::Signal(SignalProtocolError::InvalidArgument(_)) + | SignalJniError::AesGcmSiv(_) => { "java/lang/IllegalArgumentException" } diff --git a/swift/Sources/SignalClient/Aes256GcmSiv.swift b/swift/Sources/SignalClient/Aes256GcmSiv.swift new file mode 100644 index 00000000..36e203f9 --- /dev/null +++ b/swift/Sources/SignalClient/Aes256GcmSiv.swift @@ -0,0 +1,77 @@ +// +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import SignalFfi +import Foundation + +public class Aes256GcmSiv: ClonableHandleOwner { + public init(_ bytes: Bytes) throws { + let handle: OpaquePointer? = try bytes.withUnsafeBytes { + var result: OpaquePointer? + try checkError(signal_aes256_gcm_siv_new(&result, $0.baseAddress?.assumingMemoryBound(to: UInt8.self), $0.count)) + return result + } + super.init(owned: handle!) + } + + internal override class func destroyNativeHandle(_ handle: OpaquePointer) { + signal_aes256_gcm_siv_destroy(handle) + } + + public func encrypt( + _ message: MessageBytes, + _ nonce: NonceBytes, + _ associated_data: AssociatedDataBytes) throws -> [UInt8] + where MessageBytes: ContiguousBytes, + NonceBytes: ContiguousBytes, + AssociatedDataBytes: ContiguousBytes { + + try message.withUnsafeBytes { messageBytes in + try nonce.withUnsafeBytes { nonceBytes in + try associated_data.withUnsafeBytes { adBytes in + try invokeFnReturningArray { + signal_aes256_gcm_siv_encrypt(nativeHandle, + $0, + $1, + messageBytes.baseAddress?.assumingMemoryBound(to: UInt8.self), + messageBytes.count, + nonceBytes.baseAddress?.assumingMemoryBound(to: UInt8.self), + nonceBytes.count, + adBytes.baseAddress?.assumingMemoryBound(to: UInt8.self), + adBytes.count) + } + } + } + } + } + + public func decrypt ( + _ message: MessageBytes, + _ nonce: NonceBytes, + _ associated_data: AssociatedDataBytes) throws -> [UInt8] + where MessageBytes: ContiguousBytes, + NonceBytes: ContiguousBytes, + AssociatedDataBytes: ContiguousBytes { + + try message.withUnsafeBytes { messageBytes in + try nonce.withUnsafeBytes { nonceBytes in + try associated_data.withUnsafeBytes { adBytes in + try invokeFnReturningArray { + signal_aes256_gcm_siv_decrypt(nativeHandle, + $0, + $1, + messageBytes.baseAddress?.assumingMemoryBound(to: UInt8.self), + messageBytes.count, + nonceBytes.baseAddress?.assumingMemoryBound(to: UInt8.self), + nonceBytes.count, + adBytes.baseAddress?.assumingMemoryBound(to: UInt8.self), + adBytes.count) + } + } + } + } + } + +} diff --git a/swift/Tests/SignalClientTests/PublicAPITests.swift b/swift/Tests/SignalClientTests/PublicAPITests.swift index 76ed5cd0..f0ab3dfb 100644 --- a/swift/Tests/SignalClientTests/PublicAPITests.swift +++ b/swift/Tests/SignalClientTests/PublicAPITests.swift @@ -52,6 +52,26 @@ class PublicAPITests: XCTestCase { XCTAssertEqual(derived, okm) } + func testAesGcmSiv() { + let ptext: [UInt8] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + let expected_ctext: [UInt8] = [0x1d, 0xe2, 0x29, 0x67, 0x23, 0x7a, 0x81, 0x32, 0x91, 0x21, 0x3f, 0x26, 0x7e, 0x3b, 0x45, 0x2f, 0x02, 0xd0, 0x1a, 0xe3, 0x3e, 0x4e, 0xc8, 0x54] + let ad: [UInt8] = [0x01] + let key: [UInt8] = [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + let nonce: [UInt8] = [0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + + let gcm_siv = try! Aes256GcmSiv(key) + + let ctext = try! gcm_siv.encrypt(ptext, nonce, ad) + XCTAssertEqual(ctext, expected_ctext) + + let recovered = try! gcm_siv.decrypt(ctext, nonce, ad) + XCTAssertEqual(recovered, ptext) + + XCTAssertThrowsError(try gcm_siv.decrypt(ptext, nonce, ad)) + XCTAssertThrowsError(try gcm_siv.decrypt(ctext, ad, nonce)) + } + func testAddress() { let addr = try! ProtocolAddress(name: "addr1", deviceId: 5) XCTAssertEqual(addr.name, "addr1") @@ -277,6 +297,7 @@ class PublicAPITests: XCTestCase { ("testPkOperations", testPkOperations), ("testHkdfSimple", testHkdfSimple), ("testHkdfUsingRFCExample", testHkdfUsingRFCExample), + ("testAesGcmSiv", testAesGcmSiv), ("testGroupCipher", testGroupCipher), ("testSenderCertifications", testSenderCertificates), ]