Loading keystore/java/android/security/keystore2/AndroidKeyStoreEdECPrivateKey.java 0 → 100644 +46 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.security.keystore2; import android.annotation.NonNull; import android.security.KeyStoreSecurityLevel; import android.system.keystore2.Authorization; import android.system.keystore2.KeyDescriptor; import java.security.PrivateKey; import java.security.interfaces.EdECKey; import java.security.spec.NamedParameterSpec; /** * EdEC private key (instance of {@link PrivateKey} and {@link EdECKey}) backed by keystore. * * @hide */ public class AndroidKeyStoreEdECPrivateKey extends AndroidKeyStorePrivateKey implements EdECKey { public AndroidKeyStoreEdECPrivateKey( @NonNull KeyDescriptor descriptor, long keyId, @NonNull Authorization[] authorizations, @NonNull String algorithm, @NonNull KeyStoreSecurityLevel securityLevel) { super(descriptor, keyId, authorizations, algorithm, securityLevel); } @Override public NamedParameterSpec getParams() { return NamedParameterSpec.ED25519; } } keystore/java/android/security/keystore2/AndroidKeyStoreEdECPublicKey.java 0 → 100644 +145 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.security.keystore2; import android.annotation.NonNull; import android.security.KeyStoreSecurityLevel; import android.system.keystore2.KeyDescriptor; import android.system.keystore2.KeyMetadata; import java.math.BigInteger; import java.security.interfaces.EdECPublicKey; import java.security.spec.EdECPoint; import java.security.spec.NamedParameterSpec; import java.util.Arrays; import java.util.Objects; /** * {@link EdECPublicKey} backed by keystore. * * @hide */ public class AndroidKeyStoreEdECPublicKey extends AndroidKeyStorePublicKey implements EdECPublicKey { /** * DER sequence, as defined in https://datatracker.ietf.org/doc/html/rfc8410#section-4 and * https://datatracker.ietf.org/doc/html/rfc5280#section-4.1. * SEQUENCE (2 elem) * SEQUENCE (1 elem) * OBJECT IDENTIFIER 1.3.101.112 curveEd25519 (EdDSA 25519 signature algorithm) * as defined in https://datatracker.ietf.org/doc/html/rfc8410#section-3 * BIT STRING (256 bit) as defined in * https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2 */ private static final byte[] DER_KEY_PREFIX = new byte[] { 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, }; private static final int ED25519_KEY_SIZE_BYTES = 32; private byte[] mEncodedKey; private EdECPoint mPoint; public AndroidKeyStoreEdECPublicKey( @NonNull KeyDescriptor descriptor, @NonNull KeyMetadata metadata, @NonNull String algorithm, @NonNull KeyStoreSecurityLevel iSecurityLevel, @NonNull byte[] encodedKey) { super(descriptor, metadata, encodedKey, algorithm, iSecurityLevel); mEncodedKey = encodedKey; int preambleLength = matchesPreamble(DER_KEY_PREFIX, encodedKey); if (preambleLength == 0) { throw new IllegalArgumentException("Key size is not correct size"); } mPoint = pointFromKeyByteArray( Arrays.copyOfRange(encodedKey, preambleLength, encodedKey.length)); } @Override AndroidKeyStorePrivateKey getPrivateKey() { return new AndroidKeyStoreEdECPrivateKey( getUserKeyDescriptor(), getKeyIdDescriptor().nspace, getAuthorizations(), "EdDSA", getSecurityLevel()); } @Override public NamedParameterSpec getParams() { return NamedParameterSpec.ED25519; } @Override public EdECPoint getPoint() { return mPoint; } private static int matchesPreamble(byte[] preamble, byte[] encoded) { if (encoded.length != (preamble.length + ED25519_KEY_SIZE_BYTES)) { return 0; } if (Arrays.compare(preamble, Arrays.copyOf(encoded, preamble.length)) != 0) { return 0; } return preamble.length; } private static EdECPoint pointFromKeyByteArray(byte[] coordinates) { Objects.requireNonNull(coordinates); // Oddity of the key is the most-significant bit of the last byte. boolean isOdd = (0x80 & coordinates[coordinates.length - 1]) != 0; // Zero out the oddity bit. coordinates[coordinates.length - 1] &= (byte) 0x7f; // Representation of Y is in little-endian, according to rfc8032 section-3.1. reverse(coordinates); // The integer representing Y starts from the first bit in the coordinates array. BigInteger y = new BigInteger(1, coordinates); return new EdECPoint(isOdd, y); } private static void reverse(byte[] coordinateArray) { int start = 0; int end = coordinateArray.length - 1; while (start < end) { byte tmp = coordinateArray[start]; coordinateArray[start] = coordinateArray[end]; coordinateArray[end] = tmp; start++; end--; } } @Override public byte[] getEncoded() { return mEncodedKey.clone(); } } keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java +3 −3 Original line number Diff line number Diff line Loading @@ -224,7 +224,6 @@ public class AndroidKeyStoreProvider extends Provider { String jcaKeyAlgorithm = publicKey.getAlgorithm(); KeyStoreSecurityLevel securityLevel = iSecurityLevel; if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(jcaKeyAlgorithm)) { return new AndroidKeyStoreECPublicKey(descriptor, metadata, iSecurityLevel, (ECPublicKey) publicKey); Loading @@ -232,8 +231,9 @@ public class AndroidKeyStoreProvider extends Provider { return new AndroidKeyStoreRSAPublicKey(descriptor, metadata, iSecurityLevel, (RSAPublicKey) publicKey); } else if (ED25519_OID.equalsIgnoreCase(jcaKeyAlgorithm)) { //TODO(b/214203951) missing classes in conscrypt throw new ProviderException("Curve " + ED25519_OID + " not supported yet"); final byte[] publicKeyEncoded = publicKey.getEncoded(); return new AndroidKeyStoreEdECPublicKey(descriptor, metadata, ED25519_OID, iSecurityLevel, publicKeyEncoded); } else if (X25519_ALIAS.equalsIgnoreCase(jcaKeyAlgorithm)) { //TODO(b/214203951) missing classes in conscrypt throw new ProviderException("Curve " + X25519_ALIAS + " not supported yet"); Loading keystore/tests/src/android/security/keystore2/AndroidKeyStoreEdECPublicKeyTest.java 0 → 100644 +123 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.security.keystore2; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import android.security.KeyStoreSecurityLevel; import android.system.keystore2.Authorization; import android.system.keystore2.Domain; import android.system.keystore2.KeyDescriptor; import android.system.keystore2.KeyMetadata; import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import java.math.BigInteger; import java.util.Base64; @RunWith(AndroidJUnit4.class) public class AndroidKeyStoreEdECPublicKeyTest { private static KeyDescriptor descriptor() { final KeyDescriptor keyDescriptor = new KeyDescriptor(); keyDescriptor.alias = "key"; keyDescriptor.blob = null; keyDescriptor.domain = Domain.APP; keyDescriptor.nspace = -1; return keyDescriptor; } private static KeyMetadata metadata(byte[] cert, byte[] certChain) { KeyMetadata metadata = new KeyMetadata(); metadata.authorizations = new Authorization[0]; metadata.certificate = cert; metadata.certificateChain = certChain; metadata.key = descriptor(); metadata.modificationTimeMs = 0; metadata.keySecurityLevel = 1; return metadata; } @Mock private KeyStoreSecurityLevel mKeystoreSecurityLevel; private static class EdECTestVector { public final byte[] encodedKeyBytes; public final boolean isOdd; public final BigInteger yValue; EdECTestVector(String b64KeyBytes, boolean isOdd, String yValue) { this.encodedKeyBytes = Base64.getDecoder().decode(b64KeyBytes); this.isOdd = isOdd; this.yValue = new BigInteger(yValue); } } private static final EdECTestVector[] ED_EC_TEST_VECTORS = new EdECTestVector[]{ new EdECTestVector("MCowBQYDK2VwAyEADE+wvQqNHxaERPhAZ0rCFlgFbfWLs/YonPXdSTw0VSo=", false, "19147682157189290216699341180089409126316261024914226007941553249095116672780" ), new EdECTestVector("MCowBQYDK2VwAyEA/0E1IRNzGj85Ot/TPeXqifkqTkdk4voleH0hIq59D9w=", true, "41640152188550647350742178040529506688513911269563908889464821205156322689535" ), new EdECTestVector("MCowBQYDK2VwAyEAunOvGuenetl9GQSXGVo5L3RIr4OOIpFIv/Zre8qTc/8=", true, "57647939198144376128225770417635248407428273266444593100194116168980378907578" ), new EdECTestVector("MCowBQYDK2VwAyEA2hHqaZ5IolswN1Yd58Y4hzhmUMCCqc4PW5A/SFLmTX8=", false, "57581368614046789120409806291852629847774713088410311752049592044694364885466" ), }; @Test public void testParsingOfValidKeys() { for (EdECTestVector testVector : ED_EC_TEST_VECTORS) { AndroidKeyStoreEdECPublicKey pkey = new AndroidKeyStoreEdECPublicKey(descriptor(), metadata(null, null), "EdDSA", mKeystoreSecurityLevel, testVector.encodedKeyBytes); assertEquals(pkey.getPoint().isXOdd(), testVector.isOdd); assertEquals(pkey.getPoint().getY(), testVector.yValue); } } @Test public void testFailedParsingOfKeysWithDifferentOid() { final byte[] testVectorWithIncorrectOid = Base64.getDecoder().decode( "MCowBQYDLGVwAyEADE+wvQqNHxaERPhAZ0rCFlgFbfWLs/YonPXdSTw0VSo="); assertThrows("OID should be unrecognized", IllegalArgumentException.class, () -> new AndroidKeyStoreEdECPublicKey(descriptor(), metadata(null, null), "EdDSA", mKeystoreSecurityLevel, testVectorWithIncorrectOid)); } @Test public void testFailedParsingOfKeysWithWrongSize() { final byte[] testVectorWithIncorrectKeySize = Base64.getDecoder().decode( "MCwwBQYDK2VwAyMADE+wvQqNHxaERPhAZ0rCFlgFbfWLs/YonPXdSTw0VSrOzg=="); assertThrows("Key length should be invalid", IllegalArgumentException.class, () -> new AndroidKeyStoreEdECPublicKey(descriptor(), metadata(null, null), "EdDSA", mKeystoreSecurityLevel, testVectorWithIncorrectKeySize)); } } Loading
keystore/java/android/security/keystore2/AndroidKeyStoreEdECPrivateKey.java 0 → 100644 +46 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.security.keystore2; import android.annotation.NonNull; import android.security.KeyStoreSecurityLevel; import android.system.keystore2.Authorization; import android.system.keystore2.KeyDescriptor; import java.security.PrivateKey; import java.security.interfaces.EdECKey; import java.security.spec.NamedParameterSpec; /** * EdEC private key (instance of {@link PrivateKey} and {@link EdECKey}) backed by keystore. * * @hide */ public class AndroidKeyStoreEdECPrivateKey extends AndroidKeyStorePrivateKey implements EdECKey { public AndroidKeyStoreEdECPrivateKey( @NonNull KeyDescriptor descriptor, long keyId, @NonNull Authorization[] authorizations, @NonNull String algorithm, @NonNull KeyStoreSecurityLevel securityLevel) { super(descriptor, keyId, authorizations, algorithm, securityLevel); } @Override public NamedParameterSpec getParams() { return NamedParameterSpec.ED25519; } }
keystore/java/android/security/keystore2/AndroidKeyStoreEdECPublicKey.java 0 → 100644 +145 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.security.keystore2; import android.annotation.NonNull; import android.security.KeyStoreSecurityLevel; import android.system.keystore2.KeyDescriptor; import android.system.keystore2.KeyMetadata; import java.math.BigInteger; import java.security.interfaces.EdECPublicKey; import java.security.spec.EdECPoint; import java.security.spec.NamedParameterSpec; import java.util.Arrays; import java.util.Objects; /** * {@link EdECPublicKey} backed by keystore. * * @hide */ public class AndroidKeyStoreEdECPublicKey extends AndroidKeyStorePublicKey implements EdECPublicKey { /** * DER sequence, as defined in https://datatracker.ietf.org/doc/html/rfc8410#section-4 and * https://datatracker.ietf.org/doc/html/rfc5280#section-4.1. * SEQUENCE (2 elem) * SEQUENCE (1 elem) * OBJECT IDENTIFIER 1.3.101.112 curveEd25519 (EdDSA 25519 signature algorithm) * as defined in https://datatracker.ietf.org/doc/html/rfc8410#section-3 * BIT STRING (256 bit) as defined in * https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2 */ private static final byte[] DER_KEY_PREFIX = new byte[] { 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, }; private static final int ED25519_KEY_SIZE_BYTES = 32; private byte[] mEncodedKey; private EdECPoint mPoint; public AndroidKeyStoreEdECPublicKey( @NonNull KeyDescriptor descriptor, @NonNull KeyMetadata metadata, @NonNull String algorithm, @NonNull KeyStoreSecurityLevel iSecurityLevel, @NonNull byte[] encodedKey) { super(descriptor, metadata, encodedKey, algorithm, iSecurityLevel); mEncodedKey = encodedKey; int preambleLength = matchesPreamble(DER_KEY_PREFIX, encodedKey); if (preambleLength == 0) { throw new IllegalArgumentException("Key size is not correct size"); } mPoint = pointFromKeyByteArray( Arrays.copyOfRange(encodedKey, preambleLength, encodedKey.length)); } @Override AndroidKeyStorePrivateKey getPrivateKey() { return new AndroidKeyStoreEdECPrivateKey( getUserKeyDescriptor(), getKeyIdDescriptor().nspace, getAuthorizations(), "EdDSA", getSecurityLevel()); } @Override public NamedParameterSpec getParams() { return NamedParameterSpec.ED25519; } @Override public EdECPoint getPoint() { return mPoint; } private static int matchesPreamble(byte[] preamble, byte[] encoded) { if (encoded.length != (preamble.length + ED25519_KEY_SIZE_BYTES)) { return 0; } if (Arrays.compare(preamble, Arrays.copyOf(encoded, preamble.length)) != 0) { return 0; } return preamble.length; } private static EdECPoint pointFromKeyByteArray(byte[] coordinates) { Objects.requireNonNull(coordinates); // Oddity of the key is the most-significant bit of the last byte. boolean isOdd = (0x80 & coordinates[coordinates.length - 1]) != 0; // Zero out the oddity bit. coordinates[coordinates.length - 1] &= (byte) 0x7f; // Representation of Y is in little-endian, according to rfc8032 section-3.1. reverse(coordinates); // The integer representing Y starts from the first bit in the coordinates array. BigInteger y = new BigInteger(1, coordinates); return new EdECPoint(isOdd, y); } private static void reverse(byte[] coordinateArray) { int start = 0; int end = coordinateArray.length - 1; while (start < end) { byte tmp = coordinateArray[start]; coordinateArray[start] = coordinateArray[end]; coordinateArray[end] = tmp; start++; end--; } } @Override public byte[] getEncoded() { return mEncodedKey.clone(); } }
keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java +3 −3 Original line number Diff line number Diff line Loading @@ -224,7 +224,6 @@ public class AndroidKeyStoreProvider extends Provider { String jcaKeyAlgorithm = publicKey.getAlgorithm(); KeyStoreSecurityLevel securityLevel = iSecurityLevel; if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(jcaKeyAlgorithm)) { return new AndroidKeyStoreECPublicKey(descriptor, metadata, iSecurityLevel, (ECPublicKey) publicKey); Loading @@ -232,8 +231,9 @@ public class AndroidKeyStoreProvider extends Provider { return new AndroidKeyStoreRSAPublicKey(descriptor, metadata, iSecurityLevel, (RSAPublicKey) publicKey); } else if (ED25519_OID.equalsIgnoreCase(jcaKeyAlgorithm)) { //TODO(b/214203951) missing classes in conscrypt throw new ProviderException("Curve " + ED25519_OID + " not supported yet"); final byte[] publicKeyEncoded = publicKey.getEncoded(); return new AndroidKeyStoreEdECPublicKey(descriptor, metadata, ED25519_OID, iSecurityLevel, publicKeyEncoded); } else if (X25519_ALIAS.equalsIgnoreCase(jcaKeyAlgorithm)) { //TODO(b/214203951) missing classes in conscrypt throw new ProviderException("Curve " + X25519_ALIAS + " not supported yet"); Loading
keystore/tests/src/android/security/keystore2/AndroidKeyStoreEdECPublicKeyTest.java 0 → 100644 +123 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.security.keystore2; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import android.security.KeyStoreSecurityLevel; import android.system.keystore2.Authorization; import android.system.keystore2.Domain; import android.system.keystore2.KeyDescriptor; import android.system.keystore2.KeyMetadata; import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import java.math.BigInteger; import java.util.Base64; @RunWith(AndroidJUnit4.class) public class AndroidKeyStoreEdECPublicKeyTest { private static KeyDescriptor descriptor() { final KeyDescriptor keyDescriptor = new KeyDescriptor(); keyDescriptor.alias = "key"; keyDescriptor.blob = null; keyDescriptor.domain = Domain.APP; keyDescriptor.nspace = -1; return keyDescriptor; } private static KeyMetadata metadata(byte[] cert, byte[] certChain) { KeyMetadata metadata = new KeyMetadata(); metadata.authorizations = new Authorization[0]; metadata.certificate = cert; metadata.certificateChain = certChain; metadata.key = descriptor(); metadata.modificationTimeMs = 0; metadata.keySecurityLevel = 1; return metadata; } @Mock private KeyStoreSecurityLevel mKeystoreSecurityLevel; private static class EdECTestVector { public final byte[] encodedKeyBytes; public final boolean isOdd; public final BigInteger yValue; EdECTestVector(String b64KeyBytes, boolean isOdd, String yValue) { this.encodedKeyBytes = Base64.getDecoder().decode(b64KeyBytes); this.isOdd = isOdd; this.yValue = new BigInteger(yValue); } } private static final EdECTestVector[] ED_EC_TEST_VECTORS = new EdECTestVector[]{ new EdECTestVector("MCowBQYDK2VwAyEADE+wvQqNHxaERPhAZ0rCFlgFbfWLs/YonPXdSTw0VSo=", false, "19147682157189290216699341180089409126316261024914226007941553249095116672780" ), new EdECTestVector("MCowBQYDK2VwAyEA/0E1IRNzGj85Ot/TPeXqifkqTkdk4voleH0hIq59D9w=", true, "41640152188550647350742178040529506688513911269563908889464821205156322689535" ), new EdECTestVector("MCowBQYDK2VwAyEAunOvGuenetl9GQSXGVo5L3RIr4OOIpFIv/Zre8qTc/8=", true, "57647939198144376128225770417635248407428273266444593100194116168980378907578" ), new EdECTestVector("MCowBQYDK2VwAyEA2hHqaZ5IolswN1Yd58Y4hzhmUMCCqc4PW5A/SFLmTX8=", false, "57581368614046789120409806291852629847774713088410311752049592044694364885466" ), }; @Test public void testParsingOfValidKeys() { for (EdECTestVector testVector : ED_EC_TEST_VECTORS) { AndroidKeyStoreEdECPublicKey pkey = new AndroidKeyStoreEdECPublicKey(descriptor(), metadata(null, null), "EdDSA", mKeystoreSecurityLevel, testVector.encodedKeyBytes); assertEquals(pkey.getPoint().isXOdd(), testVector.isOdd); assertEquals(pkey.getPoint().getY(), testVector.yValue); } } @Test public void testFailedParsingOfKeysWithDifferentOid() { final byte[] testVectorWithIncorrectOid = Base64.getDecoder().decode( "MCowBQYDLGVwAyEADE+wvQqNHxaERPhAZ0rCFlgFbfWLs/YonPXdSTw0VSo="); assertThrows("OID should be unrecognized", IllegalArgumentException.class, () -> new AndroidKeyStoreEdECPublicKey(descriptor(), metadata(null, null), "EdDSA", mKeystoreSecurityLevel, testVectorWithIncorrectOid)); } @Test public void testFailedParsingOfKeysWithWrongSize() { final byte[] testVectorWithIncorrectKeySize = Base64.getDecoder().decode( "MCwwBQYDK2VwAyMADE+wvQqNHxaERPhAZ0rCFlgFbfWLs/YonPXdSTw0VSrOzg=="); assertThrows("Key length should be invalid", IllegalArgumentException.class, () -> new AndroidKeyStoreEdECPublicKey(descriptor(), metadata(null, null), "EdDSA", mKeystoreSecurityLevel, testVectorWithIncorrectKeySize)); } }