• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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