All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tozny.e3db.PlainCrypto Maven / Gradle / Ivy

There is a newer version: 7.2.3
Show newest version
/*
 * TOZNY NON-COMMERCIAL LICENSE
 *
 * Tozny dual licenses this product. For commercial use, please contact
 * [email protected]. For non-commercial use, the contents of this file are
 * subject to the TOZNY NON-COMMERCIAL LICENSE (the "License") which
 * permits use of the software only by government agencies, schools,
 * universities, non-profit organizations or individuals on projects that
 * do not receive external funding other than government research grants
 * and contracts.  Any other use requires a commercial license. You may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at https://tozny.com/legal/non-commercial-license.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations under
 * the License. Portions of the software are Copyright (c) TOZNY LLC, 2018.
 * All rights reserved.
 *
 */

package com.tozny.e3db;

import com.goterl.lazycode.lazysodium.LazySodium;
import com.goterl.lazycode.lazysodium.LazySodiumJava;
import com.goterl.lazycode.lazysodium.SodiumJava;
import com.goterl.lazycode.lazysodium.exceptions.SodiumException;
import com.goterl.lazycode.lazysodium.interfaces.Box;
import com.goterl.lazycode.lazysodium.interfaces.SecretBox;
import com.goterl.lazycode.lazysodium.interfaces.SecretStream;
import com.goterl.lazycode.lazysodium.interfaces.Sign;
import com.tozny.e3db.crypto.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import static com.tozny.e3db.Checks.*;

class PlainCrypto implements Crypto {
  private static final Charset UTF8 = StandardCharsets.UTF_8;
  private static final int BLOCK_SIZE = 65_536;
  private static final int SECRET_STREAM_TAG_MESSAGE = 0x0;
  private final LazySodium lazySodium;

  public PlainCrypto() {
    lazySodium = new LazySodiumJava(Init.sodium);
  }

  private static class Init {
    // Static inner class as a singleton to make sure
    // Sodium library is initalized once and only once.
    public static final SodiumJava sodium;
    static {
      try {
        sodium = new SodiumJava();
      }
      catch(Throwable ex) {
        ex.printStackTrace();
        throw new RuntimeException(ex);
      }
    }
  }

  @Override
  public CipherSuite suite() {
    return CipherSuite.Sodium;
  }

  @Override
  public CipherWithNonce encryptSecretBox(byte[] message, byte[] key) throws E3DBEncryptionException {
    checkNotNull(message, "message");
    checkNotEmpty(key, "key");
    byte[] nonce = lazySodium.randomBytesBuf(SecretBox.NONCEBYTES);
    byte[] cipher = new byte[SecretBox.MACBYTES + message.length];
    if(! lazySodium.cryptoSecretBoxEasy(cipher, message, message.length, nonce, key))
      throw new E3DBEncryptionException("Could not encrypt message.");

    return new CipherWithNonce(cipher, nonce);
  }

  @Override
  public byte[] decryptSecretBox(CipherWithNonce message, byte[] key) throws E3DBDecryptionException {
    checkNotNull(message, "message");
    checkNotEmpty(key, "key");
    byte[] messageBytes = new byte[message.getCipher().length - SecretBox.MACBYTES];
    if(! lazySodium.cryptoSecretBoxOpenEasy(messageBytes, message.getCipher(), message.getCipher().length, message.getNonce(), key))
      throw new E3DBDecryptionException("Could not decrypt message.");
    return messageBytes;
  }

  @Override
  public CipherWithNonce encryptBox(byte[] message, byte[] publicKey, byte[] privateKey) throws E3DBEncryptionException {
    checkNotNull(message, "message");
    checkNotEmpty(publicKey, "publicKey");
    checkNotEmpty(privateKey, "privateKey");

    byte[] nonce = lazySodium.randomBytesBuf(Box.NONCEBYTES);
    byte[] cipher = new byte[Box.MACBYTES + message.length];
    if(! lazySodium.cryptoBoxEasy(cipher, message, message.length, nonce, publicKey, privateKey))
      throw new E3DBEncryptionException("Unable to encrypt message.");

    return new CipherWithNonce(cipher, nonce);
  }

