From 50f8e86a9eafc5efcee363c033e3e1b0496fe6e8 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 16:14:50 +0300 Subject: [PATCH 1/8] Add com.codename1.security crypto API + OtpField widget Introduces a first-party cryptography API in com.codename1.security so apps no longer need an external cn1lib for routine hashing, MAC, encryption, signing, JWT, and OTP work. Pure-Java algorithms produce identical output on every platform; AES/RSA/Signature/SecureRandom route through each port's native crypto provider. New public API: - Hash MD5, SHA-1, SHA-224, SHA-256, SHA-384, SHA-512 (pure Java) - Hmac RFC 2104 + constant-time compare (pure Java) - SecureRandom platform CSPRNG with bias-free intBelow/longBelow - Cipher AES (GCM, CBC, ECB) and RSA (OAEP, PKCS#1) - Signature RSA and ECDSA signing/verification - KeyGenerator AES/HMAC keys, RSA key pairs - Jwt JWT sign/verify for HS, RS, ES families (RFC 7519) - Otp HOTP (RFC 4226) + TOTP (RFC 6238), authenticator-compatible - Base32 / Base64Url, CryptoException UI: com.codename1.components.OtpField -- segmented OTP input with auto-advance, backspace stepping, paste distribution, and a complete listener. Impl layer: - CodenameOneImplementation gains aesEncrypt/Decrypt, rsaEncrypt/Decrypt, cryptoSign/Verify, secureRandomBytes, generateRsaKeyPair, generateSymmetricKey. Default implementation uses java.security via reflection so JavaSE simulator and Android work out of the box. - com.codename1.io.Util exposes narrow public delegates the security package uses; getImplementation() stays package-private. iOS port: - New Ports/iOSPort/nativeSources/CN1Crypto.{h,m} backs AES-CBC/GCM, RSA OAEP/PKCS#1, RSA/ECDSA sign+verify, RSA keypair generation, and secure random against Apple's Security framework + CommonCrypto. - IOSNative + IOSImplementation override every crypto bridge method to route through CN1Crypto. Tests: 32 new tests under maven/core-unittests with RFC reference vectors (FIPS 180-4 for hashes, RFC 4231 for HMAC, RFC 4226/6238 for OTP), plus AES-GCM tamper detection, RSA OAEP round-trip, RSA sign/verify with tampering, and a JWT RS256 round-trip. Developer guide: new "Cryptographic primitives" section in docs/developer-guide/security.asciidoc with examples for every entry point, an algorithm-constant reference table, and recommended-key-size guidance. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/codename1/components/OtpField.java | 229 +++++++ .../impl/CodenameOneImplementation.java | 241 +++++++ CodenameOne/src/com/codename1/io/Util.java | 65 ++ .../src/com/codename1/security/Base32.java | 103 +++ .../src/com/codename1/security/Base64Url.java | 65 ++ .../src/com/codename1/security/Cipher.java | 137 ++++ .../codename1/security/CryptoException.java | 54 ++ .../src/com/codename1/security/Hash.java | 221 ++++++ .../src/com/codename1/security/Hmac.java | 185 +++++ .../src/com/codename1/security/Jwt.java | 410 +++++++++++ .../com/codename1/security/KeyGenerator.java | 77 +++ .../src/com/codename1/security/KeyPair.java | 44 ++ .../codename1/security/MessageDigestImpl.java | 643 ++++++++++++++++++ .../src/com/codename1/security/Otp.java | 159 +++++ .../com/codename1/security/PrivateKey.java | 68 ++ .../src/com/codename1/security/PublicKey.java | 73 ++ .../src/com/codename1/security/SecretKey.java | 67 ++ .../com/codename1/security/SecureRandom.java | 101 +++ .../src/com/codename1/security/Signature.java | 74 ++ .../com/codename1/security/package-info.java | 36 + Ports/iOSPort/nativeSources/CN1Crypto.h | 121 ++++ Ports/iOSPort/nativeSources/CN1Crypto.m | 442 ++++++++++++ Ports/iOSPort/nativeSources/IOSNative.m | 80 +++ .../codename1/impl/ios/IOSImplementation.java | 109 +++ .../src/com/codename1/impl/ios/IOSNative.java | 30 +- docs/developer-guide/security.asciidoc | 268 ++++++++ .../security/CipherSignatureTest.java | 109 +++ .../java/com/codename1/security/HashTest.java | 84 +++ .../java/com/codename1/security/HmacTest.java | 86 +++ .../java/com/codename1/security/JwtTest.java | 97 +++ .../java/com/codename1/security/OtpTest.java | 73 ++ 31 files changed, 4550 insertions(+), 1 deletion(-) create mode 100644 CodenameOne/src/com/codename1/components/OtpField.java create mode 100644 CodenameOne/src/com/codename1/security/Base32.java create mode 100644 CodenameOne/src/com/codename1/security/Base64Url.java create mode 100644 CodenameOne/src/com/codename1/security/Cipher.java create mode 100644 CodenameOne/src/com/codename1/security/CryptoException.java create mode 100644 CodenameOne/src/com/codename1/security/Hash.java create mode 100644 CodenameOne/src/com/codename1/security/Hmac.java create mode 100644 CodenameOne/src/com/codename1/security/Jwt.java create mode 100644 CodenameOne/src/com/codename1/security/KeyGenerator.java create mode 100644 CodenameOne/src/com/codename1/security/KeyPair.java create mode 100644 CodenameOne/src/com/codename1/security/MessageDigestImpl.java create mode 100644 CodenameOne/src/com/codename1/security/Otp.java create mode 100644 CodenameOne/src/com/codename1/security/PrivateKey.java create mode 100644 CodenameOne/src/com/codename1/security/PublicKey.java create mode 100644 CodenameOne/src/com/codename1/security/SecretKey.java create mode 100644 CodenameOne/src/com/codename1/security/SecureRandom.java create mode 100644 CodenameOne/src/com/codename1/security/Signature.java create mode 100644 CodenameOne/src/com/codename1/security/package-info.java create mode 100644 Ports/iOSPort/nativeSources/CN1Crypto.h create mode 100644 Ports/iOSPort/nativeSources/CN1Crypto.m create mode 100644 maven/core-unittests/src/test/java/com/codename1/security/CipherSignatureTest.java create mode 100644 maven/core-unittests/src/test/java/com/codename1/security/HashTest.java create mode 100644 maven/core-unittests/src/test/java/com/codename1/security/HmacTest.java create mode 100644 maven/core-unittests/src/test/java/com/codename1/security/JwtTest.java create mode 100644 maven/core-unittests/src/test/java/com/codename1/security/OtpTest.java diff --git a/CodenameOne/src/com/codename1/components/OtpField.java b/CodenameOne/src/com/codename1/components/OtpField.java new file mode 100644 index 0000000000..db6113a4c1 --- /dev/null +++ b/CodenameOne/src/com/codename1/components/OtpField.java @@ -0,0 +1,229 @@ +/* + * 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() { + 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 (int i = 0; i < completeListeners.size(); i++) { + completeListeners.get(i).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 a9cc64b2b8..454ee77442 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -10129,4 +10129,245 @@ public void run() { } } } + + // ================================================================ + // Crypto bridge -- see com.codename1.security package + // + // Each port is expected to override these methods with calls into the + // platform's native crypto provider (java.security / javax.crypto on JVM + // ports, CommonCrypto/Security on iOS, etc.). The default implementation + // tries java.security via reflection so that it works on any port that + // runs on top of a full JRE (JavaSE simulator and Android both qualify) + // without each port having to override individually. + // + // For iOS via ParparVM, java.security is not on the runtime classpath, so + // the reflection will throw ClassNotFoundException and these methods will + // throw RuntimeException -- the iOS port overrides them with native calls. + // + // Method signatures use only primitive types, String, and byte[] so the + // contract is portable across the various impls. + + /// Fills `out` with cryptographically secure random bytes. Override in the + /// port to route to the platform's native CSPRNG. + public void secureRandomBytes(byte[] out) { + cryptoReflectSecureRandom(out); + } + + /// 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) { + return cryptoReflectAes(transformation, key, iv, aad, plaintext, true); + } + + /// Decrypts with AES. Same parameters as `aesEncrypt`. + public byte[] aesDecrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] ciphertext) { + return cryptoReflectAes(transformation, key, iv, aad, ciphertext, false); + } + + /// 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) { + return cryptoReflectRsa(transformation, publicKeyX509, plaintext, true, true); + } + + /// Decrypts with RSA using a PKCS#8 DER-encoded private key. + public byte[] rsaDecrypt(String transformation, byte[] privateKeyPkcs8, byte[] ciphertext) { + return cryptoReflectRsa(transformation, privateKeyPkcs8, ciphertext, false, false); + } + + /// 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) { + return cryptoReflectSign(algorithm, keyAlgorithm, privateKeyPkcs8, data); + } + + /// Verifies a signature with an X.509 public key. + public boolean cryptoVerify(String algorithm, String keyAlgorithm, byte[] publicKeyX509, byte[] data, byte[] signature) { + return cryptoReflectVerify(algorithm, keyAlgorithm, publicKeyX509, data, signature); + } + + /// Generates a fresh RSA key pair of the given size in bits. Returns + /// `{publicKeyX509, privateKeyPkcs8}`. + public byte[][] generateRsaKeyPair(int bits) { + return cryptoReflectGenerateRsaKeyPair(bits); + } + + /// Generates `bytes` of fresh symmetric key material (just secure random + /// bytes; AES does not require any structure). + public byte[] generateSymmetricKey(int bytes) { + byte[] out = new byte[bytes]; + secureRandomBytes(out); + return out; + } + + // ---- default reflection-based implementations (work on JavaSE + Android) + private static Object cryptoSecureRandomInstance; + private static final Object cryptoReflectionSync = new Object(); + + private void cryptoReflectSecureRandom(byte[] out) { + try { + Object sr; + synchronized (cryptoReflectionSync) { + sr = cryptoSecureRandomInstance; + if (sr == null) { + Class srCls = Class.forName("java.security.SecureRandom"); + sr = srCls.newInstance(); + cryptoSecureRandomInstance = sr; + } + } + sr.getClass().getMethod("nextBytes", new Class[]{byte[].class}).invoke(sr, new Object[]{out}); + } catch (Throwable t) { + throw new RuntimeException("secure random not available: " + t.getMessage()); + } + } + + private byte[] cryptoReflectAes(String transformation, byte[] key, byte[] iv, byte[] aad, + byte[] input, boolean encrypt) { + try { + Class cipherCls = Class.forName("javax.crypto.Cipher"); + Class keySpecCls = Class.forName("javax.crypto.spec.SecretKeySpec"); + Object cipher = cipherCls.getMethod("getInstance", new Class[]{String.class}) + .invoke(null, new Object[]{transformation}); + Object keySpec = keySpecCls.getConstructor(new Class[]{byte[].class, String.class}) + .newInstance(new Object[]{key, "AES"}); + int mode = encrypt ? 1 : 2; // Cipher.ENCRYPT_MODE / DECRYPT_MODE + Object paramSpec = null; + String tu = transformation.toUpperCase(); + if (tu.indexOf("GCM") >= 0) { + Class gcmCls = Class.forName("javax.crypto.spec.GCMParameterSpec"); + paramSpec = gcmCls.getConstructor(new Class[]{int.class, byte[].class}) + .newInstance(new Object[]{Integer.valueOf(128), iv}); + } else if (iv != null) { + Class ivCls = Class.forName("javax.crypto.spec.IvParameterSpec"); + paramSpec = ivCls.getConstructor(new Class[]{byte[].class}) + .newInstance(new Object[]{iv}); + } + Class keyCls = Class.forName("java.security.Key"); + Class algSpecCls = Class.forName("java.security.spec.AlgorithmParameterSpec"); + if (paramSpec == null) { + cipherCls.getMethod("init", new Class[]{int.class, keyCls}) + .invoke(cipher, new Object[]{Integer.valueOf(mode), keySpec}); + } else { + cipherCls.getMethod("init", new Class[]{int.class, keyCls, algSpecCls}) + .invoke(cipher, new Object[]{Integer.valueOf(mode), keySpec, paramSpec}); + } + if (aad != null && aad.length > 0) { + cipherCls.getMethod("updateAAD", new Class[]{byte[].class}) + .invoke(cipher, new Object[]{aad}); + } + return (byte[]) cipherCls.getMethod("doFinal", new Class[]{byte[].class}) + .invoke(cipher, new Object[]{input}); + } catch (Throwable t) { + Throwable c = t.getCause() != null ? t.getCause() : t; + throw new RuntimeException("AES " + (encrypt ? "encrypt" : "decrypt") + " failed: " + c.getMessage()); + } + } + + private byte[] cryptoReflectRsa(String transformation, byte[] keyBytes, byte[] input, + boolean encrypt, boolean pub) { + try { + Class cipherCls = Class.forName("javax.crypto.Cipher"); + Class kfCls = Class.forName("java.security.KeyFactory"); + Object cipher = cipherCls.getMethod("getInstance", new Class[]{String.class}) + .invoke(null, new Object[]{transformation}); + Object kf = kfCls.getMethod("getInstance", new Class[]{String.class}) + .invoke(null, new Object[]{"RSA"}); + Object key; + if (pub) { + Class specCls = Class.forName("java.security.spec.X509EncodedKeySpec"); + Object spec = specCls.getConstructor(new Class[]{byte[].class}).newInstance(new Object[]{keyBytes}); + key = kfCls.getMethod("generatePublic", new Class[]{Class.forName("java.security.spec.KeySpec")}) + .invoke(kf, new Object[]{spec}); + } else { + Class specCls = Class.forName("java.security.spec.PKCS8EncodedKeySpec"); + Object spec = specCls.getConstructor(new Class[]{byte[].class}).newInstance(new Object[]{keyBytes}); + key = kfCls.getMethod("generatePrivate", new Class[]{Class.forName("java.security.spec.KeySpec")}) + .invoke(kf, new Object[]{spec}); + } + int mode = encrypt ? 1 : 2; + Class keyCls = Class.forName("java.security.Key"); + cipherCls.getMethod("init", new Class[]{int.class, keyCls}) + .invoke(cipher, new Object[]{Integer.valueOf(mode), key}); + return (byte[]) cipherCls.getMethod("doFinal", new Class[]{byte[].class}) + .invoke(cipher, new Object[]{input}); + } catch (Throwable t) { + Throwable c = t.getCause() != null ? t.getCause() : t; + throw new RuntimeException("RSA " + (encrypt ? "encrypt" : "decrypt") + " failed: " + c.getMessage()); + } + } + + private byte[] cryptoReflectSign(String algorithm, String keyAlgorithm, + byte[] privateKeyPkcs8, byte[] data) { + try { + Class sigCls = Class.forName("java.security.Signature"); + Class kfCls = Class.forName("java.security.KeyFactory"); + Object kf = kfCls.getMethod("getInstance", new Class[]{String.class}) + .invoke(null, new Object[]{keyAlgorithm}); + Class specCls = Class.forName("java.security.spec.PKCS8EncodedKeySpec"); + Object spec = specCls.getConstructor(new Class[]{byte[].class}).newInstance(new Object[]{privateKeyPkcs8}); + Object priv = kfCls.getMethod("generatePrivate", new Class[]{Class.forName("java.security.spec.KeySpec")}) + .invoke(kf, new Object[]{spec}); + Object sig = sigCls.getMethod("getInstance", new Class[]{String.class}) + .invoke(null, new Object[]{algorithm}); + sigCls.getMethod("initSign", new Class[]{Class.forName("java.security.PrivateKey")}) + .invoke(sig, new Object[]{priv}); + sigCls.getMethod("update", new Class[]{byte[].class}) + .invoke(sig, new Object[]{data}); + return (byte[]) sigCls.getMethod("sign", new Class[0]).invoke(sig, new Object[0]); + } catch (Throwable t) { + Throwable c = t.getCause() != null ? t.getCause() : t; + throw new RuntimeException("sign failed: " + c.getMessage()); + } + } + + private boolean cryptoReflectVerify(String algorithm, String keyAlgorithm, + byte[] publicKeyX509, byte[] data, byte[] signature) { + try { + Class sigCls = Class.forName("java.security.Signature"); + Class kfCls = Class.forName("java.security.KeyFactory"); + Object kf = kfCls.getMethod("getInstance", new Class[]{String.class}) + .invoke(null, new Object[]{keyAlgorithm}); + Class specCls = Class.forName("java.security.spec.X509EncodedKeySpec"); + Object spec = specCls.getConstructor(new Class[]{byte[].class}).newInstance(new Object[]{publicKeyX509}); + Object pub = kfCls.getMethod("generatePublic", new Class[]{Class.forName("java.security.spec.KeySpec")}) + .invoke(kf, new Object[]{spec}); + Object sig = sigCls.getMethod("getInstance", new Class[]{String.class}) + .invoke(null, new Object[]{algorithm}); + sigCls.getMethod("initVerify", new Class[]{Class.forName("java.security.PublicKey")}) + .invoke(sig, new Object[]{pub}); + sigCls.getMethod("update", new Class[]{byte[].class}) + .invoke(sig, new Object[]{data}); + Object r = sigCls.getMethod("verify", new Class[]{byte[].class}) + .invoke(sig, new Object[]{signature}); + return ((Boolean) r).booleanValue(); + } catch (Throwable t) { + Throwable c = t.getCause() != null ? t.getCause() : t; + throw new RuntimeException("verify failed: " + c.getMessage()); + } + } + + private byte[][] cryptoReflectGenerateRsaKeyPair(int bits) { + try { + Class kpgCls = Class.forName("java.security.KeyPairGenerator"); + Object kpg = kpgCls.getMethod("getInstance", new Class[]{String.class}) + .invoke(null, new Object[]{"RSA"}); + kpgCls.getMethod("initialize", new Class[]{int.class}) + .invoke(kpg, new Object[]{Integer.valueOf(bits)}); + Object kp = kpgCls.getMethod("generateKeyPair", new Class[0]).invoke(kpg, new Object[0]); + Class kpCls = Class.forName("java.security.KeyPair"); + Object pub = kpCls.getMethod("getPublic", new Class[0]).invoke(kp, new Object[0]); + Object priv = kpCls.getMethod("getPrivate", new Class[0]).invoke(kp, new Object[0]); + Class keyCls = Class.forName("java.security.Key"); + byte[] pubEnc = (byte[]) keyCls.getMethod("getEncoded", new Class[0]).invoke(pub, new Object[0]); + byte[] privEnc = (byte[]) keyCls.getMethod("getEncoded", new Class[0]).invoke(priv, new Object[0]); + return new byte[][]{pubEnc, privEnc}; + } catch (Throwable t) { + Throwable c = t.getCause() != null ? t.getCause() : t; + throw new RuntimeException("RSA keypair generation failed: " + c.getMessage()); + } + } } 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..1269369bcd --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Base32.java @@ -0,0 +1,103 @@ +/* + * 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 (int i = 0; i < data.length; i++) { + value = (value << 8) | (data[i] & 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/Base64Url.java b/CodenameOne/src/com/codename1/security/Base64Url.java new file mode 100644 index 0000000000..b18f3467be --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Base64Url.java @@ -0,0 +1,65 @@ +/* + * 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; + +/// URL-safe Base64 (RFC 4648 sec5) with the trailing `=` padding stripped -- the +/// encoding used by JWTs and most modern web token formats. +/// +/// This is a thin wrapper around [com.codename1.util.Base64] that swaps `+/` +/// for `-_` and drops padding. +public final class Base64Url { + private Base64Url() {} + + /// Encodes the bytes as a URL-safe Base64 string with no padding. + public static String encode(byte[] data) { + String s = com.codename1.util.Base64.encodeNoNewline(data); + StringBuilder b = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '=') continue; + if (c == '+') c = '-'; + else if (c == '/') c = '_'; + b.append(c); + } + return b.toString(); + } + + /// Decodes a URL-safe Base64 string. Padding is optional. + public static byte[] decode(String s) { + if (s == null) return new byte[0]; + StringBuilder b = new StringBuilder(s.length() + 4); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '-') c = '+'; + else if (c == '_') c = '/'; + b.append(c); + } + int pad = (4 - (b.length() & 3)) & 3; + for (int i = 0; i < pad; i++) b.append('='); + try { + return com.codename1.util.Base64.decode(b.toString().getBytes("UTF-8")); + } catch (java.io.UnsupportedEncodingException e) { + throw new CryptoException("UTF-8 not supported", e); + } + } +} 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..d0aaaf7919 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Hash.java @@ -0,0 +1,221 @@ +/* + * 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 (int i = 0; i < data.length; i++) { + int v = data[i] & 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..1a07a94136 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Hmac.java @@ -0,0 +1,185 @@ +/* + * 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 (a.equals("MD5") || a.equals("SHA1") || a.equals("SHA224") || a.equals("SHA256")) { + return 64; + } + if (a.equals("SHA384") || a.equals("SHA512")) { + 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..e3f5f97d6e --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Jwt.java @@ -0,0 +1,410 @@ +/* + * 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 + "." + Base64Url.encode(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 + "." + Base64Url.encode(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(Base64Url.decode(headerB64)); + Map claims = readJson(Base64Url.decode(payloadB64)); + byte[] sig = sigB64.length() == 0 ? new byte[0] : Base64Url.decode(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 Base64Url.encode(headerJson.getBytes("UTF-8")) + "." + + Base64Url.encode(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/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..7845de1726 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/KeyPair.java @@ -0,0 +1,44 @@ +/* + * 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..9c82ac6804 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/MessageDigestImpl.java @@ -0,0 +1,643 @@ +/* + * 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 (a.equals("MD5")) return new Md5(); + if (a.equals("SHA1")) return new Sha1(); + if (a.equals("SHA224")) return new Sha2_32(true); + if (a.equals("SHA256")) return new Sha2_32(false); + if (a.equals("SHA384")) return new Sha2_64(true); + if (a.equals("SHA512")) return new Sha2_64(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); + + 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; + } + } + + 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, b, c, d; + + Md5() { reset(); } + + public void reset() { + a = 0x67452301; + b = 0xefcdab89; + c = 0x98badcfe; + d = 0x10325476; + bufferLen = 0; + byteCount = 0; + } + + public int digestLength() { return 16; } + + public byte[] digest() { return finishCommon(false); } + + 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)); } + + 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, bb = b, cc = c, 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, h1, h2, h3, h4; + final int[] w = new int[80]; + + Sha1() { reset(); } + + public void reset() { + h0 = 0x67452301; + h1 = 0xEFCDAB89; + h2 = 0x98BADCFE; + h3 = 0x10325476; + h4 = 0xC3D2E1F0; + bufferLen = 0; + byteCount = 0; + } + + public int digestLength() { return 20; } + + public byte[] digest() { return finishCommon(true); } + + void writeStateBigEndian(byte[] out) { + writeBE(out, 0, h0); + writeBE(out, 4, h1); + writeBE(out, 8, h2); + writeBE(out, 12, h3); + writeBE(out, 16, h4); + } + + 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, b = h1, c = h2, d = h3, e = h4; + for (int i = 0; i < 80; i++) { + int f, 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 Sha2_32 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, h1, h2, h3, h4, h5, h6, h7; + private final int[] w = new int[64]; + + Sha2_32(boolean truncated) { + this.truncated = truncated; + reset(); + } + + 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; + } + + public int digestLength() { return truncated ? 28 : 32; } + + public byte[] digest() { return finishCommon(true); } + + 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); + } + } + + 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, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, 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 Sha2_64 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, h1, h2, h3, h4, h5, h6, 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]; + + Sha2_64(boolean truncated) { + this.truncated = truncated; + reset(); + } + + 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; + } + + public int digestLength() { return truncated ? 48 : 64; } + + 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; + } + } + + void update(byte b) { + byteCount++; + buffer[bufferLen++] = b; + if (bufferLen == 128) { + processBlock(buffer, 0); + bufferLen = 0; + } + } + + 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); + if (truncated) { + // sha-384 omits h6, h7 + } else { + 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, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, 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..49c2ef444a --- /dev/null +++ b/CodenameOne/src/com/codename1/security/Otp.java @@ -0,0 +1,159 @@ +/* + * 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); + } + + /// 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..9a803d39f0 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/PrivateKey.java @@ -0,0 +1,68 @@ +/* + * 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 { + private final String algorithm; + private final byte[] encoded; + private final String format; + + PrivateKey(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 == 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); + } + + /// Returns a fresh copy of the encoded key bytes. Treat as sensitive + /// material -- do not log or store unencrypted. + public byte[] getEncoded() { + byte[] copy = new byte[encoded.length]; + System.arraycopy(encoded, 0, copy, 0, encoded.length); + return copy; + } + + /// Returns the algorithm this key is for (e.g. "RSA"). + public String getAlgorithm() { return algorithm; } + + /// Returns the encoding format ("PKCS#8"). + public String getFormat() { return format; } +} diff --git a/CodenameOne/src/com/codename1/security/PublicKey.java b/CodenameOne/src/com/codename1/security/PublicKey.java new file mode 100644 index 0000000000..0c1927be80 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/PublicKey.java @@ -0,0 +1,73 @@ +/* + * 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 { + /// RSA algorithm identifier ("RSA"). + public static final String RSA = "RSA"; + /// Elliptic-curve algorithm identifier ("EC"). + public static final String EC = "EC"; + + private final String algorithm; + private final byte[] encoded; + private final String format; // "X.509" or a vendor format identifier + + PublicKey(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 == 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); + } + + /// Returns a fresh copy of the encoded key bytes. + public byte[] getEncoded() { + byte[] copy = new byte[encoded.length]; + System.arraycopy(encoded, 0, copy, 0, encoded.length); + return copy; + } + + /// Returns the algorithm this key is for (e.g. "RSA"). + public String getAlgorithm() { return algorithm; } + + /// Returns the encoding format ("X.509" for SPKI). + public String getFormat() { return format; } +} diff --git a/CodenameOne/src/com/codename1/security/SecretKey.java b/CodenameOne/src/com/codename1/security/SecretKey.java new file mode 100644 index 0000000000..d0bae39a6c --- /dev/null +++ b/CodenameOne/src/com/codename1/security/SecretKey.java @@ -0,0 +1,67 @@ +/* + * 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 { + private final byte[] key; + private final String algorithm; + + /// Wraps existing key material. + /// + /// #### Parameters + /// + /// - `algorithm`: algorithm identifier (e.g. "AES") + /// + /// - `keyBytes`: raw key material -- defensively copied + public SecretKey(String algorithm, byte[] keyBytes) { + if (algorithm == null) throw new CryptoException("algorithm must not be null"); + if (keyBytes == null) throw new CryptoException("keyBytes must not be null"); + this.algorithm = algorithm; + this.key = new byte[keyBytes.length]; + System.arraycopy(keyBytes, 0, this.key, 0, keyBytes.length); + } + + /// Returns a fresh copy of the raw key material. + public byte[] getEncoded() { + byte[] copy = new byte[key.length]; + System.arraycopy(key, 0, copy, 0, key.length); + return copy; + } + + /// Returns the algorithm this key is intended for. + public String getAlgorithm() { + return algorithm; + } + + /// Returns the length of the key in bits. + public int getBitLength() { + return key.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..839c120980 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/SecureRandom.java @@ -0,0 +1,101 @@ +/* + * 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..752c7f6cf6 --- /dev/null +++ b/CodenameOne/src/com/codename1/security/package-info.java @@ -0,0 +1,36 @@ +/// 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] / [Base64Url] -- encodings commonly paired with crypto code. +/// +/// 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/Ports/iOSPort/nativeSources/CN1Crypto.h b/Ports/iOSPort/nativeSources/CN1Crypto.h new file mode 100644 index 0000000000..5d1226a797 --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1Crypto.h @@ -0,0 +1,121 @@ +/* + * 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 + +#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..9b8a880ec4 --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1Crypto.m @@ -0,0 +1,442 @@ +/* + * 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" +#import +#import +#import +#import + +/* + * 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. + */ +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); + +/* --- 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) { + 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; +} + +/* --- 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; +} diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 73d88cb99e..935dc428d8 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -10886,3 +10886,83 @@ void com_codename1_impl_ios_IOSNative_announceForAccessibility___java_lang_Strin UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, nsText); 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. + +#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 + +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; +} diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index dcbbab64fb..189040d6be 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -10171,4 +10171,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 2bc0b86a4f..a9ae399e24 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java @@ -758,5 +758,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..4d5cf9f69d 100644 --- a/docs/developer-guide/security.asciidoc +++ b/docs/developer-guide/security.asciidoc @@ -247,3 +247,271 @@ 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, TOTP token generation, and so on. + +[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 is 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 very deliberately 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); +---- + +==== OTP input widget + +`com.codename1.components.OtpField` is a segmented input -- one box per character, auto-advancing as the user types and stepping 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/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..67f2d95bd1 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/security/JwtTest.java @@ -0,0 +1,97 @@ +/* + * 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, Base64Url.decode(Base64Url.encode(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..582053f9e4 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/security/OtpTest.java @@ -0,0 +1,73 @@ +/* + * 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")); + } +} From b6ee07b0ea835d14b03c6cfd640aee435d38b493 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 16:34:59 +0300 Subject: [PATCH 2/8] Gate iOS crypto bridge with CN1_INCLUDE_CRYPTO; set Info.plist exemption Apps that don't reference com.codename1.security.* now have none of the CommonCrypto / Security framework encryption symbols in their iOS binary -- and in particular, the AES-GCM SPI symbols (CCCryptorGCMAddIV etc.) stay completely out unless the app opts in. CN1Crypto.h: declare placeholder #define toggles CN1_INCLUDE_CRYPTO CN1_INCLUDE_CRYPTO_GCM (commented-out by default; flipped by IPhoneBuilder when relevant) CN1Crypto.m / IOSNative.m crypto block: - All implementations wrapped in #ifdef CN1_INCLUDE_CRYPTO. When the define is absent we emit no-op stubs that always return CN1_CRYPTO_E_UNSUPPORTED so the IOSImplementation overrides have something to link against but no encryption symbols are referenced. - The AES-GCM SPI externs and cn1_crypto_aes_gcm body are nested inside a separate #ifdef CN1_INCLUDE_CRYPTO_GCM so even apps using AES-CBC / RSA / signatures don't pull in the private GCM symbols unless they ask for it. IPhoneBuilder: - Adds usesCryptoAPI / usesCryptoGcm flags. usesCryptoAPI is set by scanClassesForPermissions when any class in com/codename1/security/ is referenced; usesCryptoGcm is opt-in via the ios.crypto.gcm build hint (default false). - Flips the CN1Crypto.h placeholders to the active defines when needed. - When the crypto API is used, injects ITSAppUsesNonExemptEncryption into Info.plist so App Store Connect doesn't re-prompt on every upload. We always route through Apple-provided crypto (and, for GCM opt-in, stable SPI symbols backed by libcommonCrypto), which qualifies for the EAR 740.17 standard-cryptography exemption. - ios.appUsesNonExemptEncryption build hint overrides the default -- pass "true" if the app links proprietary crypto on top of ours, or "" to omit the key and answer in App Store Connect. Co-Authored-By: Claude Opus 4.7 (1M context) --- Ports/iOSPort/nativeSources/CN1Crypto.h | 11 +++ Ports/iOSPort/nativeSources/CN1Crypto.m | 68 ++++++++++++++++++- Ports/iOSPort/nativeSources/IOSNative.m | 65 ++++++++++++++++++ .../com/codename1/builders/IPhoneBuilder.java | 49 +++++++++++++ 4 files changed, 192 insertions(+), 1 deletion(-) diff --git a/Ports/iOSPort/nativeSources/CN1Crypto.h b/Ports/iOSPort/nativeSources/CN1Crypto.h index 5d1226a797..281396b415 100644 --- a/Ports/iOSPort/nativeSources/CN1Crypto.h +++ b/Ports/iOSPort/nativeSources/CN1Crypto.h @@ -40,6 +40,17 @@ #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 diff --git a/Ports/iOSPort/nativeSources/CN1Crypto.m b/Ports/iOSPort/nativeSources/CN1Crypto.m index 9b8a880ec4..7de2179a7a 100644 --- a/Ports/iOSPort/nativeSources/CN1Crypto.m +++ b/Ports/iOSPort/nativeSources/CN1Crypto.m @@ -18,23 +18,29 @@ */ #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. + * 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 ----------------------------------------------------- */ @@ -79,6 +85,11 @@ int cn1_crypto_aes_gcm(int encrypt, const uint8_t* key, int keyLen, 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; } @@ -159,6 +170,7 @@ int cn1_crypto_aes_gcm(int encrypt, const uint8_t* key, int keyLen, CCCryptorRelease(cryptor); return (int) produced; +#endif /* CN1_INCLUDE_CRYPTO_GCM */ } /* --- RSA --------------------------------------------------------------- */ @@ -440,3 +452,57 @@ + (int) pubInnerLen; *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 935dc428d8..f07aa4e8c5 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -10892,6 +10892,13 @@ void com_codename1_impl_ios_IOSNative_announceForAccessibility___java_lang_Strin // 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" @@ -10903,6 +10910,8 @@ void com_codename1_impl_ios_IOSNative_announceForAccessibility___java_lang_Strin #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)); @@ -10966,3 +10975,59 @@ JAVA_INT com_codename1_impl_ios_IOSNative_generateRsaKeyPair___int_byte_1ARRAY_b 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 */ 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 e25d9fb304..88e79b2daa 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; // 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 // so leaving it commented out. @@ -646,6 +648,13 @@ public void usesClass(String cls) { if (!usesPurchaseAPI && cls.indexOf("com/codename1/payment") == 0) { usesPurchaseAPI = true; } + if (!usesCryptoAPI && cls.indexOf("com/codename1/security/") == 0) { + // Any reference into the crypto package switches on + // the native bridge in CN1Crypto.{h,m} and tells the + // Info.plist that we use encryption (the standard + // Apple-framework exemption applies -- see below). + usesCryptoAPI = true; + } } @Override @@ -708,6 +717,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"); @@ -2742,6 +2773,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) { From 77243babe747f4126b30abd0f53261ed8cbaf3b1 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 18:05:13 +0300 Subject: [PATCH 3/8] Move crypto bridge defaults out of core; fix CLDC11 + iOS compile The previous default implementation used java.security via reflection inside CodenameOneImplementation. The core compiles against the CLDC11 java.lang.Class stub (no getMethod / getConstructor) and the iOS port goes through ParparVM (no java.lang.reflect.Method symbols), so the reflection broke the Ant test-javase build, the iOS native compile, and every transitively-failing job (build, build-ios, build-ios-metal, native-ios, packaging, build-test 8/17/21). Refactor: - CodenameOneImplementation: crypto methods now throw a clear "not supported on this platform" RuntimeException by default; zero java.security / javax.crypto references in core. - JavaSEPort: real implementations using direct java.security / javax.crypto calls (full JDK is on the classpath at compile time and at runtime in the simulator). - AndroidImplementation: same direct-JCE overrides; Android ships the standard JCE provider. - TestCodenameOneImplementation (unit-test fixture in core-unittests): same overrides so the existing 32 crypto tests still exercise the bridge end-to-end. - iOS port already overrides via IOSNative + CN1Crypto.{h,m}; no change there. Also fixes vale-linter alerts on the new developer guide section: - "and so on" -> rewritten without the suggestion - "it is" -> "it's" - "very deliberately" -> "have decided" - "auto-advancing" -> "advances to the next box" 32/32 tests pass on the JavaSE bridge (Hash, Hmac, OtpField, Jwt, Cipher, Signature, KeyGenerator, SecureRandom round-trips). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../impl/CodenameOneImplementation.java | 214 ++---------------- .../impl/android/AndroidImplementation.java | 119 ++++++++++ .../com/codename1/impl/javase/JavaSEPort.java | 123 +++++++++- docs/developer-guide/security.asciidoc | 8 +- .../TestCodenameOneImplementation.java | 117 ++++++++++ 5 files changed, 385 insertions(+), 196 deletions(-) diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index 00f314f339..c38f7514ff 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -10151,26 +10151,24 @@ public void run() { } // ================================================================ - // Crypto bridge -- see com.codename1.security package + // Crypto bridge -- see com.codename1.security package. // - // Each port is expected to override these methods with calls into the - // platform's native crypto provider (java.security / javax.crypto on JVM - // ports, CommonCrypto/Security on iOS, etc.). The default implementation - // tries java.security via reflection so that it works on any port that - // runs on top of a full JRE (JavaSE simulator and Android both qualify) - // without each port having to override individually. - // - // For iOS via ParparVM, java.security is not on the runtime classpath, so - // the reflection will throw ClassNotFoundException and these methods will - // throw RuntimeException -- the iOS port overrides them with native calls. - // - // Method signatures use only primitive types, String, and byte[] so the - // contract is portable across the various impls. + // 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) { - cryptoReflectSecureRandom(out); + throw cryptoUnsupported("secureRandomBytes"); } /// Encrypts with AES. Modes / paddings supported: AES/CBC/PKCS5Padding, @@ -10179,215 +10177,49 @@ public void secureRandomBytes(byte[] out) { /// 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) { - return cryptoReflectAes(transformation, key, iv, aad, plaintext, true); + throw cryptoUnsupported("aesEncrypt"); } /// Decrypts with AES. Same parameters as `aesEncrypt`. public byte[] aesDecrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] ciphertext) { - return cryptoReflectAes(transformation, key, iv, aad, ciphertext, false); + 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) { - return cryptoReflectRsa(transformation, publicKeyX509, plaintext, true, true); + throw cryptoUnsupported("rsaEncrypt"); } /// Decrypts with RSA using a PKCS#8 DER-encoded private key. public byte[] rsaDecrypt(String transformation, byte[] privateKeyPkcs8, byte[] ciphertext) { - return cryptoReflectRsa(transformation, privateKeyPkcs8, ciphertext, false, false); + 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) { - return cryptoReflectSign(algorithm, keyAlgorithm, privateKeyPkcs8, 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) { - return cryptoReflectVerify(algorithm, keyAlgorithm, publicKeyX509, data, signature); + throw cryptoUnsupported("cryptoVerify"); } /// Generates a fresh RSA key pair of the given size in bits. Returns /// `{publicKeyX509, privateKeyPkcs8}`. public byte[][] generateRsaKeyPair(int bits) { - return cryptoReflectGenerateRsaKeyPair(bits); + throw cryptoUnsupported("generateRsaKeyPair"); } - /// Generates `bytes` of fresh symmetric key material (just secure random - /// bytes; AES does not require any structure). + /// 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; } - - // ---- default reflection-based implementations (work on JavaSE + Android) - private static Object cryptoSecureRandomInstance; - private static final Object cryptoReflectionSync = new Object(); - - private void cryptoReflectSecureRandom(byte[] out) { - try { - Object sr; - synchronized (cryptoReflectionSync) { - sr = cryptoSecureRandomInstance; - if (sr == null) { - Class srCls = Class.forName("java.security.SecureRandom"); - sr = srCls.newInstance(); - cryptoSecureRandomInstance = sr; - } - } - sr.getClass().getMethod("nextBytes", new Class[]{byte[].class}).invoke(sr, new Object[]{out}); - } catch (Throwable t) { - throw new RuntimeException("secure random not available: " + t.getMessage()); - } - } - - private byte[] cryptoReflectAes(String transformation, byte[] key, byte[] iv, byte[] aad, - byte[] input, boolean encrypt) { - try { - Class cipherCls = Class.forName("javax.crypto.Cipher"); - Class keySpecCls = Class.forName("javax.crypto.spec.SecretKeySpec"); - Object cipher = cipherCls.getMethod("getInstance", new Class[]{String.class}) - .invoke(null, new Object[]{transformation}); - Object keySpec = keySpecCls.getConstructor(new Class[]{byte[].class, String.class}) - .newInstance(new Object[]{key, "AES"}); - int mode = encrypt ? 1 : 2; // Cipher.ENCRYPT_MODE / DECRYPT_MODE - Object paramSpec = null; - String tu = transformation.toUpperCase(); - if (tu.indexOf("GCM") >= 0) { - Class gcmCls = Class.forName("javax.crypto.spec.GCMParameterSpec"); - paramSpec = gcmCls.getConstructor(new Class[]{int.class, byte[].class}) - .newInstance(new Object[]{Integer.valueOf(128), iv}); - } else if (iv != null) { - Class ivCls = Class.forName("javax.crypto.spec.IvParameterSpec"); - paramSpec = ivCls.getConstructor(new Class[]{byte[].class}) - .newInstance(new Object[]{iv}); - } - Class keyCls = Class.forName("java.security.Key"); - Class algSpecCls = Class.forName("java.security.spec.AlgorithmParameterSpec"); - if (paramSpec == null) { - cipherCls.getMethod("init", new Class[]{int.class, keyCls}) - .invoke(cipher, new Object[]{Integer.valueOf(mode), keySpec}); - } else { - cipherCls.getMethod("init", new Class[]{int.class, keyCls, algSpecCls}) - .invoke(cipher, new Object[]{Integer.valueOf(mode), keySpec, paramSpec}); - } - if (aad != null && aad.length > 0) { - cipherCls.getMethod("updateAAD", new Class[]{byte[].class}) - .invoke(cipher, new Object[]{aad}); - } - return (byte[]) cipherCls.getMethod("doFinal", new Class[]{byte[].class}) - .invoke(cipher, new Object[]{input}); - } catch (Throwable t) { - Throwable c = t.getCause() != null ? t.getCause() : t; - throw new RuntimeException("AES " + (encrypt ? "encrypt" : "decrypt") + " failed: " + c.getMessage()); - } - } - - private byte[] cryptoReflectRsa(String transformation, byte[] keyBytes, byte[] input, - boolean encrypt, boolean pub) { - try { - Class cipherCls = Class.forName("javax.crypto.Cipher"); - Class kfCls = Class.forName("java.security.KeyFactory"); - Object cipher = cipherCls.getMethod("getInstance", new Class[]{String.class}) - .invoke(null, new Object[]{transformation}); - Object kf = kfCls.getMethod("getInstance", new Class[]{String.class}) - .invoke(null, new Object[]{"RSA"}); - Object key; - if (pub) { - Class specCls = Class.forName("java.security.spec.X509EncodedKeySpec"); - Object spec = specCls.getConstructor(new Class[]{byte[].class}).newInstance(new Object[]{keyBytes}); - key = kfCls.getMethod("generatePublic", new Class[]{Class.forName("java.security.spec.KeySpec")}) - .invoke(kf, new Object[]{spec}); - } else { - Class specCls = Class.forName("java.security.spec.PKCS8EncodedKeySpec"); - Object spec = specCls.getConstructor(new Class[]{byte[].class}).newInstance(new Object[]{keyBytes}); - key = kfCls.getMethod("generatePrivate", new Class[]{Class.forName("java.security.spec.KeySpec")}) - .invoke(kf, new Object[]{spec}); - } - int mode = encrypt ? 1 : 2; - Class keyCls = Class.forName("java.security.Key"); - cipherCls.getMethod("init", new Class[]{int.class, keyCls}) - .invoke(cipher, new Object[]{Integer.valueOf(mode), key}); - return (byte[]) cipherCls.getMethod("doFinal", new Class[]{byte[].class}) - .invoke(cipher, new Object[]{input}); - } catch (Throwable t) { - Throwable c = t.getCause() != null ? t.getCause() : t; - throw new RuntimeException("RSA " + (encrypt ? "encrypt" : "decrypt") + " failed: " + c.getMessage()); - } - } - - private byte[] cryptoReflectSign(String algorithm, String keyAlgorithm, - byte[] privateKeyPkcs8, byte[] data) { - try { - Class sigCls = Class.forName("java.security.Signature"); - Class kfCls = Class.forName("java.security.KeyFactory"); - Object kf = kfCls.getMethod("getInstance", new Class[]{String.class}) - .invoke(null, new Object[]{keyAlgorithm}); - Class specCls = Class.forName("java.security.spec.PKCS8EncodedKeySpec"); - Object spec = specCls.getConstructor(new Class[]{byte[].class}).newInstance(new Object[]{privateKeyPkcs8}); - Object priv = kfCls.getMethod("generatePrivate", new Class[]{Class.forName("java.security.spec.KeySpec")}) - .invoke(kf, new Object[]{spec}); - Object sig = sigCls.getMethod("getInstance", new Class[]{String.class}) - .invoke(null, new Object[]{algorithm}); - sigCls.getMethod("initSign", new Class[]{Class.forName("java.security.PrivateKey")}) - .invoke(sig, new Object[]{priv}); - sigCls.getMethod("update", new Class[]{byte[].class}) - .invoke(sig, new Object[]{data}); - return (byte[]) sigCls.getMethod("sign", new Class[0]).invoke(sig, new Object[0]); - } catch (Throwable t) { - Throwable c = t.getCause() != null ? t.getCause() : t; - throw new RuntimeException("sign failed: " + c.getMessage()); - } - } - - private boolean cryptoReflectVerify(String algorithm, String keyAlgorithm, - byte[] publicKeyX509, byte[] data, byte[] signature) { - try { - Class sigCls = Class.forName("java.security.Signature"); - Class kfCls = Class.forName("java.security.KeyFactory"); - Object kf = kfCls.getMethod("getInstance", new Class[]{String.class}) - .invoke(null, new Object[]{keyAlgorithm}); - Class specCls = Class.forName("java.security.spec.X509EncodedKeySpec"); - Object spec = specCls.getConstructor(new Class[]{byte[].class}).newInstance(new Object[]{publicKeyX509}); - Object pub = kfCls.getMethod("generatePublic", new Class[]{Class.forName("java.security.spec.KeySpec")}) - .invoke(kf, new Object[]{spec}); - Object sig = sigCls.getMethod("getInstance", new Class[]{String.class}) - .invoke(null, new Object[]{algorithm}); - sigCls.getMethod("initVerify", new Class[]{Class.forName("java.security.PublicKey")}) - .invoke(sig, new Object[]{pub}); - sigCls.getMethod("update", new Class[]{byte[].class}) - .invoke(sig, new Object[]{data}); - Object r = sigCls.getMethod("verify", new Class[]{byte[].class}) - .invoke(sig, new Object[]{signature}); - return ((Boolean) r).booleanValue(); - } catch (Throwable t) { - Throwable c = t.getCause() != null ? t.getCause() : t; - throw new RuntimeException("verify failed: " + c.getMessage()); - } - } - - private byte[][] cryptoReflectGenerateRsaKeyPair(int bits) { - try { - Class kpgCls = Class.forName("java.security.KeyPairGenerator"); - Object kpg = kpgCls.getMethod("getInstance", new Class[]{String.class}) - .invoke(null, new Object[]{"RSA"}); - kpgCls.getMethod("initialize", new Class[]{int.class}) - .invoke(kpg, new Object[]{Integer.valueOf(bits)}); - Object kp = kpgCls.getMethod("generateKeyPair", new Class[0]).invoke(kpg, new Object[0]); - Class kpCls = Class.forName("java.security.KeyPair"); - Object pub = kpCls.getMethod("getPublic", new Class[0]).invoke(kp, new Object[0]); - Object priv = kpCls.getMethod("getPrivate", new Class[0]).invoke(kp, new Object[0]); - Class keyCls = Class.forName("java.security.Key"); - byte[] pubEnc = (byte[]) keyCls.getMethod("getEncoded", new Class[0]).invoke(pub, new Object[0]); - byte[] privEnc = (byte[]) keyCls.getMethod("getEncoded", new Class[0]).invoke(priv, new Object[0]); - return new byte[][]{pubEnc, privEnc}; - } catch (Throwable t) { - Throwable c = t.getCause() != null ? t.getCause() : t; - throw new RuntimeException("RSA keypair generation failed: " + c.getMessage()); - } - } } 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/docs/developer-guide/security.asciidoc b/docs/developer-guide/security.asciidoc index 4d5cf9f69d..c1d668cd85 100644 --- a/docs/developer-guide/security.asciidoc +++ b/docs/developer-guide/security.asciidoc @@ -286,7 +286,7 @@ The algorithm identifier strings (`Hash.MD5`, `Hash.SHA256`, etc.) are also acce ==== 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, TOTP token generation, and so on. +`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] ---- @@ -318,7 +318,7 @@ 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 is authenticated -- a single tag-mismatch failure detects any tampering of the ciphertext or the associated data. +`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] ---- @@ -406,7 +406,7 @@ 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 very deliberately decided that the transport is trusted. +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 @@ -438,7 +438,7 @@ String code = Otp.hotp(secret, counter++, 6); ==== OTP input widget -`com.codename1.components.OtpField` is a segmented input -- one box per character, auto-advancing as the user types and stepping back on backspace. This is the standard pattern for SMS confirmation and authenticator-app entry screens. +`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] ---- 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; From ba42f8149f0c1d35672a2a01b8b3a827a008fe0a Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 18:32:09 +0300 Subject: [PATCH 4/8] Satisfy PMD quickstart ruleset on new com.codename1.security code CI's static-analysis quality gate fails the build on a set of forbidden PMD rules. Bring the new package into compliance: - ControlStatementBraces: add { ... } around single-statement if / else / for / while bodies that the previous draft wrote inline. - MissingOverride: annotate every method that overrides one of MessageDigestImpl's abstract / virtual hooks (reset, digest, digestLength, update, processBlock, writeStateBigEndian) and the Block64 base-class update overrides. Also annotate the DataChangedListener.dataChanged inner class in OtpField. - LiteralsFirstInComparisons: flip s.equals("X") -> "X".equals(s) in MessageDigestImpl.create and Hmac.blockSizeFor. - EmptyControlStatement: drop the empty if-truncated branch in Sha512Family.digest; collapse to a single !truncated check. - ForLoopCanBeForeach: convert the array index-loops to enhanced for in Hash.toHex, Base32.encode and OtpField.fireCompleteIfFull. - OneDeclarationPerLine: split chained int/long h0,h1,h2,... declarations in the SHA inner classes onto separate lines. - ClassNamingConventions: rename inner classes Sha2_32 -> Sha256Family and Sha2_64 -> Sha512Family (the underscore violated the rule). Local pmd:check on the new files now reports 0 violations from this PR. Also pulls in the vale-asciidoc fixes for security.asciidoc that the previous commit dropped (and that the build-docs job had already flagged before this). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/codename1/components/OtpField.java | 29 ++-- .../src/com/codename1/security/Base32.java | 18 +-- .../src/com/codename1/security/Base64Url.java | 20 ++- .../src/com/codename1/security/Hash.java | 10 +- .../src/com/codename1/security/Hmac.java | 4 +- .../src/com/codename1/security/Jwt.java | 88 ++++++++---- .../src/com/codename1/security/KeyPair.java | 8 +- .../codename1/security/MessageDigestImpl.java | 126 +++++++++++++----- .../src/com/codename1/security/Otp.java | 16 ++- .../com/codename1/security/PrivateKey.java | 8 +- .../src/com/codename1/security/PublicKey.java | 8 +- .../src/com/codename1/security/SecretKey.java | 8 +- .../com/codename1/security/SecureRandom.java | 12 +- 13 files changed, 250 insertions(+), 105 deletions(-) diff --git a/CodenameOne/src/com/codename1/components/OtpField.java b/CodenameOne/src/com/codename1/components/OtpField.java index db6113a4c1..c4ed0a9e55 100644 --- a/CodenameOne/src/com/codename1/components/OtpField.java +++ b/CodenameOne/src/com/codename1/components/OtpField.java @@ -104,8 +104,11 @@ private void buildBoxes() { tf.setConstraint(TextField.NUMERIC); } tf.addDataChangedListener(new DataChangedListener() { + @Override public void dataChanged(int type, int idx) { - if (updating) return; + if (updating) { + return; + } handleChange(index, tf); } }); @@ -116,7 +119,9 @@ public void dataChanged(int type, int idx) { private void handleChange(int index, TextField source) { String text = source.getText(); - if (text == null) text = ""; + if (text == null) { + text = ""; + } // If multiple chars were pasted, distribute across boxes. if (text.length() > 1) { distributePaste(index, text); @@ -143,7 +148,9 @@ private void distributePaste(int startIndex, String text) { 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; + if (numericOnly && (c < '0' || c > '9')) { + continue; + } boxes[p].setText(String.valueOf(c)); p++; } @@ -164,9 +171,11 @@ private void fireCompleteIfFull() { String code = getText(); if (code.length() == length) { ActionEvent evt = new ActionEvent(this); - for (int i = 0; i < completeListeners.size(); i++) { - completeListeners.get(i).actionPerformed(evt); - if (evt.isConsumed()) break; + for (ActionListener listener : completeListeners) { + listener.actionPerformed(evt); + if (evt.isConsumed()) { + break; + } } } } @@ -177,7 +186,9 @@ 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); + if (t != null) { + b.append(t); + } } return b.toString(); } @@ -208,7 +219,9 @@ public void clear() { /// 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); + if (l != null) { + completeListeners.add(l); + } } /// Removes a previously-registered listener. diff --git a/CodenameOne/src/com/codename1/security/Base32.java b/CodenameOne/src/com/codename1/security/Base32.java index 1269369bcd..c4a23ae77b 100644 --- a/CodenameOne/src/com/codename1/security/Base32.java +++ b/CodenameOne/src/com/codename1/security/Base32.java @@ -39,24 +39,24 @@ private Base32() {} "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; + 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; + 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 ""; + 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 (int i = 0; i < data.length; i++) { - value = (value << 8) | (data[i] & 0xff); + for (byte aData : data) { + value = (value << 8) | (aData & 0xff); bits += 8; while (bits >= 5) { b.append(ALPHABET[(value >>> (bits - 5)) & 0x1f]); @@ -66,19 +66,19 @@ public static String encode(byte[] data) { if (bits > 0) { b.append(ALPHABET[(value << (5 - bits)) & 0x1f]); } - while (b.length() < output) b.append('='); + 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]; + 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; + if (c == '=' || c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '-') { continue; } cleaned.append(c); } int len = cleaned.length(); diff --git a/CodenameOne/src/com/codename1/security/Base64Url.java b/CodenameOne/src/com/codename1/security/Base64Url.java index b18f3467be..4c75451042 100644 --- a/CodenameOne/src/com/codename1/security/Base64Url.java +++ b/CodenameOne/src/com/codename1/security/Base64Url.java @@ -36,9 +36,12 @@ public static String encode(byte[] data) { StringBuilder b = new StringBuilder(s.length()); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); - if (c == '=') continue; - if (c == '+') c = '-'; - else if (c == '/') c = '_'; + if (c == '=') { continue; } + if (c == '+') { + c = '-'; + } else if (c == '/') { + c = '_'; + } b.append(c); } return b.toString(); @@ -46,16 +49,19 @@ public static String encode(byte[] data) { /// Decodes a URL-safe Base64 string. Padding is optional. public static byte[] decode(String s) { - if (s == null) return new byte[0]; + if (s == null) { return new byte[0]; } StringBuilder b = new StringBuilder(s.length() + 4); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); - if (c == '-') c = '+'; - else if (c == '_') c = '/'; + if (c == '-') { + c = '+'; + } else if (c == '_') { + c = '/'; + } b.append(c); } int pad = (4 - (b.length() & 3)) & 3; - for (int i = 0; i < pad; i++) b.append('='); + for (int i = 0; i < pad; i++) { b.append('='); } try { return com.codename1.util.Base64.decode(b.toString().getBytes("UTF-8")); } catch (java.io.UnsupportedEncodingException e) { diff --git a/CodenameOne/src/com/codename1/security/Hash.java b/CodenameOne/src/com/codename1/security/Hash.java index d0aaaf7919..d07703b9bf 100644 --- a/CodenameOne/src/com/codename1/security/Hash.java +++ b/CodenameOne/src/com/codename1/security/Hash.java @@ -180,8 +180,8 @@ public static String toHex(byte[] data) { return null; } StringBuilder b = new StringBuilder(data.length * 2); - for (int i = 0; i < data.length; i++) { - int v = data[i] & 0xff; + for (byte d : data) { + int v = d & 0xff; b.append(HEX[v >>> 4]); b.append(HEX[v & 0x0f]); } @@ -209,9 +209,9 @@ public static byte[] fromHex(String hex) { } 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; + 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"); } diff --git a/CodenameOne/src/com/codename1/security/Hmac.java b/CodenameOne/src/com/codename1/security/Hmac.java index 1a07a94136..81f20315ee 100644 --- a/CodenameOne/src/com/codename1/security/Hmac.java +++ b/CodenameOne/src/com/codename1/security/Hmac.java @@ -90,10 +90,10 @@ public static Hmac create(String algorithm, byte[] key) { private static int blockSizeFor(String algorithm) { String a = MessageDigestImpl.normalise(algorithm); - if (a.equals("MD5") || a.equals("SHA1") || a.equals("SHA224") || a.equals("SHA256")) { + if ("MD5".equals(a) || "SHA1".equals(a) || "SHA224".equals(a) || "SHA256".equals(a)) { return 64; } - if (a.equals("SHA384") || a.equals("SHA512")) { + if ("SHA384".equals(a) || "SHA512".equals(a)) { return 128; } throw new CryptoException("unsupported HMAC algorithm: " + algorithm); diff --git a/CodenameOne/src/com/codename1/security/Jwt.java b/CodenameOne/src/com/codename1/security/Jwt.java index e3f5f97d6e..c5c61fcebb 100644 --- a/CodenameOne/src/com/codename1/security/Jwt.java +++ b/CodenameOne/src/com/codename1/security/Jwt.java @@ -122,8 +122,12 @@ public static String signHs512(Map claims, byte[] secret) { /// 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"); + 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 + "." + Base64Url.encode(sig); @@ -140,8 +144,12 @@ public static String sign(Map claims, byte[] secret, String algo /// - `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"); + 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; @@ -171,11 +179,17 @@ public static String signNone(Map claims) { /// 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"); + if (token == null) { + throw new CryptoException("token must not be null"); + } int firstDot = token.indexOf('.'); - if (firstDot < 0) throw new CryptoException("malformed JWT: no '.'"); + 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 '.'"); + 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); @@ -208,7 +222,9 @@ public void setVerifyAllowNoneAlgorithm(boolean allow) { public boolean verifyHs512(byte[] secret) { return verifyHmac(HS512, secret); } private boolean verifyHmac(String expectedAlg, byte[] secret) { - if (!expectedAlg.equals(getAlgorithm())) return false; + if (!expectedAlg.equals(getAlgorithm())) { + return false; + } byte[] expected = computeHmac(expectedAlg, secret, signingInput); return Hmac.constantTimeEquals(expected, signature); } @@ -286,10 +302,15 @@ private static String signingInput(String algorithm, Map claims) 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); + 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) { @@ -298,19 +319,19 @@ private static byte[] computeHmac(String algorithm, byte[] secret, String signin } 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; + 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 + 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); } @@ -338,11 +359,15 @@ private static byte[] derToJoseEcdsa(byte[] der, int coordLen) { int n = der[1] & 0x7f; p = 2 + n; } - if ((der[p] & 0xff) != 0x02) throw new CryptoException("bad ECDSA DER signature"); + 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"); + 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]; @@ -354,12 +379,17 @@ private static byte[] derToJoseEcdsa(byte[] der, int coordLen) { 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--; } + 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; + for (int i = 0; i < pad; i++) { + dst[dstOff + i] = 0; + } System.arraycopy(src, srcOff, dst, dstOff + pad, srcLen); } @@ -398,12 +428,16 @@ private static byte[] joseToDerEcdsa(byte[] jose, int coordLen) { 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++; + 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; + if (needPad) { + out[p++] = 0; + } System.arraycopy(src, start, out, p, end - start); return out; } diff --git a/CodenameOne/src/com/codename1/security/KeyPair.java b/CodenameOne/src/com/codename1/security/KeyPair.java index 7845de1726..a34ba8233f 100644 --- a/CodenameOne/src/com/codename1/security/KeyPair.java +++ b/CodenameOne/src/com/codename1/security/KeyPair.java @@ -30,8 +30,12 @@ public final class KeyPair { /// 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"); + 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; } diff --git a/CodenameOne/src/com/codename1/security/MessageDigestImpl.java b/CodenameOne/src/com/codename1/security/MessageDigestImpl.java index 9c82ac6804..c523dea493 100644 --- a/CodenameOne/src/com/codename1/security/MessageDigestImpl.java +++ b/CodenameOne/src/com/codename1/security/MessageDigestImpl.java @@ -40,12 +40,12 @@ static MessageDigestImpl create(String algorithm) { throw new CryptoException("algorithm must not be null"); } String a = normalise(algorithm); - if (a.equals("MD5")) return new Md5(); - if (a.equals("SHA1")) return new Sha1(); - if (a.equals("SHA224")) return new Sha2_32(true); - if (a.equals("SHA256")) return new Sha2_32(false); - if (a.equals("SHA384")) return new Sha2_64(true); - if (a.equals("SHA512")) return new Sha2_64(false); + 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); } @@ -53,8 +53,8 @@ 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'); + if (c == '-' || c == '_' || c == ' ') { continue; } + if (c >= 'a' && c <= 'z') { c = (char) (c - 'a' + 'A'); } b.append(c); } return b.toString(); @@ -70,11 +70,12 @@ abstract static class Block64 extends MessageDigestImpl { 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; + if (copy > length) { copy = length; } System.arraycopy(data, offset, buffer, bufferLen, copy); bufferLen += copy; offset += copy; @@ -95,6 +96,7 @@ void update(byte[] data, int offset, int length) { } } + @Override void update(byte b) { byteCount++; buffer[bufferLen++] = b; @@ -108,11 +110,11 @@ final byte[] finishCommon(boolean bigEndianLength) { long bits = byteCount * 8L; buffer[bufferLen++] = (byte) 0x80; if (bufferLen > 56) { - while (bufferLen < 64) buffer[bufferLen++] = 0; + while (bufferLen < 64) { buffer[bufferLen++] = 0; } processBlock(buffer, 0); bufferLen = 0; } - while (bufferLen < 56) buffer[bufferLen++] = 0; + while (bufferLen < 56) { buffer[bufferLen++] = 0; } if (bigEndianLength) { buffer[56] = (byte) (bits >>> 56); buffer[57] = (byte) (bits >>> 48); @@ -143,10 +145,14 @@ final byte[] finishCommon(boolean bigEndianLength) { // =============================================================== // MD5 -- RFC 1321 (little-endian length, little-endian word loads) static final class Md5 extends Block64 { - int a, b, c, d; + int a; + int b; + int c; + int d; Md5() { reset(); } + @Override public void reset() { a = 0x67452301; b = 0xefcdab89; @@ -156,10 +162,13 @@ public void reset() { 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. @@ -185,6 +194,7 @@ private static int readLE(byte[] src, int o) { 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); @@ -203,7 +213,10 @@ void processBlock(byte[] block, int o) { int x14 = readLE(block, o + 56); int x15 = readLE(block, o + 60); - int aa = a, bb = b, cc = c, dd = d; + 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); @@ -287,11 +300,16 @@ void processBlock(byte[] block, int o) { // =============================================================== // SHA-1 -- RFC 3174 static final class Sha1 extends Block64 { - int h0, h1, h2, h3, h4; + 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; @@ -302,10 +320,13 @@ public void reset() { 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); @@ -314,6 +335,7 @@ void writeStateBigEndian(byte[] out) { 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 @@ -325,9 +347,14 @@ void processBlock(byte[] block, int o) { int t = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]; w[i] = (t << 1) | (t >>> 31); } - int a = h0, b = h1, c = h2, d = h3, e = h4; + int a = h0; + int b = h1; + int c = h2; + int d = h3; + int e = h4; for (int i = 0; i < 80; i++) { - int f, k; + int f; + int k; if (i < 20) { f = (b & c) | (~b & d); k = 0x5A827999; @@ -358,7 +385,7 @@ void processBlock(byte[] block, int o) { // =============================================================== // SHA-224 / SHA-256 -- FIPS 180-4 (32-bit word version) - static final class Sha2_32 extends Block64 { + static final class Sha256Family extends Block64 { private static final int[] K = { 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, @@ -379,14 +406,22 @@ static final class Sha2_32 extends Block64 { }; private final boolean truncated; // sha-224 if true - private int h0, h1, h2, h3, h4, h5, h6, h7; + 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]; - Sha2_32(boolean truncated) { + Sha256Family(boolean truncated) { this.truncated = truncated; reset(); } + @Override public void reset() { if (truncated) { h0 = 0xc1059ed8; h1 = 0x367cd507; h2 = 0x3070dd17; h3 = 0xf70e5939; @@ -399,10 +434,13 @@ public void reset() { 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); @@ -416,6 +454,7 @@ void writeStateBigEndian(byte[] out) { } } + @Override void processBlock(byte[] block, int o) { for (int i = 0; i < 16; i++) { w[i] = (block[o + i * 4] & 0xff) << 24 @@ -430,7 +469,14 @@ void processBlock(byte[] block, int o) { 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, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7; + 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); @@ -451,7 +497,7 @@ void processBlock(byte[] block, int o) { // =============================================================== // 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 Sha2_64 extends MessageDigestImpl { + static final class Sha512Family extends MessageDigestImpl { private static final long[] K = { 0x428a2f98d728ae22L, 0x7137449123ef65cdL, 0xb5c0fbcfec4d3b2fL, 0xe9b5dba58189dbbcL, 0x3956c25bf348b538L, 0x59f111f1b605d019L, 0x923f82a4af194f9bL, 0xab1c5ed5da6d8118L, @@ -476,17 +522,25 @@ static final class Sha2_64 extends MessageDigestImpl { }; private final boolean truncated; // sha-384 if true - private long h0, h1, h2, h3, h4, h5, h6, h7; + 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]; - Sha2_64(boolean truncated) { + Sha512Family(boolean truncated) { this.truncated = truncated; reset(); } + @Override public void reset() { if (truncated) { h0 = 0xcbbb9d5dc1059ed8L; h1 = 0x629a292a367cd507L; h2 = 0x9159015a3070dd17L; @@ -501,13 +555,15 @@ public void reset() { 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; + if (copy > length) { copy = length; } System.arraycopy(data, offset, buffer, bufferLen, copy); bufferLen += copy; offset += copy; @@ -528,6 +584,7 @@ void update(byte[] data, int offset, int length) { } } + @Override void update(byte b) { byteCount++; buffer[bufferLen++] = b; @@ -537,18 +594,19 @@ void update(byte b) { } } + @Override public byte[] digest() { long bits = byteCount * 8L; buffer[bufferLen++] = (byte) 0x80; if (bufferLen > 112) { - while (bufferLen < 128) buffer[bufferLen++] = 0; + while (bufferLen < 128) { buffer[bufferLen++] = 0; } processBlock(buffer, 0); bufferLen = 0; } - while (bufferLen < 112) buffer[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; + 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); @@ -566,9 +624,8 @@ public byte[] digest() { writeBE64(out, 24, h3); writeBE64(out, 32, h4); writeBE64(out, 40, h5); - if (truncated) { - // sha-384 omits h6, h7 - } else { + // sha-384 omits h6, h7 + if (!truncated) { writeBE64(out, 48, h6); writeBE64(out, 56, h7); } @@ -599,7 +656,14 @@ private void processBlock(byte[] block, int o) { ^ (v2 >>> 6); w[i] = w[i - 16] + s0 + w[i - 7] + s1; } - long a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7; + 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)) diff --git a/CodenameOne/src/com/codename1/security/Otp.java b/CodenameOne/src/com/codename1/security/Otp.java index 49c2ef444a..adfabf3256 100644 --- a/CodenameOne/src/com/codename1/security/Otp.java +++ b/CodenameOne/src/com/codename1/security/Otp.java @@ -78,7 +78,7 @@ public static String hotp(byte[] secret, long counter, int digits, String hashAl | ((mac[offset + 2] & 0xff) << 8) | (mac[offset + 3] & 0xff); int mod = 1; - for (int i = 0; i < digits; i++) mod *= 10; + for (int i = 0; i < digits; i++) { mod *= 10; } code %= mod; return pad(Integer.toString(code), digits); } @@ -129,7 +129,9 @@ public static boolean verifyTotp(byte[] secret, String code, int tolerance) { public static boolean verifyTotp(byte[] secret, String code, int tolerance, long currentTimeMillis, int stepSeconds, int digits, String hashAlgorithm) { - if (code == null) return false; + 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); @@ -141,7 +143,9 @@ public static boolean verifyTotp(byte[] secret, String code, int tolerance, } private static boolean constantTimeEqualsString(String a, String b) { - if (a == null || b == null || a.length() != b.length()) return false; + 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)); @@ -150,9 +154,11 @@ private static boolean constantTimeEqualsString(String a, String b) { } private static String pad(String s, int digits) { - if (s.length() >= digits) return s; + if (s.length() >= digits) { + return s; + } StringBuilder b = new StringBuilder(digits); - for (int i = s.length(); i < digits; i++) b.append('0'); + 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 index 9a803d39f0..1635803961 100644 --- a/CodenameOne/src/com/codename1/security/PrivateKey.java +++ b/CodenameOne/src/com/codename1/security/PrivateKey.java @@ -33,8 +33,12 @@ public final class PrivateKey { private final String format; PrivateKey(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"); + 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); diff --git a/CodenameOne/src/com/codename1/security/PublicKey.java b/CodenameOne/src/com/codename1/security/PublicKey.java index 0c1927be80..f62349aabf 100644 --- a/CodenameOne/src/com/codename1/security/PublicKey.java +++ b/CodenameOne/src/com/codename1/security/PublicKey.java @@ -39,8 +39,12 @@ public final class PublicKey { private final String format; // "X.509" or a vendor format identifier PublicKey(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"); + 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); diff --git a/CodenameOne/src/com/codename1/security/SecretKey.java b/CodenameOne/src/com/codename1/security/SecretKey.java index d0bae39a6c..e9f449fb41 100644 --- a/CodenameOne/src/com/codename1/security/SecretKey.java +++ b/CodenameOne/src/com/codename1/security/SecretKey.java @@ -41,8 +41,12 @@ public final class SecretKey { /// /// - `keyBytes`: raw key material -- defensively copied public SecretKey(String algorithm, byte[] keyBytes) { - if (algorithm == null) throw new CryptoException("algorithm must not be null"); - if (keyBytes == null) throw new CryptoException("keyBytes must not be null"); + if (algorithm == null) { + throw new CryptoException("algorithm must not be null"); + } + if (keyBytes == null) { + throw new CryptoException("keyBytes must not be null"); + } this.algorithm = algorithm; this.key = new byte[keyBytes.length]; System.arraycopy(keyBytes, 0, this.key, 0, keyBytes.length); diff --git a/CodenameOne/src/com/codename1/security/SecureRandom.java b/CodenameOne/src/com/codename1/security/SecureRandom.java index 839c120980..e646fbceb8 100644 --- a/CodenameOne/src/com/codename1/security/SecureRandom.java +++ b/CodenameOne/src/com/codename1/security/SecureRandom.java @@ -52,7 +52,9 @@ public static byte[] bytes(int length) { /// Fills `out` with secure random bytes. public static void fill(byte[] out) { - if (out == null) throw new CryptoException("out must not be null"); + if (out == null) { + throw new CryptoException("out must not be null"); + } try { Util.secureRandomBytes(out); } catch (RuntimeException re) { @@ -63,7 +65,9 @@ public static void fill(byte[] out) { /// 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"); + if (bound <= 0) { + throw new CryptoException("bound must be positive"); + } // Rejection sampling to avoid modulo bias. byte[] buf = new byte[4]; while (true) { @@ -80,7 +84,9 @@ public static int intBelow(int bound) { /// 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"); + if (bound <= 0) { + throw new CryptoException("bound must be positive"); + } byte[] buf = new byte[8]; while (true) { fill(buf); From 301c089da40762c77020370105cd9fc9e9218aad Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 19:17:01 +0300 Subject: [PATCH 5/8] Satisfy Checkstyle on new com.codename1.security code A second CI gate -- Checkstyle, also bound to the verify phase of core-unittests -- flagged 109 brace-placement, whitespace, and continuation-indent issues across the new files. Bring them to zero: - LeftCurlyCheck (94 instances): split single-line `if (x) { y; }` and single-line method definitions like `public byte[] md5(byte[] d) { return create(MD5).digest(d); }` into the canonical three-line form. Codenames One's checkstyle config requires a newline immediately after the opening brace. - WhitespaceAfterCheck (15 instances): add spaces after commas in Hash.HEX array literal and after primitive `(long)` casts in SecureRandom.longBelow. - IndentationCheck (16 instances): bump SHA-512 word-mixing continuation lines from 23 to 24 columns to match the expected level (the `^` operator was one column shy of the canonical four-space continuation). Local mvn checkstyle:check + pmd:check both report 0 violations from this PR after these changes; 32/32 security tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/com/codename1/security/Base32.java | 28 +++-- .../src/com/codename1/security/Base64Url.java | 12 +- .../src/com/codename1/security/Hash.java | 38 ++++-- .../src/com/codename1/security/Hmac.java | 24 +++- .../src/com/codename1/security/Jwt.java | 48 ++++++-- .../src/com/codename1/security/KeyPair.java | 8 +- .../codename1/security/MessageDigestImpl.java | 116 +++++++++++++----- .../src/com/codename1/security/Otp.java | 8 +- .../com/codename1/security/PrivateKey.java | 8 +- .../src/com/codename1/security/PublicKey.java | 8 +- .../com/codename1/security/SecureRandom.java | 22 ++-- 11 files changed, 231 insertions(+), 89 deletions(-) diff --git a/CodenameOne/src/com/codename1/security/Base32.java b/CodenameOne/src/com/codename1/security/Base32.java index c4a23ae77b..c4723b9973 100644 --- a/CodenameOne/src/com/codename1/security/Base32.java +++ b/CodenameOne/src/com/codename1/security/Base32.java @@ -39,18 +39,26 @@ private Base32() {} "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; } + 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; } + 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 ""; } + if (data == null || data.length == 0) { + return ""; + } int output = ((data.length + 4) / 5) * 8; StringBuilder b = new StringBuilder(output); int bits = 0; @@ -66,19 +74,25 @@ public static String encode(byte[] data) { if (bits > 0) { b.append(ALPHABET[(value << (5 - bits)) & 0x1f]); } - while (b.length() < output) { b.append('='); } + 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]; } + 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; } + if (c == '=' || c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '-') { + continue; + } cleaned.append(c); } int len = cleaned.length(); diff --git a/CodenameOne/src/com/codename1/security/Base64Url.java b/CodenameOne/src/com/codename1/security/Base64Url.java index 4c75451042..765638aba9 100644 --- a/CodenameOne/src/com/codename1/security/Base64Url.java +++ b/CodenameOne/src/com/codename1/security/Base64Url.java @@ -36,7 +36,9 @@ public static String encode(byte[] data) { StringBuilder b = new StringBuilder(s.length()); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); - if (c == '=') { continue; } + if (c == '=') { + continue; + } if (c == '+') { c = '-'; } else if (c == '/') { @@ -49,7 +51,9 @@ public static String encode(byte[] data) { /// Decodes a URL-safe Base64 string. Padding is optional. public static byte[] decode(String s) { - if (s == null) { return new byte[0]; } + if (s == null) { + return new byte[0]; + } StringBuilder b = new StringBuilder(s.length() + 4); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); @@ -61,7 +65,9 @@ public static byte[] decode(String s) { b.append(c); } int pad = (4 - (b.length() & 3)) & 3; - for (int i = 0; i < pad; i++) { b.append('='); } + for (int i = 0; i < pad; i++) { + b.append('='); + } try { return com.codename1.util.Base64.decode(b.toString().getBytes("UTF-8")); } catch (java.io.UnsupportedEncodingException e) { diff --git a/CodenameOne/src/com/codename1/security/Hash.java b/CodenameOne/src/com/codename1/security/Hash.java index d07703b9bf..f2d07c6d57 100644 --- a/CodenameOne/src/com/codename1/security/Hash.java +++ b/CodenameOne/src/com/codename1/security/Hash.java @@ -154,22 +154,34 @@ public void reset() { // one-shot convenience entry points /// One-shot MD5 hash. - public static byte[] md5(byte[] data) { return create(MD5).digest(data); } + 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); } + 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); } + 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); } + 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); } + 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); } + public static byte[] sha512(byte[] data) { + return create(SHA512).digest(data); + } // ---------------------------------------------------------------- // hex helpers -- handy for displaying digests and writing test vectors @@ -209,13 +221,19 @@ public static byte[] fromHex(String hex) { } 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; } + 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' + '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 index 81f20315ee..22200c76fc 100644 --- a/CodenameOne/src/com/codename1/security/Hmac.java +++ b/CodenameOne/src/com/codename1/security/Hmac.java @@ -149,22 +149,34 @@ public int tagLength() { // 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); } + 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); } + 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); } + 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); } + 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); } + 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); } + public static byte[] sha512(byte[] key, byte[] data) { + return create(Hash.SHA512, key).doFinal(data); + } // ---------------------------------------------------------------- diff --git a/CodenameOne/src/com/codename1/security/Jwt.java b/CodenameOne/src/com/codename1/security/Jwt.java index c5c61fcebb..0c9ac135c8 100644 --- a/CodenameOne/src/com/codename1/security/Jwt.java +++ b/CodenameOne/src/com/codename1/security/Jwt.java @@ -213,13 +213,19 @@ public void setVerifyAllowNoneAlgorithm(boolean 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); } + public boolean verifyHs256(byte[] secret) { + return verifyHmac(HS256, secret); + } /// HMAC verification with HS384. - public boolean verifyHs384(byte[] secret) { return verifyHmac(HS384, secret); } + public boolean verifyHs384(byte[] secret) { + return verifyHmac(HS384, secret); + } /// HMAC verification with HS512. - public boolean verifyHs512(byte[] secret) { return verifyHmac(HS512, secret); } + public boolean verifyHs512(byte[] secret) { + return verifyHmac(HS512, secret); + } private boolean verifyHmac(String expectedAlg, byte[] secret) { if (!expectedAlg.equals(getAlgorithm())) { @@ -319,19 +325,37 @@ private static byte[] computeHmac(String algorithm, byte[] secret, String signin } 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; } + 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 + 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); } diff --git a/CodenameOne/src/com/codename1/security/KeyPair.java b/CodenameOne/src/com/codename1/security/KeyPair.java index a34ba8233f..a63e2d50c3 100644 --- a/CodenameOne/src/com/codename1/security/KeyPair.java +++ b/CodenameOne/src/com/codename1/security/KeyPair.java @@ -41,8 +41,12 @@ public KeyPair(PublicKey publicKey, PrivateKey privateKey) { } /// Returns the public part of this pair. - public PublicKey getPublicKey() { return publicKey; } + public PublicKey getPublicKey() { + return publicKey; + } /// Returns the private part of this pair. - public PrivateKey getPrivateKey() { return privateKey; } + public PrivateKey getPrivateKey() { + return privateKey; + } } diff --git a/CodenameOne/src/com/codename1/security/MessageDigestImpl.java b/CodenameOne/src/com/codename1/security/MessageDigestImpl.java index c523dea493..211b8a3ba0 100644 --- a/CodenameOne/src/com/codename1/security/MessageDigestImpl.java +++ b/CodenameOne/src/com/codename1/security/MessageDigestImpl.java @@ -40,12 +40,24 @@ static MessageDigestImpl create(String algorithm) { 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); } + 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); } @@ -53,8 +65,12 @@ 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'); } + if (c == '-' || c == '_' || c == ' ') { + continue; + } + if (c >= 'a' && c <= 'z') { + c = (char) (c - 'a' + 'A'); + } b.append(c); } return b.toString(); @@ -75,7 +91,9 @@ void update(byte[] data, int offset, int length) { byteCount += length; if (bufferLen > 0) { int copy = 64 - bufferLen; - if (copy > length) { copy = length; } + if (copy > length) { + copy = length; + } System.arraycopy(data, offset, buffer, bufferLen, copy); bufferLen += copy; offset += copy; @@ -110,11 +128,15 @@ final byte[] finishCommon(boolean bigEndianLength) { long bits = byteCount * 8L; buffer[bufferLen++] = (byte) 0x80; if (bufferLen > 56) { - while (bufferLen < 64) { buffer[bufferLen++] = 0; } + while (bufferLen < 64) { + buffer[bufferLen++] = 0; + } processBlock(buffer, 0); bufferLen = 0; } - while (bufferLen < 56) { buffer[bufferLen++] = 0; } + while (bufferLen < 56) { + buffer[bufferLen++] = 0; + } if (bigEndianLength) { buffer[56] = (byte) (bits >>> 56); buffer[57] = (byte) (bits >>> 48); @@ -150,7 +172,9 @@ static final class Md5 extends Block64 { int c; int d; - Md5() { reset(); } + Md5() { + reset(); + } @Override public void reset() { @@ -163,10 +187,14 @@ public void reset() { } @Override - public int digestLength() { return 16; } + public int digestLength() { + return 16; + } @Override - public byte[] digest() { return finishCommon(false); } + public byte[] digest() { + return finishCommon(false); + } @Override void writeStateBigEndian(byte[] out) { @@ -192,7 +220,9 @@ private static int readLE(byte[] src, int o) { | (src[o + 3] & 0xff) << 24; } - private static int rol(int v, int s) { return (v << s) | (v >>> (32 - s)); } + private static int rol(int v, int s) { + return (v << s) | (v >>> (32 - s)); + } @Override void processBlock(byte[] block, int o) { @@ -307,7 +337,9 @@ static final class Sha1 extends Block64 { int h4; final int[] w = new int[80]; - Sha1() { reset(); } + Sha1() { + reset(); + } @Override public void reset() { @@ -321,10 +353,14 @@ public void reset() { } @Override - public int digestLength() { return 20; } + public int digestLength() { + return 20; + } @Override - public byte[] digest() { return finishCommon(true); } + public byte[] digest() { + return finishCommon(true); + } @Override void writeStateBigEndian(byte[] out) { @@ -435,10 +471,14 @@ public void reset() { } @Override - public int digestLength() { return truncated ? 28 : 32; } + public int digestLength() { + return truncated ? 28 : 32; + } @Override - public byte[] digest() { return finishCommon(true); } + public byte[] digest() { + return finishCommon(true); + } @Override void writeStateBigEndian(byte[] out) { @@ -556,14 +596,18 @@ public void reset() { } @Override - public int digestLength() { return truncated ? 48 : 64; } + 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; } + if (copy > length) { + copy = length; + } System.arraycopy(data, offset, buffer, bufferLen, copy); bufferLen += copy; offset += copy; @@ -599,14 +643,20 @@ public byte[] digest() { long bits = byteCount * 8L; buffer[bufferLen++] = (byte) 0x80; if (bufferLen > 112) { - while (bufferLen < 128) { buffer[bufferLen++] = 0; } + while (bufferLen < 128) { + buffer[bufferLen++] = 0; + } processBlock(buffer, 0); bufferLen = 0; } - while (bufferLen < 112) { buffer[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; } + 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); @@ -648,12 +698,12 @@ private void processBlock(byte[] block, int o) { for (int i = 16; i < 80; i++) { long v15 = w[i - 15]; long s0 = ((v15 >>> 1) | (v15 << 63)) - ^ ((v15 >>> 8) | (v15 << 56)) - ^ (v15 >>> 7); + ^ ((v15 >>> 8) | (v15 << 56)) + ^ (v15 >>> 7); long v2 = w[i - 2]; long s1 = ((v2 >>> 19) | (v2 << 45)) - ^ ((v2 >>> 61) | (v2 << 3)) - ^ (v2 >>> 6); + ^ ((v2 >>> 61) | (v2 << 3)) + ^ (v2 >>> 6); w[i] = w[i - 16] + s0 + w[i - 7] + s1; } long a = h0; @@ -666,13 +716,13 @@ private void processBlock(byte[] block, int o) { long h = h7; for (int i = 0; i < 80; i++) { long s1 = ((e >>> 14) | (e << 50)) - ^ ((e >>> 18) | (e << 46)) - ^ ((e >>> 41) | (e << 23)); + ^ ((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)); + ^ ((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; diff --git a/CodenameOne/src/com/codename1/security/Otp.java b/CodenameOne/src/com/codename1/security/Otp.java index adfabf3256..14027b41b7 100644 --- a/CodenameOne/src/com/codename1/security/Otp.java +++ b/CodenameOne/src/com/codename1/security/Otp.java @@ -78,7 +78,9 @@ public static String hotp(byte[] secret, long counter, int digits, String hashAl | ((mac[offset + 2] & 0xff) << 8) | (mac[offset + 3] & 0xff); int mod = 1; - for (int i = 0; i < digits; i++) { mod *= 10; } + for (int i = 0; i < digits; i++) { + mod *= 10; + } code %= mod; return pad(Integer.toString(code), digits); } @@ -158,7 +160,9 @@ private static String pad(String s, int digits) { return s; } StringBuilder b = new StringBuilder(digits); - for (int i = s.length(); i < digits; i++) { b.append('0'); } + 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 index 1635803961..314f9afa34 100644 --- a/CodenameOne/src/com/codename1/security/PrivateKey.java +++ b/CodenameOne/src/com/codename1/security/PrivateKey.java @@ -65,8 +65,12 @@ public byte[] getEncoded() { } /// Returns the algorithm this key is for (e.g. "RSA"). - public String getAlgorithm() { return algorithm; } + public String getAlgorithm() { + return algorithm; + } /// Returns the encoding format ("PKCS#8"). - public String getFormat() { return format; } + public String getFormat() { + return format; + } } diff --git a/CodenameOne/src/com/codename1/security/PublicKey.java b/CodenameOne/src/com/codename1/security/PublicKey.java index f62349aabf..cb509525fa 100644 --- a/CodenameOne/src/com/codename1/security/PublicKey.java +++ b/CodenameOne/src/com/codename1/security/PublicKey.java @@ -70,8 +70,12 @@ public byte[] getEncoded() { } /// Returns the algorithm this key is for (e.g. "RSA"). - public String getAlgorithm() { return algorithm; } + public String getAlgorithm() { + return algorithm; + } /// Returns the encoding format ("X.509" for SPKI). - public String getFormat() { return format; } + public String getFormat() { + return format; + } } diff --git a/CodenameOne/src/com/codename1/security/SecureRandom.java b/CodenameOne/src/com/codename1/security/SecureRandom.java index e646fbceb8..5e34fb0ca1 100644 --- a/CodenameOne/src/com/codename1/security/SecureRandom.java +++ b/CodenameOne/src/com/codename1/security/SecureRandom.java @@ -72,8 +72,10 @@ public static int intBelow(int bound) { 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 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; @@ -90,14 +92,14 @@ public static long longBelow(long bound) { 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 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; From 3a9f2cce88d748a714765bea1b141eaaf8e47d7c Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 20:56:18 +0300 Subject: [PATCH 6/8] crypto API review feedback: reuse Base64, abstract Key, OTP tutorial, device test Code-review feedback on PR #4994: 1. Reuse the existing util.Base64 instead of adding a parallel Base64Url class. util.Base64 is already SIMD-optimized; layer URL-safe encode/decode on top of its encodeNoNewline path and inherit the speedup. - util.Base64: new encodeUrlSafe(byte[]) / decodeUrlSafe(String) that run through the same fast encoder, then map +-> / -> _ and strip trailing = padding (RFC 4648 sec5). - Jwt, JwtTest, OtpTest, package-info: switch to the new helpers. - Drop security/Base64Url.java entirely. 2. Pull the common key fields (algorithm, encoded, format) into a new abstract Key base class. PublicKey/PrivateKey/SecretKey now extend it and only carry the type-specific extras (static factories for the asymmetric pair; getBitLength on the symmetric one). 3. Otp.otpauthUri(issuer, accountName, secret, ...): builds the canonical otpauth://totp/... URI per the Google Authenticator KeyUri spec so the secret can be displayed as a QR code on enrolment screens. Six-digit / 30-second / SHA-1 default matches what every authenticator app expects. 4. Developer guide: expand the OTP section into a real two-factor-auth tutorial -- enrolment via otpauthUri + QR rendering, verification via OtpField + Otp.verifyTotp, server-side guidance, rate-limiting note. Documents three approaches for actually rendering the QR (server-side render URL, QR cn1lib, plain Base32 typed entry) since core doesn't ship a QR encoder yet. 5. CryptoApiTest: device-side coverage in scripts/hellocodenameone that mirrors the JUnit assertions (hash + HMAC RFC vectors, HOTP/TOTP RFC vectors, AES-GCM round-trip + tamper detection, RSA-OAEP round-trip, RSA-SHA-256 sign/verify, JWT HS256 + RS256 round-trips, otpauthUri shape). Registered in Cn1ssDeviceRunner; skipped on HTML5 (no crypto bridge on the JS port yet). SIMD acceleration for hashing was investigated but declined: SHA-2's round state machine has data dependencies that block single-message vectorization, and the current Simd surface doesn't expose 32/64-bit rotation. The natural SIMD beneficiary in this PR -- Base64 -- is already covered by util.Base64. Local: pmd:check, checkstyle:check both 0 violations; 34 JUnit tests pass (32 from prior commits + 2 new otpauthUri tests). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/com/codename1/security/Base64Url.java | 77 ----- .../src/com/codename1/security/Jwt.java | 14 +- .../src/com/codename1/security/Key.java | 91 ++++++ .../src/com/codename1/security/Otp.java | 117 +++++++ .../com/codename1/security/PrivateKey.java | 34 +-- .../src/com/codename1/security/PublicKey.java | 34 +-- .../src/com/codename1/security/SecretKey.java | 28 +- .../com/codename1/security/package-info.java | 6 +- .../src/com/codename1/util/Base64.java | 71 +++++ docs/developer-guide/security.asciidoc | 72 +++++ .../java/com/codename1/security/JwtTest.java | 3 +- .../java/com/codename1/security/OtpTest.java | 27 ++ .../tests/Cn1ssDeviceRunner.java | 7 +- .../hellocodenameone/tests/CryptoApiTest.java | 289 ++++++++++++++++++ 14 files changed, 694 insertions(+), 176 deletions(-) delete mode 100644 CodenameOne/src/com/codename1/security/Base64Url.java create mode 100644 CodenameOne/src/com/codename1/security/Key.java create mode 100644 scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CryptoApiTest.java diff --git a/CodenameOne/src/com/codename1/security/Base64Url.java b/CodenameOne/src/com/codename1/security/Base64Url.java deleted file mode 100644 index 765638aba9..0000000000 --- a/CodenameOne/src/com/codename1/security/Base64Url.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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; - -/// URL-safe Base64 (RFC 4648 sec5) with the trailing `=` padding stripped -- the -/// encoding used by JWTs and most modern web token formats. -/// -/// This is a thin wrapper around [com.codename1.util.Base64] that swaps `+/` -/// for `-_` and drops padding. -public final class Base64Url { - private Base64Url() {} - - /// Encodes the bytes as a URL-safe Base64 string with no padding. - public static String encode(byte[] data) { - String s = com.codename1.util.Base64.encodeNoNewline(data); - StringBuilder b = new StringBuilder(s.length()); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (c == '=') { - continue; - } - if (c == '+') { - c = '-'; - } else if (c == '/') { - c = '_'; - } - b.append(c); - } - return b.toString(); - } - - /// Decodes a URL-safe Base64 string. Padding is optional. - public static byte[] decode(String s) { - if (s == null) { - return new byte[0]; - } - StringBuilder b = new StringBuilder(s.length() + 4); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (c == '-') { - c = '+'; - } else if (c == '_') { - c = '/'; - } - b.append(c); - } - int pad = (4 - (b.length() & 3)) & 3; - for (int i = 0; i < pad; i++) { - b.append('='); - } - try { - return com.codename1.util.Base64.decode(b.toString().getBytes("UTF-8")); - } catch (java.io.UnsupportedEncodingException e) { - throw new CryptoException("UTF-8 not supported", e); - } - } -} diff --git a/CodenameOne/src/com/codename1/security/Jwt.java b/CodenameOne/src/com/codename1/security/Jwt.java index 0c9ac135c8..39b73dff6e 100644 --- a/CodenameOne/src/com/codename1/security/Jwt.java +++ b/CodenameOne/src/com/codename1/security/Jwt.java @@ -130,7 +130,7 @@ public static String sign(Map claims, byte[] secret, String algo } String signingInput = signingInput(algorithm, claims); byte[] sig = computeHmac(algorithm, secret, signingInput); - return signingInput + "." + Base64Url.encode(sig); + return signingInput + "." + com.codename1.util.Base64.encodeUrlSafe(sig); } /// Signs `claims` with the given RSA or ECDSA algorithm. @@ -164,7 +164,7 @@ public static String sign(Map claims, PrivateKey privateKey, Str if (algorithm.startsWith("ES")) { sig = derToJoseEcdsa(sig, ecdsaCoordinateLength(algorithm)); } - return signingInput + "." + Base64Url.encode(sig); + return signingInput + "." + com.codename1.util.Base64.encodeUrlSafe(sig); } /// Builds an unsigned token (header `{"alg":"none"}`). Accepting these on @@ -193,9 +193,9 @@ public static Jwt parse(String token) { String headerB64 = token.substring(0, firstDot); String payloadB64 = token.substring(firstDot + 1, secondDot); String sigB64 = token.substring(secondDot + 1); - Map header = readJson(Base64Url.decode(headerB64)); - Map claims = readJson(Base64Url.decode(payloadB64)); - byte[] sig = sigB64.length() == 0 ? new byte[0] : Base64Url.decode(sigB64); + 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); } @@ -299,8 +299,8 @@ private static String signingInput(String algorithm, Map claims) String headerJson = JSONParser.mapToJson(hdr); String claimsJson = JSONParser.mapToJson(claims); try { - return Base64Url.encode(headerJson.getBytes("UTF-8")) + "." - + Base64Url.encode(claimsJson.getBytes("UTF-8")); + 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); } 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/Otp.java b/CodenameOne/src/com/codename1/security/Otp.java index 14027b41b7..747fecd778 100644 --- a/CodenameOne/src/com/codename1/security/Otp.java +++ b/CodenameOne/src/com/codename1/security/Otp.java @@ -127,6 +127,123 @@ public static boolean verifyTotp(byte[] secret, String code, int 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, diff --git a/CodenameOne/src/com/codename1/security/PrivateKey.java b/CodenameOne/src/com/codename1/security/PrivateKey.java index 314f9afa34..07e8cfb43a 100644 --- a/CodenameOne/src/com/codename1/security/PrivateKey.java +++ b/CodenameOne/src/com/codename1/security/PrivateKey.java @@ -27,22 +27,10 @@ /// /// For interop with PEM files (`-----BEGIN PRIVATE KEY-----`) feed the /// PKCS#8 DER bytes to [#fromPkcs8]. -public final class PrivateKey { - private final String algorithm; - private final byte[] encoded; - private final String format; +public final class PrivateKey extends Key { PrivateKey(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 == null ? "PKCS#8" : format; + super(algorithm, encoded, format == null ? "PKCS#8" : format); } /// Wraps a PKCS#8 DER blob. This is the format produced by `openssl @@ -55,22 +43,4 @@ public static PrivateKey fromPkcs8(String algorithm, byte[] pkcs8Der) { public static PrivateKey rsa(byte[] pkcs8Der) { return fromPkcs8(PublicKey.RSA, pkcs8Der); } - - /// Returns a fresh copy of the encoded key bytes. Treat as sensitive - /// material -- do not log or store unencrypted. - public byte[] getEncoded() { - byte[] copy = new byte[encoded.length]; - System.arraycopy(encoded, 0, copy, 0, encoded.length); - return copy; - } - - /// Returns the algorithm this key is for (e.g. "RSA"). - public String getAlgorithm() { - return algorithm; - } - - /// Returns the encoding format ("PKCS#8"). - public String getFormat() { - return format; - } } diff --git a/CodenameOne/src/com/codename1/security/PublicKey.java b/CodenameOne/src/com/codename1/security/PublicKey.java index cb509525fa..39acaf51cb 100644 --- a/CodenameOne/src/com/codename1/security/PublicKey.java +++ b/CodenameOne/src/com/codename1/security/PublicKey.java @@ -28,27 +28,14 @@ /// 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 { +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"; - private final String algorithm; - private final byte[] encoded; - private final String format; // "X.509" or a vendor format identifier - PublicKey(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 == null ? "X.509" : format; + super(algorithm, encoded, format == null ? "X.509" : format); } /// Wraps an X.509 / SubjectPublicKeyInfo (SPKI) DER blob. This is the @@ -61,21 +48,4 @@ public static PublicKey fromX509(String algorithm, byte[] x509Der) { public static PublicKey rsa(byte[] x509Der) { return fromX509(RSA, x509Der); } - - /// Returns a fresh copy of the encoded key bytes. - public byte[] getEncoded() { - byte[] copy = new byte[encoded.length]; - System.arraycopy(encoded, 0, copy, 0, encoded.length); - return copy; - } - - /// Returns the algorithm this key is for (e.g. "RSA"). - public String getAlgorithm() { - return algorithm; - } - - /// Returns the encoding format ("X.509" for SPKI). - public String getFormat() { - return format; - } } diff --git a/CodenameOne/src/com/codename1/security/SecretKey.java b/CodenameOne/src/com/codename1/security/SecretKey.java index e9f449fb41..f84f7cd717 100644 --- a/CodenameOne/src/com/codename1/security/SecretKey.java +++ b/CodenameOne/src/com/codename1/security/SecretKey.java @@ -29,9 +29,7 @@ /// 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 { - private final byte[] key; - private final String algorithm; +public final class SecretKey extends Key { /// Wraps existing key material. /// @@ -41,31 +39,11 @@ public final class SecretKey { /// /// - `keyBytes`: raw key material -- defensively copied public SecretKey(String algorithm, byte[] keyBytes) { - if (algorithm == null) { - throw new CryptoException("algorithm must not be null"); - } - if (keyBytes == null) { - throw new CryptoException("keyBytes must not be null"); - } - this.algorithm = algorithm; - this.key = new byte[keyBytes.length]; - System.arraycopy(keyBytes, 0, this.key, 0, keyBytes.length); - } - - /// Returns a fresh copy of the raw key material. - public byte[] getEncoded() { - byte[] copy = new byte[key.length]; - System.arraycopy(key, 0, copy, 0, key.length); - return copy; - } - - /// Returns the algorithm this key is intended for. - public String getAlgorithm() { - return algorithm; + super(algorithm, keyBytes, "RAW"); } /// Returns the length of the key in bits. public int getBitLength() { - return key.length * 8; + return getEncoded().length * 8; } } diff --git a/CodenameOne/src/com/codename1/security/package-info.java b/CodenameOne/src/com/codename1/security/package-info.java index 752c7f6cf6..060d2ba697 100644 --- a/CodenameOne/src/com/codename1/security/package-info.java +++ b/CodenameOne/src/com/codename1/security/package-info.java @@ -16,7 +16,11 @@ /// - [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] / [Base64Url] -- encodings commonly paired with crypto code. +/// - [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]. diff --git a/CodenameOne/src/com/codename1/util/Base64.java b/CodenameOne/src/com/codename1/util/Base64.java index 75caf6f04e..986c6282f6 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 §5: `+` 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 §5). 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/docs/developer-guide/security.asciidoc b/docs/developer-guide/security.asciidoc index c1d668cd85..08c30a7f89 100644 --- a/docs/developer-guide/security.asciidoc +++ b/docs/developer-guide/security.asciidoc @@ -436,6 +436,78 @@ For HOTP, supply the counter explicitly and increment it after every successful 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. 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 index 67f2d95bd1..743cbd334e 100644 --- a/maven/core-unittests/src/test/java/com/codename1/security/JwtTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/security/JwtTest.java @@ -92,6 +92,7 @@ void noneAlgorithmRejectedByDefault() throws Exception { void base64UrlRoundTrip() throws Exception { byte[] data = new byte[256]; for (int i = 0; i < 256; i++) data[i] = (byte) i; - assertArrayEquals(data, Base64Url.decode(Base64Url.encode(data))); + 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 index 582053f9e4..2638804bfe 100644 --- a/maven/core-unittests/src/test/java/com/codename1/security/OtpTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/security/OtpTest.java @@ -70,4 +70,31 @@ void base32RoundTrip() { 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/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; + } +} From 6aca5124ff8f7228c25c70783a85cc6bff6ff362 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 21:17:31 +0300 Subject: [PATCH 7/8] Strip non-ASCII section sign from Base64 URL-safe docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's javac on JDK 8 / 17 / 21 uses the US-ASCII default file.encoding (file.encoding=UTF-8 is only the JDK 18+ default per JEP 400) and choked on the section-sign character in two new doc comments: RFC 4648 §5 Swap the symbol for the spelled-out "sec" abbreviation, mirroring the ASCII-only convention noted in the previous biometrics commit (#4987) that fixed the same family of failures for the em-dash. Co-Authored-By: Claude Opus 4.7 (1M context) --- CodenameOne/src/com/codename1/util/Base64.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CodenameOne/src/com/codename1/util/Base64.java b/CodenameOne/src/com/codename1/util/Base64.java index 986c6282f6..392a089655 100644 --- a/CodenameOne/src/com/codename1/util/Base64.java +++ b/CodenameOne/src/com/codename1/util/Base64.java @@ -349,7 +349,7 @@ public static String encodeNoNewline(byte[] in) { return com.codename1.util.StringUtil.newString(out, 0, outputLength); } - /// URL-safe Base64 encoding per RFC 4648 §5: `+` becomes `-`, `/` becomes + /// 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 @@ -386,7 +386,7 @@ public static String encodeUrlSafe(byte[] in) { return com.codename1.util.StringUtil.newString(out, 0, unpadded); } - /// Decodes a URL-safe Base64 string (RFC 4648 §5). Padding is optional -- + /// 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 `=`. /// From 032a97491d3b5a3fa2497103a5c17389e214ecd6 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 22:04:19 +0300 Subject: [PATCH 8/8] iOS: emit ParparVM _R_int wrappers for the new crypto natives The crypto bridge methods on IOSNative -- aesCbc, aesGcm, rsaEncrypt, rsaDecrypt, sign, verify, generateRsaKeyPair -- all return an int. ParparVM emits two C entry points for every non-void native: the unmangled implementation (com_..._methodName___paramTypes) and a _R_-suffixed forwarder that the bytecode dispatcher actually calls. The earlier commits provided only the first half, so the previous build-ios job (which transpiled the framework but didn't transitively include the crypto code path) passed -- the symbols were dead-code- eliminated. Adding CryptoApiTest to scripts/hellocodenameone now keeps those methods alive, the linker hunts for the _R_int wrappers, and the iOS packaging job fails with: Undefined symbols for architecture arm64: "_com_codename1_impl_ios_IOSNative_aesCbc___..._R_int" "_com_codename1_impl_ios_IOSNative_aesGcm___..._R_int" ... Forward each wrapper to the matching base implementation. The base itself is either the real CommonCrypto-backed version (when CN1_INCLUDE_CRYPTO is defined by IPhoneBuilder) or the CN1_CRYPTO_E_UNSUPPORTED stub, so this single set of wrappers serves both build configurations. Co-Authored-By: Claude Opus 4.7 (1M context) --- Ports/iOSPort/nativeSources/IOSNative.m | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index cbb9ffa84c..2c9c651385 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -11301,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); +}