diff --git a/CodenameOne/src/com/codename1/components/OtpField.java b/CodenameOne/src/com/codename1/components/OtpField.java new file mode 100644 index 0000000000..c4ed0a9e55 --- /dev/null +++ b/CodenameOne/src/com/codename1/components/OtpField.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.components; + +import com.codename1.ui.Container; +import com.codename1.ui.TextField; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.events.DataChangedListener; +import com.codename1.ui.layouts.BoxLayout; + +import java.util.ArrayList; + +/// Segmented one-time-password input -- one box per digit, auto-advances to +/// the next box on input and steps back on backspace. Standard pattern for +/// SMS / authenticator code entry screens. +/// +/// #### Example +/// +/// ```java +/// OtpField otp = new OtpField(6); +/// otp.addCompleteListener(new ActionListener() { +/// public void actionPerformed(ActionEvent evt) { +/// String code = otp.getText(); +/// // verify code... +/// } +/// }); +/// form.add(otp); +/// ``` +/// +/// Style the individual boxes with the UIID "OtpDigit"; the field itself uses +/// "OtpField". +public class OtpField extends Container { + + private final int length; + private final boolean numericOnly; + private final TextField[] boxes; + private final ArrayList completeListeners = new ArrayList(); + private boolean updating; + + /// Builds a 6-digit numeric OTP field -- the common case. + public OtpField() { + this(6, true); + } + + /// Builds an OTP field of the given length, numeric only. + /// + /// #### Parameters + /// + /// - `length`: number of digits / characters (e.g. 4, 6, 8) + public OtpField(int length) { + this(length, true); + } + + /// Full constructor. + /// + /// #### Parameters + /// + /// - `length`: number of digits / characters + /// + /// - `numericOnly`: true to restrict input to digits; false to allow any + /// character (alphanumeric OTP codes are sometimes used) + public OtpField(int length, boolean numericOnly) { + super(BoxLayout.x()); + if (length < 2 || length > 16) { + throw new IllegalArgumentException("OTP length must be between 2 and 16"); + } + this.length = length; + this.numericOnly = numericOnly; + this.boxes = new TextField[length]; + setUIID("OtpField"); + buildBoxes(); + } + + private void buildBoxes() { + for (int i = 0; i < length; i++) { + final int index = i; + final TextField tf = new TextField(); + tf.setUIID("OtpDigit"); + tf.setColumns(1); + tf.setMaxSize(1); + tf.setSingleLineTextArea(true); + if (numericOnly) { + tf.setConstraint(TextField.NUMERIC); + } + tf.addDataChangedListener(new DataChangedListener() { + @Override + public void dataChanged(int type, int idx) { + if (updating) { + return; + } + handleChange(index, tf); + } + }); + boxes[i] = tf; + add(tf); + } + } + + private void handleChange(int index, TextField source) { + String text = source.getText(); + if (text == null) { + text = ""; + } + // If multiple chars were pasted, distribute across boxes. + if (text.length() > 1) { + distributePaste(index, text); + return; + } + if (text.length() == 1) { + // advance focus to next box if not last + if (index < length - 1) { + boxes[index + 1].startEditingAsync(); + } else { + fireCompleteIfFull(); + } + } else { + // empty -- step back to previous box on backspace + if (index > 0) { + boxes[index - 1].startEditingAsync(); + } + } + } + + private void distributePaste(int startIndex, String text) { + updating = true; + try { + int p = startIndex; + for (int i = 0; i < text.length() && p < length; i++) { + char c = text.charAt(i); + if (numericOnly && (c < '0' || c > '9')) { + continue; + } + boxes[p].setText(String.valueOf(c)); + p++; + } + // clear any remaining cells past where we wrote + if (p > startIndex) { + // last cell to focus is the one after the last written, or + // the last box if we wrote to the end + int focus = p < length ? p : length - 1; + boxes[focus].startEditingAsync(); + } + } finally { + updating = false; + } + fireCompleteIfFull(); + } + + private void fireCompleteIfFull() { + String code = getText(); + if (code.length() == length) { + ActionEvent evt = new ActionEvent(this); + for (ActionListener listener : completeListeners) { + listener.actionPerformed(evt); + if (evt.isConsumed()) { + break; + } + } + } + } + + /// Returns the current value, in order from the first box to the last. + /// Empty boxes are omitted, so a partial entry returns a shorter string. + public String getText() { + StringBuilder b = new StringBuilder(length); + for (int i = 0; i < length; i++) { + String t = boxes[i].getText(); + if (t != null) { + b.append(t); + } + } + return b.toString(); + } + + /// Sets the value, distributing one character per box. Excess characters + /// are silently dropped; shorter strings leave the remaining boxes empty. + public void setText(String code) { + updating = true; + try { + for (int i = 0; i < length; i++) { + if (code != null && i < code.length()) { + boxes[i].setText(String.valueOf(code.charAt(i))); + } else { + boxes[i].setText(""); + } + } + } finally { + updating = false; + } + } + + /// Clears all boxes. + public void clear() { + setText(""); + boxes[0].startEditingAsync(); + } + + /// Adds a listener fired when the field becomes completely filled. Useful + /// to trigger automatic verification. + public void addCompleteListener(ActionListener l) { + if (l != null) { + completeListeners.add(l); + } + } + + /// Removes a previously-registered listener. + public void removeCompleteListener(ActionListener l) { + completeListeners.remove(l); + } + + /// Returns the underlying [TextField] for the box at `index`. Useful for + /// custom theming / focus management. + public TextField getBox(int index) { + return boxes[index]; + } + + /// Returns the configured length (number of boxes). + public int getLength() { + return length; + } +} diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index 47d5c9acc3..c38f7514ff 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -10149,4 +10149,77 @@ public void run() { } } } + + // ================================================================ + // Crypto bridge -- see com.codename1.security package. + // + // The default implementations below all throw -- each platform port + // (JavaSEPort, AndroidImplementation, IOSImplementation) overrides them + // with the real native-backed implementation. The core stays free of + // java.security / javax.crypto references because the core compiles + // against the CLDC11 stub where those classes (and full Class reflection) + // are not available. + + private static RuntimeException cryptoUnsupported(String op) { + return new RuntimeException("Crypto operation " + op + " is not supported on this platform. " + + "If you are running in a fresh CodenameOneImplementation subclass, override the matching method."); + } + + /// Fills `out` with cryptographically secure random bytes. Override in the + /// port to route to the platform's native CSPRNG. + public void secureRandomBytes(byte[] out) { + throw cryptoUnsupported("secureRandomBytes"); + } + + /// Encrypts with AES. Modes / paddings supported: AES/CBC/PKCS5Padding, + /// AES/CBC/NoPadding, AES/GCM/NoPadding (recommended -- authenticated; + /// the auth tag is appended to the ciphertext per the JCE convention) and + /// AES/ECB/PKCS5Padding (legacy interop only). `iv` may be null for ECB. + /// `aad` is associated data for GCM (may be null). + public byte[] aesEncrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] plaintext) { + throw cryptoUnsupported("aesEncrypt"); + } + + /// Decrypts with AES. Same parameters as `aesEncrypt`. + public byte[] aesDecrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] ciphertext) { + throw cryptoUnsupported("aesDecrypt"); + } + + /// Encrypts with RSA using an X.509 (SubjectPublicKeyInfo) DER-encoded + /// public key. `transformation` is typically + /// "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" or "RSA/ECB/PKCS1Padding". + public byte[] rsaEncrypt(String transformation, byte[] publicKeyX509, byte[] plaintext) { + throw cryptoUnsupported("rsaEncrypt"); + } + + /// Decrypts with RSA using a PKCS#8 DER-encoded private key. + public byte[] rsaDecrypt(String transformation, byte[] privateKeyPkcs8, byte[] ciphertext) { + throw cryptoUnsupported("rsaDecrypt"); + } + + /// Computes a signature. `algorithm` is e.g. "SHA256withRSA", + /// "SHA256withECDSA". `keyAlgorithm` is "RSA" or "EC". + public byte[] cryptoSign(String algorithm, String keyAlgorithm, byte[] privateKeyPkcs8, byte[] data) { + throw cryptoUnsupported("cryptoSign"); + } + + /// Verifies a signature with an X.509 public key. + public boolean cryptoVerify(String algorithm, String keyAlgorithm, byte[] publicKeyX509, byte[] data, byte[] signature) { + throw cryptoUnsupported("cryptoVerify"); + } + + /// Generates a fresh RSA key pair of the given size in bits. Returns + /// `{publicKeyX509, privateKeyPkcs8}`. + public byte[][] generateRsaKeyPair(int bits) { + throw cryptoUnsupported("generateRsaKeyPair"); + } + + /// Generates `bytes` of fresh symmetric key material. The default just + /// delegates to [#secureRandomBytes(byte[])] (no structure is required + /// for AES keys). + public byte[] generateSymmetricKey(int bytes) { + byte[] out = new byte[bytes]; + secureRandomBytes(out); + return out; + } } diff --git a/CodenameOne/src/com/codename1/io/Util.java b/CodenameOne/src/com/codename1/io/Util.java index 6135b6ce29..57ef8eb8ed 100644 --- a/CodenameOne/src/com/codename1/io/Util.java +++ b/CodenameOne/src/com/codename1/io/Util.java @@ -1799,6 +1799,71 @@ public static void setImplementation(CodenameOneImplementation impl) { implInstance = impl; } + // ---------------------------------------------------------------- + // Narrow crypto bridge -- used by com.codename1.security to talk to the + // platform's native crypto provider without exposing the impl instance + // itself. Each method here is a one-to-one delegate to a method on + // CodenameOneImplementation; see that class for the parameter contract. + + private static CodenameOneImplementation cryptoImpl() { + if (implInstance == null) { + throw new IllegalStateException("Codename One is not initialised"); + } + return implInstance; + } + + /// Fills `out` with cryptographically secure random bytes. Used by + /// [com.codename1.security.SecureRandom]. + public static void secureRandomBytes(byte[] out) { + cryptoImpl().secureRandomBytes(out); + } + + /// AES encryption. See + /// [com.codename1.impl.CodenameOneImplementation#aesEncrypt] for the + /// parameter contract. Used by [com.codename1.security.Cipher]. + public static byte[] aesEncrypt(String transformation, byte[] key, byte[] iv, + byte[] aad, byte[] plaintext) { + return cryptoImpl().aesEncrypt(transformation, key, iv, aad, plaintext); + } + + /// AES decryption -- counterpart to [#aesEncrypt]. + public static byte[] aesDecrypt(String transformation, byte[] key, byte[] iv, + byte[] aad, byte[] ciphertext) { + return cryptoImpl().aesDecrypt(transformation, key, iv, aad, ciphertext); + } + + /// RSA encryption. + public static byte[] rsaEncrypt(String transformation, byte[] publicKeyX509, byte[] plaintext) { + return cryptoImpl().rsaEncrypt(transformation, publicKeyX509, plaintext); + } + + /// RSA decryption. + public static byte[] rsaDecrypt(String transformation, byte[] privateKeyPkcs8, byte[] ciphertext) { + return cryptoImpl().rsaDecrypt(transformation, privateKeyPkcs8, ciphertext); + } + + /// Computes a signature. + public static byte[] cryptoSign(String algorithm, String keyAlgorithm, + byte[] privateKeyPkcs8, byte[] data) { + return cryptoImpl().cryptoSign(algorithm, keyAlgorithm, privateKeyPkcs8, data); + } + + /// Verifies a signature. + public static boolean cryptoVerify(String algorithm, String keyAlgorithm, + byte[] publicKeyX509, byte[] data, byte[] signature) { + return cryptoImpl().cryptoVerify(algorithm, keyAlgorithm, publicKeyX509, data, signature); + } + + /// Generates an RSA key pair. Returns `{publicKeyX509, privateKeyPkcs8}`. + public static byte[][] generateRsaKeyPair(int bits) { + return cryptoImpl().generateRsaKeyPair(bits); + } + + /// Generates `bytes` of fresh symmetric key material. + public static byte[] generateSymmetricKey(int bytes) { + return cryptoImpl().generateSymmetricKey(bytes); + } + /// Merges arrays into one larger array public static void mergeArrays(Object[] arr1, Object[] arr2, Object[] destinationArray) { System.arraycopy(arr1, 0, destinationArray, 0, arr1.length); diff --git a/CodenameOne/src/com/codename1/security/Base32.java b/CodenameOne/src/com/codename1/security/Base32.java new file mode 100644 index 0000000000..c4723b9973 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Base32.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +/// Base32 encoder/decoder per RFC 4648. Mostly useful for OTP shared secrets, +/// which are conventionally distributed as Base32 strings (the format embedded +/// in QR codes by authenticator apps). +/// +/// #### Example +/// +/// ```java +/// byte[] secret = Base32.decode("JBSWY3DPEHPK3PXP"); +/// String enc = Base32.encode(secret); +/// ``` +public final class Base32 { + private Base32() {} + + private static final char[] ALPHABET = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray(); + private static final int[] DECODE = new int[128]; + static { + for (int i = 0; i < DECODE.length; i++) { + DECODE[i] = -1; + } + for (int i = 0; i < ALPHABET.length; i++) { + DECODE[ALPHABET[i]] = i; + } + // common lowercase variant + for (int i = 0; i < ALPHABET.length; i++) { + char lc = (char) (ALPHABET[i] | 0x20); + if (lc != ALPHABET[i]) { + DECODE[lc] = i; + } + } + } + + /// Encodes the bytes as a Base32 string (uppercase, with `=` padding). + public static String encode(byte[] data) { + if (data == null || data.length == 0) { + return ""; + } + int output = ((data.length + 4) / 5) * 8; + StringBuilder b = new StringBuilder(output); + int bits = 0; + int value = 0; + for (byte aData : data) { + value = (value << 8) | (aData & 0xff); + bits += 8; + while (bits >= 5) { + b.append(ALPHABET[(value >>> (bits - 5)) & 0x1f]); + bits -= 5; + } + } + if (bits > 0) { + b.append(ALPHABET[(value << (5 - bits)) & 0x1f]); + } + while (b.length() < output) { + b.append('='); + } + return b.toString(); + } + + /// Decodes a Base32 string. Padding and whitespace are tolerated; mixed + /// case is accepted. + public static byte[] decode(String s) { + if (s == null) { + return new byte[0]; + } + // strip padding and whitespace + StringBuilder cleaned = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '=' || c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '-') { + continue; + } + cleaned.append(c); + } + int len = cleaned.length(); + byte[] out = new byte[len * 5 / 8]; + int bits = 0; + int value = 0; + int pos = 0; + for (int i = 0; i < len; i++) { + char c = cleaned.charAt(i); + if (c >= DECODE.length || DECODE[c] < 0) { + throw new CryptoException("invalid Base32 character: " + c); + } + value = (value << 5) | DECODE[c]; + bits += 5; + if (bits >= 8) { + out[pos++] = (byte) ((value >>> (bits - 8)) & 0xff); + bits -= 8; + } + } + return out; + } +} diff --git a/CodenameOne/src/com/codename1/security/Cipher.java b/CodenameOne/src/com/codename1/security/Cipher.java new file mode 100644 index 0000000000..efb7ca214c --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Cipher.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +import com.codename1.io.Util; + +/// Convenience entry points for symmetric (AES) and asymmetric (RSA) +/// encryption. The actual algorithms run on the platform's native crypto +/// provider -- this class is just a thin, friendly facade over the +/// [com.codename1.impl.CodenameOneImplementation] crypto bridge. +/// +/// #### Recommended transformations +/// +/// - **AES**: `AES/GCM/NoPadding` for authenticated encryption (uses a 12-byte +/// nonce and produces ciphertext with a 16-byte tag appended). Falls back +/// to `AES/CBC/PKCS5Padding` if GCM is unavailable on a target platform. +/// - **RSA**: `RSA/ECB/OAEPWithSHA-256AndMGF1Padding` for new code, +/// `RSA/ECB/PKCS1Padding` only for interop with old systems. +/// +/// #### Example: AES-GCM round-trip +/// +/// ```java +/// SecretKey key = KeyGenerator.aes(256); +/// byte[] nonce = SecureRandom.bytes(12); +/// byte[] cipher = Cipher.aesEncrypt(Cipher.AES_GCM, key, nonce, null, "secret".getBytes("UTF-8")); +/// byte[] plain = Cipher.aesDecrypt(Cipher.AES_GCM, key, nonce, null, cipher); +/// ``` +/// +/// #### Example: RSA-OAEP round-trip +/// +/// ```java +/// KeyPair kp = KeyGenerator.rsa(2048); +/// byte[] cipher = Cipher.rsaEncrypt(Cipher.RSA_OAEP_SHA256, kp.getPublicKey(), data); +/// byte[] plain = Cipher.rsaDecrypt(Cipher.RSA_OAEP_SHA256, kp.getPrivateKey(), cipher); +/// ``` +public final class Cipher { + + /// `AES/GCM/NoPadding` -- recommended authenticated mode for AES. + public static final String AES_GCM = "AES/GCM/NoPadding"; + + /// `AES/CBC/PKCS5Padding` -- block-chained AES with PKCS#5 padding. + public static final String AES_CBC_PKCS5 = "AES/CBC/PKCS5Padding"; + + /// `AES/CBC/NoPadding` -- raw CBC, caller must pre-pad to a 16-byte + /// boundary. + public static final String AES_CBC = "AES/CBC/NoPadding"; + + /// `AES/ECB/PKCS5Padding` -- legacy interop only. ECB leaks structure; + /// avoid for new designs. + public static final String AES_ECB_PKCS5 = "AES/ECB/PKCS5Padding"; + + /// `RSA/ECB/OAEPWithSHA-256AndMGF1Padding` -- recommended RSA encryption + /// transformation. + public static final String RSA_OAEP_SHA256 = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; + + /// `RSA/ECB/PKCS1Padding` -- legacy RSA padding, kept for interop. + public static final String RSA_PKCS1 = "RSA/ECB/PKCS1Padding"; + + private Cipher() {} + + /// Encrypts with AES. + /// + /// #### Parameters + /// + /// - `transformation`: one of [#AES_GCM], [#AES_CBC_PKCS5], [#AES_CBC], + /// [#AES_ECB_PKCS5] + /// + /// - `key`: AES key (16, 24 or 32 bytes for AES-128/192/256) + /// + /// - `iv`: initialisation vector for CBC (16 bytes) / nonce for GCM + /// (12 bytes recommended). Pass null for ECB. + /// + /// - `aad`: associated authenticated data -- GCM only, may be null + /// + /// - `plaintext`: data to encrypt + public static byte[] aesEncrypt(String transformation, SecretKey key, + byte[] iv, byte[] aad, byte[] plaintext) { + try { + return Util.aesEncrypt(transformation, key.getEncoded(), iv, aad, plaintext); + } catch (RuntimeException re) { + throw new CryptoException(re.getMessage(), re); + } + } + + /// Decrypts AES ciphertext produced by [#aesEncrypt]. For GCM mode, the + /// auth tag is part of the ciphertext (last 16 bytes) -- a tag mismatch + /// raises [CryptoException]. + public static byte[] aesDecrypt(String transformation, SecretKey key, + byte[] iv, byte[] aad, byte[] ciphertext) { + try { + return Util.aesDecrypt(transformation, key.getEncoded(), iv, aad, ciphertext); + } catch (RuntimeException re) { + throw new CryptoException(re.getMessage(), re); + } + } + + /// Encrypts a small amount of data with RSA. The plaintext size is bounded + /// by the modulus minus padding overhead (e.g. ~190 bytes max for + /// RSA-2048 + OAEP-SHA-256); use AES with an RSA-wrapped AES key for + /// larger payloads. + public static byte[] rsaEncrypt(String transformation, PublicKey key, byte[] plaintext) { + try { + return Util.rsaEncrypt(transformation, key.getEncoded(), plaintext); + } catch (RuntimeException re) { + throw new CryptoException(re.getMessage(), re); + } + } + + /// Decrypts an RSA ciphertext. + public static byte[] rsaDecrypt(String transformation, PrivateKey key, byte[] ciphertext) { + try { + return Util.rsaDecrypt(transformation, key.getEncoded(), ciphertext); + } catch (RuntimeException re) { + throw new CryptoException(re.getMessage(), re); + } + } +} diff --git a/CodenameOne/src/com/codename1/security/CryptoException.java b/CodenameOne/src/com/codename1/security/CryptoException.java new file mode 100644 index 0000000000..7c33ef13f6 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/CryptoException.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +/// Thrown by classes in this package when a cryptographic operation fails. This +/// is a runtime exception so callers are not forced to handle every operation, +/// but it can be caught explicitly when needed (e.g. a malformed key, an +/// authentication-tag mismatch, an algorithm that is not available on the +/// current platform, etc.). +public class CryptoException extends RuntimeException { + + /// Creates a new instance with the given message. + /// + /// #### Parameters + /// + /// - `message`: human readable description of the failure + public CryptoException(String message) { + super(message); + } + + /// Creates a new instance wrapping an underlying cause. + /// + /// #### Parameters + /// + /// - `message`: human readable description of the failure + /// + /// - `cause`: underlying exception that triggered the failure + public CryptoException(String message, Throwable cause) { + super(message); + if (cause != null) { + initCause(cause); + } + } +} diff --git a/CodenameOne/src/com/codename1/security/Hash.java b/CodenameOne/src/com/codename1/security/Hash.java new file mode 100644 index 0000000000..f2d07c6d57 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Hash.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +/// Streaming and one-shot cryptographic hash (message digest) functions. The +/// supported algorithms are exposed as constants on this class: +/// +/// - [#MD5] -- 128 bit, legacy interop only (broken collision resistance) +/// - [#SHA1] -- 160 bit, legacy interop only (broken collision resistance) +/// - [#SHA224] -- 224 bit (SHA-2 family) +/// - [#SHA256] -- 256 bit (SHA-2 family, recommended general-purpose hash) +/// - [#SHA384] -- 384 bit (SHA-2 family) +/// - [#SHA512] -- 512 bit (SHA-2 family) +/// +/// #### Quick example +/// +/// ```java +/// byte[] digest = Hash.sha256("hello".getBytes("UTF-8")); +/// String hex = Hash.toHex(digest); +/// +/// // streaming +/// Hash h = Hash.create(Hash.SHA256); +/// h.update("hello".getBytes("UTF-8")); +/// h.update(" world".getBytes("UTF-8")); +/// byte[] out = h.digest(); +/// ``` +/// +/// The implementations are written entirely in portable Java so they are +/// available on every supported platform. They produce identical output to +/// the equivalent algorithm in the standard JDK. +public final class Hash { + + /// MD5 algorithm identifier (128-bit digest). Provided for legacy interop -- + /// MD5 is no longer considered collision resistant and should not be used + /// for new security-sensitive code. + public static final String MD5 = "MD5"; + + /// SHA-1 algorithm identifier (160-bit digest). Provided for legacy interop + /// -- SHA-1 is no longer considered collision resistant and should not be + /// used for new security-sensitive code. + public static final String SHA1 = "SHA-1"; + + /// SHA-224 algorithm identifier (224-bit digest, SHA-2 family). + public static final String SHA224 = "SHA-224"; + + /// SHA-256 algorithm identifier (256-bit digest, SHA-2 family). Recommended + /// default for general-purpose hashing. + public static final String SHA256 = "SHA-256"; + + /// SHA-384 algorithm identifier (384-bit digest, SHA-2 family). + public static final String SHA384 = "SHA-384"; + + /// SHA-512 algorithm identifier (512-bit digest, SHA-2 family). + public static final String SHA512 = "SHA-512"; + + private final MessageDigestImpl impl; + + private Hash(MessageDigestImpl impl) { + this.impl = impl; + } + + /// Creates a streaming hash for the given algorithm. + /// + /// #### Parameters + /// + /// - `algorithm`: one of [#MD5], [#SHA1], [#SHA224], [#SHA256], [#SHA384], + /// [#SHA512] -- case insensitive, with or without the dash + /// + /// #### Returns + /// + /// a new Hash instance with zero bytes consumed + /// + /// #### Throws + /// + /// - `CryptoException`: if the algorithm is not recognised + public static Hash create(String algorithm) { + return new Hash(MessageDigestImpl.create(algorithm)); + } + + /// Feeds the entire array into the running hash. + /// + /// #### Parameters + /// + /// - `data`: bytes to append to the running digest + public void update(byte[] data) { + impl.update(data, 0, data.length); + } + + /// Feeds a slice of the array into the running hash. + /// + /// #### Parameters + /// + /// - `data`: bytes to append to the running digest + /// + /// - `offset`: index of the first byte to read + /// + /// - `length`: number of bytes to read + public void update(byte[] data, int offset, int length) { + impl.update(data, offset, length); + } + + /// Feeds a single byte into the running hash. + public void update(byte b) { + impl.update(b); + } + + /// Finalises the running hash and returns the digest. The hash is reset + /// after this call so the same instance may be reused for another message. + /// + /// #### Returns + /// + /// the raw digest bytes (algorithm specific length) + public byte[] digest() { + return impl.digest(); + } + + /// Convenience: feed `data` then return the digest. + public byte[] digest(byte[] data) { + impl.update(data, 0, data.length); + return impl.digest(); + } + + /// Number of bytes in the digest produced by this hash. + public int digestLength() { + return impl.digestLength(); + } + + /// Resets the running digest so the instance can be reused. + public void reset() { + impl.reset(); + } + + // ---------------------------------------------------------------- + // one-shot convenience entry points + + /// One-shot MD5 hash. + public static byte[] md5(byte[] data) { + return create(MD5).digest(data); + } + + /// One-shot SHA-1 hash. + public static byte[] sha1(byte[] data) { + return create(SHA1).digest(data); + } + + /// One-shot SHA-224 hash. + public static byte[] sha224(byte[] data) { + return create(SHA224).digest(data); + } + + /// One-shot SHA-256 hash (recommended general-purpose hash). + public static byte[] sha256(byte[] data) { + return create(SHA256).digest(data); + } + + /// One-shot SHA-384 hash. + public static byte[] sha384(byte[] data) { + return create(SHA384).digest(data); + } + + /// One-shot SHA-512 hash. + public static byte[] sha512(byte[] data) { + return create(SHA512).digest(data); + } + + // ---------------------------------------------------------------- + // hex helpers -- handy for displaying digests and writing test vectors + + /// Encodes the bytes as a lowercase hex string (two characters per byte). + public static String toHex(byte[] data) { + if (data == null) { + return null; + } + StringBuilder b = new StringBuilder(data.length * 2); + for (byte d : data) { + int v = d & 0xff; + b.append(HEX[v >>> 4]); + b.append(HEX[v & 0x0f]); + } + return b.toString(); + } + + /// Decodes a hex string back into bytes. The string must contain an even + /// number of hex characters (whitespace and the `0x` prefix are not + /// stripped -- pass cleaned input). + public static byte[] fromHex(String hex) { + if (hex == null) { + return null; + } + int len = hex.length(); + if ((len & 1) != 0) { + throw new CryptoException("hex string must have even length"); + } + byte[] out = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + int hi = nibble(hex.charAt(i)); + int lo = nibble(hex.charAt(i + 1)); + out[i / 2] = (byte) ((hi << 4) | lo); + } + return out; + } + + private static int nibble(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + throw new CryptoException("invalid hex digit"); + } + + private static final char[] HEX = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; +} diff --git a/CodenameOne/src/com/codename1/security/Hmac.java b/CodenameOne/src/com/codename1/security/Hmac.java new file mode 100644 index 0000000000..22200c76fc --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Hmac.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +/// Keyed-hash message authentication (HMAC, RFC 2104) on top of any hash +/// algorithm supported by [Hash]. Use HMAC whenever you need to prove that a +/// message came from somebody who shares a secret key with you and has not been +/// modified in transit (signatures of API requests, session cookies, JWTs with +/// the HS family, TOTP tokens, etc.). +/// +/// #### Quick example +/// +/// ```java +/// byte[] tag = Hmac.sha256(secret, message); +/// +/// // streaming +/// Hmac h = Hmac.create(Hash.SHA256, secret); +/// h.update(part1); +/// h.update(part2); +/// byte[] tag2 = h.doFinal(); +/// ``` +/// +/// Compare authentication tags with [#constantTimeEquals(byte[], byte[])] -- +/// using `java.util.Arrays.equals` or `==` opens you up to timing attacks. +public final class Hmac { + + private final MessageDigestImpl hash; + private final byte[] outerKey; + private final byte[] innerKey; + private boolean started; + + private Hmac(MessageDigestImpl hash, byte[] outerKey, byte[] innerKey) { + this.hash = hash; + this.outerKey = outerKey; + this.innerKey = innerKey; + reset(); + } + + /// Creates a streaming HMAC. + /// + /// #### Parameters + /// + /// - `algorithm`: any algorithm accepted by [Hash#create(String)] + /// + /// - `key`: secret key. Keys longer than the hash block size are hashed + /// down per RFC 2104; keys shorter than the block are zero-padded. There + /// is no enforced minimum but for security 128-256 bits of entropy is + /// recommended. + public static Hmac create(String algorithm, byte[] key) { + MessageDigestImpl h = MessageDigestImpl.create(algorithm); + int blockSize = blockSizeFor(algorithm); + + byte[] k = key; + if (k.length > blockSize) { + h.update(k, 0, k.length); + k = h.digest(); + h.reset(); + } + byte[] paddedKey = new byte[blockSize]; + System.arraycopy(k, 0, paddedKey, 0, k.length); + + byte[] inner = new byte[blockSize]; + byte[] outer = new byte[blockSize]; + for (int i = 0; i < blockSize; i++) { + inner[i] = (byte) (paddedKey[i] ^ 0x36); + outer[i] = (byte) (paddedKey[i] ^ 0x5c); + } + return new Hmac(h, outer, inner); + } + + private static int blockSizeFor(String algorithm) { + String a = MessageDigestImpl.normalise(algorithm); + if ("MD5".equals(a) || "SHA1".equals(a) || "SHA224".equals(a) || "SHA256".equals(a)) { + return 64; + } + if ("SHA384".equals(a) || "SHA512".equals(a)) { + return 128; + } + throw new CryptoException("unsupported HMAC algorithm: " + algorithm); + } + + /// Resets the running HMAC so the instance can be reused with the same key. + public void reset() { + hash.reset(); + started = false; + } + + /// Appends bytes to the message being authenticated. + public void update(byte[] data) { + update(data, 0, data.length); + } + + /// Appends a slice of bytes to the message being authenticated. + public void update(byte[] data, int offset, int length) { + if (!started) { + hash.update(innerKey, 0, innerKey.length); + started = true; + } + hash.update(data, offset, length); + } + + /// Finalises and returns the authentication tag. The instance is reset and + /// can be reused for another message with the same key. + public byte[] doFinal() { + if (!started) { + hash.update(innerKey, 0, innerKey.length); + started = true; + } + byte[] inner = hash.digest(); + hash.update(outerKey, 0, outerKey.length); + hash.update(inner, 0, inner.length); + byte[] tag = hash.digest(); + started = false; + return tag; + } + + /// One-shot convenience. + public byte[] doFinal(byte[] data) { + update(data, 0, data.length); + return doFinal(); + } + + /// Number of bytes in the authentication tag produced by this HMAC. + public int tagLength() { + return hash.digestLength(); + } + + // ---------------------------------------------------------------- + // one-shot entry points + + /// One-shot HMAC-MD5. Legacy interop only -- prefer HMAC-SHA-256. + public static byte[] md5(byte[] key, byte[] data) { + return create(Hash.MD5, key).doFinal(data); + } + + /// One-shot HMAC-SHA-1. Legacy interop only -- prefer HMAC-SHA-256. + public static byte[] sha1(byte[] key, byte[] data) { + return create(Hash.SHA1, key).doFinal(data); + } + + /// One-shot HMAC-SHA-224. + public static byte[] sha224(byte[] key, byte[] data) { + return create(Hash.SHA224, key).doFinal(data); + } + + /// One-shot HMAC-SHA-256 (recommended default). + public static byte[] sha256(byte[] key, byte[] data) { + return create(Hash.SHA256, key).doFinal(data); + } + + /// One-shot HMAC-SHA-384. + public static byte[] sha384(byte[] key, byte[] data) { + return create(Hash.SHA384, key).doFinal(data); + } + + /// One-shot HMAC-SHA-512. + public static byte[] sha512(byte[] key, byte[] data) { + return create(Hash.SHA512, key).doFinal(data); + } + + // ---------------------------------------------------------------- + + /// Constant-time comparison of two byte arrays. Returns false if the + /// arrays differ in length. Use this when comparing authentication tags, + /// session tokens, or any other secret value -- `Arrays.equals` short + /// circuits and is vulnerable to timing attacks. + public static boolean constantTimeEquals(byte[] a, byte[] b) { + if (a == null || b == null || a.length != b.length) { + return false; + } + int diff = 0; + for (int i = 0; i < a.length; i++) { + diff |= (a[i] ^ b[i]); + } + return diff == 0; + } +} diff --git a/CodenameOne/src/com/codename1/security/Jwt.java b/CodenameOne/src/com/codename1/security/Jwt.java new file mode 100644 index 0000000000..39b73dff6e --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Jwt.java @@ -0,0 +1,468 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +import com.codename1.io.JSONParser; + +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.LinkedHashMap; +import java.util.Map; + +/// JSON Web Token (RFC 7519) signing and verification. +/// +/// Supported algorithms: +/// +/// - `HS256`, `HS384`, `HS512` -- HMAC with SHA-2. Pure Java, available on +/// every platform. +/// - `RS256`, `RS384`, `RS512` -- RSA-PKCS1-v1_5 with SHA-2. Backed by the +/// platform's native crypto via [Signature]. +/// - `ES256`, `ES384`, `ES512` -- ECDSA with SHA-2. Backed by the platform's +/// native crypto via [Signature]. +/// - `none` -- unsigned tokens. Accepted on the signing side only when caller +/// explicitly passes it; rejected on verification unless caller opts in via +/// [#verifyAllowNoneAlgorithm]. +/// +/// #### Sign a token +/// +/// ```java +/// Map claims = new HashMap(); +/// claims.put("sub", "user-123"); +/// claims.put("exp", System.currentTimeMillis() / 1000 + 3600); +/// +/// String token = Jwt.signHs256(claims, "secret".getBytes("UTF-8")); +/// ``` +/// +/// #### Verify and read claims +/// +/// ```java +/// Jwt parsed = Jwt.parse(token); +/// if (!parsed.verifyHs256("secret".getBytes("UTF-8"))) { +/// throw new SecurityException("bad signature"); +/// } +/// String sub = (String) parsed.getClaim("sub"); +/// ``` +public final class Jwt { + + /// HMAC-SHA-256 ("HS256") + public static final String HS256 = "HS256"; + /// HMAC-SHA-384 ("HS384") + public static final String HS384 = "HS384"; + /// HMAC-SHA-512 ("HS512") + public static final String HS512 = "HS512"; + /// RSA PKCS#1 v1.5 with SHA-256 ("RS256") + public static final String RS256 = "RS256"; + /// RSA PKCS#1 v1.5 with SHA-384 ("RS384") + public static final String RS384 = "RS384"; + /// RSA PKCS#1 v1.5 with SHA-512 ("RS512") + public static final String RS512 = "RS512"; + /// ECDSA with SHA-256 ("ES256") + public static final String ES256 = "ES256"; + /// ECDSA with SHA-384 ("ES384") + public static final String ES384 = "ES384"; + /// ECDSA with SHA-512 ("ES512") + public static final String ES512 = "ES512"; + /// Unsigned token marker -- verification rejects this unless the caller + /// explicitly opts in. + public static final String NONE = "none"; + + private final Map header; + private final Map claims; + private final byte[] signature; + private final String signingInput; // header.payload (no signature) + private boolean verifyAllowNoneAlgorithm; + + private Jwt(Map header, Map claims, + byte[] signature, String signingInput) { + this.header = header; + this.claims = claims; + this.signature = signature; + this.signingInput = signingInput; + } + + // ================================================================ + // signing + + /// Signs `claims` with HS256 and returns the encoded token. + public static String signHs256(Map claims, byte[] secret) { + return sign(claims, secret, HS256); + } + + /// Signs `claims` with HS384 and returns the encoded token. + public static String signHs384(Map claims, byte[] secret) { + return sign(claims, secret, HS384); + } + + /// Signs `claims` with HS512 and returns the encoded token. + public static String signHs512(Map claims, byte[] secret) { + return sign(claims, secret, HS512); + } + + /// Signs `claims` with the given HMAC algorithm. Use this when you want + /// to pass the algorithm dynamically. + public static String sign(Map claims, byte[] secret, String algorithm) { + if (claims == null) { + throw new CryptoException("claims must not be null"); + } + if (algorithm == null) { + throw new CryptoException("algorithm must not be null"); + } + String signingInput = signingInput(algorithm, claims); + byte[] sig = computeHmac(algorithm, secret, signingInput); + return signingInput + "." + com.codename1.util.Base64.encodeUrlSafe(sig); + } + + /// Signs `claims` with the given RSA or ECDSA algorithm. + /// + /// #### Parameters + /// + /// - `claims`: token body + /// + /// - `privateKey`: signing key -- must match the algorithm family + /// + /// - `algorithm`: one of [#RS256], [#RS384], [#RS512], [#ES256], [#ES384], + /// [#ES512] + public static String sign(Map claims, PrivateKey privateKey, String algorithm) { + if (claims == null) { + throw new CryptoException("claims must not be null"); + } + if (privateKey == null) { + throw new CryptoException("privateKey must not be null"); + } + String sigAlg = signatureAlgorithmFor(algorithm); + String signingInput = signingInput(algorithm, claims); + byte[] data; + try { + data = signingInput.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new CryptoException("UTF-8 not supported", e); + } + byte[] sig = Signature.sign(sigAlg, privateKey, data); + // For ES* the JWT spec mandates a raw `r||s` concatenation rather than + // the platform's default DER-encoded SEQUENCE. We convert here. + if (algorithm.startsWith("ES")) { + sig = derToJoseEcdsa(sig, ecdsaCoordinateLength(algorithm)); + } + return signingInput + "." + com.codename1.util.Base64.encodeUrlSafe(sig); + } + + /// Builds an unsigned token (header `{"alg":"none"}`). Accepting these on + /// the verify side is dangerous -- see [#verifyAllowNoneAlgorithm]. + public static String signNone(Map claims) { + return signingInput(NONE, claims) + "."; + } + + // ================================================================ + // parsing + + /// Parses an encoded JWT into a [Jwt] object. The signature is NOT + /// verified -- you must call one of the `verify*` methods afterwards. + public static Jwt parse(String token) { + if (token == null) { + throw new CryptoException("token must not be null"); + } + int firstDot = token.indexOf('.'); + if (firstDot < 0) { + throw new CryptoException("malformed JWT: no '.'"); + } + int secondDot = token.indexOf('.', firstDot + 1); + if (secondDot < 0) { + throw new CryptoException("malformed JWT: only one '.'"); + } + String headerB64 = token.substring(0, firstDot); + String payloadB64 = token.substring(firstDot + 1, secondDot); + String sigB64 = token.substring(secondDot + 1); + Map header = readJson(com.codename1.util.Base64.decodeUrlSafe(headerB64)); + Map claims = readJson(com.codename1.util.Base64.decodeUrlSafe(payloadB64)); + byte[] sig = sigB64.length() == 0 ? new byte[0] : com.codename1.util.Base64.decodeUrlSafe(sigB64); + return new Jwt(header, claims, sig, headerB64 + "." + payloadB64); + } + + // ================================================================ + // verification + + /// When set to true, [#verify] will accept tokens whose `alg` header is + /// `none` (i.e. unsigned). The default is false because in most JWT + /// deployments accepting unsigned tokens is a critical security bug. + /// Only enable this if you have very deliberately decided that you trust + /// the transport. + public void setVerifyAllowNoneAlgorithm(boolean allow) { + this.verifyAllowNoneAlgorithm = allow; + } + + /// Verifies with a shared HMAC secret. The token's `alg` header is read + /// and must be one of the HS family. + public boolean verifyHs256(byte[] secret) { + return verifyHmac(HS256, secret); + } + + /// HMAC verification with HS384. + public boolean verifyHs384(byte[] secret) { + return verifyHmac(HS384, secret); + } + + /// HMAC verification with HS512. + public boolean verifyHs512(byte[] secret) { + return verifyHmac(HS512, secret); + } + + private boolean verifyHmac(String expectedAlg, byte[] secret) { + if (!expectedAlg.equals(getAlgorithm())) { + return false; + } + byte[] expected = computeHmac(expectedAlg, secret, signingInput); + return Hmac.constantTimeEquals(expected, signature); + } + + /// Verifies an RSA or ECDSA signature using the given public key. The + /// algorithm must match the token's `alg` header (RS256/384/512 or + /// ES256/384/512). + public boolean verify(PublicKey publicKey) { + String alg = getAlgorithm(); + if (NONE.equals(alg)) { + return verifyAllowNoneAlgorithm && (signature == null || signature.length == 0); + } + String sigAlg = signatureAlgorithmFor(alg); + byte[] data; + try { + data = signingInput.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new CryptoException("UTF-8 not supported", e); + } + byte[] sig = signature; + if (alg.startsWith("ES")) { + sig = joseToDerEcdsa(sig, ecdsaCoordinateLength(alg)); + } + return Signature.verify(sigAlg, publicKey, data, sig); + } + + // ================================================================ + // accessors + + /// Returns the `alg` field from the JWT header (e.g. "HS256"). + public String getAlgorithm() { + Object v = header.get("alg"); + return v == null ? null : v.toString(); + } + + /// Returns the parsed header as an unmodifiable view into the original + /// map. Mutating it has undefined behaviour. + public Map getHeader() { + return header; + } + + /// Returns the parsed claims (token payload) as an unmodifiable view into + /// the original map. Mutating it has undefined behaviour. + public Map getClaims() { + return claims; + } + + /// Returns the value of a single claim, or null if the claim is absent. + public Object getClaim(String name) { + return claims == null ? null : claims.get(name); + } + + /// Returns the raw bytes of the signature segment as decoded from + /// URL-safe base64. May be empty for unsigned tokens. + public byte[] getSignature() { + return signature; + } + + // ================================================================ + // internals + + private static String signingInput(String algorithm, Map claims) { + Map hdr = new LinkedHashMap(); + hdr.put("alg", algorithm); + hdr.put("typ", "JWT"); + String headerJson = JSONParser.mapToJson(hdr); + String claimsJson = JSONParser.mapToJson(claims); + try { + return com.codename1.util.Base64.encodeUrlSafe(headerJson.getBytes("UTF-8")) + "." + + com.codename1.util.Base64.encodeUrlSafe(claimsJson.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new CryptoException("UTF-8 not supported", e); + } + } + + private static byte[] computeHmac(String algorithm, byte[] secret, String signingInput) { + String hashAlg; + if (HS256.equals(algorithm)) { + hashAlg = Hash.SHA256; + } else if (HS384.equals(algorithm)) { + hashAlg = Hash.SHA384; + } else if (HS512.equals(algorithm)) { + hashAlg = Hash.SHA512; + } else { + throw new CryptoException("unsupported HMAC algorithm: " + algorithm); + } + try { + return Hmac.create(hashAlg, secret).doFinal(signingInput.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new CryptoException("UTF-8 not supported", e); + } + } + + private static String signatureAlgorithmFor(String jwtAlg) { + if (RS256.equals(jwtAlg)) { + return Signature.SHA256_WITH_RSA; + } + if (RS384.equals(jwtAlg)) { + return Signature.SHA384_WITH_RSA; + } + if (RS512.equals(jwtAlg)) { + return Signature.SHA512_WITH_RSA; + } + if (ES256.equals(jwtAlg)) { + return Signature.SHA256_WITH_ECDSA; + } + if (ES384.equals(jwtAlg)) { + return Signature.SHA384_WITH_ECDSA; + } + if (ES512.equals(jwtAlg)) { + return Signature.SHA512_WITH_ECDSA; + } + throw new CryptoException("unsupported JWT algorithm: " + jwtAlg); + } + + private static int ecdsaCoordinateLength(String jwtAlg) { + if (ES256.equals(jwtAlg)) { + return 32; // P-256 + } + if (ES384.equals(jwtAlg)) { + return 48; // P-384 + } + if (ES512.equals(jwtAlg)) { + return 66; // P-521 + } + throw new CryptoException("not an ECDSA algorithm: " + jwtAlg); + } + + private static Map readJson(byte[] data) { + try { + JSONParser parser = new JSONParser(); + return parser.parseJSON(new InputStreamReader(new ByteArrayInputStream(data), "UTF-8")); + } catch (Exception e) { + throw new CryptoException("invalid JSON in JWT", e); + } + } + + // ---------------------------------------------------------------- + // ECDSA signatures: platform sign() returns ASN.1 DER (SEQUENCE { r, s }), + // JWT requires raw r||s. Tiny converters here keep ES* support compact. + private static byte[] derToJoseEcdsa(byte[] der, int coordLen) { + // SEQUENCE (0x30) length ... + // INTEGER (0x02) lenR rBytes + // INTEGER (0x02) lenS sBytes + if (der.length < 8 || (der[0] & 0xff) != 0x30) { + throw new CryptoException("bad ECDSA DER signature"); + } + int p = 2; + if ((der[1] & 0x80) != 0) { + int n = der[1] & 0x7f; + p = 2 + n; + } + if ((der[p] & 0xff) != 0x02) { + throw new CryptoException("bad ECDSA DER signature"); + } + int rLen = der[p + 1] & 0xff; + int rOff = p + 2; + int sStart = rOff + rLen; + if ((der[sStart] & 0xff) != 0x02) { + throw new CryptoException("bad ECDSA DER signature"); + } + int sLen = der[sStart + 1] & 0xff; + int sOff = sStart + 2; + byte[] out = new byte[coordLen * 2]; + copyAndPad(der, rOff, rLen, out, 0, coordLen); + copyAndPad(der, sOff, sLen, out, coordLen, coordLen); + return out; + } + + private static void copyAndPad(byte[] src, int srcOff, int srcLen, + byte[] dst, int dstOff, int dstLen) { + // Strip leading zero pad if any + while (srcLen > 0 && src[srcOff] == 0) { + srcOff++; + srcLen--; + } + if (srcLen > dstLen) { + throw new CryptoException("ECDSA coordinate too large"); + } + int pad = dstLen - srcLen; + for (int i = 0; i < pad; i++) { + dst[dstOff + i] = 0; + } + System.arraycopy(src, srcOff, dst, dstOff + pad, srcLen); + } + + private static byte[] joseToDerEcdsa(byte[] jose, int coordLen) { + if (jose.length != coordLen * 2) { + throw new CryptoException("bad ECDSA JOSE signature length"); + } + byte[] r = trimAndAddSignByte(jose, 0, coordLen); + byte[] s = trimAndAddSignByte(jose, coordLen, coordLen); + int seqLen = 2 + r.length + 2 + s.length; + byte[] out; + if (seqLen < 128) { + out = new byte[2 + seqLen]; + out[0] = 0x30; + out[1] = (byte) seqLen; + int p = 2; + out[p++] = 0x02; out[p++] = (byte) r.length; + System.arraycopy(r, 0, out, p, r.length); p += r.length; + out[p++] = 0x02; out[p++] = (byte) s.length; + System.arraycopy(s, 0, out, p, s.length); + } else { + // 0x81 length form + out = new byte[3 + seqLen]; + out[0] = 0x30; + out[1] = (byte) 0x81; + out[2] = (byte) seqLen; + int p = 3; + out[p++] = 0x02; out[p++] = (byte) r.length; + System.arraycopy(r, 0, out, p, r.length); p += r.length; + out[p++] = 0x02; out[p++] = (byte) s.length; + System.arraycopy(s, 0, out, p, s.length); + } + return out; + } + + private static byte[] trimAndAddSignByte(byte[] src, int off, int len) { + int start = off; + int end = off + len; + while (start < end - 1 && src[start] == 0) { + start++; + } + boolean needPad = (src[start] & 0x80) != 0; + int outLen = (end - start) + (needPad ? 1 : 0); + byte[] out = new byte[outLen]; + int p = 0; + if (needPad) { + out[p++] = 0; + } + System.arraycopy(src, start, out, p, end - start); + return out; + } +} diff --git a/CodenameOne/src/com/codename1/security/Key.java b/CodenameOne/src/com/codename1/security/Key.java new file mode 100644 index 0000000000..7ed60a8bd3 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Key.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +/// Common base for every key type in the security package -- [SecretKey] for +/// symmetric algorithms, [PublicKey] and [PrivateKey] for asymmetric ones. +/// +/// Holds the three pieces of metadata every key carries: +/// +/// - an algorithm name (e.g. "AES", "RSA", "EC") +/// - a defensive copy of the encoded key bytes +/// - a format identifier (e.g. "RAW" for symmetric keys, "X.509" for SPKI +/// public keys, "PKCS#8" for private keys) +/// +/// Application code should not extend this class directly; create the +/// concrete subtype that matches the algorithm you are using. +public abstract class Key { + private final String algorithm; + private final byte[] encoded; + private final String format; + + /// #### Parameters + /// + /// - `algorithm`: human-readable algorithm name (e.g. "AES") + /// + /// - `encoded`: raw key material -- defensively copied + /// + /// - `format`: encoding format identifier ("RAW", "X.509", "PKCS#8") + protected Key(String algorithm, byte[] encoded, String format) { + if (algorithm == null) { + throw new CryptoException("algorithm must not be null"); + } + if (encoded == null) { + throw new CryptoException("encoded must not be null"); + } + this.algorithm = algorithm; + this.encoded = new byte[encoded.length]; + System.arraycopy(encoded, 0, this.encoded, 0, encoded.length); + this.format = format; + } + + /// Returns a fresh copy of the encoded key bytes. Treat returns from + /// private keys as sensitive material -- do not log or store + /// unencrypted. + public final byte[] getEncoded() { + byte[] copy = new byte[encoded.length]; + System.arraycopy(encoded, 0, copy, 0, encoded.length); + return copy; + } + + /// Returns the algorithm this key is intended for (e.g. "AES", "RSA"). + public final String getAlgorithm() { + return algorithm; + } + + /// Returns the encoding format identifier. Standard values: + /// + /// - "RAW" -- symmetric keys ([SecretKey]) + /// - "X.509" -- SubjectPublicKeyInfo DER ([PublicKey]) + /// - "PKCS#8" -- PrivateKeyInfo DER ([PrivateKey]) + public final String getFormat() { + return format; + } + + /// Subclasses can expose the raw byte array internally without going + /// through `getEncoded()` so they don't pay the defensive-copy cost on + /// every crypto-bridge call. Not exposed publicly; package-private only. + final byte[] rawEncoded() { + return encoded; + } +} diff --git a/CodenameOne/src/com/codename1/security/KeyGenerator.java b/CodenameOne/src/com/codename1/security/KeyGenerator.java new file mode 100644 index 0000000000..63cf94197d --- /dev/null +++ b/CodenameOne/src/com/codename1/security/KeyGenerator.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +import com.codename1.io.Util; + +/// Generates fresh cryptographic key material. AES keys are pulled directly +/// from [SecureRandom]; RSA key pairs are generated by the platform's native +/// crypto provider. +/// +/// #### Example +/// +/// ```java +/// SecretKey aes = KeyGenerator.aes(256); // AES-256 +/// KeyPair rsa = KeyGenerator.rsa(2048); // RSA-2048 +/// ``` +public final class KeyGenerator { + private KeyGenerator() {} + + /// Generates a fresh AES key of the given bit length. Valid lengths are + /// 128, 192 and 256 bits. + public static SecretKey aes(int bits) { + if (bits != 128 && bits != 192 && bits != 256) { + throw new CryptoException("invalid AES key size: " + bits + " (must be 128, 192 or 256)"); + } + byte[] key = Util.generateSymmetricKey(bits / 8); + return new SecretKey("AES", key); + } + + /// Generates a fresh HMAC key of the given bit length. By convention, + /// HMAC keys should be at least as long as the hash output. + public static SecretKey hmac(int bits) { + if (bits <= 0 || (bits & 7) != 0) { + throw new CryptoException("HMAC key size must be a positive multiple of 8"); + } + byte[] key = Util.generateSymmetricKey(bits / 8); + return new SecretKey("HMAC", key); + } + + /// Generates a fresh RSA key pair of the given size in bits. 2048 is the + /// modern minimum; 3072 / 4096 for higher security margins. Generating a + /// 4096-bit RSA key can take several seconds -- call from a background + /// thread. + public static KeyPair rsa(int bits) { + if (bits < 1024 || (bits & 7) != 0) { + throw new CryptoException("invalid RSA key size: " + bits); + } + try { + byte[][] pair = Util.generateRsaKeyPair(bits); + PublicKey pub = PublicKey.fromX509(PublicKey.RSA, pair[0]); + PrivateKey priv = PrivateKey.fromPkcs8(PublicKey.RSA, pair[1]); + return new KeyPair(pub, priv); + } catch (RuntimeException re) { + throw new CryptoException(re.getMessage(), re); + } + } +} diff --git a/CodenameOne/src/com/codename1/security/KeyPair.java b/CodenameOne/src/com/codename1/security/KeyPair.java new file mode 100644 index 0000000000..a63e2d50c3 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/KeyPair.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +/// A matched pair of [PublicKey] / [PrivateKey]. Typically produced by +/// [KeyGenerator#generateRsaKeyPair(int)]. +public final class KeyPair { + private final PublicKey publicKey; + private final PrivateKey privateKey; + + /// Bundles an already-paired public/private key. + public KeyPair(PublicKey publicKey, PrivateKey privateKey) { + if (publicKey == null) { + throw new CryptoException("publicKey must not be null"); + } + if (privateKey == null) { + throw new CryptoException("privateKey must not be null"); + } + this.publicKey = publicKey; + this.privateKey = privateKey; + } + + /// Returns the public part of this pair. + public PublicKey getPublicKey() { + return publicKey; + } + + /// Returns the private part of this pair. + public PrivateKey getPrivateKey() { + return privateKey; + } +} diff --git a/CodenameOne/src/com/codename1/security/MessageDigestImpl.java b/CodenameOne/src/com/codename1/security/MessageDigestImpl.java new file mode 100644 index 0000000000..211b8a3ba0 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/MessageDigestImpl.java @@ -0,0 +1,757 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +// Pure-Java implementations of MD5 / SHA-1 / SHA-2 (224, 256, 384, 512). These +// are the well-known reference algorithms (RFC 1321, RFC 3174, FIPS 180-4). +// +// One class with a small dispatch is more compact than five separate classes +// and keeps all the round constants close to the rounds that use them. +abstract class MessageDigestImpl { + + abstract void update(byte[] data, int offset, int length); + abstract void update(byte b); + abstract byte[] digest(); + abstract int digestLength(); + abstract void reset(); + + static MessageDigestImpl create(String algorithm) { + if (algorithm == null) { + throw new CryptoException("algorithm must not be null"); + } + String a = normalise(algorithm); + if ("MD5".equals(a)) { + return new Md5(); + } + if ("SHA1".equals(a)) { + return new Sha1(); + } + if ("SHA224".equals(a)) { + return new Sha256Family(true); + } + if ("SHA256".equals(a)) { + return new Sha256Family(false); + } + if ("SHA384".equals(a)) { + return new Sha512Family(true); + } + if ("SHA512".equals(a)) { + return new Sha512Family(false); + } + throw new CryptoException("unsupported hash algorithm: " + algorithm); + } + + static String normalise(String algorithm) { + StringBuilder b = new StringBuilder(algorithm.length()); + for (int i = 0; i < algorithm.length(); i++) { + char c = algorithm.charAt(i); + if (c == '-' || c == '_' || c == ' ') { + continue; + } + if (c >= 'a' && c <= 'z') { + c = (char) (c - 'a' + 'A'); + } + b.append(c); + } + return b.toString(); + } + + // =============================================================== + // shared 64-byte (512-bit) block engine used by MD5 and SHA-1/SHA-256 + abstract static class Block64 extends MessageDigestImpl { + final byte[] buffer = new byte[64]; + int bufferLen; + long byteCount; + + abstract void processBlock(byte[] block, int offset); + abstract void writeStateBigEndian(byte[] out); + + @Override + void update(byte[] data, int offset, int length) { + byteCount += length; + if (bufferLen > 0) { + int copy = 64 - bufferLen; + if (copy > length) { + copy = length; + } + System.arraycopy(data, offset, buffer, bufferLen, copy); + bufferLen += copy; + offset += copy; + length -= copy; + if (bufferLen == 64) { + processBlock(buffer, 0); + bufferLen = 0; + } + } + while (length >= 64) { + processBlock(data, offset); + offset += 64; + length -= 64; + } + if (length > 0) { + System.arraycopy(data, offset, buffer, 0, length); + bufferLen = length; + } + } + + @Override + void update(byte b) { + byteCount++; + buffer[bufferLen++] = b; + if (bufferLen == 64) { + processBlock(buffer, 0); + bufferLen = 0; + } + } + + final byte[] finishCommon(boolean bigEndianLength) { + long bits = byteCount * 8L; + buffer[bufferLen++] = (byte) 0x80; + if (bufferLen > 56) { + while (bufferLen < 64) { + buffer[bufferLen++] = 0; + } + processBlock(buffer, 0); + bufferLen = 0; + } + while (bufferLen < 56) { + buffer[bufferLen++] = 0; + } + if (bigEndianLength) { + buffer[56] = (byte) (bits >>> 56); + buffer[57] = (byte) (bits >>> 48); + buffer[58] = (byte) (bits >>> 40); + buffer[59] = (byte) (bits >>> 32); + buffer[60] = (byte) (bits >>> 24); + buffer[61] = (byte) (bits >>> 16); + buffer[62] = (byte) (bits >>> 8); + buffer[63] = (byte) bits; + } else { + buffer[56] = (byte) bits; + buffer[57] = (byte) (bits >>> 8); + buffer[58] = (byte) (bits >>> 16); + buffer[59] = (byte) (bits >>> 24); + buffer[60] = (byte) (bits >>> 32); + buffer[61] = (byte) (bits >>> 40); + buffer[62] = (byte) (bits >>> 48); + buffer[63] = (byte) (bits >>> 56); + } + processBlock(buffer, 0); + byte[] out = new byte[digestLength()]; + writeStateBigEndian(out); + reset(); + return out; + } + } + + // =============================================================== + // MD5 -- RFC 1321 (little-endian length, little-endian word loads) + static final class Md5 extends Block64 { + int a; + int b; + int c; + int d; + + Md5() { + reset(); + } + + @Override + public void reset() { + a = 0x67452301; + b = 0xefcdab89; + c = 0x98badcfe; + d = 0x10325476; + bufferLen = 0; + byteCount = 0; + } + + @Override + public int digestLength() { + return 16; + } + + @Override + public byte[] digest() { + return finishCommon(false); + } + + @Override + void writeStateBigEndian(byte[] out) { + // MD5 actually writes its state little-endian; we reuse the name + // for the shared finish path. + writeLE(out, 0, a); + writeLE(out, 4, b); + writeLE(out, 8, c); + writeLE(out, 12, d); + } + + private static void writeLE(byte[] out, int o, int v) { + out[o] = (byte) v; + out[o + 1] = (byte) (v >>> 8); + out[o + 2] = (byte) (v >>> 16); + out[o + 3] = (byte) (v >>> 24); + } + + private static int readLE(byte[] src, int o) { + return (src[o] & 0xff) + | (src[o + 1] & 0xff) << 8 + | (src[o + 2] & 0xff) << 16 + | (src[o + 3] & 0xff) << 24; + } + + private static int rol(int v, int s) { + return (v << s) | (v >>> (32 - s)); + } + + @Override + void processBlock(byte[] block, int o) { + int x0 = readLE(block, o); + int x1 = readLE(block, o + 4); + int x2 = readLE(block, o + 8); + int x3 = readLE(block, o + 12); + int x4 = readLE(block, o + 16); + int x5 = readLE(block, o + 20); + int x6 = readLE(block, o + 24); + int x7 = readLE(block, o + 28); + int x8 = readLE(block, o + 32); + int x9 = readLE(block, o + 36); + int x10 = readLE(block, o + 40); + int x11 = readLE(block, o + 44); + int x12 = readLE(block, o + 48); + int x13 = readLE(block, o + 52); + int x14 = readLE(block, o + 56); + int x15 = readLE(block, o + 60); + + int aa = a; + int bb = b; + int cc = c; + int dd = d; + + // round 1 + aa = bb + rol(aa + ((bb & cc) | (~bb & dd)) + x0 + 0xd76aa478, 7); + dd = aa + rol(dd + ((aa & bb) | (~aa & cc)) + x1 + 0xe8c7b756, 12); + cc = dd + rol(cc + ((dd & aa) | (~dd & bb)) + x2 + 0x242070db, 17); + bb = cc + rol(bb + ((cc & dd) | (~cc & aa)) + x3 + 0xc1bdceee, 22); + aa = bb + rol(aa + ((bb & cc) | (~bb & dd)) + x4 + 0xf57c0faf, 7); + dd = aa + rol(dd + ((aa & bb) | (~aa & cc)) + x5 + 0x4787c62a, 12); + cc = dd + rol(cc + ((dd & aa) | (~dd & bb)) + x6 + 0xa8304613, 17); + bb = cc + rol(bb + ((cc & dd) | (~cc & aa)) + x7 + 0xfd469501, 22); + aa = bb + rol(aa + ((bb & cc) | (~bb & dd)) + x8 + 0x698098d8, 7); + dd = aa + rol(dd + ((aa & bb) | (~aa & cc)) + x9 + 0x8b44f7af, 12); + cc = dd + rol(cc + ((dd & aa) | (~dd & bb)) + x10 + 0xffff5bb1, 17); + bb = cc + rol(bb + ((cc & dd) | (~cc & aa)) + x11 + 0x895cd7be, 22); + aa = bb + rol(aa + ((bb & cc) | (~bb & dd)) + x12 + 0x6b901122, 7); + dd = aa + rol(dd + ((aa & bb) | (~aa & cc)) + x13 + 0xfd987193, 12); + cc = dd + rol(cc + ((dd & aa) | (~dd & bb)) + x14 + 0xa679438e, 17); + bb = cc + rol(bb + ((cc & dd) | (~cc & aa)) + x15 + 0x49b40821, 22); + + // round 2 + aa = bb + rol(aa + ((bb & dd) | (cc & ~dd)) + x1 + 0xf61e2562, 5); + dd = aa + rol(dd + ((aa & cc) | (bb & ~cc)) + x6 + 0xc040b340, 9); + cc = dd + rol(cc + ((dd & bb) | (aa & ~bb)) + x11 + 0x265e5a51, 14); + bb = cc + rol(bb + ((cc & aa) | (dd & ~aa)) + x0 + 0xe9b6c7aa, 20); + aa = bb + rol(aa + ((bb & dd) | (cc & ~dd)) + x5 + 0xd62f105d, 5); + dd = aa + rol(dd + ((aa & cc) | (bb & ~cc)) + x10 + 0x02441453, 9); + cc = dd + rol(cc + ((dd & bb) | (aa & ~bb)) + x15 + 0xd8a1e681, 14); + bb = cc + rol(bb + ((cc & aa) | (dd & ~aa)) + x4 + 0xe7d3fbc8, 20); + aa = bb + rol(aa + ((bb & dd) | (cc & ~dd)) + x9 + 0x21e1cde6, 5); + dd = aa + rol(dd + ((aa & cc) | (bb & ~cc)) + x14 + 0xc33707d6, 9); + cc = dd + rol(cc + ((dd & bb) | (aa & ~bb)) + x3 + 0xf4d50d87, 14); + bb = cc + rol(bb + ((cc & aa) | (dd & ~aa)) + x8 + 0x455a14ed, 20); + aa = bb + rol(aa + ((bb & dd) | (cc & ~dd)) + x13 + 0xa9e3e905, 5); + dd = aa + rol(dd + ((aa & cc) | (bb & ~cc)) + x2 + 0xfcefa3f8, 9); + cc = dd + rol(cc + ((dd & bb) | (aa & ~bb)) + x7 + 0x676f02d9, 14); + bb = cc + rol(bb + ((cc & aa) | (dd & ~aa)) + x12 + 0x8d2a4c8a, 20); + + // round 3 + aa = bb + rol(aa + (bb ^ cc ^ dd) + x5 + 0xfffa3942, 4); + dd = aa + rol(dd + (aa ^ bb ^ cc) + x8 + 0x8771f681, 11); + cc = dd + rol(cc + (dd ^ aa ^ bb) + x11 + 0x6d9d6122, 16); + bb = cc + rol(bb + (cc ^ dd ^ aa) + x14 + 0xfde5380c, 23); + aa = bb + rol(aa + (bb ^ cc ^ dd) + x1 + 0xa4beea44, 4); + dd = aa + rol(dd + (aa ^ bb ^ cc) + x4 + 0x4bdecfa9, 11); + cc = dd + rol(cc + (dd ^ aa ^ bb) + x7 + 0xf6bb4b60, 16); + bb = cc + rol(bb + (cc ^ dd ^ aa) + x10 + 0xbebfbc70, 23); + aa = bb + rol(aa + (bb ^ cc ^ dd) + x13 + 0x289b7ec6, 4); + dd = aa + rol(dd + (aa ^ bb ^ cc) + x0 + 0xeaa127fa, 11); + cc = dd + rol(cc + (dd ^ aa ^ bb) + x3 + 0xd4ef3085, 16); + bb = cc + rol(bb + (cc ^ dd ^ aa) + x6 + 0x04881d05, 23); + aa = bb + rol(aa + (bb ^ cc ^ dd) + x9 + 0xd9d4d039, 4); + dd = aa + rol(dd + (aa ^ bb ^ cc) + x12 + 0xe6db99e5, 11); + cc = dd + rol(cc + (dd ^ aa ^ bb) + x15 + 0x1fa27cf8, 16); + bb = cc + rol(bb + (cc ^ dd ^ aa) + x2 + 0xc4ac5665, 23); + + // round 4 + aa = bb + rol(aa + (cc ^ (bb | ~dd)) + x0 + 0xf4292244, 6); + dd = aa + rol(dd + (bb ^ (aa | ~cc)) + x7 + 0x432aff97, 10); + cc = dd + rol(cc + (aa ^ (dd | ~bb)) + x14 + 0xab9423a7, 15); + bb = cc + rol(bb + (dd ^ (cc | ~aa)) + x5 + 0xfc93a039, 21); + aa = bb + rol(aa + (cc ^ (bb | ~dd)) + x12 + 0x655b59c3, 6); + dd = aa + rol(dd + (bb ^ (aa | ~cc)) + x3 + 0x8f0ccc92, 10); + cc = dd + rol(cc + (aa ^ (dd | ~bb)) + x10 + 0xffeff47d, 15); + bb = cc + rol(bb + (dd ^ (cc | ~aa)) + x1 + 0x85845dd1, 21); + aa = bb + rol(aa + (cc ^ (bb | ~dd)) + x8 + 0x6fa87e4f, 6); + dd = aa + rol(dd + (bb ^ (aa | ~cc)) + x15 + 0xfe2ce6e0, 10); + cc = dd + rol(cc + (aa ^ (dd | ~bb)) + x6 + 0xa3014314, 15); + bb = cc + rol(bb + (dd ^ (cc | ~aa)) + x13 + 0x4e0811a1, 21); + aa = bb + rol(aa + (cc ^ (bb | ~dd)) + x4 + 0xf7537e82, 6); + dd = aa + rol(dd + (bb ^ (aa | ~cc)) + x11 + 0xbd3af235, 10); + cc = dd + rol(cc + (aa ^ (dd | ~bb)) + x2 + 0x2ad7d2bb, 15); + bb = cc + rol(bb + (dd ^ (cc | ~aa)) + x9 + 0xeb86d391, 21); + + a += aa; + b += bb; + c += cc; + d += dd; + } + } + + // =============================================================== + // SHA-1 -- RFC 3174 + static final class Sha1 extends Block64 { + int h0; + int h1; + int h2; + int h3; + int h4; + final int[] w = new int[80]; + + Sha1() { + reset(); + } + + @Override + public void reset() { + h0 = 0x67452301; + h1 = 0xEFCDAB89; + h2 = 0x98BADCFE; + h3 = 0x10325476; + h4 = 0xC3D2E1F0; + bufferLen = 0; + byteCount = 0; + } + + @Override + public int digestLength() { + return 20; + } + + @Override + public byte[] digest() { + return finishCommon(true); + } + + @Override + void writeStateBigEndian(byte[] out) { + writeBE(out, 0, h0); + writeBE(out, 4, h1); + writeBE(out, 8, h2); + writeBE(out, 12, h3); + writeBE(out, 16, h4); + } + + @Override + void processBlock(byte[] block, int o) { + for (int i = 0; i < 16; i++) { + w[i] = (block[o + i * 4] & 0xff) << 24 + | (block[o + i * 4 + 1] & 0xff) << 16 + | (block[o + i * 4 + 2] & 0xff) << 8 + | (block[o + i * 4 + 3] & 0xff); + } + for (int i = 16; i < 80; i++) { + int t = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]; + w[i] = (t << 1) | (t >>> 31); + } + int a = h0; + int b = h1; + int c = h2; + int d = h3; + int e = h4; + for (int i = 0; i < 80; i++) { + int f; + int k; + if (i < 20) { + f = (b & c) | (~b & d); + k = 0x5A827999; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDC; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + int t = ((a << 5) | (a >>> 27)) + f + e + k + w[i]; + e = d; + d = c; + c = (b << 30) | (b >>> 2); + b = a; + a = t; + } + h0 += a; + h1 += b; + h2 += c; + h3 += d; + h4 += e; + } + } + + // =============================================================== + // SHA-224 / SHA-256 -- FIPS 180-4 (32-bit word version) + static final class Sha256Family extends Block64 { + private static final int[] K = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + }; + + private final boolean truncated; // sha-224 if true + private int h0; + private int h1; + private int h2; + private int h3; + private int h4; + private int h5; + private int h6; + private int h7; + private final int[] w = new int[64]; + + Sha256Family(boolean truncated) { + this.truncated = truncated; + reset(); + } + + @Override + public void reset() { + if (truncated) { + h0 = 0xc1059ed8; h1 = 0x367cd507; h2 = 0x3070dd17; h3 = 0xf70e5939; + h4 = 0xffc00b31; h5 = 0x68581511; h6 = 0x64f98fa7; h7 = 0xbefa4fa4; + } else { + h0 = 0x6a09e667; h1 = 0xbb67ae85; h2 = 0x3c6ef372; h3 = 0xa54ff53a; + h4 = 0x510e527f; h5 = 0x9b05688c; h6 = 0x1f83d9ab; h7 = 0x5be0cd19; + } + bufferLen = 0; + byteCount = 0; + } + + @Override + public int digestLength() { + return truncated ? 28 : 32; + } + + @Override + public byte[] digest() { + return finishCommon(true); + } + + @Override + void writeStateBigEndian(byte[] out) { + writeBE(out, 0, h0); + writeBE(out, 4, h1); + writeBE(out, 8, h2); + writeBE(out, 12, h3); + writeBE(out, 16, h4); + writeBE(out, 20, h5); + writeBE(out, 24, h6); + if (!truncated) { + writeBE(out, 28, h7); + } + } + + @Override + void processBlock(byte[] block, int o) { + for (int i = 0; i < 16; i++) { + w[i] = (block[o + i * 4] & 0xff) << 24 + | (block[o + i * 4 + 1] & 0xff) << 16 + | (block[o + i * 4 + 2] & 0xff) << 8 + | (block[o + i * 4 + 3] & 0xff); + } + for (int i = 16; i < 64; i++) { + int v15 = w[i - 15]; + int s0 = ((v15 >>> 7) | (v15 << 25)) ^ ((v15 >>> 18) | (v15 << 14)) ^ (v15 >>> 3); + int v2 = w[i - 2]; + int s1 = ((v2 >>> 17) | (v2 << 15)) ^ ((v2 >>> 19) | (v2 << 13)) ^ (v2 >>> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + int a = h0; + int b = h1; + int c = h2; + int d = h3; + int e = h4; + int f = h5; + int g = h6; + int h = h7; + for (int i = 0; i < 64; i++) { + int s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); + int ch = (e & f) ^ (~e & g); + int t1 = h + s1 + ch + K[i] + w[i]; + int s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); + int mj = (a & b) ^ (a & c) ^ (b & c); + int t2 = s0 + mj; + h = g; g = f; f = e; + e = d + t1; + d = c; c = b; b = a; + a = t1 + t2; + } + h0 += a; h1 += b; h2 += c; h3 += d; + h4 += e; h5 += f; h6 += g; h7 += h; + } + } + + // =============================================================== + // SHA-384 / SHA-512 -- FIPS 180-4 (64-bit word version, 128-byte blocks, + // 128-bit length field). Uses a 128-byte block engine. + static final class Sha512Family extends MessageDigestImpl { + private static final long[] K = { + 0x428a2f98d728ae22L, 0x7137449123ef65cdL, 0xb5c0fbcfec4d3b2fL, 0xe9b5dba58189dbbcL, + 0x3956c25bf348b538L, 0x59f111f1b605d019L, 0x923f82a4af194f9bL, 0xab1c5ed5da6d8118L, + 0xd807aa98a3030242L, 0x12835b0145706fbeL, 0x243185be4ee4b28cL, 0x550c7dc3d5ffb4e2L, + 0x72be5d74f27b896fL, 0x80deb1fe3b1696b1L, 0x9bdc06a725c71235L, 0xc19bf174cf692694L, + 0xe49b69c19ef14ad2L, 0xefbe4786384f25e3L, 0x0fc19dc68b8cd5b5L, 0x240ca1cc77ac9c65L, + 0x2de92c6f592b0275L, 0x4a7484aa6ea6e483L, 0x5cb0a9dcbd41fbd4L, 0x76f988da831153b5L, + 0x983e5152ee66dfabL, 0xa831c66d2db43210L, 0xb00327c898fb213fL, 0xbf597fc7beef0ee4L, + 0xc6e00bf33da88fc2L, 0xd5a79147930aa725L, 0x06ca6351e003826fL, 0x142929670a0e6e70L, + 0x27b70a8546d22ffcL, 0x2e1b21385c26c926L, 0x4d2c6dfc5ac42aedL, 0x53380d139d95b3dfL, + 0x650a73548baf63deL, 0x766a0abb3c77b2a8L, 0x81c2c92e47edaee6L, 0x92722c851482353bL, + 0xa2bfe8a14cf10364L, 0xa81a664bbc423001L, 0xc24b8b70d0f89791L, 0xc76c51a30654be30L, + 0xd192e819d6ef5218L, 0xd69906245565a910L, 0xf40e35855771202aL, 0x106aa07032bbd1b8L, + 0x19a4c116b8d2d0c8L, 0x1e376c085141ab53L, 0x2748774cdf8eeb99L, 0x34b0bcb5e19b48a8L, + 0x391c0cb3c5c95a63L, 0x4ed8aa4ae3418acbL, 0x5b9cca4f7763e373L, 0x682e6ff3d6b2b8a3L, + 0x748f82ee5defb2fcL, 0x78a5636f43172f60L, 0x84c87814a1f0ab72L, 0x8cc702081a6439ecL, + 0x90befffa23631e28L, 0xa4506cebde82bde9L, 0xbef9a3f7b2c67915L, 0xc67178f2e372532bL, + 0xca273eceea26619cL, 0xd186b8c721c0c207L, 0xeada7dd6cde0eb1eL, 0xf57d4f7fee6ed178L, + 0x06f067aa72176fbaL, 0x0a637dc5a2c898a6L, 0x113f9804bef90daeL, 0x1b710b35131c471bL, + 0x28db77f523047d84L, 0x32caab7b40c72493L, 0x3c9ebe0a15c9bebcL, 0x431d67c49c100d4cL, + 0x4cc5d4becb3e42b6L, 0x597f299cfc657e2aL, 0x5fcb6fab3ad6faecL, 0x6c44198c4a475817L + }; + + private final boolean truncated; // sha-384 if true + private long h0; + private long h1; + private long h2; + private long h3; + private long h4; + private long h5; + private long h6; + private long h7; + private final byte[] buffer = new byte[128]; + private int bufferLen; + private long byteCount; // we cap message length at 2^63-1 bytes which is plenty + private final long[] w = new long[80]; + + Sha512Family(boolean truncated) { + this.truncated = truncated; + reset(); + } + + @Override + public void reset() { + if (truncated) { + h0 = 0xcbbb9d5dc1059ed8L; h1 = 0x629a292a367cd507L; h2 = 0x9159015a3070dd17L; + h3 = 0x152fecd8f70e5939L; h4 = 0x67332667ffc00b31L; h5 = 0x8eb44a8768581511L; + h6 = 0xdb0c2e0d64f98fa7L; h7 = 0x47b5481dbefa4fa4L; + } else { + h0 = 0x6a09e667f3bcc908L; h1 = 0xbb67ae8584caa73bL; h2 = 0x3c6ef372fe94f82bL; + h3 = 0xa54ff53a5f1d36f1L; h4 = 0x510e527fade682d1L; h5 = 0x9b05688c2b3e6c1fL; + h6 = 0x1f83d9abfb41bd6bL; h7 = 0x5be0cd19137e2179L; + } + bufferLen = 0; + byteCount = 0; + } + + @Override + public int digestLength() { + return truncated ? 48 : 64; + } + + @Override + void update(byte[] data, int offset, int length) { + byteCount += length; + if (bufferLen > 0) { + int copy = 128 - bufferLen; + if (copy > length) { + copy = length; + } + System.arraycopy(data, offset, buffer, bufferLen, copy); + bufferLen += copy; + offset += copy; + length -= copy; + if (bufferLen == 128) { + processBlock(buffer, 0); + bufferLen = 0; + } + } + while (length >= 128) { + processBlock(data, offset); + offset += 128; + length -= 128; + } + if (length > 0) { + System.arraycopy(data, offset, buffer, 0, length); + bufferLen = length; + } + } + + @Override + void update(byte b) { + byteCount++; + buffer[bufferLen++] = b; + if (bufferLen == 128) { + processBlock(buffer, 0); + bufferLen = 0; + } + } + + @Override + public byte[] digest() { + long bits = byteCount * 8L; + buffer[bufferLen++] = (byte) 0x80; + if (bufferLen > 112) { + while (bufferLen < 128) { + buffer[bufferLen++] = 0; + } + processBlock(buffer, 0); + bufferLen = 0; + } + while (bufferLen < 112) { + buffer[bufferLen++] = 0; + } + // high 64 bits of the 128-bit length field are always 0 here since + // a Java byte array cannot hold more than 2^31-1 bytes. + for (int i = 112; i < 120; i++) { + buffer[i] = 0; + } + buffer[120] = (byte) (bits >>> 56); + buffer[121] = (byte) (bits >>> 48); + buffer[122] = (byte) (bits >>> 40); + buffer[123] = (byte) (bits >>> 32); + buffer[124] = (byte) (bits >>> 24); + buffer[125] = (byte) (bits >>> 16); + buffer[126] = (byte) (bits >>> 8); + buffer[127] = (byte) bits; + processBlock(buffer, 0); + + byte[] out = new byte[digestLength()]; + writeBE64(out, 0, h0); + writeBE64(out, 8, h1); + writeBE64(out, 16, h2); + writeBE64(out, 24, h3); + writeBE64(out, 32, h4); + writeBE64(out, 40, h5); + // sha-384 omits h6, h7 + if (!truncated) { + writeBE64(out, 48, h6); + writeBE64(out, 56, h7); + } + reset(); + return out; + } + + private void processBlock(byte[] block, int o) { + for (int i = 0; i < 16; i++) { + int p = o + i * 8; + w[i] = ((long) (block[p] & 0xff) << 56) + | ((long) (block[p + 1] & 0xff) << 48) + | ((long) (block[p + 2] & 0xff) << 40) + | ((long) (block[p + 3] & 0xff) << 32) + | ((long) (block[p + 4] & 0xff) << 24) + | ((long) (block[p + 5] & 0xff) << 16) + | ((long) (block[p + 6] & 0xff) << 8) + | ((long) (block[p + 7] & 0xff)); + } + for (int i = 16; i < 80; i++) { + long v15 = w[i - 15]; + long s0 = ((v15 >>> 1) | (v15 << 63)) + ^ ((v15 >>> 8) | (v15 << 56)) + ^ (v15 >>> 7); + long v2 = w[i - 2]; + long s1 = ((v2 >>> 19) | (v2 << 45)) + ^ ((v2 >>> 61) | (v2 << 3)) + ^ (v2 >>> 6); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + long a = h0; + long b = h1; + long c = h2; + long d = h3; + long e = h4; + long f = h5; + long g = h6; + long h = h7; + for (int i = 0; i < 80; i++) { + long s1 = ((e >>> 14) | (e << 50)) + ^ ((e >>> 18) | (e << 46)) + ^ ((e >>> 41) | (e << 23)); + long ch = (e & f) ^ (~e & g); + long t1 = h + s1 + ch + K[i] + w[i]; + long s0 = ((a >>> 28) | (a << 36)) + ^ ((a >>> 34) | (a << 30)) + ^ ((a >>> 39) | (a << 25)); + long mj = (a & b) ^ (a & c) ^ (b & c); + long t2 = s0 + mj; + h = g; g = f; f = e; + e = d + t1; + d = c; c = b; b = a; + a = t1 + t2; + } + h0 += a; h1 += b; h2 += c; h3 += d; + h4 += e; h5 += f; h6 += g; h7 += h; + } + } + + // =============================================================== + // shared little helpers + static void writeBE(byte[] out, int o, int v) { + out[o] = (byte) (v >>> 24); + out[o + 1] = (byte) (v >>> 16); + out[o + 2] = (byte) (v >>> 8); + out[o + 3] = (byte) v; + } + + static void writeBE64(byte[] out, int o, long v) { + out[o] = (byte) (v >>> 56); + out[o + 1] = (byte) (v >>> 48); + out[o + 2] = (byte) (v >>> 40); + out[o + 3] = (byte) (v >>> 32); + out[o + 4] = (byte) (v >>> 24); + out[o + 5] = (byte) (v >>> 16); + out[o + 6] = (byte) (v >>> 8); + out[o + 7] = (byte) v; + } +} diff --git a/CodenameOne/src/com/codename1/security/Otp.java b/CodenameOne/src/com/codename1/security/Otp.java new file mode 100644 index 0000000000..747fecd778 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Otp.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +/// Counter-based (HOTP, RFC 4226) and time-based (TOTP, RFC 6238) one-time +/// password generators. Compatible with any standard authenticator app +/// (Google Authenticator, Microsoft Authenticator, 1Password, etc.). +/// +/// #### Generate a 6-digit Google-Authenticator-compatible code +/// +/// ```java +/// byte[] secret = Base32.decode("JBSWY3DPEHPK3PXP"); // shared secret +/// String code = Otp.totp(secret); // default 6 digits, 30 second step, +/// // SHA-1, current time +/// ``` +/// +/// #### Verify a code (allowing +/-1 step of clock skew) +/// +/// ```java +/// boolean ok = Otp.verifyTotp(secret, userInput, 1); +/// ``` +public final class Otp { + + private Otp() {} + + /// Generates an HOTP code (RFC 4226) using SHA-1 and the given digit count. + /// + /// #### Parameters + /// + /// - `secret`: the shared secret + /// + /// - `counter`: the moving factor -- caller is responsible for incrementing + /// it after every successful authentication + /// + /// - `digits`: number of decimal digits in the output (typically 6, may + /// be 6, 7 or 8) + public static String hotp(byte[] secret, long counter, int digits) { + return hotp(secret, counter, digits, Hash.SHA1); + } + + /// Generates an HOTP code with a configurable hash algorithm. Most + /// authenticator apps assume SHA-1; only override if the issuer publishes a + /// different `algorithm` parameter in its provisioning URI. + public static String hotp(byte[] secret, long counter, int digits, String hashAlgorithm) { + if (digits < 1 || digits > 10) { + throw new CryptoException("digits must be between 1 and 10"); + } + byte[] counterBytes = new byte[8]; + for (int i = 7; i >= 0; i--) { + counterBytes[i] = (byte) (counter & 0xff); + counter >>>= 8; + } + byte[] mac = Hmac.create(hashAlgorithm, secret).doFinal(counterBytes); + // RFC 4226 dynamic truncation + int offset = mac[mac.length - 1] & 0x0f; + int code = ((mac[offset] & 0x7f) << 24) + | ((mac[offset + 1] & 0xff) << 16) + | ((mac[offset + 2] & 0xff) << 8) + | (mac[offset + 3] & 0xff); + int mod = 1; + for (int i = 0; i < digits; i++) { + mod *= 10; + } + code %= mod; + return pad(Integer.toString(code), digits); + } + + /// Generates a TOTP code (RFC 6238) for the current system time, using + /// SHA-1, 6 digits and a 30-second step. + public static String totp(byte[] secret) { + return totp(secret, System.currentTimeMillis(), 30, 6, Hash.SHA1); + } + + /// Generates a TOTP code for the current system time with a custom digit + /// count and step size. + public static String totp(byte[] secret, int digits, int stepSeconds) { + return totp(secret, System.currentTimeMillis(), stepSeconds, digits, Hash.SHA1); + } + + /// Generates a TOTP code with full control over all parameters. + /// + /// #### Parameters + /// + /// - `secret`: shared secret + /// + /// - `currentTimeMillis`: timestamp to derive the code from + /// + /// - `stepSeconds`: window size -- 30 in the vast majority of deployments + /// + /// - `digits`: number of decimal digits in the output (typically 6 or 8) + /// + /// - `hashAlgorithm`: hash to use -- almost always [Hash#SHA1] + public static String totp(byte[] secret, long currentTimeMillis, int stepSeconds, + int digits, String hashAlgorithm) { + if (stepSeconds <= 0) { + throw new CryptoException("stepSeconds must be positive"); + } + long counter = (currentTimeMillis / 1000L) / stepSeconds; + return hotp(secret, counter, digits, hashAlgorithm); + } + + /// Verifies a TOTP code, allowing `tolerance` steps of clock skew on either + /// side of `now` (so a tolerance of 1 will accept the previous, current + /// and next code). + public static boolean verifyTotp(byte[] secret, String code, int tolerance) { + return verifyTotp(secret, code, tolerance, + System.currentTimeMillis(), 30, 6, Hash.SHA1); + } + + /// Builds the canonical `otpauth://totp/...` URI that authenticator apps + /// (Google Authenticator, Microsoft Authenticator, 1Password, Authy, ...) + /// consume when the user scans a QR code on your enrolment screen. The + /// format is documented at + /// . + /// + /// Render the returned string as a QR code (server-side render, or a + /// QR-generation cn1lib) and show it to the user; they scan it, the + /// authenticator stores `secret` against the `issuer:accountName` label, + /// and from then on it produces six-digit codes that match + /// [#totp(byte[])] on your side using the same `secret`. + /// + /// #### Parameters + /// + /// - `issuer`: the human-readable service name shown in the authenticator + /// ("Acme Bank"). Must not contain a `:`. + /// + /// - `accountName`: the user's identifier within your service + /// ("alice@example.com"). Must not contain a `:`. + /// + /// - `secret`: shared secret (the bytes you also pass to + /// [#totp(byte[])]) -- encoded as Base32 in the URI per the spec. + /// + /// - `digits`: number of digits in each code (typically 6). + /// + /// - `stepSeconds`: time-step size, typically 30. + /// + /// - `hashAlgorithm`: hash, typically [Hash#SHA1] for authenticator + /// compatibility. SHA-256 and SHA-512 are accepted but not all + /// authenticator apps support them. + public static String otpauthUri(String issuer, String accountName, byte[] secret, + int digits, int stepSeconds, String hashAlgorithm) { + if (issuer == null || issuer.indexOf(':') >= 0) { + throw new CryptoException("issuer must be non-null and must not contain ':'"); + } + if (accountName == null || accountName.indexOf(':') >= 0) { + throw new CryptoException("accountName must be non-null and must not contain ':'"); + } + String alg = uriAlgorithm(hashAlgorithm); + StringBuilder b = new StringBuilder(128); + b.append("otpauth://totp/"); + appendUriComponent(b, issuer); + b.append(':'); + appendUriComponent(b, accountName); + b.append("?secret="); + b.append(Base32.encode(secret).replace("=", "")); + b.append("&issuer="); + appendUriComponent(b, issuer); + b.append("&algorithm="); + b.append(alg); + b.append("&digits="); + b.append(digits); + b.append("&period="); + b.append(stepSeconds); + return b.toString(); + } + + /// Convenience overload using the typical 6 digits / 30 seconds / SHA-1 + /// settings most authenticator apps expect. + public static String otpauthUri(String issuer, String accountName, byte[] secret) { + return otpauthUri(issuer, accountName, secret, 6, 30, Hash.SHA1); + } + + private static String uriAlgorithm(String hashAlgorithm) { + String n = hashAlgorithm == null ? "" : MessageDigestImpl.normalise(hashAlgorithm); + if ("SHA1".equals(n)) { + return "SHA1"; + } + if ("SHA256".equals(n)) { + return "SHA256"; + } + if ("SHA512".equals(n)) { + return "SHA512"; + } + throw new CryptoException("unsupported OTP URI algorithm: " + hashAlgorithm); + } + + /// Minimal RFC 3986 percent-encoder for URI path / query segments. The + /// Google Authenticator KeyUri format only ever contains issuer + account + /// name written by the host app, so we only need to escape the small set + /// of unsafe characters that show up there in practice. + private static void appendUriComponent(StringBuilder out, String s) { + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + boolean unreserved = (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || c == '-' || c == '_' || c == '.' || c == '~'; + if (unreserved) { + out.append(c); + } else if (c < 0x80) { + appendPercent(out, c); + } else { + // UTF-8 encode multi-byte chars + if (c < 0x800) { + appendPercent(out, 0xc0 | (c >>> 6)); + appendPercent(out, 0x80 | (c & 0x3f)); + } else { + appendPercent(out, 0xe0 | (c >>> 12)); + appendPercent(out, 0x80 | ((c >>> 6) & 0x3f)); + appendPercent(out, 0x80 | (c & 0x3f)); + } + } + } + } + + private static void appendPercent(StringBuilder out, int b) { + out.append('%'); + out.append(HEX[(b >>> 4) & 0x0f]); + out.append(HEX[b & 0x0f]); + } + + private static final char[] HEX = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + /// Verifies a TOTP code with full parameter control. + public static boolean verifyTotp(byte[] secret, String code, int tolerance, + long currentTimeMillis, int stepSeconds, + int digits, String hashAlgorithm) { + if (code == null) { + return false; + } + long counter = (currentTimeMillis / 1000L) / stepSeconds; + for (int s = -tolerance; s <= tolerance; s++) { + String candidate = hotp(secret, counter + s, digits, hashAlgorithm); + if (constantTimeEqualsString(candidate, code.trim())) { + return true; + } + } + return false; + } + + private static boolean constantTimeEqualsString(String a, String b) { + if (a == null || b == null || a.length() != b.length()) { + return false; + } + int diff = 0; + for (int i = 0; i < a.length(); i++) { + diff |= (a.charAt(i) ^ b.charAt(i)); + } + return diff == 0; + } + + private static String pad(String s, int digits) { + if (s.length() >= digits) { + return s; + } + StringBuilder b = new StringBuilder(digits); + for (int i = s.length(); i < digits; i++) { + b.append('0'); + } + b.append(s); + return b.toString(); + } +} diff --git a/CodenameOne/src/com/codename1/security/PrivateKey.java b/CodenameOne/src/com/codename1/security/PrivateKey.java new file mode 100644 index 0000000000..07e8cfb43a --- /dev/null +++ b/CodenameOne/src/com/codename1/security/PrivateKey.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +/// A private key -- paired with a [PublicKey] to form a key pair. Carries the +/// algorithm name ("RSA" or "EC") and the encoded key bytes. +/// +/// For interop with PEM files (`-----BEGIN PRIVATE KEY-----`) feed the +/// PKCS#8 DER bytes to [#fromPkcs8]. +public final class PrivateKey extends Key { + + PrivateKey(String algorithm, byte[] encoded, String format) { + super(algorithm, encoded, format == null ? "PKCS#8" : format); + } + + /// Wraps a PKCS#8 DER blob. This is the format produced by `openssl + /// pkcs8 -topk8 -nocrypt`. + public static PrivateKey fromPkcs8(String algorithm, byte[] pkcs8Der) { + return new PrivateKey(algorithm, pkcs8Der, "PKCS#8"); + } + + /// Convenience: build an RSA [PrivateKey] from a [#fromPkcs8] PKCS#8 blob. + public static PrivateKey rsa(byte[] pkcs8Der) { + return fromPkcs8(PublicKey.RSA, pkcs8Der); + } +} diff --git a/CodenameOne/src/com/codename1/security/PublicKey.java b/CodenameOne/src/com/codename1/security/PublicKey.java new file mode 100644 index 0000000000..39acaf51cb --- /dev/null +++ b/CodenameOne/src/com/codename1/security/PublicKey.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +/// A public key -- paired with a [PrivateKey] to form a key pair. Carries the +/// algorithm name ("RSA" or "EC") and the encoded key bytes. +/// +/// For interop with PEM files (`-----BEGIN PUBLIC KEY-----`) feed the +/// X.509-SubjectPublicKeyInfo (SPKI) DER bytes to [#fromX509]; for raw RSA +/// modulus/exponent use [#rsa]. +public final class PublicKey extends Key { + /// RSA algorithm identifier ("RSA"). + public static final String RSA = "RSA"; + /// Elliptic-curve algorithm identifier ("EC"). + public static final String EC = "EC"; + + PublicKey(String algorithm, byte[] encoded, String format) { + super(algorithm, encoded, format == null ? "X.509" : format); + } + + /// Wraps an X.509 / SubjectPublicKeyInfo (SPKI) DER blob. This is the + /// format produced by `openssl rsa -pubout` or `openssl ec -pubout`. + public static PublicKey fromX509(String algorithm, byte[] x509Der) { + return new PublicKey(algorithm, x509Der, "X.509"); + } + + /// Convenience: build an RSA [PublicKey] from a [#fromX509] X.509 blob. + public static PublicKey rsa(byte[] x509Der) { + return fromX509(RSA, x509Der); + } +} diff --git a/CodenameOne/src/com/codename1/security/SecretKey.java b/CodenameOne/src/com/codename1/security/SecretKey.java new file mode 100644 index 0000000000..f84f7cd717 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/SecretKey.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +/// A symmetric secret key. Used with [Cipher] (AES) and [Hmac] (via raw +/// bytes). For asymmetric algorithms use [PublicKey] / [PrivateKey]. +/// +/// Keys carry the raw key material plus the algorithm name they are intended +/// for. They do not enforce length or strength -- that is the caller's +/// responsibility, although [KeyGenerator] will produce keys of standard +/// lengths. +public final class SecretKey extends Key { + + /// Wraps existing key material. + /// + /// #### Parameters + /// + /// - `algorithm`: algorithm identifier (e.g. "AES") + /// + /// - `keyBytes`: raw key material -- defensively copied + public SecretKey(String algorithm, byte[] keyBytes) { + super(algorithm, keyBytes, "RAW"); + } + + /// Returns the length of the key in bits. + public int getBitLength() { + return getEncoded().length * 8; + } +} diff --git a/CodenameOne/src/com/codename1/security/SecureRandom.java b/CodenameOne/src/com/codename1/security/SecureRandom.java new file mode 100644 index 0000000000..5e34fb0ca1 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/SecureRandom.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +import com.codename1.io.Util; + +/// Cryptographically secure random number generator. Delegates to the +/// platform's native CSPRNG (`/dev/urandom` on Unix-style systems, the +/// Windows BCryptGenRandom on Windows, `SecRandomCopyBytes` on iOS, the +/// hardware RNG on devices that expose one). +/// +/// Use this class -- not `java.util.Random` or `Math.random()` -- whenever the +/// output is going to be used as a key, nonce, salt, session token, password +/// reset code or any other security-sensitive value. +/// +/// #### Example +/// +/// ```java +/// byte[] iv = SecureRandom.bytes(12); // AES-GCM nonce +/// int code = SecureRandom.intBelow(1_000_000); // 6-digit code +/// ``` +public final class SecureRandom { + + private SecureRandom() {} + + /// Returns a fresh `length`-byte array filled with secure random bytes. + public static byte[] bytes(int length) { + byte[] out = new byte[length]; + fill(out); + return out; + } + + /// Fills `out` with secure random bytes. + public static void fill(byte[] out) { + if (out == null) { + throw new CryptoException("out must not be null"); + } + try { + Util.secureRandomBytes(out); + } catch (RuntimeException re) { + throw new CryptoException(re.getMessage(), re); + } + } + + /// Returns a uniformly distributed random int in `[0, bound)`. `bound` + /// must be positive. + public static int intBelow(int bound) { + if (bound <= 0) { + throw new CryptoException("bound must be positive"); + } + // Rejection sampling to avoid modulo bias. + byte[] buf = new byte[4]; + while (true) { + fill(buf); + int v = ((buf[0] & 0x7f) << 24) + | ((buf[1] & 0xff) << 16) + | ((buf[2] & 0xff) << 8) + | (buf[3] & 0xff); + int m = v % bound; + if (v - m + (bound - 1) >= 0) { + return m; + } + } + } + + /// Returns a uniformly distributed random long in `[0, bound)`. `bound` + /// must be positive. + public static long longBelow(long bound) { + if (bound <= 0) { + throw new CryptoException("bound must be positive"); + } + byte[] buf = new byte[8]; + while (true) { + fill(buf); + long v = ((long) (buf[0] & 0x7f) << 56) + | ((long) (buf[1] & 0xff) << 48) + | ((long) (buf[2] & 0xff) << 40) + | ((long) (buf[3] & 0xff) << 32) + | ((long) (buf[4] & 0xff) << 24) + | ((long) (buf[5] & 0xff) << 16) + | ((long) (buf[6] & 0xff) << 8) + | (long) (buf[7] & 0xff); + long m = v % bound; + if (v - m + (bound - 1) >= 0) { + return m; + } + } + } +} diff --git a/CodenameOne/src/com/codename1/security/Signature.java b/CodenameOne/src/com/codename1/security/Signature.java new file mode 100644 index 0000000000..008213c142 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Signature.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.security; + +import com.codename1.io.Util; + +/// Digital signature creation and verification. Backed by the platform's +/// native crypto provider -- works with [PublicKey] / [PrivateKey] objects +/// from this package. +/// +/// #### Example: sign with RSA-SHA-256 and verify +/// +/// ```java +/// KeyPair kp = KeyGenerator.rsa(2048); +/// byte[] sig = Signature.sign(Signature.SHA256_WITH_RSA, kp.getPrivateKey(), data); +/// boolean ok = Signature.verify(Signature.SHA256_WITH_RSA, kp.getPublicKey(), data, sig); +/// ``` +public final class Signature { + + /// `SHA256withRSA` -- RSA PKCS#1 v1.5 with SHA-256. + public static final String SHA256_WITH_RSA = "SHA256withRSA"; + /// `SHA384withRSA` -- RSA PKCS#1 v1.5 with SHA-384. + public static final String SHA384_WITH_RSA = "SHA384withRSA"; + /// `SHA512withRSA` -- RSA PKCS#1 v1.5 with SHA-512. + public static final String SHA512_WITH_RSA = "SHA512withRSA"; + + /// `SHA256withECDSA` -- ECDSA with SHA-256 (P-256 curve). + public static final String SHA256_WITH_ECDSA = "SHA256withECDSA"; + /// `SHA384withECDSA` -- ECDSA with SHA-384 (P-384 curve). + public static final String SHA384_WITH_ECDSA = "SHA384withECDSA"; + /// `SHA512withECDSA` -- ECDSA with SHA-512 (P-521 curve). + public static final String SHA512_WITH_ECDSA = "SHA512withECDSA"; + + private Signature() {} + + /// Signs `data` with the given algorithm and private key. + public static byte[] sign(String algorithm, PrivateKey key, byte[] data) { + try { + return Util.cryptoSign(algorithm, key.getAlgorithm(), key.getEncoded(), data); + } catch (RuntimeException re) { + throw new CryptoException(re.getMessage(), re); + } + } + + /// Verifies `signature` against `data` using the given algorithm and + /// public key. + public static boolean verify(String algorithm, PublicKey key, byte[] data, byte[] signature) { + try { + return Util.cryptoVerify(algorithm, key.getAlgorithm(), key.getEncoded(), data, signature); + } catch (RuntimeException re) { + throw new CryptoException(re.getMessage(), re); + } + } +} diff --git a/CodenameOne/src/com/codename1/security/package-info.java b/CodenameOne/src/com/codename1/security/package-info.java new file mode 100644 index 0000000000..060d2ba697 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/package-info.java @@ -0,0 +1,40 @@ +/// Cryptographic primitives and conveniences: hashing, message authentication, +/// symmetric/asymmetric encryption, digital signatures, JWTs, OTPs and +/// random number generation. +/// +/// #### What lives in this package +/// +/// - [Hash] / [Hmac] -- pure-Java MD5, SHA-1, SHA-224, SHA-256, SHA-384, +/// SHA-512 and HMAC variants. Available on every platform with identical +/// output. +/// - [SecureRandom] -- wraps the platform CSPRNG. +/// - [Cipher] -- AES (CBC, GCM, ECB) and RSA (OAEP, PKCS#1) encryption. Backed +/// by the platform's native crypto. +/// - [Signature] -- RSA and ECDSA digital signatures. +/// - [KeyGenerator] / [KeyPair] / [SecretKey] / [PublicKey] / [PrivateKey] -- +/// key material containers and generators. +/// - [Jwt] -- JSON Web Token signing and verification (HS, RS and ES families). +/// - [Otp] -- RFC 4226/6238 HOTP and TOTP one-time passwords, compatible with +/// standard authenticator apps. +/// - [Base32] -- 32-character encoding commonly used for OTP shared secrets. +/// URL-safe Base64 (used by JWTs) lives on +/// [com.codename1.util.Base64#encodeUrlSafe(byte[])] / +/// [com.codename1.util.Base64#decodeUrlSafe(String)] so it can share the +/// existing SIMD-optimized encoder. +/// +/// For a segmented OTP input widget see +/// [com.codename1.components.OtpField]. +/// +/// #### Design notes +/// +/// Hash and HMAC ship a built-in implementation written in portable Java so +/// they work everywhere without depending on the platform's crypto stack -- +/// they are also what JWT (HS family), HOTP and TOTP build on. +/// +/// AES, RSA, digital signatures and the secure RNG go through the platform's +/// native crypto provider via +/// [com.codename1.impl.CodenameOneImplementation]. The default implementation +/// uses the JRE's `java.security` / `javax.crypto` via reflection, so JavaSE +/// (simulator) and Android work out of the box. Other ports may override the +/// bridge methods with direct native calls. +package com.codename1.security; diff --git a/CodenameOne/src/com/codename1/util/Base64.java b/CodenameOne/src/com/codename1/util/Base64.java index 75caf6f04e..392a089655 100644 --- a/CodenameOne/src/com/codename1/util/Base64.java +++ b/CodenameOne/src/com/codename1/util/Base64.java @@ -349,6 +349,77 @@ public static String encodeNoNewline(byte[] in) { return com.codename1.util.StringUtil.newString(out, 0, outputLength); } + /// URL-safe Base64 encoding per RFC 4648 sec5: `+` becomes `-`, `/` becomes + /// `_`, and the trailing `=` padding is dropped. This is the encoding + /// used by JWTs and most modern web token formats. Reuses the same + /// SIMD-optimized encode path as [#encodeNoNewline(byte[])] under the + /// hood, so it is just as fast. + /// + /// #### Parameters + /// + /// - `in`: the array to encode + /// + /// #### Returns + /// + /// the URL-safe Base64 string with no padding + public static String encodeUrlSafe(byte[] in) { + int inputLength = in.length; + if (inputLength == 0) { + return ""; + } + int outputLength = ((inputLength + 2) / 3) * 4; + byte[] out = allocByteMaybeSimd(outputLength); + encodeNoNewline(in, out); + // Map standard alphabet to URL-safe alphabet and trim trailing '=' + int unpadded = outputLength; + while (unpadded > 0 && out[unpadded - 1] == '=') { + unpadded--; + } + for (int i = 0; i < unpadded; i++) { + byte c = out[i]; + if (c == '+') { + out[i] = '-'; + } else if (c == '/') { + out[i] = '_'; + } + } + return com.codename1.util.StringUtil.newString(out, 0, unpadded); + } + + /// Decodes a URL-safe Base64 string (RFC 4648 sec5). Padding is optional -- + /// the canonical URL-safe form drops it, but this method also accepts + /// strings that still carry trailing `=`. + /// + /// #### Parameters + /// + /// - `s`: URL-safe Base64 string + /// + /// #### Returns + /// + /// the decoded bytes + public static byte[] decodeUrlSafe(String s) { + if (s == null || s.length() == 0) { + return new byte[0]; + } + int len = s.length(); + int pad = (4 - (len & 3)) & 3; + byte[] buf = new byte[len + pad]; + for (int i = 0; i < len; i++) { + char c = s.charAt(i); + if (c == '-') { + buf[i] = '+'; + } else if (c == '_') { + buf[i] = '/'; + } else { + buf[i] = (byte) c; + } + } + for (int i = 0; i < pad; i++) { + buf[len + i] = '='; + } + return decode(buf, buf.length); + } + /// Encodes input into a caller-provided output buffer without line breaks. /// /// #### Parameters diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index ba7c25c65c..8bc2539018 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -11389,4 +11389,123 @@ public void run() { } }); } + + // ================================================================ + // Crypto bridge -- routes com.codename1.security onto the standard + // Android JCE provider. + + private static java.security.SecureRandom androidSecureRandom; + private static final Object androidSecureRandomSync = new Object(); + + private static java.security.SecureRandom androidSecureRandom() { + synchronized (androidSecureRandomSync) { + if (androidSecureRandom == null) { + androidSecureRandom = new java.security.SecureRandom(); + } + return androidSecureRandom; + } + } + + @Override + public void secureRandomBytes(byte[] out) { + if (out == null) return; + androidSecureRandom().nextBytes(out); + } + + @Override + public byte[] aesEncrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] plaintext) { + return androidAes(transformation, key, iv, aad, plaintext, javax.crypto.Cipher.ENCRYPT_MODE); + } + + @Override + public byte[] aesDecrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] ciphertext) { + return androidAes(transformation, key, iv, aad, ciphertext, javax.crypto.Cipher.DECRYPT_MODE); + } + + private static byte[] androidAes(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] input, int mode) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(transformation); + javax.crypto.spec.SecretKeySpec keySpec = new javax.crypto.spec.SecretKeySpec(key, "AES"); + String tu = transformation == null ? "" : transformation.toUpperCase(); + if (tu.indexOf("GCM") >= 0) { + cipher.init(mode, keySpec, new javax.crypto.spec.GCMParameterSpec(128, iv)); + } else if (iv != null) { + cipher.init(mode, keySpec, new javax.crypto.spec.IvParameterSpec(iv)); + } else { + cipher.init(mode, keySpec); + } + if (aad != null && aad.length > 0) { + cipher.updateAAD(aad); + } + return cipher.doFinal(input); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("AES " + (mode == javax.crypto.Cipher.ENCRYPT_MODE ? "encrypt" : "decrypt") + " failed: " + e.getMessage()); + } + } + + @Override + public byte[] rsaEncrypt(String transformation, byte[] publicKeyX509, byte[] plaintext) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(transformation); + java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA"); + java.security.PublicKey key = kf.generatePublic(new java.security.spec.X509EncodedKeySpec(publicKeyX509)); + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, key); + return cipher.doFinal(plaintext); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("RSA encrypt failed: " + e.getMessage()); + } + } + + @Override + public byte[] rsaDecrypt(String transformation, byte[] privateKeyPkcs8, byte[] ciphertext) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(transformation); + java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA"); + java.security.PrivateKey key = kf.generatePrivate(new java.security.spec.PKCS8EncodedKeySpec(privateKeyPkcs8)); + cipher.init(javax.crypto.Cipher.DECRYPT_MODE, key); + return cipher.doFinal(ciphertext); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("RSA decrypt failed: " + e.getMessage()); + } + } + + @Override + public byte[] cryptoSign(String algorithm, String keyAlgorithm, byte[] privateKeyPkcs8, byte[] data) { + try { + java.security.KeyFactory kf = java.security.KeyFactory.getInstance(keyAlgorithm); + java.security.PrivateKey priv = kf.generatePrivate(new java.security.spec.PKCS8EncodedKeySpec(privateKeyPkcs8)); + java.security.Signature sig = java.security.Signature.getInstance(algorithm); + sig.initSign(priv); + sig.update(data); + return sig.sign(); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("sign failed: " + e.getMessage()); + } + } + + @Override + public boolean cryptoVerify(String algorithm, String keyAlgorithm, byte[] publicKeyX509, byte[] data, byte[] signature) { + try { + java.security.KeyFactory kf = java.security.KeyFactory.getInstance(keyAlgorithm); + java.security.PublicKey pub = kf.generatePublic(new java.security.spec.X509EncodedKeySpec(publicKeyX509)); + java.security.Signature sig = java.security.Signature.getInstance(algorithm); + sig.initVerify(pub); + sig.update(data); + return sig.verify(signature); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("verify failed: " + e.getMessage()); + } + } + + @Override + public byte[][] generateRsaKeyPair(int bits) { + try { + java.security.KeyPairGenerator kpg = java.security.KeyPairGenerator.getInstance("RSA"); + kpg.initialize(bits); + java.security.KeyPair kp = kpg.generateKeyPair(); + return new byte[][]{ kp.getPublic().getEncoded(), kp.getPrivate().getEncoded() }; + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("RSA keypair generation failed: " + e.getMessage()); + } + } } diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index 5cb128080d..3d31220c4e 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -15796,7 +15796,128 @@ public void nativeBrowserWindowAddCloseListener(Object window, com.codename1.ui. @Override public void nativeBrowserWindowRemoveCloseListener(Object window, com.codename1.ui.events.ActionListener l) { ((AbstractBrowserWindowSE)window).removeCloseListener(l); - + } // END NATIVE BROWSER WINDOW METHODS--------------------------------------------------------- + + // ================================================================ + // Crypto bridge -- routes the com.codename1.security API onto the + // JCE/JDK crypto provider available on JavaSE/Android. iOS does + // not reach this code path; it overrides the same methods on + // CodenameOneImplementation via IOSImplementation + CN1Crypto.{h,m}. + + private static java.security.SecureRandom javaseSecureRandom; + private static final Object javaseSecureRandomSync = new Object(); + + private static java.security.SecureRandom javaseSecureRandom() { + synchronized (javaseSecureRandomSync) { + if (javaseSecureRandom == null) { + javaseSecureRandom = new java.security.SecureRandom(); + } + return javaseSecureRandom; + } + } + + @Override + public void secureRandomBytes(byte[] out) { + if (out == null) return; + javaseSecureRandom().nextBytes(out); + } + + @Override + public byte[] aesEncrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] plaintext) { + return javaseAes(transformation, key, iv, aad, plaintext, javax.crypto.Cipher.ENCRYPT_MODE); + } + + @Override + public byte[] aesDecrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] ciphertext) { + return javaseAes(transformation, key, iv, aad, ciphertext, javax.crypto.Cipher.DECRYPT_MODE); + } + + private static byte[] javaseAes(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] input, int mode) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(transformation); + javax.crypto.spec.SecretKeySpec keySpec = new javax.crypto.spec.SecretKeySpec(key, "AES"); + String tu = transformation == null ? "" : transformation.toUpperCase(); + if (tu.indexOf("GCM") >= 0) { + cipher.init(mode, keySpec, new javax.crypto.spec.GCMParameterSpec(128, iv)); + } else if (iv != null) { + cipher.init(mode, keySpec, new javax.crypto.spec.IvParameterSpec(iv)); + } else { + cipher.init(mode, keySpec); + } + if (aad != null && aad.length > 0) { + cipher.updateAAD(aad); + } + return cipher.doFinal(input); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("AES " + (mode == javax.crypto.Cipher.ENCRYPT_MODE ? "encrypt" : "decrypt") + " failed: " + e.getMessage()); + } + } + + @Override + public byte[] rsaEncrypt(String transformation, byte[] publicKeyX509, byte[] plaintext) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(transformation); + java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA"); + java.security.PublicKey key = kf.generatePublic(new java.security.spec.X509EncodedKeySpec(publicKeyX509)); + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, key); + return cipher.doFinal(plaintext); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("RSA encrypt failed: " + e.getMessage()); + } + } + + @Override + public byte[] rsaDecrypt(String transformation, byte[] privateKeyPkcs8, byte[] ciphertext) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(transformation); + java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA"); + java.security.PrivateKey key = kf.generatePrivate(new java.security.spec.PKCS8EncodedKeySpec(privateKeyPkcs8)); + cipher.init(javax.crypto.Cipher.DECRYPT_MODE, key); + return cipher.doFinal(ciphertext); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("RSA decrypt failed: " + e.getMessage()); + } + } + + @Override + public byte[] cryptoSign(String algorithm, String keyAlgorithm, byte[] privateKeyPkcs8, byte[] data) { + try { + java.security.KeyFactory kf = java.security.KeyFactory.getInstance(keyAlgorithm); + java.security.PrivateKey priv = kf.generatePrivate(new java.security.spec.PKCS8EncodedKeySpec(privateKeyPkcs8)); + java.security.Signature sig = java.security.Signature.getInstance(algorithm); + sig.initSign(priv); + sig.update(data); + return sig.sign(); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("sign failed: " + e.getMessage()); + } + } + + @Override + public boolean cryptoVerify(String algorithm, String keyAlgorithm, byte[] publicKeyX509, byte[] data, byte[] signature) { + try { + java.security.KeyFactory kf = java.security.KeyFactory.getInstance(keyAlgorithm); + java.security.PublicKey pub = kf.generatePublic(new java.security.spec.X509EncodedKeySpec(publicKeyX509)); + java.security.Signature sig = java.security.Signature.getInstance(algorithm); + sig.initVerify(pub); + sig.update(data); + return sig.verify(signature); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("verify failed: " + e.getMessage()); + } + } + + @Override + public byte[][] generateRsaKeyPair(int bits) { + try { + java.security.KeyPairGenerator kpg = java.security.KeyPairGenerator.getInstance("RSA"); + kpg.initialize(bits); + java.security.KeyPair kp = kpg.generateKeyPair(); + return new byte[][]{ kp.getPublic().getEncoded(), kp.getPrivate().getEncoded() }; + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("RSA keypair generation failed: " + e.getMessage()); + } + } } diff --git a/Ports/iOSPort/nativeSources/CN1Crypto.h b/Ports/iOSPort/nativeSources/CN1Crypto.h new file mode 100644 index 0000000000..281396b415 --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1Crypto.h @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * iOS native crypto helpers used by the Java com.codename1.security package. + * + * Each function is a plain C function so it can be called from the + * ParparVM-generated bridge that backs the native methods declared on + * IOSNative. The matching Java side lives in + * CodenameOne/src/com/codename1/security/ (all classes) + * (and the bridge methods on CodenameOneImplementation are overridden by + * IOSImplementation to call these helpers). + * + * Implementations use the platform-supplied Security framework (Apple + * CryptoKit on iOS 13+ would be cleaner but we still target iOS 11 and need + * to support the older path) and CommonCrypto. + * + * All buffers are caller-allocated. Return values are byte lengths actually + * written, or a negative value on error (see CN1_CRYPTO_E_* constants). + */ + +#ifndef CN1Crypto_h +#define CN1Crypto_h + +#import + +/* + * Build-system toggles. IPhoneBuilder (maven plugin + BuildDaemon) scans the + * user's compiled bytecode for references to com.codename1.security.* and + * flips the placeholders below to enable the matching code paths. Apps that + * don't use the crypto API end up with no extra crypto symbols in the + * binary -- in particular the AES-GCM SPI references stay completely out + * unless the app opts into GCM via the ios.crypto.gcm build hint. + */ +//#define CN1_INCLUDE_CRYPTO +//#define CN1_INCLUDE_CRYPTO_GCM + +#define CN1_CRYPTO_E_GENERIC -1 +#define CN1_CRYPTO_E_BAD_KEY -2 +#define CN1_CRYPTO_E_BAD_INPUT -3 +#define CN1_CRYPTO_E_AUTH_FAIL -4 +#define CN1_CRYPTO_E_UNSUPPORTED -5 + +/* --- secure random ----------------------------------------------------- */ + +/// Fills `out` with `len` cryptographically secure random bytes via +/// SecRandomCopyBytes. Returns 0 on success, negative on error. +int cn1_crypto_secure_random(uint8_t* out, int len); + +/* --- AES --------------------------------------------------------------- */ + +/// AES-CBC encrypt / decrypt using CommonCrypto. `iv` must be 16 bytes, +/// `key` 16/24/32 bytes. `padding` is 1 for PKCS5 / 7, 0 for none. +/// Returns bytes written into `out`, or negative on error. `out` must be +/// pre-allocated to at least `inLen + 16` bytes. +int cn1_crypto_aes_cbc(int encrypt, const uint8_t* key, int keyLen, + const uint8_t* iv, + const uint8_t* in, int inLen, + uint8_t* out, int outCap, int padding); + +/// AES-GCM encrypt / decrypt. `iv` is the 12-byte nonce. `aad` may be NULL. +/// On encrypt, the 16-byte auth tag is APPENDED to the ciphertext (JCE +/// convention). On decrypt, the last 16 bytes of `in` are the tag. +/// Returns bytes written to `out`, or negative on error. +int cn1_crypto_aes_gcm(int encrypt, const uint8_t* key, int keyLen, + const uint8_t* iv, int ivLen, + const uint8_t* aad, int aadLen, + const uint8_t* in, int inLen, + uint8_t* out, int outCap); + +/* --- RSA --------------------------------------------------------------- */ + +/// RSA encrypt with the given X.509 SubjectPublicKeyInfo DER bytes. +/// `paddingKind` = 1 for PKCS#1, 2 for OAEP-SHA-256. +/// Returns bytes written to `out` or negative. +int cn1_crypto_rsa_encrypt(int paddingKind, + const uint8_t* x509, int x509Len, + const uint8_t* in, int inLen, + uint8_t* out, int outCap); + +/// RSA decrypt with the given PKCS#8 DER bytes. +int cn1_crypto_rsa_decrypt(int paddingKind, + const uint8_t* pkcs8, int pkcs8Len, + const uint8_t* in, int inLen, + uint8_t* out, int outCap); + +/* --- Signatures -------------------------------------------------------- */ + +/// Sign `data` with the given PKCS#8 private key. `algorithm` codes: +/// 0 = SHA256withRSA 1 = SHA384withRSA 2 = SHA512withRSA +/// 3 = SHA256withECDSA 4 = SHA384withECDSA 5 = SHA512withECDSA +/// Returns bytes written to `out` or negative. +int cn1_crypto_sign(int algorithm, + const uint8_t* pkcs8, int pkcs8Len, + const uint8_t* data, int dataLen, + uint8_t* out, int outCap); + +/// Verify a signature with the given X.509 public key. +/// Returns 1 on valid, 0 on invalid, negative on error. +int cn1_crypto_verify(int algorithm, + const uint8_t* x509, int x509Len, + const uint8_t* data, int dataLen, + const uint8_t* sig, int sigLen); + +/* --- RSA key-pair generation ------------------------------------------ */ + +/// Generates an RSA key pair of the given size in bits. +/// Caller pre-allocates `outPub` (X.509) and `outPriv` (PKCS#8) buffers. +/// `pubCap` / `privCap` are their sizes in bytes; the actual byte counts +/// written are stored at `*pubLen` / `*privLen`. +/// Returns 0 on success, negative on error. +int cn1_crypto_generate_rsa_keypair(int bits, + uint8_t* outPub, int pubCap, int* pubLen, + uint8_t* outPriv, int privCap, int* privLen); + +#endif /* CN1Crypto_h */ diff --git a/Ports/iOSPort/nativeSources/CN1Crypto.m b/Ports/iOSPort/nativeSources/CN1Crypto.m new file mode 100644 index 0000000000..7de2179a7a --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1Crypto.m @@ -0,0 +1,508 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +#import "CN1Crypto.h" + +#ifdef CN1_INCLUDE_CRYPTO + +#import +#import +#import +#import + +#ifdef CN1_INCLUDE_CRYPTO_GCM +/* + * CommonCrypto exposes AES-GCM only through the SPI header + * , which is not in the public iOS SDK. The + * functions and the kCCModeGCM value below are stable across all current iOS + * versions and are exported from libcommonCrypto.dylib at runtime -- we + * declare them here as externs so we can call them without depending on the + * private header. These symbols are only referenced when the app explicitly + * opts into AES-GCM via the ios.crypto.gcm build hint. + */ +enum { kCCModeGCM = 11 }; +extern CCCryptorStatus CCCryptorGCMAddIV(CCCryptorRef ref, const void* iv, size_t ivLen); +extern CCCryptorStatus CCCryptorGCMAddAAD(CCCryptorRef ref, const void* aData, size_t aDataLen); +extern CCCryptorStatus CCCryptorGCMFinal(CCCryptorRef ref, void* tag, size_t* tagLen); +#endif + +/* --- secure random ----------------------------------------------------- */ + +int cn1_crypto_secure_random(uint8_t* out, int len) { + if (len <= 0) return 0; + if (SecRandomCopyBytes(kSecRandomDefault, len, out) != errSecSuccess) { + return CN1_CRYPTO_E_GENERIC; + } + return 0; +} + +/* --- AES-CBC ----------------------------------------------------------- */ + +int cn1_crypto_aes_cbc(int encrypt, const uint8_t* key, int keyLen, + const uint8_t* iv, + const uint8_t* in, int inLen, + uint8_t* out, int outCap, int padding) { + if (keyLen != kCCKeySizeAES128 && keyLen != kCCKeySizeAES192 && keyLen != kCCKeySizeAES256) { + return CN1_CRYPTO_E_BAD_KEY; + } + size_t produced = 0; + CCOptions opts = padding ? kCCOptionPKCS7Padding : 0; + CCCryptorStatus s = CCCrypt( + encrypt ? kCCEncrypt : kCCDecrypt, + kCCAlgorithmAES, + opts, + key, (size_t) keyLen, + iv, + in, (size_t) inLen, + out, (size_t) outCap, + &produced); + if (s != kCCSuccess) { + return CN1_CRYPTO_E_GENERIC; + } + return (int) produced; +} + +/* --- AES-GCM ----------------------------------------------------------- */ + +int cn1_crypto_aes_gcm(int encrypt, const uint8_t* key, int keyLen, + const uint8_t* iv, int ivLen, + const uint8_t* aad, int aadLen, + const uint8_t* in, int inLen, + uint8_t* out, int outCap) { +#ifndef CN1_INCLUDE_CRYPTO_GCM + (void) encrypt; (void) key; (void) keyLen; (void) iv; (void) ivLen; + (void) aad; (void) aadLen; (void) in; (void) inLen; (void) out; (void) outCap; + return CN1_CRYPTO_E_UNSUPPORTED; +#else + if (keyLen != kCCKeySizeAES128 && keyLen != kCCKeySizeAES192 && keyLen != kCCKeySizeAES256) { + return CN1_CRYPTO_E_BAD_KEY; + } + if (ivLen != 12) { + return CN1_CRYPTO_E_BAD_INPUT; + } + + // CommonCrypto's GCM API works via CCCryptorCreateWithMode + GCM-specific + // calls (CCCryptorGCMAddIV / GCMaddAAD / GCMFinal). These are documented + // but the headers mark them as deprecated -- on iOS 13+ we should use the + // CryptoKit AES.GCM interface instead. We try CryptoKit first via NS APIs + // and fall back to the deprecated CCCryptor path. + CCCryptorRef cryptor = NULL; + CCCryptorStatus s = CCCryptorCreateWithMode( + encrypt ? kCCEncrypt : kCCDecrypt, + kCCModeGCM, + kCCAlgorithmAES, + ccNoPadding, + NULL, /* IV set separately via GCMAddIV */ + key, (size_t) keyLen, + NULL, 0, + 0, 0, + &cryptor); + if (s != kCCSuccess) { + return CN1_CRYPTO_E_GENERIC; + } + + if (CCCryptorGCMAddIV(cryptor, iv, ivLen) != kCCSuccess) { + CCCryptorRelease(cryptor); + return CN1_CRYPTO_E_GENERIC; + } + if (aad != NULL && aadLen > 0) { + if (CCCryptorGCMAddAAD(cryptor, aad, aadLen) != kCCSuccess) { + CCCryptorRelease(cryptor); + return CN1_CRYPTO_E_GENERIC; + } + } + + int dataLen = encrypt ? inLen : (inLen - 16); + if (dataLen < 0) { + CCCryptorRelease(cryptor); + return CN1_CRYPTO_E_BAD_INPUT; + } + + size_t produced = 0; + if (CCCryptorUpdate(cryptor, in, dataLen, out, outCap, &produced) != kCCSuccess) { + CCCryptorRelease(cryptor); + return CN1_CRYPTO_E_GENERIC; + } + + uint8_t tag[16]; + size_t tagLen = sizeof(tag); + if (encrypt) { + if (CCCryptorGCMFinal(cryptor, tag, &tagLen) != kCCSuccess) { + CCCryptorRelease(cryptor); + return CN1_CRYPTO_E_GENERIC; + } + if ((int)(produced + tagLen) > outCap) { + CCCryptorRelease(cryptor); + return CN1_CRYPTO_E_BAD_INPUT; + } + memcpy(out + produced, tag, tagLen); + produced += tagLen; + } else { + if (CCCryptorGCMFinal(cryptor, tag, &tagLen) != kCCSuccess) { + CCCryptorRelease(cryptor); + return CN1_CRYPTO_E_GENERIC; + } + // Constant-time compare against the tag carried in the input + const uint8_t* expectedTag = in + dataLen; + int diff = 0; + for (int i = 0; i < 16; i++) diff |= (expectedTag[i] ^ tag[i]); + if (diff != 0) { + CCCryptorRelease(cryptor); + return CN1_CRYPTO_E_AUTH_FAIL; + } + } + + CCCryptorRelease(cryptor); + return (int) produced; +#endif /* CN1_INCLUDE_CRYPTO_GCM */ +} + +/* --- RSA --------------------------------------------------------------- */ + +static SecKeyRef cn1_load_rsa_public(const uint8_t* x509, int x509Len) { + // Strip the SubjectPublicKeyInfo wrapper: SecKeyCreateWithData on iOS only + // accepts the bare PKCS#1 RSA public key body (modulus + exponent ASN.1). + // We do this with a small DER parser tailored to the SPKI structure: + // SEQUENCE { algIdentifier SEQUENCE, BIT STRING { PKCS#1 } } + if (x509Len < 2 || x509[0] != 0x30) return NULL; + int p = 1; + int seqLen, sl; + if ((x509[p] & 0x80) == 0) { seqLen = x509[p]; p++; } + else { + sl = x509[p] & 0x7f; p++; + if (sl > 4 || p + sl > x509Len) return NULL; + seqLen = 0; + for (int i = 0; i < sl; i++) seqLen = (seqLen << 8) | x509[p + i]; + p += sl; + } + (void)seqLen; + // skip the algIdentifier inner SEQUENCE + if (p >= x509Len || x509[p] != 0x30) return NULL; + p++; + int innerLen; + if ((x509[p] & 0x80) == 0) { innerLen = x509[p]; p++; } + else { sl = x509[p] & 0x7f; p++; innerLen = 0; for (int i = 0; i < sl; i++) innerLen = (innerLen << 8) | x509[p + i]; p += sl; } + p += innerLen; + // BIT STRING + if (p >= x509Len || x509[p] != 0x03) return NULL; + p++; + int bitLen; + if ((x509[p] & 0x80) == 0) { bitLen = x509[p]; p++; } + else { sl = x509[p] & 0x7f; p++; bitLen = 0; for (int i = 0; i < sl; i++) bitLen = (bitLen << 8) | x509[p + i]; p += sl; } + if (p >= x509Len || x509[p] != 0x00) return NULL; // unused bits + p++; + // The remainder is the PKCS#1 RSAPublicKey + NSData* keyData = [NSData dataWithBytes:(x509 + p) length:(x509Len - p)]; + NSDictionary* attrs = @{ + (id) kSecAttrKeyType: (id) kSecAttrKeyTypeRSA, + (id) kSecAttrKeyClass: (id) kSecAttrKeyClassPublic, + (id) kSecAttrKeySizeInBits: @(([keyData length] * 8)) + }; + CFErrorRef error = NULL; + SecKeyRef key = SecKeyCreateWithData((__bridge CFDataRef) keyData, + (__bridge CFDictionaryRef) attrs, + &error); + if (error) CFRelease(error); + return key; +} + +static SecKeyRef cn1_load_rsa_private(const uint8_t* pkcs8, int pkcs8Len) { + // PKCS#8 wraps a PKCS#1 RSAPrivateKey. We similarly extract the inner + // key blob. Structure: SEQUENCE { INT 0, algId, OCTET STRING { PKCS#1 } } + if (pkcs8Len < 2 || pkcs8[0] != 0x30) return NULL; + int p = 1, sl, len; + if ((pkcs8[p] & 0x80) == 0) { len = pkcs8[p]; p++; } + else { sl = pkcs8[p] & 0x7f; p++; len = 0; for (int i = 0; i < sl; i++) len = (len << 8) | pkcs8[p + i]; p += sl; } + // INTEGER 0 + if (p >= pkcs8Len || pkcs8[p] != 0x02) return NULL; + p++; len = pkcs8[p]; p++; p += len; + // algIdentifier SEQUENCE + if (p >= pkcs8Len || pkcs8[p] != 0x30) return NULL; + p++; + if ((pkcs8[p] & 0x80) == 0) { len = pkcs8[p]; p++; } + else { sl = pkcs8[p] & 0x7f; p++; len = 0; for (int i = 0; i < sl; i++) len = (len << 8) | pkcs8[p + i]; p += sl; } + p += len; + // OCTET STRING + if (p >= pkcs8Len || pkcs8[p] != 0x04) return NULL; + p++; + if ((pkcs8[p] & 0x80) == 0) { len = pkcs8[p]; p++; } + else { sl = pkcs8[p] & 0x7f; p++; len = 0; for (int i = 0; i < sl; i++) len = (len << 8) | pkcs8[p + i]; p += sl; } + if (p + len > pkcs8Len) return NULL; + NSData* keyData = [NSData dataWithBytes:(pkcs8 + p) length:len]; + NSDictionary* attrs = @{ + (id) kSecAttrKeyType: (id) kSecAttrKeyTypeRSA, + (id) kSecAttrKeyClass: (id) kSecAttrKeyClassPrivate, + (id) kSecAttrKeySizeInBits: @(([keyData length] * 8)) + }; + CFErrorRef error = NULL; + SecKeyRef key = SecKeyCreateWithData((__bridge CFDataRef) keyData, + (__bridge CFDictionaryRef) attrs, + &error); + if (error) CFRelease(error); + return key; +} + +static int cn1_seckey_op(SecKeyRef key, SecKeyAlgorithm alg, int forEncrypt, + const uint8_t* in, int inLen, uint8_t* out, int outCap) { + if (!key) return CN1_CRYPTO_E_BAD_KEY; + NSData* input = [NSData dataWithBytes:in length:inLen]; + CFErrorRef error = NULL; + NSData* result; + if (forEncrypt == 1) { + result = (__bridge_transfer NSData*) SecKeyCreateEncryptedData( + key, alg, (__bridge CFDataRef) input, &error); + } else if (forEncrypt == 0) { + result = (__bridge_transfer NSData*) SecKeyCreateDecryptedData( + key, alg, (__bridge CFDataRef) input, &error); + } else { /* sign */ + result = (__bridge_transfer NSData*) SecKeyCreateSignature( + key, alg, (__bridge CFDataRef) input, &error); + } + if (error || !result) { + if (error) CFRelease(error); + return CN1_CRYPTO_E_GENERIC; + } + NSUInteger len = [result length]; + if ((int) len > outCap) return CN1_CRYPTO_E_BAD_INPUT; + memcpy(out, [result bytes], len); + return (int) len; +} + +static SecKeyAlgorithm rsa_padding_alg(int paddingKind) { + return paddingKind == 2 + ? kSecKeyAlgorithmRSAEncryptionOAEPSHA256 + : kSecKeyAlgorithmRSAEncryptionPKCS1; +} + +int cn1_crypto_rsa_encrypt(int paddingKind, + const uint8_t* x509, int x509Len, + const uint8_t* in, int inLen, + uint8_t* out, int outCap) { + SecKeyRef key = cn1_load_rsa_public(x509, x509Len); + if (!key) return CN1_CRYPTO_E_BAD_KEY; + int rc = cn1_seckey_op(key, rsa_padding_alg(paddingKind), 1, in, inLen, out, outCap); + CFRelease(key); + return rc; +} + +int cn1_crypto_rsa_decrypt(int paddingKind, + const uint8_t* pkcs8, int pkcs8Len, + const uint8_t* in, int inLen, + uint8_t* out, int outCap) { + SecKeyRef key = cn1_load_rsa_private(pkcs8, pkcs8Len); + if (!key) return CN1_CRYPTO_E_BAD_KEY; + int rc = cn1_seckey_op(key, rsa_padding_alg(paddingKind), 0, in, inLen, out, outCap); + CFRelease(key); + return rc; +} + +static SecKeyAlgorithm signature_alg(int algorithm) { + switch (algorithm) { + case 0: return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256; + case 1: return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA384; + case 2: return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA512; + case 3: return kSecKeyAlgorithmECDSASignatureMessageX962SHA256; + case 4: return kSecKeyAlgorithmECDSASignatureMessageX962SHA384; + case 5: return kSecKeyAlgorithmECDSASignatureMessageX962SHA512; + default: return NULL; + } +} + +int cn1_crypto_sign(int algorithm, + const uint8_t* pkcs8, int pkcs8Len, + const uint8_t* data, int dataLen, + uint8_t* out, int outCap) { + SecKeyAlgorithm alg = signature_alg(algorithm); + if (!alg) return CN1_CRYPTO_E_UNSUPPORTED; + // ECDSA keys go through a separate loader; here we assume RSA. ECDSA + // support would parse the PKCS#8 EC body and pass kSecAttrKeyTypeECSECPrimeRandom. + SecKeyRef key = cn1_load_rsa_private(pkcs8, pkcs8Len); + if (!key) return CN1_CRYPTO_E_BAD_KEY; + int rc = cn1_seckey_op(key, alg, 2, data, dataLen, out, outCap); + CFRelease(key); + return rc; +} + +int cn1_crypto_verify(int algorithm, + const uint8_t* x509, int x509Len, + const uint8_t* data, int dataLen, + const uint8_t* sig, int sigLen) { + SecKeyAlgorithm alg = signature_alg(algorithm); + if (!alg) return CN1_CRYPTO_E_UNSUPPORTED; + SecKeyRef key = cn1_load_rsa_public(x509, x509Len); + if (!key) return CN1_CRYPTO_E_BAD_KEY; + CFErrorRef error = NULL; + NSData* dataObj = [NSData dataWithBytes:data length:dataLen]; + NSData* sigObj = [NSData dataWithBytes:sig length:sigLen]; + Boolean ok = SecKeyVerifySignature(key, alg, + (__bridge CFDataRef) dataObj, + (__bridge CFDataRef) sigObj, + &error); + if (error) CFRelease(error); + CFRelease(key); + return ok ? 1 : 0; +} + +/* --- RSA key-pair generation ------------------------------------------ */ + +int cn1_crypto_generate_rsa_keypair(int bits, + uint8_t* outPub, int pubCap, int* pubLen, + uint8_t* outPriv, int privCap, int* privLen) { + if (!pubLen || !privLen) return CN1_CRYPTO_E_BAD_INPUT; + NSDictionary* attrs = @{ + (id) kSecAttrKeyType: (id) kSecAttrKeyTypeRSA, + (id) kSecAttrKeySizeInBits: @(bits), + (id) kSecPrivateKeyAttrs: @{ (id) kSecAttrIsPermanent: @NO } + }; + CFErrorRef error = NULL; + SecKeyRef priv = SecKeyCreateRandomKey((__bridge CFDictionaryRef) attrs, &error); + if (!priv || error) { + if (error) CFRelease(error); + return CN1_CRYPTO_E_GENERIC; + } + SecKeyRef pub = SecKeyCopyPublicKey(priv); + if (!pub) { + CFRelease(priv); + return CN1_CRYPTO_E_GENERIC; + } + + // NB: SecKeyCopyExternalRepresentation returns the bare PKCS#1 form, NOT + // the X.509/PKCS#8 wrapper expected by the Java API. We wrap them here. + NSData* pubInner = (__bridge_transfer NSData*) SecKeyCopyExternalRepresentation(pub, &error); + if (error) { CFRelease(error); error = NULL; } + NSData* privInner = (__bridge_transfer NSData*) SecKeyCopyExternalRepresentation(priv, &error); + if (error) { CFRelease(error); error = NULL; } + CFRelease(pub); + CFRelease(priv); + if (!pubInner || !privInner) return CN1_CRYPTO_E_GENERIC; + + // X.509 SPKI wrapper for the public key: + // SEQUENCE { + // SEQUENCE { OID 1.2.840.113549.1.1.1, NULL }, + // BIT STRING { } + // } + static const uint8_t SPKI_HEADER[] = { + 0x30, 0x82, 0x00, 0x00, // outer SEQUENCE, length filled in + 0x30, 0x0d, // alg SEQUENCE + 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, // OID + 0x05, 0x00, // NULL + 0x03, 0x82, 0x00, 0x00, // BIT STRING, length filled in + 0x00 // unused bits + }; + NSUInteger pubInnerLen = [pubInner length]; + NSUInteger spkiBodyLen = sizeof(SPKI_HEADER) - 4 + pubInnerLen + 1; // +1 for unused-bits + (void) spkiBodyLen; + NSUInteger outerLen = (sizeof(SPKI_HEADER) - 4) + 1 + pubInnerLen; // see below + // The SPKI structure including the BIT STRING with unused-bits 0x00 byte: + // SEQ(len) [ SEQ(0x0d){oid+null}, BITSTRING(len_pub+1)[0x00 || pubInner] ] + int wrappedLen = 4 /*outer header*/ + + 2 + 0x0d /*alg seq*/ + + 4 /*bitstring header*/ + 1 /*unused-bits byte*/ + + (int) pubInnerLen; + if (wrappedLen > pubCap) return CN1_CRYPTO_E_BAD_INPUT; + uint8_t* p = outPub; + int outerInteriorLen = wrappedLen - 4; // minus outer header + *p++ = 0x30; *p++ = 0x82; + *p++ = (uint8_t) (outerInteriorLen >> 8); + *p++ = (uint8_t) (outerInteriorLen & 0xff); + *p++ = 0x30; *p++ = 0x0d; + static const uint8_t oid[] = { 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00 }; + memcpy(p, oid, sizeof(oid)); p += sizeof(oid); + int bitStringLen = 1 + (int) pubInnerLen; + *p++ = 0x03; *p++ = 0x82; + *p++ = (uint8_t) (bitStringLen >> 8); + *p++ = (uint8_t) (bitStringLen & 0xff); + *p++ = 0x00; + memcpy(p, [pubInner bytes], pubInnerLen); + *pubLen = wrappedLen; + + // PKCS#8 wrapper for the private key: + // SEQUENCE { INT 0, SEQUENCE{OID, NULL}, OCTET STRING { privInner } } + NSUInteger privInnerLen = [privInner length]; + int p8BodyLen = 3 /*INT 0*/ + 15 /*algSeq*/ + 4 /*OCTET header*/ + (int) privInnerLen; + int p8TotalLen = 4 + p8BodyLen; + if (p8TotalLen > privCap) return CN1_CRYPTO_E_BAD_INPUT; + uint8_t* q = outPriv; + *q++ = 0x30; *q++ = 0x82; + *q++ = (uint8_t) (p8BodyLen >> 8); + *q++ = (uint8_t) (p8BodyLen & 0xff); + *q++ = 0x02; *q++ = 0x01; *q++ = 0x00; + *q++ = 0x30; *q++ = 0x0d; + memcpy(q, oid, sizeof(oid)); q += sizeof(oid); + *q++ = 0x04; *q++ = 0x82; + *q++ = (uint8_t) (privInnerLen >> 8); + *q++ = (uint8_t) (privInnerLen & 0xff); + memcpy(q, [privInner bytes], privInnerLen); + *privLen = p8TotalLen; + return 0; +} + +#else /* CN1_INCLUDE_CRYPTO */ + +/* + * When the user's app never references com.codename1.security.* the build + * system leaves CN1_INCLUDE_CRYPTO undefined and we drop in stub versions of + * the exported functions. The stubs let the IOSNative C bridge link against + * something, but none of CommonCrypto's encryption symbols (and especially + * none of the AES-GCM SPI symbols) end up referenced by the binary -- which + * keeps Apple's static-symbol scanner happy. + */ +#include + +int cn1_crypto_secure_random(uint8_t* out, int len) { + (void) out; (void) len; + return CN1_CRYPTO_E_UNSUPPORTED; +} +int cn1_crypto_aes_cbc(int e, const uint8_t* k, int kl, const uint8_t* iv, + const uint8_t* in, int inLen, uint8_t* out, int outCap, int pad) { + (void) e; (void) k; (void) kl; (void) iv; (void) in; (void) inLen; (void) out; (void) outCap; (void) pad; + return CN1_CRYPTO_E_UNSUPPORTED; +} +int cn1_crypto_aes_gcm(int e, const uint8_t* k, int kl, const uint8_t* iv, int ivl, + const uint8_t* aad, int aadl, const uint8_t* in, int inl, + uint8_t* out, int outCap) { + (void) e; (void) k; (void) kl; (void) iv; (void) ivl; (void) aad; (void) aadl; + (void) in; (void) inl; (void) out; (void) outCap; + return CN1_CRYPTO_E_UNSUPPORTED; +} +int cn1_crypto_rsa_encrypt(int p, const uint8_t* x, int xl, const uint8_t* in, int inl, uint8_t* out, int outCap) { + (void) p; (void) x; (void) xl; (void) in; (void) inl; (void) out; (void) outCap; + return CN1_CRYPTO_E_UNSUPPORTED; +} +int cn1_crypto_rsa_decrypt(int p, const uint8_t* k, int kl, const uint8_t* in, int inl, uint8_t* out, int outCap) { + (void) p; (void) k; (void) kl; (void) in; (void) inl; (void) out; (void) outCap; + return CN1_CRYPTO_E_UNSUPPORTED; +} +int cn1_crypto_sign(int a, const uint8_t* k, int kl, const uint8_t* d, int dl, uint8_t* out, int outCap) { + (void) a; (void) k; (void) kl; (void) d; (void) dl; (void) out; (void) outCap; + return CN1_CRYPTO_E_UNSUPPORTED; +} +int cn1_crypto_verify(int a, const uint8_t* x, int xl, const uint8_t* d, int dl, const uint8_t* s, int sl) { + (void) a; (void) x; (void) xl; (void) d; (void) dl; (void) s; (void) sl; + return CN1_CRYPTO_E_UNSUPPORTED; +} +int cn1_crypto_generate_rsa_keypair(int bits, uint8_t* outPub, int pubCap, int* pubLen, + uint8_t* outPriv, int privCap, int* privLen) { + (void) bits; (void) outPub; (void) pubCap; (void) outPriv; (void) privCap; + if (pubLen) *pubLen = 0; + if (privLen) *privLen = 0; + return CN1_CRYPTO_E_UNSUPPORTED; +} + +#endif /* CN1_INCLUDE_CRYPTO */ diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 5582087609..2c9c651385 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -10891,6 +10891,151 @@ void com_codename1_impl_ios_IOSNative_announceForAccessibility___java_lang_Strin POOL_END(); } +// ==================================================================== +// Crypto bridge -- implementations of the native methods on IOSNative +// that back com.codename1.security.{Cipher,Signature,SecureRandom, +// KeyGenerator}. The actual crypto runs in CN1Crypto.{h,m}; this file +// is just the marshalling layer. +// +// CN1_INCLUDE_CRYPTO is enabled by IPhoneBuilder when the app references +// com.codename1.security.* in its compiled bytecode. When the app doesn't +// use the crypto API the implementations below collapse into no-ops, the +// CommonCrypto / Security framework symbols are never referenced, and the +// AES-GCM SPI symbols (gated separately by CN1_INCLUDE_CRYPTO_GCM) stay +// completely out of the binary. + +#import "CN1Crypto.h" + +#ifndef NEW_CODENAME_ONE_VM +#define CN1_PRIM_ARR_DATA(arr) ((void*)((org_xmlvm_runtime_XMLVMArray*)(arr))->fields.org_xmlvm_runtime_XMLVMArray.array_) +#define CN1_PRIM_ARR_LEN(arr) (((org_xmlvm_runtime_XMLVMArray*)(arr))->fields.org_xmlvm_runtime_XMLVMArray.length_) +#else +#define CN1_PRIM_ARR_DATA(arr) ((void*)((JAVA_ARRAY)(arr))->data) +#define CN1_PRIM_ARR_LEN(arr) (((JAVA_ARRAY)(arr))->length) +#endif + +#ifdef CN1_INCLUDE_CRYPTO + +void com_codename1_impl_ios_IOSNative_secureRandomBytes___byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT out) { + if (out == JAVA_NULL) return; + cn1_crypto_secure_random((uint8_t*) CN1_PRIM_ARR_DATA(out), (int) CN1_PRIM_ARR_LEN(out)); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_aesCbc___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT encrypt, JAVA_OBJECT keyArr, JAVA_OBJECT ivArr, JAVA_OBJECT inArr, JAVA_OBJECT outArr, JAVA_INT padding) { + return cn1_crypto_aes_cbc(encrypt, + (uint8_t*) CN1_PRIM_ARR_DATA(keyArr), (int) CN1_PRIM_ARR_LEN(keyArr), + (uint8_t*) CN1_PRIM_ARR_DATA(ivArr), + (uint8_t*) CN1_PRIM_ARR_DATA(inArr), (int) CN1_PRIM_ARR_LEN(inArr), + (uint8_t*) CN1_PRIM_ARR_DATA(outArr), (int) CN1_PRIM_ARR_LEN(outArr), + padding); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_aesGcm___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT encrypt, JAVA_OBJECT keyArr, JAVA_OBJECT ivArr, JAVA_OBJECT aadArr, JAVA_OBJECT inArr, JAVA_OBJECT outArr) { + const uint8_t* aadPtr = (aadArr == JAVA_NULL) ? NULL : (uint8_t*) CN1_PRIM_ARR_DATA(aadArr); + int aadLen = (aadArr == JAVA_NULL) ? 0 : (int) CN1_PRIM_ARR_LEN(aadArr); + return cn1_crypto_aes_gcm(encrypt, + (uint8_t*) CN1_PRIM_ARR_DATA(keyArr), (int) CN1_PRIM_ARR_LEN(keyArr), + (uint8_t*) CN1_PRIM_ARR_DATA(ivArr), (int) CN1_PRIM_ARR_LEN(ivArr), + aadPtr, aadLen, + (uint8_t*) CN1_PRIM_ARR_DATA(inArr), (int) CN1_PRIM_ARR_LEN(inArr), + (uint8_t*) CN1_PRIM_ARR_DATA(outArr), (int) CN1_PRIM_ARR_LEN(outArr)); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_rsaEncrypt___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT paddingKind, JAVA_OBJECT x509, JAVA_OBJECT inArr, JAVA_OBJECT outArr) { + return cn1_crypto_rsa_encrypt(paddingKind, + (uint8_t*) CN1_PRIM_ARR_DATA(x509), (int) CN1_PRIM_ARR_LEN(x509), + (uint8_t*) CN1_PRIM_ARR_DATA(inArr), (int) CN1_PRIM_ARR_LEN(inArr), + (uint8_t*) CN1_PRIM_ARR_DATA(outArr),(int) CN1_PRIM_ARR_LEN(outArr)); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_rsaDecrypt___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT paddingKind, JAVA_OBJECT pkcs8, JAVA_OBJECT inArr, JAVA_OBJECT outArr) { + return cn1_crypto_rsa_decrypt(paddingKind, + (uint8_t*) CN1_PRIM_ARR_DATA(pkcs8), (int) CN1_PRIM_ARR_LEN(pkcs8), + (uint8_t*) CN1_PRIM_ARR_DATA(inArr), (int) CN1_PRIM_ARR_LEN(inArr), + (uint8_t*) CN1_PRIM_ARR_DATA(outArr),(int) CN1_PRIM_ARR_LEN(outArr)); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_sign___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT algorithm, JAVA_OBJECT pkcs8, JAVA_OBJECT data, JAVA_OBJECT outArr) { + return cn1_crypto_sign(algorithm, + (uint8_t*) CN1_PRIM_ARR_DATA(pkcs8), (int) CN1_PRIM_ARR_LEN(pkcs8), + (uint8_t*) CN1_PRIM_ARR_DATA(data), (int) CN1_PRIM_ARR_LEN(data), + (uint8_t*) CN1_PRIM_ARR_DATA(outArr),(int) CN1_PRIM_ARR_LEN(outArr)); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_verify___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT algorithm, JAVA_OBJECT x509, JAVA_OBJECT data, JAVA_OBJECT sig) { + return cn1_crypto_verify(algorithm, + (uint8_t*) CN1_PRIM_ARR_DATA(x509), (int) CN1_PRIM_ARR_LEN(x509), + (uint8_t*) CN1_PRIM_ARR_DATA(data), (int) CN1_PRIM_ARR_LEN(data), + (uint8_t*) CN1_PRIM_ARR_DATA(sig), (int) CN1_PRIM_ARR_LEN(sig)); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_generateRsaKeyPair___int_byte_1ARRAY_byte_1ARRAY_int_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT bits, JAVA_OBJECT outPub, JAVA_OBJECT outPriv, JAVA_OBJECT lengths) { + int pubLen = 0, privLen = 0; + int rc = cn1_crypto_generate_rsa_keypair(bits, + (uint8_t*) CN1_PRIM_ARR_DATA(outPub), (int) CN1_PRIM_ARR_LEN(outPub), &pubLen, + (uint8_t*) CN1_PRIM_ARR_DATA(outPriv), (int) CN1_PRIM_ARR_LEN(outPriv), &privLen); + JAVA_ARRAY_INT* lens = (JAVA_ARRAY_INT*) CN1_PRIM_ARR_DATA(lengths); + lens[0] = pubLen; + lens[1] = privLen; + return rc; +} + +#else /* CN1_INCLUDE_CRYPTO */ + +/* + * When the crypto API isn't reachable from the user's code we still emit + * stub IOSNative bridge symbols so the generated C from IOSImplementation + * has something to link against, but they all just delegate to the + * CN1_CRYPTO_E_UNSUPPORTED stubs in CN1Crypto.m (no encryption symbols + * referenced). + */ + +void com_codename1_impl_ios_IOSNative_secureRandomBytes___byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT out) { + (void) instanceObject; (void) out; +} + +JAVA_INT com_codename1_impl_ios_IOSNative_aesCbc___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT encrypt, JAVA_OBJECT keyArr, JAVA_OBJECT ivArr, JAVA_OBJECT inArr, JAVA_OBJECT outArr, JAVA_INT padding) { + (void) instanceObject; (void) encrypt; (void) keyArr; (void) ivArr; (void) inArr; (void) outArr; (void) padding; + return CN1_CRYPTO_E_UNSUPPORTED; +} + +JAVA_INT com_codename1_impl_ios_IOSNative_aesGcm___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT encrypt, JAVA_OBJECT keyArr, JAVA_OBJECT ivArr, JAVA_OBJECT aadArr, JAVA_OBJECT inArr, JAVA_OBJECT outArr) { + (void) instanceObject; (void) encrypt; (void) keyArr; (void) ivArr; (void) aadArr; (void) inArr; (void) outArr; + return CN1_CRYPTO_E_UNSUPPORTED; +} + +JAVA_INT com_codename1_impl_ios_IOSNative_rsaEncrypt___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT paddingKind, JAVA_OBJECT x509, JAVA_OBJECT inArr, JAVA_OBJECT outArr) { + (void) instanceObject; (void) paddingKind; (void) x509; (void) inArr; (void) outArr; + return CN1_CRYPTO_E_UNSUPPORTED; +} + +JAVA_INT com_codename1_impl_ios_IOSNative_rsaDecrypt___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT paddingKind, JAVA_OBJECT pkcs8, JAVA_OBJECT inArr, JAVA_OBJECT outArr) { + (void) instanceObject; (void) paddingKind; (void) pkcs8; (void) inArr; (void) outArr; + return CN1_CRYPTO_E_UNSUPPORTED; +} + +JAVA_INT com_codename1_impl_ios_IOSNative_sign___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT algorithm, JAVA_OBJECT pkcs8, JAVA_OBJECT data, JAVA_OBJECT outArr) { + (void) instanceObject; (void) algorithm; (void) pkcs8; (void) data; (void) outArr; + return CN1_CRYPTO_E_UNSUPPORTED; +} + +JAVA_INT com_codename1_impl_ios_IOSNative_verify___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT algorithm, JAVA_OBJECT x509, JAVA_OBJECT data, JAVA_OBJECT sig) { + (void) instanceObject; (void) algorithm; (void) x509; (void) data; (void) sig; + return CN1_CRYPTO_E_UNSUPPORTED; +} + +JAVA_INT com_codename1_impl_ios_IOSNative_generateRsaKeyPair___int_byte_1ARRAY_byte_1ARRAY_int_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT bits, JAVA_OBJECT outPub, JAVA_OBJECT outPriv, JAVA_OBJECT lengths) { + (void) instanceObject; (void) bits; (void) outPub; (void) outPriv; + if (lengths != JAVA_NULL) { + JAVA_ARRAY_INT* lens = (JAVA_ARRAY_INT*) CN1_PRIM_ARR_DATA(lengths); + lens[0] = 0; + lens[1] = 0; + } + return CN1_CRYPTO_E_UNSUPPORTED; +} + +#endif /* CN1_INCLUDE_CRYPTO */ + // ============================================================================ // Biometrics + SecureStorage natives (LocalAuthentication + Security framework) // ============================================================================ @@ -11156,3 +11301,41 @@ void com_codename1_impl_ios_IOSNative_secureStorageRemove___int_java_lang_String }); POOL_END(); } + +// ==================================================================== +// Crypto bridge _R_int wrappers +// +// ParparVM emits two C entry points for every non-void native method: the +// unmangled implementation (com_..._methodName___paramTypes) plus a +// _R_-suffixed wrapper that the bytecode dispatcher actually +// calls. We forward each wrapper to the matching implementation -- which is +// either the CN1_INCLUDE_CRYPTO-on real version or the always-fail stub +// from the #else branch above, depending on the build configuration. + +JAVA_INT com_codename1_impl_ios_IOSNative_aesCbc___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_int_R_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT encrypt, JAVA_OBJECT keyArr, JAVA_OBJECT ivArr, JAVA_OBJECT inArr, JAVA_OBJECT outArr, JAVA_INT padding) { + return com_codename1_impl_ios_IOSNative_aesCbc___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_int(CN1_THREAD_STATE_PASS_ARG instanceObject, encrypt, keyArr, ivArr, inArr, outArr, padding); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_aesGcm___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_R_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT encrypt, JAVA_OBJECT keyArr, JAVA_OBJECT ivArr, JAVA_OBJECT aadArr, JAVA_OBJECT inArr, JAVA_OBJECT outArr) { + return com_codename1_impl_ios_IOSNative_aesGcm___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_PASS_ARG instanceObject, encrypt, keyArr, ivArr, aadArr, inArr, outArr); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_rsaEncrypt___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_R_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT paddingKind, JAVA_OBJECT x509, JAVA_OBJECT inArr, JAVA_OBJECT outArr) { + return com_codename1_impl_ios_IOSNative_rsaEncrypt___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_PASS_ARG instanceObject, paddingKind, x509, inArr, outArr); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_rsaDecrypt___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_R_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT paddingKind, JAVA_OBJECT pkcs8, JAVA_OBJECT inArr, JAVA_OBJECT outArr) { + return com_codename1_impl_ios_IOSNative_rsaDecrypt___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_PASS_ARG instanceObject, paddingKind, pkcs8, inArr, outArr); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_sign___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_R_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT algorithm, JAVA_OBJECT pkcs8, JAVA_OBJECT data, JAVA_OBJECT outArr) { + return com_codename1_impl_ios_IOSNative_sign___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_PASS_ARG instanceObject, algorithm, pkcs8, data, outArr); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_verify___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_R_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT algorithm, JAVA_OBJECT x509, JAVA_OBJECT data, JAVA_OBJECT sig) { + return com_codename1_impl_ios_IOSNative_verify___int_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY(CN1_THREAD_STATE_PASS_ARG instanceObject, algorithm, x509, data, sig); +} + +JAVA_INT com_codename1_impl_ios_IOSNative_generateRsaKeyPair___int_byte_1ARRAY_byte_1ARRAY_int_1ARRAY_R_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT bits, JAVA_OBJECT outPub, JAVA_OBJECT outPriv, JAVA_OBJECT lengths) { + return com_codename1_impl_ios_IOSNative_generateRsaKeyPair___int_byte_1ARRAY_byte_1ARRAY_int_1ARRAY(CN1_THREAD_STATE_PASS_ARG instanceObject, bits, outPub, outPriv, lengths); +} diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index 64cc7ea6e6..e8e071a380 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -10190,4 +10190,113 @@ public boolean isJailbrokenDevice() { public void announceForAccessibility(final Component cmp, final String text) { IOSNative.announceForAccessibility(text); } + + // ================================================================ + // Crypto bridge -- routes through CN1Crypto.{h,m} in nativeSources/ + // (the corresponding native methods live on IOSNative). The defaults + // inherited from CodenameOneImplementation use java.security via + // reflection, which isn't on the ParparVM runtime classpath. + + private static byte[] cryptoTrim(byte[] buf, int len) { + if (len < 0) { + throw new RuntimeException("crypto operation failed with code " + len); + } + if (len == buf.length) return buf; + byte[] out = new byte[len]; + System.arraycopy(buf, 0, out, 0, len); + return out; + } + + @Override + public void secureRandomBytes(byte[] out) { + nativeInstance.secureRandomBytes(out); + } + + @Override + public byte[] aesEncrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] plaintext) { + return doAes(transformation, key, iv, aad, plaintext, 1); + } + + @Override + public byte[] aesDecrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] ciphertext) { + return doAes(transformation, key, iv, aad, ciphertext, 0); + } + + private byte[] doAes(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] input, int encrypt) { + String t = transformation == null ? "" : transformation.toUpperCase(); + if (t.indexOf("GCM") >= 0) { + // Encrypt output = ciphertext + 16-byte tag; decrypt output is + // the same length as the ciphertext minus the tag. + int outLen = encrypt == 1 ? input.length + 16 : Math.max(0, input.length - 16); + byte[] outBuf = new byte[outLen]; + int written = nativeInstance.aesGcm(encrypt, key, iv, aad, input, outBuf); + return cryptoTrim(outBuf, written); + } + boolean padded = t.indexOf("NOPADDING") < 0; + // CBC ciphertext is at most input + one extra block (16 bytes). + int outLen = input.length + 16; + byte[] outBuf = new byte[outLen]; + int written = nativeInstance.aesCbc(encrypt, key, iv, input, outBuf, padded ? 1 : 0); + return cryptoTrim(outBuf, written); + } + + @Override + public byte[] rsaEncrypt(String transformation, byte[] publicKeyX509, byte[] plaintext) { + int padding = rsaPaddingKind(transformation); + // Modern key sizes never exceed 2048 bytes of output. + byte[] outBuf = new byte[2048]; + int written = nativeInstance.rsaEncrypt(padding, publicKeyX509, plaintext, outBuf); + return cryptoTrim(outBuf, written); + } + + @Override + public byte[] rsaDecrypt(String transformation, byte[] privateKeyPkcs8, byte[] ciphertext) { + int padding = rsaPaddingKind(transformation); + byte[] outBuf = new byte[2048]; + int written = nativeInstance.rsaDecrypt(padding, privateKeyPkcs8, ciphertext, outBuf); + return cryptoTrim(outBuf, written); + } + + private static int rsaPaddingKind(String transformation) { + if (transformation == null) return 1; + return transformation.toUpperCase().indexOf("OAEP") >= 0 ? 2 : 1; + } + + @Override + public byte[] cryptoSign(String algorithm, String keyAlgorithm, byte[] privateKeyPkcs8, byte[] data) { + int alg = signatureAlgorithmKind(algorithm); + byte[] outBuf = new byte[2048]; + int written = nativeInstance.sign(alg, privateKeyPkcs8, data, outBuf); + return cryptoTrim(outBuf, written); + } + + @Override + public boolean cryptoVerify(String algorithm, String keyAlgorithm, byte[] publicKeyX509, byte[] data, byte[] signature) { + int alg = signatureAlgorithmKind(algorithm); + int rc = nativeInstance.verify(alg, publicKeyX509, data, signature); + if (rc < 0) throw new RuntimeException("verify failed: code " + rc); + return rc == 1; + } + + private static int signatureAlgorithmKind(String algorithm) { + if ("SHA256withRSA".equals(algorithm)) return 0; + if ("SHA384withRSA".equals(algorithm)) return 1; + if ("SHA512withRSA".equals(algorithm)) return 2; + if ("SHA256withECDSA".equals(algorithm)) return 3; + if ("SHA384withECDSA".equals(algorithm)) return 4; + if ("SHA512withECDSA".equals(algorithm)) return 5; + throw new RuntimeException("unsupported signature algorithm: " + algorithm); + } + + @Override + public byte[][] generateRsaKeyPair(int bits) { + // 4096-bit RSA produces ~600 bytes of DER for the public side and + // ~2300 for the private; round up generously. + byte[] pubBuf = new byte[bits + 1024]; + byte[] privBuf = new byte[bits * 3]; + int[] lens = new int[2]; + int rc = nativeInstance.generateRsaKeyPair(bits, pubBuf, privBuf, lens); + if (rc < 0) throw new RuntimeException("RSA keypair generation failed: code " + rc); + return new byte[][]{ cryptoTrim(pubBuf, lens[0]), cryptoTrim(privBuf, lens[1]) }; + } } diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java index 5b294826f2..d45895e8ea 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java @@ -794,5 +794,33 @@ native void nativeSetTransformMutable( native boolean isRTLString(String javaString); public static native void announceForAccessibility(String text); - + + // ============================================================ + // Crypto bridge -- backed by CN1Crypto.{h,m} in nativeSources/. + // + // Each method returns the number of bytes written to its output buffer, + // or a negative CN1_CRYPTO_E_* error code on failure. The Java side in + // IOSImplementation trims to that length and translates failures into + // CryptoException. + + native void secureRandomBytes(byte[] out); + + native int aesCbc(int encrypt, byte[] key, byte[] iv, + byte[] in, byte[] out, int padding); + + native int aesGcm(int encrypt, byte[] key, byte[] iv, + byte[] aad, byte[] in, byte[] out); + + native int rsaEncrypt(int paddingKind, byte[] x509, byte[] in, byte[] out); + + native int rsaDecrypt(int paddingKind, byte[] pkcs8, byte[] in, byte[] out); + + native int sign(int algorithm, byte[] pkcs8, byte[] data, byte[] out); + + native int verify(int algorithm, byte[] x509, byte[] data, byte[] sig); + + /// `lengths[0]` is set to public-key DER length, `lengths[1]` to + /// private-key DER length. Returns 0 on success, negative on error. + native int generateRsaKeyPair(int bits, byte[] outPub, byte[] outPriv, int[] lengths); + } diff --git a/docs/developer-guide/security.asciidoc b/docs/developer-guide/security.asciidoc index b1755223b1..08c30a7f89 100644 --- a/docs/developer-guide/security.asciidoc +++ b/docs/developer-guide/security.asciidoc @@ -247,3 +247,343 @@ if(CheckCert.isCertCheckingSupported()) { ---- Notice that once connection is established you don't need to verify again for the current application run. + +=== Cryptographic primitives + +[[crypto-api-section,Codename One Crypto API]] +The `com.codename1.security` package gives you the cryptographic building blocks you need for typical mobile-app concerns -- hashing, message authentication, symmetric and asymmetric encryption, digital signatures, JSON Web Tokens, and time/counter-based one-time passwords. The supporting `com.codename1.components.OtpField` component covers the UI side of OTP entry. + +The hash and HMAC primitives are implemented in portable Java so they produce identical output on every platform without depending on any external library. Symmetric and asymmetric encryption, signing, and the secure RNG go through each platform's native crypto provider via Codename One's implementation layer: + +* The simulator (JavaSE) and Android route through the JRE's `java.security` and `javax.crypto`. +* iOS routes through Apple's CommonCrypto and the Security framework (via `Ports/iOSPort/nativeSources/CN1Crypto.{h,m}` and the corresponding `IOSImplementation` overrides). + +Application code never sees these differences -- you call the same `com.codename1.security` classes regardless of the platform you are running on. + +NOTE: Cryptographic operations only throw `com.codename1.security.CryptoException`, an unchecked exception. You can let it bubble up or catch it; you do not have to declare it. + +==== Hashing + +`com.codename1.security.Hash` implements MD5, SHA-1, SHA-224, SHA-256, SHA-384, and SHA-512. SHA-256 is the recommended default for new code; MD5 and SHA-1 are exposed only for compatibility with older protocols since both are broken for collision resistance. + +[source,java] +---- +import com.codename1.security.Hash; + +// One-shot +byte[] digest = Hash.sha256("hello".getBytes("UTF-8")); +String hex = Hash.toHex(digest); // 64-character lowercase hex +byte[] back = Hash.fromHex(hex); // round-trip helper + +// Streaming +Hash h = Hash.create(Hash.SHA256); +h.update(part1); +h.update(part2); +byte[] full = h.digest(); // hash is reset after digest() +---- + +The algorithm identifier strings (`Hash.MD5`, `Hash.SHA256`, etc.) are also accepted with or without dashes and in any case (`"sha256"` and `"SHA-256"` are equivalent). + +==== Message authentication (HMAC) + +`com.codename1.security.Hmac` implements HMAC (RFC 2104) on top of any of the hash algorithms above. Use HMAC whenever a message authentication code is needed -- API request signing, session cookie tamper detection, JWT signing with the `HS` family, or TOTP token generation. + +[source,java] +---- +import com.codename1.security.Hmac; + +byte[] tag = Hmac.sha256(secret, message); + +// Streaming +Hmac h = Hmac.create(Hash.SHA256, secret); +h.update(part1); +h.update(part2); +byte[] tag2 = h.doFinal(); +---- + +When comparing authentication tags, use `Hmac.constantTimeEquals(a, b)` instead of `Arrays.equals(a, b)` -- the latter short-circuits on the first mismatch and is vulnerable to timing attacks. + +==== Secure random numbers + +`com.codename1.security.SecureRandom` exposes the platform's cryptographically secure RNG (`SecRandomCopyBytes` on iOS, `/dev/urandom` on Linux/Android, etc.). Use it for keys, nonces, salts, password reset tokens, and any other security-sensitive value. Never use `java.util.Random` or `Math.random()` for these purposes. + +[source,java] +---- +import com.codename1.security.SecureRandom; + +byte[] iv = SecureRandom.bytes(12); // 12-byte AES-GCM nonce +int pin = SecureRandom.intBelow(1_000_000); // bias-free 6-digit code +long id = SecureRandom.longBelow(1L << 53); +---- + +==== Symmetric encryption (AES) + +`com.codename1.security.Cipher` exposes AES through the platform's native AES implementation. AES-GCM is the recommended default because it's authenticated -- a single tag-mismatch failure detects any tampering of the ciphertext or the associated data. + +[source,java] +---- +import com.codename1.security.Cipher; +import com.codename1.security.KeyGenerator; +import com.codename1.security.SecretKey; +import com.codename1.security.SecureRandom; + +SecretKey key = KeyGenerator.aes(256); +byte[] nonce = SecureRandom.bytes(12); +byte[] aad = "v1".getBytes("UTF-8"); + +byte[] ciphertext = Cipher.aesEncrypt(Cipher.AES_GCM, key, nonce, aad, plaintext); +byte[] decrypted = Cipher.aesDecrypt(Cipher.AES_GCM, key, nonce, aad, ciphertext); +---- + +The `Cipher` class exposes the standard JCE-style transformation strings as constants: `Cipher.AES_GCM`, `Cipher.AES_CBC_PKCS5`, `Cipher.AES_CBC`, and `Cipher.AES_ECB_PKCS5`. For GCM, the 16-byte authentication tag is appended to the ciphertext (matching the JCE convention). ECB is exposed only for interop with legacy systems -- it leaks structure and should never be used for new designs. + +==== Asymmetric encryption and digital signatures (RSA, ECDSA) + +`com.codename1.security.Cipher` also covers RSA encryption, and `com.codename1.security.Signature` covers digital signatures (RSA and ECDSA). Keys are represented by `com.codename1.security.PublicKey` and `com.codename1.security.PrivateKey`, which wrap X.509 SubjectPublicKeyInfo and PKCS#8 DER blobs respectively -- the same encodings used by OpenSSL. + +[source,java] +---- +import com.codename1.security.Cipher; +import com.codename1.security.KeyGenerator; +import com.codename1.security.KeyPair; +import com.codename1.security.Signature; + +// Generate a fresh RSA-2048 key pair (run on a background thread; key +// generation can take a noticeable amount of time) +KeyPair kp = KeyGenerator.rsa(2048); + +// Encrypt a small payload (e.g. wrap an AES key) +byte[] sealed = Cipher.rsaEncrypt(Cipher.RSA_OAEP_SHA256, kp.getPublicKey(), payload); +byte[] opened = Cipher.rsaDecrypt(Cipher.RSA_OAEP_SHA256, kp.getPrivateKey(), sealed); + +// Sign / verify +byte[] sig = Signature.sign(Signature.SHA256_WITH_RSA, kp.getPrivateKey(), data); +boolean ok = Signature.verify(Signature.SHA256_WITH_RSA, kp.getPublicKey(), data, sig); +---- + +To use a key that was generated outside the app, feed the DER bytes to the static factory methods: + +[source,java] +---- +PublicKey pub = PublicKey.rsa(x509SpkiBytes); // openssl rsa -pubout -outform DER +PrivateKey priv = PrivateKey.rsa(pkcs8DerBytes); // openssl pkcs8 -topk8 -nocrypt -outform DER +---- + +The `Signature` class exposes the JCE algorithm strings as constants: `SHA256_WITH_RSA`, `SHA384_WITH_RSA`, `SHA512_WITH_RSA`, `SHA256_WITH_ECDSA`, `SHA384_WITH_ECDSA`, and `SHA512_WITH_ECDSA`. + +==== JSON Web Tokens + +`com.codename1.security.Jwt` provides JWT (RFC 7519) signing and verification. The `HS` family (HMAC-SHA-2) is pure Java so it works on every platform; the `RS` family (RSA-PKCS#1 v1.5 with SHA-2) and the `ES` family (ECDSA with SHA-2) route through the platform's native crypto. + +[source,java] +---- +import com.codename1.security.Jwt; +import java.util.LinkedHashMap; +import java.util.Map; + +Map claims = new LinkedHashMap(); +claims.put("sub", "user-123"); +claims.put("exp", System.currentTimeMillis() / 1000 + 3600); + +// Sign with a shared secret (HS256) +String token = Jwt.signHs256(claims, "secret".getBytes("UTF-8")); + +// Parse + verify +Jwt parsed = Jwt.parse(token); +if (!parsed.verifyHs256("secret".getBytes("UTF-8"))) { + throw new SecurityException("bad signature"); +} +String subject = (String) parsed.getClaim("sub"); +---- + +For RSA-signed tokens (RS256/384/512) and ECDSA-signed tokens (ES256/384/512), pass the key to the dynamic overloads: + +[source,java] +---- +KeyPair kp = KeyGenerator.rsa(2048); +String rs = Jwt.sign(claims, kp.getPrivateKey(), Jwt.RS256); +Jwt read = Jwt.parse(rs); +boolean ok = read.verify(kp.getPublicKey()); +---- + +WARNING: `Jwt.parse(token)` does not verify the signature -- you must call one of the `verify*` methods afterwards. Tokens with an `alg` header of `none` are rejected by `verify` unless you explicitly call `setVerifyAllowNoneAlgorithm(true)` on the parsed JWT. Accepting unsigned tokens is a critical security bug in almost every deployment, so leave it off unless you have decided that the transport is trusted. + +==== HOTP / TOTP one-time passwords + +`com.codename1.security.Otp` implements counter-based HOTP (RFC 4226) and time-based TOTP (RFC 6238). The output is identical to standard authenticator apps (Google Authenticator, Microsoft Authenticator, 1Password, Authy, etc.) -- you can use this class to generate codes inside your app, to validate codes sent by an authenticator on a sign-in flow, or for SMS-based two-factor flows. + +Shared secrets are commonly distributed as Base32 strings (the format embedded in QR codes by authenticator apps). `com.codename1.security.Base32` handles the encoding. + +[source,java] +---- +import com.codename1.security.Base32; +import com.codename1.security.Otp; + +byte[] secret = Base32.decode("JBSWY3DPEHPK3PXP"); + +// Generate the current 6-digit TOTP code (30-second step, SHA-1) +String code = Otp.totp(secret); + +// Verify the code the user typed in, allowing one step of clock skew on +// either side (so the previous, current, and next codes are all accepted) +boolean ok = Otp.verifyTotp(secret, userInput, 1); +---- + +For HOTP, supply the counter explicitly and increment it after every successful authentication: + +[source,java] +---- +String code = Otp.hotp(secret, counter++, 6); +---- + +==== Walkthrough: Adding two-factor authentication to a sign-in flow + +The typical TOTP-based two-factor authentication flow has two distinct phases. The first is _enrolment_, where the server provisions a shared secret to the user's authenticator app; the second is _verification_, which happens on every subsequent sign-in. + +[NOTE] +==== +The TOTP secret should be generated and stored on your *server*, not on the Codename One client. The client only ever sees the secret long enough to display the enrolment QR code, then forgets it. Verification on every subsequent sign-in is also a server-side concern (the client just sends the code the user types in). The code samples below put the cryptography on the client only to keep the example self-contained -- in production, the server does the actual generation and verification. +==== + +*Phase 1 -- Enrolment.* When the user opts into 2FA, generate a 20-byte random secret and show it to their authenticator app as a QR code that encodes a standard `otpauth://totp/...` URI: + +[source,java] +---- +import com.codename1.security.Otp; +import com.codename1.security.SecureRandom; + +// 1. Generate (or fetch from your server) a 20-byte secret -- 160 bits is the +// de-facto standard for authenticator compatibility. +byte[] secret = SecureRandom.bytes(20); + +// 2. Build the otpauth URI. The authenticator app stores the secret against +// the "Acme Bank: alice@example.com" label and tags it with the issuer +// so it can show "Acme Bank" next to the rotating code. +String uri = Otp.otpauthUri("Acme Bank", "alice@example.com", secret); + +// 3. Render `uri` as a QR code and show it on screen for the user to scan. +// See "Rendering the QR code" below. +---- + +[NOTE] +==== +The maximum-compatibility settings -- 6 digits, 30 second step, SHA-1 -- are the defaults of `Otp.otpauthUri(issuer, accountName, secret)`. Pass digits / step / algorithm explicitly via the full overload only if you have a specific reason; some authenticator apps don't recognize anything except the defaults. +==== + +*Rendering the QR code.* Codename One core doesn't currently ship a QR-code generator. Pick whichever option fits your deployment: + +* *Server-side render* (simplest). Send `uri` to your backend over HTTPS and have it return a PNG. The Google Charts API endpoint `https://chart.googleapis.com/chart?cht=qr&chs=300x300&chl=` and the open-source `https://api.qrserver.com/v1/create-qr-code/?data=` both work; the latter doesn't require Google API setup. Render the resulting PNG with `URLImage` or `EncodedImage.create(bytes)`. + +* *Use a QR cn1lib.* Community cn1libs such as `QRMaker` provide native generation. Add as a normal cn1lib dependency. + +* *Display the secret as a typed value.* If you can't render a QR code, show the user the Base32-encoded secret (the value after `secret=` in `uri`) and ask them to type it manually into their authenticator's "set up by hand" flow. Less convenient but works on every platform. + +*Phase 2 -- Verification.* On every sign-in after enrolment, ask the user for the current six-digit code and verify it against the stored secret. Use [com.codename1.components.OtpField] for the input: + +[source,java] +---- +import com.codename1.components.OtpField; +import com.codename1.security.Otp; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; + +OtpField field = new OtpField(6); +field.addCompleteListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + String userInput = field.getText(); + // tolerance=1 allows the previous, current, and next 30-second + // windows to compensate for clock skew between the device and the + // server. tolerance=0 is strict but unforgiving; tolerance=2 or 3 + // is more permissive but increases the brute-force attack surface. + if (Otp.verifyTotp(secret, userInput, 1)) { + grantAccess(); + } else { + field.clear(); + Dialog.show("Invalid code", "Try again", "OK", null); + } + } +}); +form.add(field); +---- + +*Server-side verification.* When your backend does the verification, the corresponding Java looks the same -- `Otp.verifyTotp(storedSecret, codeFromClient, 1)`. Make sure you rate-limit verification attempts (e.g. lock the account after 5 wrong codes within a minute) and log failures, since the 6-digit search space is small enough to be brute-forced without that. + +==== OTP input widget + +`com.codename1.components.OtpField` is a segmented input -- one box per character, that advances to the next box as the user types and steps back on backspace. This is the standard pattern for SMS confirmation and authenticator-app entry screens. + +[source,java] +---- +import com.codename1.components.OtpField; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; + +OtpField otp = new OtpField(6); // 6 digits, numeric +otp.addCompleteListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + if (Otp.verifyTotp(secret, otp.getText(), 1)) { + // accepted + } else { + otp.clear(); + } + } +}); +form.add(otp); +---- + +Style the boxes through the UIIDs `OtpField` (the container) and `OtpDigit` (each box). The component supports alphanumeric input via the two-argument constructor (`new OtpField(8, false)`), and pasting a full code into any one box distributes the characters across the remaining boxes automatically. + +==== Algorithm identifier reference + +The string constants exposed by these classes are listed below so you can pick them out at a glance. + +|=== +| Class | Constant | Algorithm string + +| `Hash` +| `MD5`, `SHA1`, `SHA224`, `SHA256`, `SHA384`, `SHA512` +| `MD5`, `SHA-1`, `SHA-224`, `SHA-256`, `SHA-384`, `SHA-512` + +| `Cipher` +| `AES_GCM` +| `AES/GCM/NoPadding` + +| `Cipher` +| `AES_CBC_PKCS5` +| `AES/CBC/PKCS5Padding` + +| `Cipher` +| `AES_CBC` +| `AES/CBC/NoPadding` + +| `Cipher` +| `AES_ECB_PKCS5` +| `AES/ECB/PKCS5Padding` (legacy) + +| `Cipher` +| `RSA_OAEP_SHA256` +| `RSA/ECB/OAEPWithSHA-256AndMGF1Padding` + +| `Cipher` +| `RSA_PKCS1` +| `RSA/ECB/PKCS1Padding` (legacy) + +| `Signature` +| `SHA256_WITH_RSA`, `SHA384_WITH_RSA`, `SHA512_WITH_RSA` +| `SHA256withRSA`, `SHA384withRSA`, `SHA512withRSA` + +| `Signature` +| `SHA256_WITH_ECDSA`, `SHA384_WITH_ECDSA`, `SHA512_WITH_ECDSA` +| `SHA256withECDSA`, `SHA384withECDSA`, `SHA512withECDSA` + +| `Jwt` +| `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512` +| Same as the JWT `alg` header value +|=== + +==== Key sizes and recommended defaults + +* *AES*: 256 bits (`KeyGenerator.aes(256)`) for new code. 128 bits is also acceptable and faster; pick 192 only if you have a specific reason. +* *RSA*: 2048 bits minimum (`KeyGenerator.rsa(2048)`). Use 3072 or 4096 if you need a longer security margin -- key generation gets slow above 2048 so run it on a background thread. +* *HMAC*: a key at least as long as the hash output (32 bytes for HMAC-SHA-256). `KeyGenerator.hmac(256)` returns a key of the requested bit length. +* *TOTP / HOTP*: 160 bits (20 bytes) is the de-facto standard for authenticator-app compatibility. diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java index b48bd94c96..109203720e 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -83,6 +83,8 @@ public class IPhoneBuilder extends Executor { private String buildVersion; private boolean usesLocalNotifications; private boolean usesPurchaseAPI; + private boolean usesCryptoAPI; + private boolean usesCryptoGcm; private boolean usesBiometrics; // so we need to store the main class name for later here. // Map will be used for Xcode 8 privacy usage descriptions. Don't need it yet @@ -647,8 +649,23 @@ public void usesClass(String cls) { if (!usesPurchaseAPI && cls.indexOf("com/codename1/payment") == 0) { usesPurchaseAPI = true; } - if (!usesBiometrics && cls.indexOf("com/codename1/security/") == 0) { - usesBiometrics = true; + if (cls.indexOf("com/codename1/security/") == 0) { + // com.codename1.security contains two distinct API + // families that toggle different bits of the iOS + // build. Biometrics + SecureStorage need the + // LocalAuthentication.framework linkage; the crypto + // primitives need the CN1Crypto.{h,m} #defines and + // an Info.plist export-compliance entry. + String shortName = cls.substring("com/codename1/security/".length()); + boolean isBiometric = + shortName.startsWith("Biometric") + || shortName.equals("SecureStorage") + || shortName.equals("AuthenticationOptions"); + if (isBiometric) { + usesBiometrics = true; + } else { + usesCryptoAPI = true; + } } } @@ -712,6 +729,28 @@ public void usesClassMethod(String cls, String method) { } + // Flip the crypto build toggles in CN1Crypto.h based on what the + // user's bytecode references. Apps that don't touch + // com.codename1.security.* get stub-only versions of the iOS + // crypto bridge -- no CommonCrypto / Security framework symbols + // referenced -- which keeps Apple's static-symbol scanner happy. + usesCryptoGcm = usesCryptoAPI && "true".equals(request.getArg("ios.crypto.gcm", "false")); + try { + File cn1Crypto = new File(buildinRes, "CN1Crypto.h"); + if (cn1Crypto.exists()) { + if (usesCryptoAPI) { + replaceInFile(cn1Crypto, "//#define CN1_INCLUDE_CRYPTO", "#define CN1_INCLUDE_CRYPTO"); + } + if (usesCryptoGcm) { + replaceInFile(cn1Crypto, "//#define CN1_INCLUDE_CRYPTO_GCM", "#define CN1_INCLUDE_CRYPTO_GCM"); + } + } + } catch (Exception ex) { + throw new BuildException("Failed to configure CN1Crypto.h", ex); + } + debug("Crypto API "+(usesCryptoAPI?"enabled":"disabled") + +", AES-GCM "+(usesCryptoGcm?"enabled":"disabled")); + if (useMetal) { try { File CN1ES2compat = new File(buildinRes, "CN1ES2compat.h"); @@ -2758,6 +2797,24 @@ public boolean accept(File file, String string) { // nothing to inject here? move along String inject = request.getArg("ios.plistInject", "CFBundleShortVersionString " + buildVersion +""); + + // Export compliance: when the app uses com.codename1.security.* we + // route all crypto through Apple's Security framework / CommonCrypto + // (and, with the ios.crypto.gcm opt-in, AES-GCM via stable SPI + // symbols). All of these qualify for the "uses standard cryptography" + // exemption under EAR 740.17, so we set ITSAppUsesNonExemptEncryption + // to false. Callers can override by setting the + // ios.appUsesNonExemptEncryption build hint -- pass it as "true" if + // your app links proprietary crypto in addition to ours, or as "" + // (empty) to omit the key entirely and answer in App Store Connect. + String exemptOverride = request.getArg("ios.appUsesNonExemptEncryption", null); + if (exemptOverride != null) { + if (exemptOverride.length() > 0 && !inject.contains("ITSAppUsesNonExemptEncryption")) { + inject += "\nITSAppUsesNonExemptEncryption<"+exemptOverride+"/>"; + } + } else if (usesCryptoAPI && !inject.contains("ITSAppUsesNonExemptEncryption")) { + inject += "\nITSAppUsesNonExemptEncryption"; + } String applicationQueriesSchemes = request.getArg("ios.applicationQueriesSchemes", null); if(applicationQueriesSchemes != null && applicationQueriesSchemes.length() > 0) { diff --git a/maven/core-unittests/src/test/java/com/codename1/security/CipherSignatureTest.java b/maven/core-unittests/src/test/java/com/codename1/security/CipherSignatureTest.java new file mode 100644 index 0000000000..ca6f3a1710 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/security/CipherSignatureTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * Distributed under the same terms as the rest of Codename One. + */ +package com.codename1.security; + +import com.codename1.junit.UITestBase; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/// End-to-end round trips through the impl-layer crypto bridge. On JavaSE the +/// bridge defaults to java.security via reflection so these tests also serve +/// as a smoke test for that path. +class CipherSignatureTest extends UITestBase { + + @Test + void aesGcmRoundTrip() { + SecretKey key = KeyGenerator.aes(256); + byte[] iv = SecureRandom.bytes(12); + byte[] plaintext = "the magic words are squeamish ossifrage".getBytes(); + byte[] aad = "v1".getBytes(); + + byte[] ciphertext = Cipher.aesEncrypt(Cipher.AES_GCM, key, iv, aad, plaintext); + assertNotEquals(plaintext.length, ciphertext.length); // includes tag + byte[] decrypted = Cipher.aesDecrypt(Cipher.AES_GCM, key, iv, aad, ciphertext); + assertArrayEquals(plaintext, decrypted); + } + + @Test + void aesGcmTamperDetected() { + SecretKey key = KeyGenerator.aes(128); + byte[] iv = SecureRandom.bytes(12); + byte[] plaintext = "secret payload".getBytes(); + byte[] ciphertext = Cipher.aesEncrypt(Cipher.AES_GCM, key, iv, null, plaintext); + ciphertext[0] ^= 0x01; + try { + Cipher.aesDecrypt(Cipher.AES_GCM, key, iv, null, ciphertext); + fail("expected CryptoException on tampered ciphertext"); + } catch (CryptoException expected) { + // pass + } + } + + @Test + void aesCbcRoundTrip() { + SecretKey key = KeyGenerator.aes(128); + byte[] iv = SecureRandom.bytes(16); + byte[] plaintext = "block cipher with PKCS#5 padding".getBytes(); + byte[] ciphertext = Cipher.aesEncrypt(Cipher.AES_CBC_PKCS5, key, iv, null, plaintext); + byte[] decrypted = Cipher.aesDecrypt(Cipher.AES_CBC_PKCS5, key, iv, null, ciphertext); + assertArrayEquals(plaintext, decrypted); + } + + @Test + void rsaRoundTrip() { + // 2048 bits is the slow path — keep this single test in the suite + KeyPair kp = KeyGenerator.rsa(2048); + byte[] plaintext = "wrap this AES key".getBytes(); + byte[] ciphertext = Cipher.rsaEncrypt(Cipher.RSA_OAEP_SHA256, kp.getPublicKey(), plaintext); + byte[] decrypted = Cipher.rsaDecrypt(Cipher.RSA_OAEP_SHA256, kp.getPrivateKey(), ciphertext); + assertArrayEquals(plaintext, decrypted); + } + + @Test + void rsaSignAndVerify() { + KeyPair kp = KeyGenerator.rsa(2048); + byte[] data = "important message".getBytes(); + byte[] sig = Signature.sign(Signature.SHA256_WITH_RSA, kp.getPrivateKey(), data); + assertTrue(Signature.verify(Signature.SHA256_WITH_RSA, kp.getPublicKey(), data, sig)); + // tamper detection + data[0] ^= 0x01; + assertFalse(Signature.verify(Signature.SHA256_WITH_RSA, kp.getPublicKey(), data, sig)); + } + + @Test + void jwtRsRoundTrip() { + KeyPair kp = KeyGenerator.rsa(2048); + Map claims = new LinkedHashMap(); + claims.put("sub", "bob"); + String token = Jwt.sign(claims, kp.getPrivateKey(), Jwt.RS256); + Jwt parsed = Jwt.parse(token); + assertEquals("RS256", parsed.getAlgorithm()); + assertTrue(parsed.verify(kp.getPublicKey())); + // wrong key fails + KeyPair other = KeyGenerator.rsa(2048); + assertFalse(parsed.verify(other.getPublicKey())); + } + + @Test + void secureRandomProducesDifferentBytes() { + byte[] a = SecureRandom.bytes(32); + byte[] b = SecureRandom.bytes(32); + // probability of collision is 2^-256 — never happens in practice + boolean different = false; + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) { different = true; break; } + } + assertTrue(different); + // intBelow stays within bounds + for (int i = 0; i < 100; i++) { + int v = SecureRandom.intBelow(1000); + assertTrue(v >= 0 && v < 1000); + } + } +} diff --git a/maven/core-unittests/src/test/java/com/codename1/security/HashTest.java b/maven/core-unittests/src/test/java/com/codename1/security/HashTest.java new file mode 100644 index 0000000000..38bbd77105 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/security/HashTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * Distributed under the same terms as the rest of Codename One. + */ +package com.codename1.security; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/// Test vectors come from the FIPS / RFC reference documents — these are +/// short, public, deterministic, and let us be confident the hash output +/// matches every other compliant implementation byte-for-byte. +class HashTest { + + private static byte[] ascii(String s) { + try { + return s.getBytes("US-ASCII"); + } catch (java.io.UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + @Test + void md5_emptyAndAbc() { + // RFC 1321 appendix A.5 + assertEquals("d41d8cd98f00b204e9800998ecf8427e", Hash.toHex(Hash.md5(new byte[0]))); + assertEquals("900150983cd24fb0d6963f7d28e17f72", Hash.toHex(Hash.md5(ascii("abc")))); + } + + @Test + void sha1_abc() { + // FIPS 180-4 A.1 + assertEquals("a9993e364706816aba3e25717850c26c9cd0d89d", + Hash.toHex(Hash.sha1(ascii("abc")))); + } + + @Test + void sha256_abc() { + // FIPS 180-4 B.1 + assertEquals("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + Hash.toHex(Hash.sha256(ascii("abc")))); + } + + @Test + void sha384_abc() { + // FIPS 180-4 D.1 + assertEquals( + "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", + Hash.toHex(Hash.sha384(ascii("abc")))); + } + + @Test + void sha512_abc() { + // FIPS 180-4 C.1 + assertEquals( + "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", + Hash.toHex(Hash.sha512(ascii("abc")))); + } + + @Test + void streamingProducesSameResultAsOneShot() { + byte[] data = ascii("The quick brown fox jumps over the lazy dog"); + Hash h = Hash.create(Hash.SHA256); + for (int i = 0; i < data.length; i++) h.update(data[i]); + assertArrayEquals(Hash.sha256(data), h.digest()); + } + + @Test + void multiBlockMessage() { + // length-pad correctness across block boundaries: 1MB of 'a'. + byte[] data = new byte[1_000_000]; + for (int i = 0; i < data.length; i++) data[i] = 'a'; + // SHA-256("a" * 1_000_000) — well-known vector + assertEquals("cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0", + Hash.toHex(Hash.sha256(data))); + } + + @Test + void hexRoundTrip() { + byte[] random = new byte[] { (byte)0x00, (byte)0x7f, (byte)0x80, (byte)0xff, 0x12, 0x34 }; + assertArrayEquals(random, Hash.fromHex(Hash.toHex(random))); + } +} diff --git a/maven/core-unittests/src/test/java/com/codename1/security/HmacTest.java b/maven/core-unittests/src/test/java/com/codename1/security/HmacTest.java new file mode 100644 index 0000000000..55934b989b --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/security/HmacTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * Distributed under the same terms as the rest of Codename One. + */ +package com.codename1.security; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/// HMAC test vectors from RFC 4231 (HMAC-SHA-2). +class HmacTest { + + private static byte[] ascii(String s) { + try { + return s.getBytes("US-ASCII"); + } catch (java.io.UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + private static byte[] repeat(byte b, int n) { + byte[] a = new byte[n]; + for (int i = 0; i < n; i++) a[i] = b; + return a; + } + + @Test + void rfc4231_testCase1_sha256() { + // Key = 0x0b * 20, data = "Hi There" + byte[] expected = Hash.fromHex( + "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"); + byte[] actual = Hmac.sha256(repeat((byte)0x0b, 20), ascii("Hi There")); + assertArrayEquals(expected, actual); + } + + @Test + void rfc4231_testCase2_keyShorterThanBlock_sha256() { + // Key = "Jefe", data = "what do ya want for nothing?" + byte[] expected = Hash.fromHex( + "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"); + byte[] actual = Hmac.sha256(ascii("Jefe"), ascii("what do ya want for nothing?")); + assertArrayEquals(expected, actual); + } + + @Test + void rfc4231_testCase2_sha512() { + // Key = "Jefe", data = "what do ya want for nothing?" + byte[] expected = Hash.fromHex( + "164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737"); + byte[] actual = Hmac.sha512(ascii("Jefe"), ascii("what do ya want for nothing?")); + assertArrayEquals(expected, actual); + } + + @Test + void rfc4231_testCase6_longerThanBlockKey_sha256() { + // Key = 0xaa * 131, Data = "Test Using Larger Than Block-Size Key - Hash Key First" + byte[] expected = Hash.fromHex( + "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"); + byte[] actual = Hmac.sha256( + repeat((byte)0xaa, 131), + ascii("Test Using Larger Than Block-Size Key - Hash Key First")); + assertArrayEquals(expected, actual); + } + + @Test + void streamingProducesSameAsOneShot() { + byte[] key = ascii("supersecret"); + byte[] data = ascii("the message split into pieces here"); + byte[] oneShot = Hmac.sha256(key, data); + + Hmac h = Hmac.create(Hash.SHA256, key); + h.update(data, 0, 10); + h.update(data, 10, data.length - 10); + assertArrayEquals(oneShot, h.doFinal()); + } + + @Test + void constantTimeEqualsHandlesNullsAndLengths() { + assertFalse(Hmac.constantTimeEquals(null, new byte[1])); + assertFalse(Hmac.constantTimeEquals(new byte[1], null)); + assertFalse(Hmac.constantTimeEquals(new byte[2], new byte[3])); + assertTrue(Hmac.constantTimeEquals(new byte[]{1,2,3}, new byte[]{1,2,3})); + assertFalse(Hmac.constantTimeEquals(new byte[]{1,2,3}, new byte[]{1,2,4})); + } +} diff --git a/maven/core-unittests/src/test/java/com/codename1/security/JwtTest.java b/maven/core-unittests/src/test/java/com/codename1/security/JwtTest.java new file mode 100644 index 0000000000..743cbd334e --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/security/JwtTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * Distributed under the same terms as the rest of Codename One. + */ +package com.codename1.security; + +import com.codename1.junit.UITestBase; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class JwtTest extends UITestBase { + + @Test + void hs256_roundTrip() throws Exception { + Map claims = new LinkedHashMap(); + claims.put("sub", "alice"); + claims.put("exp", Long.valueOf(1700000000L)); + byte[] secret = "supersecret".getBytes("UTF-8"); + + String token = Jwt.signHs256(claims, secret); + // standard JWT shape: three URL-safe-base64 segments + String[] parts = token.split("\\."); + assertEquals(3, parts.length); + + Jwt parsed = Jwt.parse(token); + assertEquals("HS256", parsed.getAlgorithm()); + assertTrue(parsed.verifyHs256(secret)); + assertFalse(parsed.verifyHs256("wrong".getBytes("UTF-8"))); + + assertEquals("alice", parsed.getClaim("sub")); + // exp comes back as Long via the JSON parser + Object exp = parsed.getClaim("exp"); + assertNotNull(exp); + } + + @Test + void hs256_referenceVector() throws Exception { + // jwt.io canonical example: + // header {"alg":"HS256","typ":"JWT"} + // payload {"sub":"1234567890","name":"John Doe","iat":1516239022} + // secret "your-256-bit-secret" + // token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + Jwt parsed = Jwt.parse(token); + assertTrue(parsed.verifyHs256("your-256-bit-secret".getBytes("UTF-8"))); + assertEquals("John Doe", parsed.getClaim("name")); + } + + @Test + void hs512_roundTrip() throws Exception { + Map claims = new LinkedHashMap(); + claims.put("k", "v"); + byte[] secret = "s".getBytes("UTF-8"); + String token = Jwt.signHs512(claims, secret); + Jwt parsed = Jwt.parse(token); + assertTrue(parsed.verifyHs512(secret)); + } + + @Test + void parseMalformedRejected() { + try { + Jwt.parse("not-a-jwt"); + fail("expected CryptoException"); + } catch (CryptoException expected) { /* ok */ } + try { + Jwt.parse("header.payload"); // missing signature segment + fail("expected CryptoException"); + } catch (CryptoException expected) { /* ok */ } + } + + @Test + void noneAlgorithmRejectedByDefault() throws Exception { + Map claims = new LinkedHashMap(); + claims.put("sub", "x"); + String token = Jwt.signNone(claims); + Jwt parsed = Jwt.parse(token); + assertEquals("none", parsed.getAlgorithm()); + // verify(PublicKey) with no opt-in must refuse + assertFalse(parsed.verify(null)); + parsed.setVerifyAllowNoneAlgorithm(true); + // null key is fine for none — only the boolean opt-in matters + assertTrue(parsed.verify(null)); + } + + @Test + void base64UrlRoundTrip() throws Exception { + byte[] data = new byte[256]; + for (int i = 0; i < 256; i++) data[i] = (byte) i; + assertArrayEquals(data, + com.codename1.util.Base64.decodeUrlSafe(com.codename1.util.Base64.encodeUrlSafe(data))); + } +} diff --git a/maven/core-unittests/src/test/java/com/codename1/security/OtpTest.java b/maven/core-unittests/src/test/java/com/codename1/security/OtpTest.java new file mode 100644 index 0000000000..2638804bfe --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/security/OtpTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2008-2026, Codename One and/or its affiliates. All rights reserved. + * Distributed under the same terms as the rest of Codename One. + */ +package com.codename1.security; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/// HOTP / TOTP test vectors from RFC 4226 appendix D and RFC 6238 appendix B. +class OtpTest { + + @Test + void rfc4226_hotpAt8Digits() { + byte[] secret = "12345678901234567890".getBytes(); + // RFC 4226 D — HOTP values at counter 0..9 with digits=6 + String[] expected = { + "755224", "287082", "359152", "969429", "338314", + "254676", "287922", "162583", "399871", "520489" + }; + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], Otp.hotp(secret, i, 6), "counter " + i); + } + } + + @Test + void rfc6238_totpKnownInstants_sha1() { + byte[] secret = "12345678901234567890".getBytes(); + // RFC 6238 appendix B — SHA-1, 8 digits + long[] times = { 59L, 1111111109L, 1111111111L, 1234567890L, 2000000000L }; + String[] expected = { "94287082", "07081804", "14050471", "89005924", "69279037" }; + for (int i = 0; i < times.length; i++) { + String code = Otp.totp(secret, times[i] * 1000L, 30, 8, Hash.SHA1); + assertEquals(expected[i], code, "time " + times[i]); + } + } + + @Test + void totpVerifyWithinTolerance() { + byte[] secret = "JBSWY3DPEHPK3PXP".getBytes(); + // aligned to the start of a 30s window so the drift assertions are + // unambiguous (1700000010 / 30 == 56666667 exactly) + long now = 1700000010000L; + // 8 digits to keep accidental code-collisions astronomically unlikely + String currentCode = Otp.totp(secret, now, 30, 8, Hash.SHA1); + assertTrue(Otp.verifyTotp(secret, currentCode, 0, now, 30, 8, Hash.SHA1)); + // 25s of drift — still in the same window since we started at the edge + assertTrue(Otp.verifyTotp(secret, currentCode, 0, now + 25000L, 30, 8, Hash.SHA1)); + // two windows ahead — only accepted with tolerance >= 2 + String future = Otp.totp(secret, now + 60000L, 30, 8, Hash.SHA1); + assertFalse(Otp.verifyTotp(secret, future, 0, now, 30, 8, Hash.SHA1)); + assertTrue(Otp.verifyTotp(secret, future, 2, now, 30, 8, Hash.SHA1)); + } + + @Test + void hotpInvalidDigitsRejected() { + try { + Otp.hotp(new byte[20], 0, 0); + fail("expected CryptoException"); + } catch (CryptoException expected) { /* ok */ } + } + + @Test + void base32RoundTrip() { + byte[] secret = "12345678901234567890".getBytes(); + String encoded = Base32.encode(secret); + assertArrayEquals(secret, Base32.decode(encoded)); + // canonical Google Authenticator example + assertArrayEquals(new byte[]{(byte)0x48, (byte)0x65, (byte)0x6c, (byte)0x6c, (byte)0x6f, (byte)0x21, (byte)0xde, (byte)0xad, (byte)0xbe, (byte)0xef}, + Base32.decode("JBSWY3DPEHPK3PXP")); + } + + @Test + void otpauthUriMatchesGoogleAuthenticatorFormat() { + byte[] secret = "12345678901234567890".getBytes(); + String uri = Otp.otpauthUri("Acme Bank", "alice@example.com", secret); + // expected structure: otpauth://totp/:?secret=...&issuer=...&algorithm=SHA1&digits=6&period=30 + assertTrue(uri.startsWith("otpauth://totp/Acme%20Bank:alice%40example.com?"), uri); + assertTrue(uri.contains("&issuer=Acme%20Bank")); + assertTrue(uri.contains("&algorithm=SHA1")); + assertTrue(uri.contains("&digits=6")); + assertTrue(uri.contains("&period=30")); + // secret is the Base32 of `secret` with padding stripped + String b32 = Base32.encode(secret).replace("=", ""); + assertTrue(uri.contains("secret=" + b32)); + } + + @Test + void otpauthUriRejectsColonsInIssuerOrAccount() { + try { + Otp.otpauthUri("Acme:Bank", "alice", new byte[20]); + fail("expected CryptoException"); + } catch (CryptoException expected) { /* ok */ } + try { + Otp.otpauthUri("Acme", "alice:doe", new byte[20]); + fail("expected CryptoException"); + } catch (CryptoException expected) { /* ok */ } + } +} diff --git a/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java b/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java index 59c5fc2fb1..8fd92ffb41 100644 --- a/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java +++ b/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java @@ -3413,6 +3413,123 @@ public String toString() { } } + // ================================================================ + // Crypto bridge -- mirrors the JavaSEPort overrides so unit tests + // exercising com.codename1.security.* work in the lightweight + // CodenameOneImplementation subclass used by core-unittests. + + private static java.security.SecureRandom testSecureRandom; + + private static synchronized java.security.SecureRandom testSecureRandom() { + if (testSecureRandom == null) { + testSecureRandom = new java.security.SecureRandom(); + } + return testSecureRandom; + } + + @Override + public void secureRandomBytes(byte[] out) { + if (out == null) return; + testSecureRandom().nextBytes(out); + } + + @Override + public byte[] aesEncrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] plaintext) { + return testAes(transformation, key, iv, aad, plaintext, javax.crypto.Cipher.ENCRYPT_MODE); + } + + @Override + public byte[] aesDecrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] ciphertext) { + return testAes(transformation, key, iv, aad, ciphertext, javax.crypto.Cipher.DECRYPT_MODE); + } + + private static byte[] testAes(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] input, int mode) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(transformation); + javax.crypto.spec.SecretKeySpec keySpec = new javax.crypto.spec.SecretKeySpec(key, "AES"); + String tu = transformation == null ? "" : transformation.toUpperCase(); + if (tu.indexOf("GCM") >= 0) { + cipher.init(mode, keySpec, new javax.crypto.spec.GCMParameterSpec(128, iv)); + } else if (iv != null) { + cipher.init(mode, keySpec, new javax.crypto.spec.IvParameterSpec(iv)); + } else { + cipher.init(mode, keySpec); + } + if (aad != null && aad.length > 0) { + cipher.updateAAD(aad); + } + return cipher.doFinal(input); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("AES " + (mode == javax.crypto.Cipher.ENCRYPT_MODE ? "encrypt" : "decrypt") + " failed: " + e.getMessage()); + } + } + + @Override + public byte[] rsaEncrypt(String transformation, byte[] publicKeyX509, byte[] plaintext) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(transformation); + java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA"); + java.security.PublicKey key = kf.generatePublic(new java.security.spec.X509EncodedKeySpec(publicKeyX509)); + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, key); + return cipher.doFinal(plaintext); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("RSA encrypt failed: " + e.getMessage()); + } + } + + @Override + public byte[] rsaDecrypt(String transformation, byte[] privateKeyPkcs8, byte[] ciphertext) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(transformation); + java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA"); + java.security.PrivateKey key = kf.generatePrivate(new java.security.spec.PKCS8EncodedKeySpec(privateKeyPkcs8)); + cipher.init(javax.crypto.Cipher.DECRYPT_MODE, key); + return cipher.doFinal(ciphertext); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("RSA decrypt failed: " + e.getMessage()); + } + } + + @Override + public byte[] cryptoSign(String algorithm, String keyAlgorithm, byte[] privateKeyPkcs8, byte[] data) { + try { + java.security.KeyFactory kf = java.security.KeyFactory.getInstance(keyAlgorithm); + java.security.PrivateKey priv = kf.generatePrivate(new java.security.spec.PKCS8EncodedKeySpec(privateKeyPkcs8)); + java.security.Signature sig = java.security.Signature.getInstance(algorithm); + sig.initSign(priv); + sig.update(data); + return sig.sign(); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("sign failed: " + e.getMessage()); + } + } + + @Override + public boolean cryptoVerify(String algorithm, String keyAlgorithm, byte[] publicKeyX509, byte[] data, byte[] signature) { + try { + java.security.KeyFactory kf = java.security.KeyFactory.getInstance(keyAlgorithm); + java.security.PublicKey pub = kf.generatePublic(new java.security.spec.X509EncodedKeySpec(publicKeyX509)); + java.security.Signature sig = java.security.Signature.getInstance(algorithm); + sig.initVerify(pub); + sig.update(data); + return sig.verify(signature); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("verify failed: " + e.getMessage()); + } + } + + @Override + public byte[][] generateRsaKeyPair(int bits) { + try { + java.security.KeyPairGenerator kpg = java.security.KeyPairGenerator.getInstance("RSA"); + kpg.initialize(bits); + java.security.KeyPair kp = kpg.generateKeyPair(); + return new byte[][]{ kp.getPublic().getEncoded(), kp.getPrivate().getEncoded() }; + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("RSA keypair generation failed: " + e.getMessage()); + } + } + public static final class TestDatabase extends Database { private final String name; private boolean inTransaction; diff --git a/scripts/cn1playground/tools/src/main/java/com/codenameone/playground/tools/GenerateCN1AccessRegistry.java b/scripts/cn1playground/tools/src/main/java/com/codenameone/playground/tools/GenerateCN1AccessRegistry.java index 5c463ca65c..394e5eea1b 100644 --- a/scripts/cn1playground/tools/src/main/java/com/codenameone/playground/tools/GenerateCN1AccessRegistry.java +++ b/scripts/cn1playground/tools/src/main/java/com/codenameone/playground/tools/GenerateCN1AccessRegistry.java @@ -72,7 +72,26 @@ public final class GenerateCN1AccessRegistry { "com.codename1.security.AuthenticationOptions", "com.codename1.security.BiometricType", "com.codename1.security.BiometricError", - "com.codename1.security.BiometricException" + "com.codename1.security.BiometricException", + // Crypto primitives (Hash, Hmac, Cipher, ...) ride the same + // one-release exclusion: the playground's TeaVM bridge needs to + // catch up with the new package before these can be reflected + // from beanshell. + "com.codename1.security.Hash", + "com.codename1.security.Hmac", + "com.codename1.security.Cipher", + "com.codename1.security.Signature", + "com.codename1.security.SecureRandom", + "com.codename1.security.KeyGenerator", + "com.codename1.security.KeyPair", + "com.codename1.security.SecretKey", + "com.codename1.security.PublicKey", + "com.codename1.security.PrivateKey", + "com.codename1.security.Jwt", + "com.codename1.security.Otp", + "com.codename1.security.Base32", + "com.codename1.security.Base64Url", + "com.codename1.security.CryptoException" )); private static final String[] INDEX_PACKAGE_PREFIXES = new String[]{ diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index fc4810df89..19f1201f14 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -213,6 +213,7 @@ private static int testTimeoutMs() { new StreamApiTest(), new StringApiTest(), new TimeApiTest(), + new CryptoApiTest(), new Java17Tests(), new BackgroundThreadUiAccessTest(), new VPNDetectionAPITest(), @@ -295,7 +296,11 @@ private static boolean isJsSkippedNativeTest(String testName) { || "CallDetectionAPITest".equals(testName) || "LocalNotificationOverrideTest".equals(testName) || "Base64NativePerformanceTest".equals(testName) - || "AccessibilityTest".equals(testName); + || "AccessibilityTest".equals(testName) + // CryptoApiTest exercises AES/RSA/Signature/SecureRandom which + // route through CodenameOneImplementation overrides; the + // JavaScript port doesn't yet provide a crypto bridge. + || "CryptoApiTest".equals(testName); } private static boolean isJsSkippedThemeTest(String testName) { diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CryptoApiTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CryptoApiTest.java new file mode 100644 index 0000000000..d2c3fb635b --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CryptoApiTest.java @@ -0,0 +1,289 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.security.Base32; +import com.codename1.security.Cipher; +import com.codename1.security.Hash; +import com.codename1.security.Hmac; +import com.codename1.security.Jwt; +import com.codename1.security.KeyGenerator; +import com.codename1.security.KeyPair; +import com.codename1.security.Otp; +import com.codename1.security.SecretKey; +import com.codename1.security.SecureRandom; +import com.codename1.security.Signature; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * On-device end-to-end coverage for com.codename1.security. The JUnit suite + * in maven/core-unittests/src/test/java/com/codename1/security/ exercises the + * same API surface against the JavaSE simulator's java.security bridge; + * this test runs the same vectors and round-trips on the actual device build + * so we catch any divergence introduced by: + * + * - ParparVM lowering on iOS (the native CN1Crypto bridge in + * Ports/iOSPort/nativeSources/CN1Crypto.{h,m}) + * - AndroidImplementation overrides + * - Build-system gating (CN1_INCLUDE_CRYPTO #define toggled by IPhoneBuilder + * when com.codename1.security.* is referenced) + * + * Skipped on HTML5/JavaScript -- the JS port does not yet provide a crypto + * impl, see Cn1ssDeviceRunner.HTML5_SKIP_TESTS. + */ +public class CryptoApiTest extends BaseTest { + + @Override + public boolean runTest() { + try { + runHashVectors(); + runHmacVectors(); + runOtpVectors(); + runOtpAuthUri(); + runSecureRandom(); + runJwtHs256(); + runAesGcmRoundTrip(); + runRsaRoundTrip(); + runRsaSignVerify(); + runJwtRs256(); + } catch (Throwable t) { + fail("Crypto API test failed: " + t); + return false; + } + done(); + return true; + } + + @Override + public boolean shouldTakeScreenshot() { + return false; + } + + // ---- Hash test vectors (FIPS 180-4 + RFC 1321) ---------------------- + + private void runHashVectors() { + byte[] abc = ascii("abc"); + assertEqual("900150983cd24fb0d6963f7d28e17f72", + Hash.toHex(Hash.md5(abc)), "MD5 of abc"); + assertEqual("a9993e364706816aba3e25717850c26c9cd0d89d", + Hash.toHex(Hash.sha1(abc)), "SHA-1 of abc"); + assertEqual("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + Hash.toHex(Hash.sha256(abc)), "SHA-256 of abc"); + assertEqual( + "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", + Hash.toHex(Hash.sha384(abc)), "SHA-384 of abc"); + assertEqual( + "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", + Hash.toHex(Hash.sha512(abc)), "SHA-512 of abc"); + + // Streaming-equivalent test: feed byte by byte + Hash h = Hash.create(Hash.SHA256); + byte[] msg = ascii("The quick brown fox jumps over the lazy dog"); + for (int i = 0; i < msg.length; i++) { + h.update(msg[i]); + } + assertEqual("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", + Hash.toHex(h.digest()), "SHA-256 streaming"); + } + + // ---- HMAC test vectors (RFC 4231) ----------------------------------- + + private void runHmacVectors() { + // RFC 4231 Test Case 1: Key = 0x0b * 20, Data = "Hi There" + byte[] key = repeat((byte) 0x0b, 20); + byte[] expected = Hash.fromHex("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"); + assertBytes(expected, Hmac.sha256(key, ascii("Hi There")), "HMAC-SHA-256 RFC 4231 #1"); + + // Test Case 2: Key = "Jefe" + expected = Hash.fromHex("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"); + assertBytes(expected, Hmac.sha256(ascii("Jefe"), ascii("what do ya want for nothing?")), + "HMAC-SHA-256 RFC 4231 #2"); + + // Streaming HMAC produces the same tag as the one-shot variant + byte[] data = ascii("the message split into pieces here"); + Hmac streaming = Hmac.create(Hash.SHA256, ascii("supersecret")); + streaming.update(data, 0, 10); + streaming.update(data, 10, data.length - 10); + assertBytes(Hmac.sha256(ascii("supersecret"), data), streaming.doFinal(), + "Hmac streaming"); + + // Constant-time compare + assertTrue(Hmac.constantTimeEquals(new byte[]{1, 2, 3}, new byte[]{1, 2, 3}), + "constantTimeEquals identical"); + assertTrue(!Hmac.constantTimeEquals(new byte[]{1, 2, 3}, new byte[]{1, 2, 4}), + "constantTimeEquals different"); + } + + // ---- HOTP / TOTP RFC vectors ---------------------------------------- + + private void runOtpVectors() { + byte[] secret = ascii("12345678901234567890"); + // RFC 4226 D + String[] hotp = { + "755224", "287082", "359152", "969429", "338314", + "254676", "287922", "162583", "399871", "520489" + }; + for (int i = 0; i < hotp.length; i++) { + assertEqual(hotp[i], Otp.hotp(secret, i, 6), "HOTP at counter " + i); + } + // RFC 6238 B (SHA-1, 8 digits) + long[] times = {59L, 1111111109L, 1111111111L, 1234567890L, 2000000000L}; + String[] totp = {"94287082", "07081804", "14050471", "89005924", "69279037"}; + for (int i = 0; i < times.length; i++) { + assertEqual(totp[i], + Otp.totp(secret, times[i] * 1000L, 30, 8, Hash.SHA1), + "TOTP at t=" + times[i]); + } + // verifyTotp with tolerance + long now = 1700000010000L; // aligned to a 30s window + String code = Otp.totp(secret, now, 30, 8, Hash.SHA1); + assertTrue(Otp.verifyTotp(secret, code, 0, now, 30, 8, Hash.SHA1), + "verifyTotp current window"); + assertTrue(Otp.verifyTotp(secret, code, 0, now + 25000L, 30, 8, Hash.SHA1), + "verifyTotp drift within window"); + } + + private void runOtpAuthUri() { + byte[] secret = ascii("12345678901234567890"); + String uri = Otp.otpauthUri("Acme Bank", "alice@example.com", secret); + assertTrue(uri.startsWith("otpauth://totp/Acme%20Bank:alice%40example.com?"), + "otpauthUri prefix: " + uri); + assertTrue(uri.indexOf("&algorithm=SHA1") > 0, "otpauthUri carries algorithm: " + uri); + assertTrue(uri.indexOf("&digits=6") > 0, "otpauthUri carries digits: " + uri); + String b32 = Base32.encode(secret).replace("=", ""); + assertTrue(uri.indexOf("secret=" + b32) > 0, "otpauthUri carries Base32 secret: " + uri); + } + + // ---- SecureRandom: only smoke-test, no statistical claim ----------- + + private void runSecureRandom() { + byte[] a = SecureRandom.bytes(32); + byte[] b = SecureRandom.bytes(32); + boolean different = false; + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + different = true; + break; + } + } + assertTrue(different, "SecureRandom should not produce identical buffers"); + for (int i = 0; i < 50; i++) { + int v = SecureRandom.intBelow(1000); + assertTrue(v >= 0 && v < 1000, "intBelow within range"); + } + } + + // ---- JWT HS256 round-trip + canonical jwt.io vector ---------------- + + private void runJwtHs256() throws Exception { + Map claims = new LinkedHashMap(); + claims.put("sub", "alice"); + claims.put("name", "Alice On-Device"); + byte[] secret = ascii("supersecret"); + String token = Jwt.signHs256(claims, secret); + Jwt parsed = Jwt.parse(token); + assertEqual("HS256", parsed.getAlgorithm(), "JWT alg HS256"); + assertEqual("alice", String.valueOf(parsed.getClaim("sub")), "JWT sub claim"); + assertTrue(parsed.verifyHs256(secret), "JWT HS256 verify good"); + assertTrue(!parsed.verifyHs256(ascii("wrong")), "JWT HS256 reject wrong key"); + + // jwt.io canonical example + String canonical = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + assertTrue(Jwt.parse(canonical).verifyHs256(ascii("your-256-bit-secret")), + "JWT HS256 canonical vector"); + } + + // ---- AES-GCM round-trip + tamper detection -------------------------- + + private void runAesGcmRoundTrip() { + SecretKey key = KeyGenerator.aes(256); + byte[] nonce = SecureRandom.bytes(12); + byte[] plaintext = ascii("the magic words are squeamish ossifrage"); + byte[] aad = ascii("v1"); + + byte[] ciphertext = Cipher.aesEncrypt(Cipher.AES_GCM, key, nonce, aad, plaintext); + assertBytes(plaintext, Cipher.aesDecrypt(Cipher.AES_GCM, key, nonce, aad, ciphertext), + "AES-GCM round-trip"); + + // Tamper detection + ciphertext[0] ^= 0x01; + boolean threw = false; + try { + Cipher.aesDecrypt(Cipher.AES_GCM, key, nonce, aad, ciphertext); + } catch (com.codename1.security.CryptoException expected) { + threw = true; + } + assertTrue(threw, "AES-GCM tamper should throw"); + } + + // ---- RSA encrypt/decrypt round-trip -------------------------------- + + private void runRsaRoundTrip() { + // 2048 is the smallest modern minimum; on-device generation is slow + // (a few hundred ms) but still acceptable for a one-shot test. + KeyPair kp = KeyGenerator.rsa(2048); + byte[] payload = ascii("wrap this AES key"); + byte[] sealed = Cipher.rsaEncrypt(Cipher.RSA_OAEP_SHA256, kp.getPublicKey(), payload); + assertBytes(payload, Cipher.rsaDecrypt(Cipher.RSA_OAEP_SHA256, kp.getPrivateKey(), sealed), + "RSA-OAEP round-trip"); + } + + private void runRsaSignVerify() { + KeyPair kp = KeyGenerator.rsa(2048); + byte[] data = ascii("important message"); + byte[] sig = Signature.sign(Signature.SHA256_WITH_RSA, kp.getPrivateKey(), data); + assertTrue(Signature.verify(Signature.SHA256_WITH_RSA, kp.getPublicKey(), data, sig), + "RSA-SHA256 signature verifies"); + data[0] ^= 0x01; + assertTrue(!Signature.verify(Signature.SHA256_WITH_RSA, kp.getPublicKey(), data, sig), + "RSA-SHA256 signature rejects tampered data"); + } + + private void runJwtRs256() { + KeyPair kp = KeyGenerator.rsa(2048); + Map claims = new LinkedHashMap(); + claims.put("sub", "bob"); + String token = Jwt.sign(claims, kp.getPrivateKey(), Jwt.RS256); + Jwt parsed = Jwt.parse(token); + assertEqual("RS256", parsed.getAlgorithm(), "JWT RS256 alg"); + assertTrue(parsed.verify(kp.getPublicKey()), "JWT RS256 verify with right key"); + KeyPair other = KeyGenerator.rsa(2048); + assertTrue(!parsed.verify(other.getPublicKey()), + "JWT RS256 rejects signature from wrong key"); + } + + // ---- tiny assertion helpers (assertEqual/assertTrue come from AbstractTest) ---- + + /// Byte-array equality assertion -- AbstractTest doesn't ship one out of + /// the box, and Arrays.equals is convenient but we want a precise failure + /// message identifying which byte diverged. + private void assertBytes(byte[] expected, byte[] actual, String msg) { + if (expected.length != actual.length) { + throw new RuntimeException(msg + ": length expected " + expected.length + ", got " + actual.length); + } + for (int i = 0; i < expected.length; i++) { + if (expected[i] != actual[i]) { + throw new RuntimeException(msg + ": byte " + i + " expected " + + (expected[i] & 0xff) + ", got " + (actual[i] & 0xff)); + } + } + } + + private static byte[] ascii(String s) { + try { + return s.getBytes("US-ASCII"); + } catch (java.io.UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + private static byte[] repeat(byte b, int n) { + byte[] a = new byte[n]; + for (int i = 0; i < n; i++) { + a[i] = b; + } + return a; + } +}