  @Override
  public byte[] decryptBox(CipherWithNonce message, byte[] publicKey, byte[] privateKey) throws E3DBDecryptionException {
    checkNotNull(message, "message");
    checkNotNull(publicKey, "publicKey");
    checkNotNull(privateKey, "privateKey");
    byte[] messageBytes = new byte[message.getCipher().length - Box.MACBYTES];
    if(! lazySodium.cryptoBoxOpenEasy(messageBytes, message.getCipher(), message.getCipher().length, message.getNonce(), publicKey, privateKey))
      throw new E3DBDecryptionException("Could not decrypt message.");

    return messageBytes;
  }

  @Override
  public byte[] newPrivateKey() throws E3DBCryptoException {
    try {
      return lazySodium.cryptoBoxKeypair().getSecretKey();
    } catch (SodiumException e) {
      throw new E3DBCryptoException(e);
    }
  }

  @Override
  public byte[] getPublicKey(byte[] privateKey) throws E3DBCryptoException {
    checkNotEmpty(privateKey, "privateKey");
    try {
      return lazySodium.cryptoScalarMultBase(privateKey).getPublicKey();
    } catch (SodiumException e) {
      throw new E3DBCryptoException(e);
    }
  }

  @Override
  public byte[] newSecretKey() {
    byte[] secretKey = new byte[SecretBox.KEYBYTES];
    lazySodium.cryptoSecretBoxKeygen(secretKey);
    return secretKey;
  }

  @Override
  public byte[] signature(byte[] message, byte[] signingKey) throws E3DBCryptoException {
    byte[] signatureBytes = new byte[Sign.BYTES];

    if(! lazySodium.cryptoSignDetached(signatureBytes, new long[]{0}, message, message.length, signingKey))
      throw new E3DBCryptoException("Unable to sign document.");

    return signatureBytes;
  }

  @Override
  public boolean verify(Signature signature, byte[] message, byte[] publicSigningKey) {
    checkNotNull(signature, "signature");
    checkNotNull(message, "message");
    checkNotNull(publicSigningKey, "publicSigningKey");

    return lazySodium.cryptoSignVerifyDetached(signature.bytes, message, message.length, publicSigningKey);
  }

  @Override
  public byte[] newPrivateSigningKey() throws E3DBCryptoException {
    try {
      return lazySodium.cryptoSignKeypair().getSecretKey();
    } catch (SodiumException e) {
      throw new E3DBCryptoException(e);
    }
  }

  @Override
  public byte[] getPublicSigningKey(byte[] privateKey) throws E3DBCryptoException {
    try {
      return lazySodium.cryptoSignSecretKeyPair(privateKey).getPublicKey();
    } catch (SodiumException e) {
      throw new E3DBCryptoException(e);
    }
  }

