Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Secure Cell passphrase API: JavaThemis #635

Merged
merged 4 commits into from
May 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,51 @@ _Code:_
| New API | Old API |
| ------- | ------- |
| `SecureCell.SealWithKey(key)` | `new SecureCell(key, SecureCell.MODE_SEAL)` |
| `SecureCell.SealWithPassphrase(passphrase)` | _not available_ |
| `SecureCell.TokenProtectWithKey(key)` | `new SecureCell(key, SecureCell.MODE_TOKEN_PROTECT)` |
| `SecureCell.ContextImprintWithKey(key)` | `new SecureCell(key, SecureCell.MODE_CONTEXT_IMPRINT)` |

- JavaThemis now supports _passphrase API_ of in Seal mode
([#635](https://github.com/cossacklabs/themis/pull/635)).

In Kotlin:

```kotlin
import com.cossacklabs.themis.SecureCell

val cell = SecureCell.SealWithPassphrase("secret")

val message = "message".toByteArray()

val encrypted = cell.encrypt(message)
val decrypted = cell.decrypt(encrypted)

assertArrayEquals(decrypted, message)
```

In Java:

```java
import com.cossacklabs.themis.SecureCell;

SecureCell.Seal cell = SecureCell.SealWithPassphrase("secret");

byte[] message = "message".getBytes(StandardCharsets.UTF_8);

byte[] encrypted = cell.encrypt(message);
byte[] decrypted = cell.decrypt(encrypted);

assertArrayEquals(decrypted, message);
```

You can safely and securely use short, human-readable passphrases as strings with this new API.

Existing symmetric key API (`SecureCell.SealWithKey(...)` or `new SecureCell(...)`)
should not be used with passphrases or passwords.
Use symmetric key API with symmetric encryption keys,
such as generated by `SymmetricKey` ([#565](https://github.com/cossacklabs/themis/pull/565)).
Use passphrase API with human-readable passphrases.

- **Node.js**

- New class `SymmetricKey` can be used to generate symmetric keys for Secure Cell ([#562](https://github.com/cossacklabs/themis/pull/562)).
Expand Down
45 changes: 45 additions & 0 deletions jni/themis_cell.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#define MODE_SEAL 0
#define MODE_TOKEN_PROTECT 1
#define MODE_CONTEXT_IMPRINT 2
#define MODE_SEAL_PASSPHRASE 3

JNIEXPORT jobjectArray JNICALL Java_com_cossacklabs_themis_SecureCell_encrypt(
JNIEnv* env, jobject thiz, jbyteArray key, jbyteArray context, jbyteArray data, jint mode)
Expand Down Expand Up @@ -80,6 +81,17 @@ JNIEXPORT jobjectArray JNICALL Java_com_cossacklabs_themis_SecureCell_encrypt(
NULL,
&encrypted_data_length);
break;
case MODE_SEAL_PASSPHRASE:
/* Passphrase bytes passed as key */
res = themis_secure_cell_encrypt_seal_with_passphrase((uint8_t*)key_buf,
key_length,
(uint8_t*)context_buf,
context_length,
(uint8_t*)data_buf,
data_length,
NULL,
&encrypted_data_length);
break;
case MODE_TOKEN_PROTECT:
res = themis_secure_cell_encrypt_token_protect((uint8_t*)key_buf,
key_length,
Expand Down Expand Up @@ -150,6 +162,17 @@ JNIEXPORT jobjectArray JNICALL Java_com_cossacklabs_themis_SecureCell_encrypt(
(uint8_t*)encrypted_data_buf,
&encrypted_data_length);
break;
case MODE_SEAL_PASSPHRASE:
/* Passphrase bytes passed as key */
res = themis_secure_cell_encrypt_seal_with_passphrase((uint8_t*)key_buf,
key_length,
(uint8_t*)context_buf,
context_length,
(uint8_t*)data_buf,
data_length,
(uint8_t*)encrypted_data_buf,
&encrypted_data_length);
break;
case MODE_TOKEN_PROTECT:
res = themis_secure_cell_encrypt_token_protect((uint8_t*)key_buf,
key_length,
Expand Down Expand Up @@ -297,6 +320,17 @@ JNIEXPORT jbyteArray JNICALL Java_com_cossacklabs_themis_SecureCell_decrypt(
NULL,
&data_length);
break;
case MODE_SEAL_PASSPHRASE:
/* Passphrase bytes passed as key */
res = themis_secure_cell_decrypt_seal_with_passphrase((uint8_t*)key_buf,
key_length,
(uint8_t*)context_buf,
context_length,
(uint8_t*)encrypted_data_buf,
encrypted_data_length,
NULL,
&data_length);
break;
case MODE_TOKEN_PROTECT:
if (!additional_data_buf) {
/* Additional data is mandatory for this mode */
Expand Down Expand Up @@ -358,6 +392,17 @@ JNIEXPORT jbyteArray JNICALL Java_com_cossacklabs_themis_SecureCell_decrypt(
(uint8_t*)data_buf,
&data_length);
break;
case MODE_SEAL_PASSPHRASE:
/* Passphrase bytes passed as key */
res = themis_secure_cell_decrypt_seal_with_passphrase((uint8_t*)key_buf,
key_length,
(uint8_t*)context_buf,
context_length,
(uint8_t*)encrypted_data_buf,
encrypted_data_length,
(uint8_t*)data_buf,
&data_length);
break;
case MODE_TOKEN_PROTECT:
if (!additional_data_buf) {
/* Additional data is mandatory for this mode */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2020 Cossack Labs Limited
*
* 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.
*/

package com.cossacklabs.themis;

import org.jetbrains.annotations.NotNull;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;

class PassphraseBytes extends KeyBytes {

PassphraseBytes(@NotNull byte[] encodedPassphrase) {
super(encodedPassphrase);
}

PassphraseBytes(@NotNull String passphrase, @NotNull Charset charset) {
super(encodePassphrase(passphrase, charset));
}

@NotNull
private static byte[] encodePassphrase(String passphrase, Charset charset) {
if (passphrase == null) {
throw new NullArgumentException("passphrase cannot be null");
}
if (charset == null) {
throw new NullArgumentException("charset cannot be null");
}
// Use strict encoding. Report errors instead of doing silly conversions
// that String#getBytes(Charset) does by default.
final CharsetEncoder encoder = charset.newEncoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
try {
CharBuffer inputBuffer = CharBuffer.wrap(passphrase);
ByteBuffer outputBuffer = encoder.encode(inputBuffer);
// Avoid allocations if the backing storage array is okay.
if (outputBuffer.hasArray() && outputBuffer.arrayOffset() == 0) {
byte[] backingStorage = outputBuffer.array();
if (backingStorage.length == outputBuffer.remaining()) {
return backingStorage;
}
}
byte[] result = new byte[outputBuffer.remaining()];
outputBuffer.get(result);
return result;
}
catch (CharacterCodingException e) {
throw new RuntimeException("failed to encode passphrase in " + charset, e);
}
}
}
62 changes: 62 additions & 0 deletions src/wrappers/themis/java/com/cossacklabs/themis/SecureCell.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

Expand Down Expand Up @@ -267,6 +268,66 @@ public static Seal SealWithKey(byte[] key) {
return new SecureCellSeal(new SymmetricKey(key));
}

/**
* Makes a new Secure Cell in Seal mode secured by passphrase.
* <p>
* The passphrase will be encoded in UTF-8 for compatibility with other platforms.
* Use {@link #SealWithPassphrase(String, Charset)} if you need other encoding.
*
* @param passphrase passphrase to use
*
* @return a new {@link SecureCell.Seal} instance
* @throws NullArgumentException if passphrase is null
* @throws InvalidArgumentException if passphrase is empty
*
* @since JavaThemis 0.13
*/
@NotNull
@Contract(value = "_ -> new", pure = true)
public static Seal SealWithPassphrase(String passphrase) {
return new SecureCellSealWithPassphrase(passphrase, StandardCharsets.UTF_8);
}

/**
* Makes a new Secure Cell in Seal mode secured by passphrase.
*
* @param passphrase passphrase to use
* @param charset how to encode the passphrase
*
* @return a new {@link SecureCell.Seal} instance
* @throws NullArgumentException if passphrase is null
* @throws InvalidArgumentException if passphrase is empty
* @throws RuntimeException if passphrase cannot be encoded in given encoding
* without data loss ({@link CharacterCodingException is the cause})
*
* @since JavaThemis 0.13
*/
@NotNull
@Contract(value = "_, _ -> new", pure = true)
public static Seal SealWithPassphrase(String passphrase, Charset charset) {
return new SecureCellSealWithPassphrase(passphrase, charset);
}

/**
* Makes a new Secure Cell in Seal mode secured by passphrase.
* <p>
* Use this method if you need a custom encoding of the passphrase,
* already have it encoded, or use a binary passphrase.
*
* @param encodedPassphrase passphrase bytes to use
*
* @return a new {@link SecureCell.Seal} instance
* @throws NullArgumentException if passphrase is null
* @throws InvalidArgumentException if passphrase is empty
*
* @since JavaThemis 0.13
*/
@NotNull
@Contract(value = "_ -> new", pure = true)
public static Seal SealWithPassphrase(byte[] encodedPassphrase) {
return new SecureCellSealWithPassphrase(encodedPassphrase);
}

/**
* Secure Cell in <em>Token Protect</em> operation mode.
* <p>
Expand Down Expand Up @@ -599,6 +660,7 @@ public SecureCell(String password, int mode) {
public static final int MODE_SEAL = 0;
public static final int MODE_TOKEN_PROTECT = 1;
public static final int MODE_CONTEXT_IMPRINT = 2;
static final int MODE_SEAL_PASSPHRASE = 3;

static native byte[][] encrypt(byte[] key, byte[] context, byte[] data, int mode);
static native byte[] decrypt(byte[] key, byte[] context, byte[][] protectedData, int mode);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2020 Cossack Labs Limited
*
* 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.
*/

package com.cossacklabs.themis;

import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.nio.charset.Charset;

class SecureCellSealWithPassphrase implements SecureCell.Seal {

@NotNull
private final PassphraseBytes passphrase;

@Contract(pure = true)
SecureCellSealWithPassphrase(@NotNull String passphrase, @NotNull Charset charset) {
this.passphrase = new PassphraseBytes(passphrase, charset);
}

@Contract(pure = true)
SecureCellSealWithPassphrase(@NotNull byte[] encodedPassphrase) {
this.passphrase = new PassphraseBytes(encodedPassphrase);
}

@NotNull
@Override
public byte[] encrypt(byte[] data, @Nullable byte[] context) {
if (data == null) {
throw new NullArgumentException("data cannot be null");
}
if (data.length == 0) {
throw new InvalidArgumentException("data cannot be empty");
}
byte[] passphraseBytes = this.passphrase.key;
byte[][] result = SecureCell.encrypt(passphraseBytes, context, data, SecureCell.MODE_SEAL_PASSPHRASE);
// TODO(ilammy, 2020-05-05): teach SecureCell#encrypt to throw SecureCellException (T1605)
if (result == null) {
throw new RuntimeException(new SecureCellException());
}
return result[0];
}

@NotNull
@Override
public byte[] encrypt(byte[] data) {
return encrypt(data, null);
}

@NotNull
@Override
public byte[] decrypt(byte[] data, @Nullable byte[] context) throws SecureCellException {
if (data == null) {
throw new NullArgumentException("data cannot be null");
}
if (data.length == 0) {
throw new InvalidArgumentException("data cannot be empty");
}
byte[] passphraseBytes = this.passphrase.key;
byte[][] encrypted = {data, null};
byte[] result = SecureCell.decrypt(passphraseBytes, context, encrypted, SecureCell.MODE_SEAL_PASSPHRASE);
// TODO(ilammy, 2020-05-05): teach SecureCell#decrypt to throw SecureCellException (T1605)
if (result == null) {
throw new SecureCellException();
}
return result;
}

@NotNull
@Override
public byte[] decrypt(byte[] data) throws SecureCellException {
return decrypt(data, null);
}
}
Loading