View Javadoc
1   /*
2    * GordianKnot: Security Suite
3    * Copyright 2012-2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  package io.github.tonywasher.joceanus.gordianknot.impl.ext.engines;
18  
19  import org.bouncycastle.crypto.InvalidCipherTextException;
20  import org.bouncycastle.crypto.ec.ECElGamalDecryptor;
21  import org.bouncycastle.crypto.ec.ECElGamalEncryptor;
22  import org.bouncycastle.crypto.ec.ECPair;
23  import org.bouncycastle.crypto.params.ECDomainParameters;
24  import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
25  import org.bouncycastle.crypto.params.ECPublicKeyParameters;
26  import org.bouncycastle.crypto.params.ParametersWithRandom;
27  import org.bouncycastle.math.ec.ECCurve;
28  import org.bouncycastle.math.ec.ECPoint;
29  
30  import java.math.BigInteger;
31  import java.security.SecureRandom;
32  import java.util.Arrays;
33  
34  /**
35   * Elliptic curve encryptor.
36   * Based on https://onlinelibrary.wiley.com/doi/pdf/10.1002/sec.1702
37   */
38  public class GordianEllipticEncryptor {
39      /**
40       * CoFactor must be less than or equal to 20. Boundary is actually 10 < b < 76.
41       */
42      private static final int MAXCOFACTOR = 20;
43  
44      /**
45       * The max iterations to try for a point.
46       */
47      private static final int MAXITERATION = 1 << Byte.SIZE;
48  
49      /**
50       * The encryptor.
51       */
52      private final ECElGamalEncryptor theEncryptor;
53  
54      /**
55       * The decryptor.
56       */
57      private final ECElGamalDecryptor theDecryptor;
58  
59      /**
60       * The ECCurve.
61       */
62      private ECCurve theCurve;
63  
64      /**
65       * is encryption available?
66       */
67      private boolean isAvailable;
68  
69      /**
70       * Are we encrypting or decrypting?
71       */
72      private boolean encrypting;
73  
74      /**
75       * Constructor.
76       */
77      public GordianEllipticEncryptor() {
78          theEncryptor = new ECElGamalEncryptor();
79          theDecryptor = new ECElGamalDecryptor();
80      }
81  
82      /**
83       * Initialise for encryption.
84       *
85       * @param pPublicKey the publicKey
86       * @param pRandom    the secureRandom
87       */
88      public void initForEncrypt(final ECPublicKeyParameters pPublicKey,
89                                 final SecureRandom pRandom) {
90          /* Access domain parameters */
91          final ECDomainParameters myDomain = pPublicKey.getParameters();
92          if (isUnsupported(myDomain)) {
93              throw new IllegalArgumentException("Unsupported curve");
94          }
95  
96          /* Record details */
97          theCurve = myDomain.getCurve();
98          isAvailable = true;
99          encrypting = true;
100 
101         /* Initialise for encryption */
102         final ParametersWithRandom myParms = new ParametersWithRandom(pPublicKey, pRandom);
103         theEncryptor.init(myParms);
104     }
105 
106     /**
107      * Initialise for decryption.
108      *
109      * @param pPrivateKey the privateKey
110      */
111     public void initForDecrypt(final ECPrivateKeyParameters pPrivateKey) {
112         /* Access domain parameters */
113         final ECDomainParameters myDomain = pPrivateKey.getParameters();
114         if (isUnsupported(myDomain)) {
115             throw new IllegalArgumentException("Unsupported curve");
116         }
117 
118         /* Record details */
119         theCurve = myDomain.getCurve();
120         isAvailable = true;
121         encrypting = false;
122 
123         /* Initialise for decryption */
124         theDecryptor.init(pPrivateKey);
125     }
126 
127     /**
128      * Check whether encryption is available for this domain.
129      *
130      * @param pDomain the domain
131      * @return true/false
132      */
133     private boolean isUnsupported(final ECDomainParameters pDomain) {
134         return pDomain.getH().compareTo(BigInteger.valueOf(MAXCOFACTOR)) > 0;
135     }
136 
137     /**
138      * Obtain the length of field.
139      *
140      * @return the length of the field.
141      */
142     private int getFieldLength() {
143         return (theCurve.getFieldSize() + Byte.SIZE - 1) / Byte.SIZE;
144     }
145 
146     /**
147      * Obtain the length of block (1 less than fieldLength).
148      *
149      * @return the length of the block.
150      */
151     private int getBlockLength() {
152         return getFieldLength() - 1;
153     }
154 
155     /**
156      * Obtain the length of the plain block (2 less than blockLength).
157      *
158      * @return the length of the block.
159      */
160     private int getPlainBlockLength() {
161         return getBlockLength() - 2;
162     }
163 
164     /**
165      * Obtain the length of the encrypted block.
166      *
167      * @return the length of the block.
168      */
169     private int getEncodedBlockLength() {
170         return (getFieldLength() + 1) << 1;
171     }
172 
173     /**
174      * Obtain the length of the buffer required to receive the decrypted data.
175      *
176      * @param pLength the length of encrypted data
177      * @return the number of bytes.
178      */
179     private int getDecryptedLength(final int pLength) {
180         return getPlainBlockLength() * getNumBlocks(pLength, getEncodedBlockLength());
181     }
182 
183     /**
184      * Obtain the length of the buffer required for the encrypted output.
185      *
186      * @param pLength the length of clear data
187      * @return the number of bytes.
188      */
189     private int getEncryptedLength(final int pLength) {
190         return getEncodedBlockLength() * getNumBlocks(pLength, getPlainBlockLength());
191     }
192 
193     /**
194      * Obtain the number of blocks required for the length in terms of blocks.
195      *
196      * @param pLength      the length of clear data
197      * @param pBlockLength the blockLength
198      * @return the number of blocks.
199      */
200     private static int getNumBlocks(final int pLength, final int pBlockLength) {
201         return (pLength + pBlockLength - 1) / pBlockLength;
202     }
203 
204     /**
205      * Encrypt a data buffer.
206      *
207      * @param pData the buffer to encrypt
208      * @return the encrypted keyPair
209      * @throws InvalidCipherTextException on error
210      */
211     public byte[] encrypt(final byte[] pData) throws InvalidCipherTextException {
212         /* Check that we are set to encrypt */
213         if (!isAvailable || !encrypting) {
214             throw new IllegalStateException("Not initialised for encrypting");
215         }
216 
217         /* Create the output buffer */
218         int myInLen = pData.length;
219         final byte[] myOutput = new byte[getEncryptedLength(pData.length)];
220 
221         /* Access block lengths */
222         final int myInBlockLength = getPlainBlockLength();
223 
224         /* Loop encrypting the blocks */
225         int myInOff = 0;
226         int myOutOff = 0;
227         while (myInLen > 0) {
228             /* Encrypt to an ECPair */
229             final int myLen = Math.min(myInLen, myInBlockLength);
230             final ECPair myPair = encryptToPair(pData, myInOff, myLen);
231 
232             /* Convert into the output buffer */
233             myOutOff += convertFromECPair(myPair, myOutput, myOutOff);
234 
235             /* Move to next block */
236             myInOff += myInBlockLength;
237             myInLen -= myInBlockLength;
238         }
239 
240         /* Return full buffer if possible */
241         return myOutOff == myOutput.length
242                 ? myOutput
243                 : Arrays.copyOf(myOutput, myOutOff);
244     }
245 
246     /**
247      * Encrypt a value.
248      *
249      * @param pData  the buffer to encrypt
250      * @param pInOff the offset in the buffer
251      * @param pInLen the length of data to encrypt
252      * @return the encrypted keyPair
253      * @throws InvalidCipherTextException on error
254      */
255     private ECPair encryptToPair(final byte[] pData,
256                                  final int pInOff,
257                                  final int pInLen) throws InvalidCipherTextException {
258         /* Convert the data to an ECPoint */
259         final ECPoint myPoint = convertToECPoint(pData, pInOff, pInLen);
260 
261         /* Encrypt the data */
262         return theEncryptor.encrypt(myPoint);
263     }
264 
265     /**
266      * Convert to ECPoint.
267      *
268      * @param pInBuffer the input buffer
269      * @param pInOff    the input offset
270      * @param pInLen    the length of data to process
271      * @return the ECPair
272      * @throws InvalidCipherTextException on error
273      */
274     private ECPoint convertToECPoint(final byte[] pInBuffer,
275                                      final int pInOff,
276                                      final int pInLen) throws InvalidCipherTextException {
277         /* Check lengths */
278         final int myLen = getBlockLength();
279         if (pInLen > myLen - 2
280                 || pInLen <= 0) {
281             throw new IllegalArgumentException("Invalid input length");
282         }
283         if (pInBuffer.length - pInOff < pInLen) {
284             throw new IllegalArgumentException("Invalid input buffer");
285         }
286 
287         /* Create the work buffer and copy data in */
288         final byte[] myX = new byte[myLen + 1];
289 
290         /* Calculate the start position and place data and padding */
291         final int myStart = myLen - pInLen;
292         System.arraycopy(pInBuffer, pInOff, myX, myStart, pInLen);
293         myX[myStart - 1] = 1;
294 
295         /* Loop to obtain point on curve */
296         for (int i = 0; i < MAXITERATION; i++) {
297             /* Check to see whether the value is on the curve */
298             final ECPoint myPoint = checkOnCurve(myX);
299 
300             /* If we have a valid point */
301             if (myPoint != null) {
302                 return myPoint;
303             }
304 
305             /* Increment the test value */
306             myX[myLen]++;
307         }
308 
309         /* No possible value found */
310         throw new InvalidCipherTextException("Unable to find point on curve");
311     }
312 
313     /**
314      * Check whether the point is on the curve.
315      *
316      * @param pX the byte buffer representing X
317      * @return the ECPoint if on curve, else null
318      */
319     private ECPoint checkOnCurve(final byte[] pX) {
320         /* Protect against exceptions */
321         try {
322             /* Create a compressed point */
323             final int myFieldLen = getFieldLength();
324             final byte[] myCompressed = new byte[myFieldLen + 1];
325             System.arraycopy(pX, 0, myCompressed, 1, myFieldLen);
326             myCompressed[0] = 2;
327             final ECPoint myPoint = theCurve.decodePoint(myCompressed);
328 
329             /* Check the point */
330             return myPoint.isValid()
331                     ? myPoint
332                     : null;
333 
334             /* Handle invalid coding */
335         } catch (IllegalArgumentException e) {
336             return null;
337         }
338     }
339 
340     /**
341      * Convert from ECPair.
342      *
343      * @param pPair      the ECPoint
344      * @param pOutBuffer the output buffer
345      * @param pOutOff    the output offset
346      * @return the length of data decoded
347      * @throws InvalidCipherTextException on error
348      */
349     private int convertFromECPair(final ECPair pPair,
350                                   final byte[] pOutBuffer,
351                                   final int pOutOff) throws InvalidCipherTextException {
352         /* Check length */
353         final int myLen = getFieldLength() + 1;
354         if (pOutBuffer.length - pOutOff < myLen << 1) {
355             throw new IllegalArgumentException("Output buffer too small");
356         }
357 
358         /* Access the two encoded parameters  */
359         final byte[] myX = pPair.getX().getEncoded(true);
360         final byte[] myY = pPair.getY().getEncoded(true);
361         if (myX.length != myLen || myY.length != myLen) {
362             throw new InvalidCipherTextException("Bad encoding");
363         }
364 
365         /* Copy to the output buffer */
366         System.arraycopy(myX, 0, pOutBuffer, pOutOff, myLen);
367         System.arraycopy(myY, 0, pOutBuffer, pOutOff + myLen, myLen);
368         return myLen << 1;
369     }
370 
371     /**
372      * Decrypt a data buffer.
373      *
374      * @param pData the buffer to encrypt
375      * @return the encrypted keyPair
376      * @throws InvalidCipherTextException on error
377      */
378     public byte[] decrypt(final byte[] pData) throws InvalidCipherTextException {
379         /* Check that we are set to encrypt */
380         if (!isAvailable || encrypting) {
381             throw new IllegalStateException("Not initialised for decrypting");
382         }
383 
384         /* Create the output buffer */
385         int myInLen = pData.length;
386         final byte[] myOutput = new byte[getDecryptedLength(pData.length)];
387 
388         /* Access block lengths */
389         final int myInBlockLength = getEncodedBlockLength();
390 
391         /* Loop decrypting the blocks */
392         int myInOff = 0;
393         int myOutOff = 0;
394         while (myInLen > 0) {
395             /* Encrypt to an ECPair */
396             final ECPair myPair = convertToECPair(pData, myInOff);
397 
398             /* Convert into the output buffer */
399             myOutOff += decryptFromECPair(myPair, myOutput, myOutOff);
400 
401             /* Move to next block */
402             myInOff += myInBlockLength;
403             myInLen -= myInBlockLength;
404         }
405 
406         /* Return full buffer if possible */
407         if (myOutOff == myOutput.length) {
408             return myOutput;
409         }
410 
411         /* Cut down buffer */
412         final byte[] myReturn = Arrays.copyOf(myOutput, myOutOff);
413         Arrays.fill(myOutput, (byte) 0);
414         return myReturn;
415     }
416 
417     /**
418      * Decrypt a value.
419      *
420      * @param pPair      the pair to decrypt
421      * @param pOutBuffer the output buffer
422      * @param pOutOff    the output offset
423      * @return the length of data decoded
424      * @throws InvalidCipherTextException on error
425      */
426     private int decryptFromECPair(final ECPair pPair,
427                                   final byte[] pOutBuffer,
428                                   final int pOutOff) throws InvalidCipherTextException {
429         /* Decrypt the pair */
430         final ECPoint myPoint = theDecryptor.decrypt(pPair);
431         return convertFromECPoint(myPoint, pOutBuffer, pOutOff);
432     }
433 
434     /**
435      * Convert from ECPoint.
436      *
437      * @param pPoint     the ECPoint
438      * @param pOutBuffer the output buffer
439      * @param pOutOff    the output offset
440      * @return the length of data decoded
441      * @throws InvalidCipherTextException on error
442      */
443     private int convertFromECPoint(final ECPoint pPoint,
444                                    final byte[] pOutBuffer,
445                                    final int pOutOff) throws InvalidCipherTextException {
446         /* Obtain the X co-ordinate */
447         final BigInteger myX = pPoint.getAffineXCoord().toBigInteger();
448         final byte[] myBuf = myX.toByteArray();
449 
450         /* Set defaults */
451         int myStart = -1;
452         final int myEnd = myBuf.length - 1;
453 
454         /* Loop through the data in fixed time */
455         for (int myIndex = 0; myIndex < myEnd; myIndex++) {
456             /* If the value is non-zero and we have not yet found start */
457             /* Disable the short-circuit logic!! */
458             if (myBuf[myIndex] != 0
459                     & myStart == -1) {
460                 myStart = myIndex;
461             }
462         }
463 
464         /* Check validity */
465         if (myStart == -1 || myBuf[myStart] != 1) {
466             throw new InvalidCipherTextException("Invalid data");
467         }
468 
469         /* Bump past padding */
470         myStart++;
471 
472         /* Check length */
473         final int myOutLen = myEnd - myStart;
474         if (pOutBuffer.length - pOutOff < myOutLen) {
475             throw new IllegalArgumentException("Output buffer too small");
476         }
477 
478         /* Copy the data out */
479         System.arraycopy(myBuf, myStart, pOutBuffer, pOutOff, myOutLen);
480         return myOutLen;
481     }
482 
483     /**
484      * Convert to ECPair.
485      *
486      * @param pInBuffer the input buffer
487      * @param pInOff    the input offset
488      * @return the ECPair
489      */
490     private ECPair convertToECPair(final byte[] pInBuffer,
491                                    final int pInOff) {
492         /* Check length */
493         final int myLen = getFieldLength() + 1;
494         if (pInBuffer.length - pInOff < myLen << 1) {
495             throw new IllegalArgumentException("Invalid input buffer");
496         }
497 
498         /* Access the X point */
499         final byte[] myXbytes = new byte[myLen];
500         System.arraycopy(pInBuffer, pInOff, myXbytes, 0, myLen);
501         final ECPoint myX = theCurve.decodePoint(myXbytes);
502 
503         /* Access the Y point */
504         final byte[] myYbytes = new byte[myLen];
505         System.arraycopy(pInBuffer, pInOff + myLen, myYbytes, 0, myLen);
506         final ECPoint myY = theCurve.decodePoint(myYbytes);
507 
508         /* Create the ECPair */
509         return new ECPair(myX, myY);
510     }
511 }