blob: 538a44e760bda97a905219a8781a56a7fda9eeb6 [file] [log] [blame]
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! JNI adapter for LDT.
//!
//! Helpful resources:
//! - <https://developer.ibm.com/articles/j-jni>
//! - <https://developer.android.com/training/articles/perf-jni>
//! - <https://www.iitk.ac.in/esc101/05Aug/tutorial/native1.1/index.html>
//! - <https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html>
// We are not actually no_std because the jni crate is pulling it in, but at least this enforces
// that this lib isn't using anything from the std lib
#![no_std]
#![deny(missing_docs)]
// Allow using Box in no_std
extern crate alloc;
use alloc::boxed::Box;
use jni::{
objects::{JByteArray, JClass},
sys::{jbyte, jchar, jint, jlong},
JNIEnv,
};
use ldt::XorPadder;
use ldt_np_adv::{LdtAdvDecryptError, LdtEncrypterXtsAes128, LdtNpAdvDecrypterXtsAes128};
use np_hkdf::NpKeySeedHkdf;
use crypto_provider_default::CryptoProviderImpl;
/// Length limits per LDT
const MIN_DATA_LEN: usize = crypto_provider::aes::BLOCK_SIZE;
const MAX_DATA_LEN: usize = crypto_provider::aes::BLOCK_SIZE * 2 - 1;
/// Required size constraints of input parameters
const KEY_SEED_SIZE: usize = 32;
const TAG_SIZE: usize = 32;
/// Error return value for create operations
const CREATE_ERROR: jlong = 0;
/// Status code returned on successful cipher operations
const SUCCESS: jint = 0;
type LdtAdvDecrypter = LdtNpAdvDecrypterXtsAes128<CryptoProviderImpl>;
type LdtAdvEncrypter = LdtEncrypterXtsAes128<CryptoProviderImpl>;
/// Marker trait to ensure above types are thread safe
trait JniThreadSafe: Send + Sync {}
impl JniThreadSafe for LdtAdvDecrypter {}
impl JniThreadSafe for LdtAdvEncrypter {}
/// Create a LDT Encryption cipher.
///
/// Returns 0 on failure, or the non-zero handle as a jlong/i64 on success.
#[no_mangle]
extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_createEncryptionCipher(
env: JNIEnv,
_class: JClass,
java_key_seed: JByteArray,
) -> jlong {
create_map_to_error(|| {
let key_seed =
env.convert_byte_array(&java_key_seed).map_err(|_| CREATE_ERROR).and_then(|seed| {
if seed.len() != KEY_SEED_SIZE {
Err(CREATE_ERROR)
} else {
Ok(seed)
}
})?;
let hkdf_key_seed = NpKeySeedHkdf::<CryptoProviderImpl>::new(
key_seed.as_slice().try_into().expect("Length is checked above"),
);
let cipher = LdtAdvEncrypter::new(&hkdf_key_seed.legacy_ldt_key());
box_to_handle(cipher).map_err(|_| CREATE_ERROR)
})
}
/// Create a LDT Decryption cipher.
///
/// Returns 0 on failure, or the non-zero handle as a jlong/i64 on success.
#[no_mangle]
extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_createDecryptionCipher(
env: JNIEnv,
_class: JClass,
java_key_seed: JByteArray,
java_hmac_tag: JByteArray,
) -> jlong {
create_map_to_error(|| {
let key_seed =
env.convert_byte_array(&java_key_seed).map_err(|_| CREATE_ERROR).and_then(|seed| {
if seed.len() != KEY_SEED_SIZE {
Err(CREATE_ERROR)
} else {
Ok(seed)
}
})?;
let hmac_tag =
env.convert_byte_array(&java_hmac_tag).map_err(|_| CREATE_ERROR).and_then(|tag| {
if tag.len() != TAG_SIZE {
Err(CREATE_ERROR)
} else {
Ok(tag)
}
})?;
let hkdf_key_seed = NpKeySeedHkdf::<CryptoProviderImpl>::new(
key_seed.as_slice().try_into().expect("Length is checked above"),
);
let cipher = ldt_np_adv::build_np_adv_decrypter_from_key_seed::<CryptoProviderImpl>(
&hkdf_key_seed,
hmac_tag.as_slice().try_into().expect("Length is checked above"),
);
box_to_handle(cipher).map_err(|_| CREATE_ERROR)
})
}
fn create_map_to_error<F: Fn() -> Result<jlong, jlong>>(f: F) -> jlong {
f().unwrap_or_else(|e| e)
}
/// Close an LDT Encryption Cipher
#[no_mangle]
extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_closeEncryptCipher(
_env: JNIEnv,
_class: JClass,
handle: jlong,
) {
// create the box, let it be dropped
let _ = boxed_from_handle::<LdtAdvEncrypter>(handle);
}
/// Close an LDT Decryption Cipher
#[no_mangle]
extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_closeDecryptCipher(
_env: JNIEnv,
_class: JClass,
handle: jlong,
) {
// create the box, let it be dropped
let _ = boxed_from_handle::<LdtAdvDecrypter>(handle);
}
/// Encrypt a buffer in place.
#[no_mangle]
extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_encrypt(
env: JNIEnv,
_class: JClass,
handle: jlong,
salt: jchar,
data: JByteArray,
) -> jint {
map_to_error_code(|| {
let mut buffer =
env.convert_byte_array(&data).map_err(|_| EncryptError::JniOp).and_then(|data| {
if !(MIN_DATA_LEN..=MAX_DATA_LEN).contains(&data.len()) {
Err(EncryptError::DataLen)
} else {
Ok(data)
}
})?;
with_handle::<LdtAdvEncrypter, _, _>(handle, |cipher| {
cipher.encrypt(buffer.as_mut_slice(), &expand_np_salt_to_padder(salt)).map_err(
|err| match err {
ldt::LdtError::InvalidLength(_) => EncryptError::DataLen,
},
)?;
// Avoid a copy since transmuting from a &[u8] to a &[i8] is safe
// Safety:
// - u8 and jbyte/i8 are the same size have the same alignment
let jbyte_buffer = bytes_to_jbytes(buffer.as_slice());
env.set_byte_array_region(&data, 0, jbyte_buffer)
.map_err(|_| EncryptError::JniOp)
.map(|_| SUCCESS)
})
})
}
/// Decrypt a buffer in place.
/// Safety: We know the data pointer is safe because it is coming directly from the JVM.
#[no_mangle]
extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_decryptAndVerify(
env: JNIEnv,
_class: JClass,
handle: jlong,
salt: jchar,
data: JByteArray,
) -> jint {
map_to_error_code(|| {
let mut buffer =
env.convert_byte_array(&data).map_err(|_| DecryptError::JniOp).and_then(|data| {
if !(MIN_DATA_LEN..=MAX_DATA_LEN).contains(&data.len()) {
Err(DecryptError::DataLen)
} else {
Ok(data)
}
})?;
with_handle::<LdtAdvDecrypter, _, _>(handle, |cipher| {
let result = cipher
.decrypt_and_verify(buffer.as_mut_slice(), &expand_np_salt_to_padder(salt))
.map_err(|err| match err {
LdtAdvDecryptError::InvalidLength(_) => DecryptError::DataLen,
LdtAdvDecryptError::MacMismatch => DecryptError::MacMisMatch,
})?;
let jbyte_buffer = bytes_to_jbytes(result.as_slice());
env.set_byte_array_region(&data, 0, jbyte_buffer)
.map_err(|_| DecryptError::JniOp)
.map(|_| SUCCESS)
})
})
}
/// A zero-copy conversion from a u8 slice to a jbyte slice
fn bytes_to_jbytes(bytes: &[u8]) -> &[jbyte] {
// Safety:
// - u8 and jbyte/i8 are the same size have the same alignment
unsafe { alloc::slice::from_raw_parts(bytes.as_ptr() as *const jbyte, bytes.len()) }
}
/// Reconstruct a `Box<T>` from `handle`, and invoke `f` with the resulting `&T`.
///
/// The `Box<T>` is leaked after invoking `block` rather than dropped so that the handle can be used
/// again.
///
/// Returns the result of evaluating `f`.
fn with_handle<T, U, F: FnMut(&T) -> U>(handle: jlong, mut f: F) -> U {
let boxed = boxed_from_handle(handle);
let ret = f(&boxed);
// don't consume the box -- need to keep the handle alive
Box::leak(boxed);
ret
}
/// Reconstruct a `Box<T>` from `handle`.
///
/// `handle` must be an aligned, non-null `jlong` representation of a pointer produced from
/// `Box::into_raw` that has not yet been deallocated.
fn boxed_from_handle<T>(handle: jlong) -> Box<T> {
// on 32-bit systems, truncate i64 to low 32 bits (which should be the only bits that were set
// when the jlong handle was created).
let handle_usize = handle as usize;
// convert pointer-sized integer to pointer
unsafe { Box::from_raw(handle_usize as *mut _) }
}
/// Constructs a `Box<T>`, leaks a pointer to it, and converts the pointer to `jlong`.
///
/// If the pointer can't fit, `Err` is returned.
fn box_to_handle<T>(thing: T) -> Result<jlong, ()> {
// Box::new heap allocates space for the thing
// Box::into_raw intentionally leaks into an aligned, non-null pointer
let pointer = Box::into_raw(Box::new(thing));
// As a best practice, cast from pointer to usize because usize is always pointer sized, so the
// cast is easy to reason about.
// https://doc.rust-lang.org/reference/expressions/operator-expr.html#pointer-to-address-cast
let ptr_usize = pointer as usize;
// Fallible conversion into a u64 -- eventually 128 bit pointer types will fail here.
// Assuming it fits, integer cast should be either no conversion or zero extension.
ptr_usize
.try_into()
.map_err(|_| {
// resuscitate the Box so that its drop can run, otherwise we would leak on error
unsafe {
let _ = Box::from_raw(pointer);
}
})
// Now that we know the pointer fits in 64 bits, can cast u64 to i64/jlong.
.map(|ptr_64: u64| ptr_64 as jlong)
}
/// Expand the NP salt to the size needed to be an LDT XorPadder.
///
/// Returns a XorPadder containing the HKDF of the salt.
fn expand_np_salt_to_padder(np_salt: jchar) -> XorPadder<{ crypto_provider::aes::BLOCK_SIZE }> {
let salt_bytes = np_salt.to_be_bytes();
ldt_np_adv::salt_padder::<16, CryptoProviderImpl>(salt_bytes.into())
}
fn map_to_error_code<E: JniError, F: Fn() -> Result<jint, E>>(f: F) -> jint {
f().unwrap_or_else(|e| e.to_jni_error_code())
}
trait JniError {
fn to_jni_error_code(&self) -> jint;
}
#[derive(Debug)]
enum EncryptError {
/// Data is the wrong length
DataLen,
/// JNI op failed
JniOp,
}
impl JniError for EncryptError {
fn to_jni_error_code(&self) -> jint {
match self {
Self::DataLen => -1,
Self::JniOp => -2,
}
}
}
#[derive(Debug)]
enum DecryptError {
/// Data is the wrong length
DataLen,
/// The mac did not match the provided tag
MacMisMatch,
/// JNI op failed
JniOp,
}
impl JniError for DecryptError {
/// Returns an error code suitable for returning from Ldt encrypt/decrypt JNI calls.
fn to_jni_error_code(&self) -> jint {
match self {
Self::DataLen => -1,
Self::JniOp => -2,
Self::MacMisMatch => -3,
}
}
}