1 // Copyright 2017 Google Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 //////////////////////////////////////////////////////////////////////////////// 16 17 package com.google.crypto.tink.streamingaead; 18 19 import com.google.crypto.tink.StreamingAead; 20 import com.google.errorprone.annotations.CanIgnoreReturnValue; 21 import java.io.IOException; 22 import java.nio.ByteBuffer; 23 import java.nio.channels.NonWritableChannelException; 24 import java.nio.channels.SeekableByteChannel; 25 import java.security.GeneralSecurityException; 26 import java.util.ArrayDeque; 27 import java.util.Deque; 28 import java.util.List; 29 import javax.annotation.concurrent.GuardedBy; 30 31 /** A decrypter for ciphertext given in a {@link SeekableByteChannel}. */ 32 final class SeekableByteChannelDecrypter implements SeekableByteChannel { 33 @GuardedBy("this") 34 SeekableByteChannel attemptingChannel; 35 @GuardedBy("this") 36 SeekableByteChannel matchingChannel; 37 @GuardedBy("this") 38 SeekableByteChannel ciphertextChannel; 39 @GuardedBy("this") 40 long cachedPosition; // Position to which attemptingChannel should be set before 1st read(); 41 @GuardedBy("this") 42 long startingPosition; // Position at which the ciphertext should begin. 43 44 // The StreamingAeads that have not yet been tried in nextAttemptingChannel. 45 Deque<StreamingAead> remainingPrimitives; 46 byte[] associatedData; 47 48 /** 49 * Constructs a new decrypter for {@code ciphertextChannel}. 50 * 51 * <p>The decrypter picks a matching {@code StreamingAead}-primitive from {@code primitives}, 52 * and uses it for decryption. The matching happens as follows: 53 * upon first {@code read()}-call each candidate primitive reads an initial portion 54 * of the channel, until it can determine whether the channel matches the key of the primitive. 55 * If a canditate does not match, then the channel is reset to its initial position, 56 * and the next candiate can attempt matching. The first successful candidate 57 * is then used exclusively on subsequent {@code read()}-calls. 58 */ SeekableByteChannelDecrypter(List<StreamingAead> allPrimitives, SeekableByteChannel ciphertextChannel, final byte[] associatedData)59 public SeekableByteChannelDecrypter(List<StreamingAead> allPrimitives, 60 SeekableByteChannel ciphertextChannel, final byte[] associatedData) throws IOException { 61 // There are 3 phases: 62 // 1) both matchingChannel and attemptingChannel are null. 63 // 2) attemptingChannel is non-null, matchingChannel is null 64 // 3) attemptingChannel is null, matchingChannel is non-null. 65 this.attemptingChannel = null; 66 this.matchingChannel = null; 67 this.remainingPrimitives = new ArrayDeque<>(); 68 for (StreamingAead primitive : allPrimitives) { 69 this.remainingPrimitives.add(primitive); 70 } 71 this.ciphertextChannel = ciphertextChannel; 72 // In phase 1) and 2), cachedPosition is always equal to the last position value set. 73 // In phase 2), attemptingChannel always has its position set to cachedPosition. 74 // In phase 3), cachedPosition is not needed. 75 this.cachedPosition = -1; 76 this.startingPosition = ciphertextChannel.position(); 77 this.associatedData = associatedData.clone(); 78 } 79 80 @GuardedBy("this") nextAttemptingChannel()81 private synchronized SeekableByteChannel nextAttemptingChannel() throws IOException { 82 while (!remainingPrimitives.isEmpty()) { 83 ciphertextChannel.position(startingPosition); 84 StreamingAead streamingAead = this.remainingPrimitives.removeFirst(); 85 try { 86 SeekableByteChannel decChannel = 87 streamingAead.newSeekableDecryptingChannel(ciphertextChannel, associatedData); 88 if (cachedPosition >= 0) { // Caller already set new position. 89 decChannel.position(cachedPosition); 90 } 91 return decChannel; 92 } catch (GeneralSecurityException e) { 93 // Try another primitive. 94 } 95 } 96 throw new IOException("No matching key found for the ciphertext in the stream."); 97 } 98 99 @Override 100 @GuardedBy("this") read(ByteBuffer dst)101 public synchronized int read(ByteBuffer dst) throws IOException { 102 if (dst.remaining() == 0) { 103 return 0; 104 } 105 if (matchingChannel != null) { 106 return matchingChannel.read(dst); 107 } else { 108 if (attemptingChannel == null) { 109 attemptingChannel = nextAttemptingChannel(); 110 } 111 while (true) { 112 try { 113 int retValue = attemptingChannel.read(dst); 114 if (retValue == 0) { 115 // No data at the moment. Not clear if decryption was successful. 116 // Try again with the same stream next time. 117 return 0; 118 } 119 // Found a matching channel. 120 matchingChannel = attemptingChannel; 121 attemptingChannel = null; 122 return retValue; 123 } catch (IOException e) { 124 // Try another key. 125 // IOException is thrown e.g. when MAC is incorrect, but also in case 126 // of I/O failures. 127 // TODO(b/66098906): Use a subclass of IOException. 128 attemptingChannel = nextAttemptingChannel(); 129 } 130 } 131 } 132 } 133 134 @CanIgnoreReturnValue 135 @Override 136 @GuardedBy("this") position(long newPosition)137 public synchronized SeekableByteChannel position(long newPosition) throws IOException { 138 if (matchingChannel != null) { 139 matchingChannel.position(newPosition); 140 } else { 141 if (newPosition < 0) { 142 throw new IllegalArgumentException("Position must be non-negative"); 143 } 144 cachedPosition = newPosition; 145 if (attemptingChannel != null) { 146 attemptingChannel.position(cachedPosition); 147 } 148 } 149 return this; 150 } 151 152 @Override 153 @GuardedBy("this") position()154 public synchronized long position() throws IOException { 155 if (matchingChannel != null) { 156 return matchingChannel.position(); 157 } else { 158 return cachedPosition; 159 } 160 } 161 162 @Override 163 @GuardedBy("this") size()164 public synchronized long size() throws IOException { 165 if (matchingChannel != null) { 166 return matchingChannel.size(); 167 } else { 168 throw new IOException("Cannot determine size before first read()-call."); 169 } 170 } 171 172 @Override truncate(long size)173 public SeekableByteChannel truncate(long size) throws IOException { 174 throw new NonWritableChannelException(); 175 } 176 177 @Override write(ByteBuffer src)178 public int write(ByteBuffer src) throws IOException { 179 throw new NonWritableChannelException(); 180 } 181 182 @Override 183 @GuardedBy("this") close()184 public synchronized void close() throws IOException { 185 ciphertextChannel.close(); 186 } 187 188 189 @Override 190 @GuardedBy("this") isOpen()191 public synchronized boolean isOpen() { 192 return ciphertextChannel.isOpen(); 193 } 194 } 195