  @Override
  public File encryptFile(File file, byte[] secretKey) throws IOException, E3DBCryptoException {
    byte[] dataKey = new byte[SecretStream.KEYBYTES];
    lazySodium.cryptoSecretStreamKeygen(dataKey);
    byte[] header = new byte[SecretStream.HEADERBYTES];
    SecretStream.State state = new SecretStream.State();
    if(! lazySodium.cryptoSecretStreamInitPush(state, header, dataKey)) {
      throw new E3DBCryptoException("Error initializing encrypt operation.");
    }

    CipherWithNonce edk = encryptSecretBox(dataKey, secretKey);
    String version = "3";
    String e3dbHeader = new StringBuilder()
                            .append(version)
                            .append(".")
                            .append(edk.toMessage())
                            .append(".")
                            .toString();

    File encryptedFile = File.createTempFile("e2e-", ".bin", new File(file.getParent()));
    try (FileOutputStream out = new FileOutputStream(encryptedFile); FileInputStream source = new FileInputStream(file)) {
      out.write(e3dbHeader.getBytes(UTF8));
      out.write(header);

      // Simulate a 2-element queue, which makes it pretty
      // easy to detect EOF.
      byte[] head = new byte[BLOCK_SIZE]; // 2^16
      byte[] next = new byte[head.length];
      byte[] cipher = new byte[head.length + SecretStream.ABYTES];

      for (int headAmt = source.read(head), nextAmt = source.read(next);
           headAmt != -1;
           headAmt = nextAmt, head = next, nextAmt = source.read(next)) {

        byte messageTag = nextAmt != -1 ? SECRET_STREAM_TAG_MESSAGE : SecretStream.TAG_FINAL;
        if(! lazySodium.cryptoSecretStreamPush(state, cipher, head, headAmt, messageTag))
          throw new E3DBCryptoException("Error encrypting file.");

        out.write(cipher, 0, headAmt + SecretStream.ABYTES);
      }
    }

    return encryptedFile;
  }

  @Override
  public void decryptFile(File encrypted, byte[] secretKey, File dest) throws IOException, E3DBCryptoException {
    try (FileOutputStream out = new FileOutputStream(dest, false); FileInputStream in = new FileInputStream(encrypted)) {
      // Read version
      FileVersion v = FileVersion.fromValue(new String(new byte[]{(byte) in.read()}, UTF8));
      if(v != FileVersion.CORRECTED_MESSAGE_TAG && v != FileVersion.INITIAL)
        throw new E3DBCryptoException("Unknown file version: " + v);

      in.read(); // "."

      // Read EDK/EDKn
      byte[] dataKey;
      {
        int separators = 0;
        StringBuilder b = new StringBuilder();
        while (separators < 2) {
          String c = new String(new byte[]{(byte) in.read()}, UTF8);
          if (c.equalsIgnoreCase("."))
            separators++;

          if (separators < 2)
            b.append(c);
        }

        dataKey = decryptSecretBox(CipherWithNonce.decode(b.toString()), secretKey);
      }

      // Read Header
      byte[] header = new byte[SecretStream.HEADERBYTES];
      in.read(header);

      // Decrypt
      {
        SecretStream.State state = new SecretStream.State();
        if(! lazySodium.cryptoSecretStreamInitPull(state, header, dataKey))
          throw new E3DBCryptoException("Error initializing decryption operation.");

        byte[] cipherBlock = new byte[BLOCK_SIZE + SecretStream.ABYTES];
        byte[] tag = new byte[1];
        boolean sawFinal = false;

        for (int cipherAmt = in.read(cipherBlock); cipherAmt != -1; cipherAmt = in.read(cipherBlock)) {
          if(sawFinal)
            throw new E3DBCryptoException("Unexpected trailing data.");

          byte[] messageBlock = new byte[cipherAmt - SecretStream.ABYTES];
          if (!lazySodium.cryptoSecretStreamPull(state, messageBlock, tag, cipherBlock, cipherAmt))
            throw new E3DBCryptoException("Decryption error.");

          switch(v) {
            case INITIAL:
              // For backwards compatibility support, as the lazysodium library had a bug
              // and all messages were tagged final.
              if(tag[0] != SecretStream.TAG_FINAL)
                throw new E3DBDecryptionException("Invalid decryption.");
              break;
            case CORRECTED_MESSAGE_TAG:
              if(tag[0] != SECRET_STREAM_TAG_MESSAGE && tag[0] != SecretStream.TAG_FINAL)
                throw new E3DBDecryptionException("Invalid decryption.");
              break;
          }

          sawFinal = tag[0] == SecretStream.TAG_FINAL;
          out.write(messageBlock);
        }

        if (tag[0] != SecretStream.TAG_FINAL)
          throw new E3DBDecryptionException("Invalid file.");
      }
    }
  }

  @Override
  public int getBlockSize() {
    return BLOCK_SIZE;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy