From d57102ed909da731021c8b1e2ac50adf5d2e8c68 Mon Sep 17 00:00:00 2001 From: Torsten Grote <t@grobox.de> Date: Sat, 21 Apr 2018 17:21:38 -0300 Subject: [PATCH] IntroductionCrypto: Create dedicated class to handle introduction related crypto --- .../introduction2/IntroductionConstants.java | 16 ++ .../introduction2/IntroductionCrypto.java | 88 +++++++ .../introduction2/IntroductionCryptoImpl.java | 216 ++++++++++++++++++ .../IntroductionCryptoImplTest.java | 142 ++++++++++++ .../introduction2/IntroductionCryptoTest.java | 46 ++++ .../test/BriarIntegrationTestComponent.java | 2 + 6 files changed, 510 insertions(+) create mode 100644 briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCrypto.java create mode 100644 briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCryptoImpl.java create mode 100644 briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoImplTest.java create mode 100644 briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoTest.java diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionConstants.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionConstants.java index 6b91b33c02..962bd0a52b 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionConstants.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionConstants.java @@ -10,4 +10,20 @@ public interface IntroductionConstants { */ int MAX_REQUEST_MESSAGE_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024; + String LABEL_SESSION_ID = "org.briarproject.briar.introduction/SESSION_ID"; + + String LABEL_MASTER_KEY = "org.briarproject.briar.introduction/MASTER_KEY"; + + String LABEL_ALICE_MAC_KEY = + "org.briarproject.briar.introduction/ALICE_MAC_KEY"; + + String LABEL_BOB_MAC_KEY = + "org.briarproject.briar.introduction/BOB_MAC_KEY"; + + String LABEL_AUTH_MAC = "org.briarproject.briar.introduction/AUTH_MAC"; + + String LABEL_AUTH_SIGN = "org.briarproject.briar.introduction/AUTH_SIGN"; + + String LABEL_AUTH_NONCE = "org.briarproject.briar.introduction/AUTH_NONCE"; + } diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCrypto.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCrypto.java new file mode 100644 index 0000000000..a91184db2a --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCrypto.java @@ -0,0 +1,88 @@ +package org.briarproject.briar.introduction2; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorId; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.briar.api.client.SessionId; + +import java.security.GeneralSecurityException; + +interface IntroductionCrypto { + + /** + * Returns the {@link SessionId} based on the introducer + * and the two introducees. + * + * Note: The roles of Alice and Bob can be switched. + */ + SessionId getSessionId(Author introducer, Author alice, Author bob); + + /** + * Returns true if the first author is indeed alice + */ + boolean isAlice(AuthorId alice, AuthorId bob); + + /** + * Generates an agreement key pair. + */ + KeyPair generateKeyPair(); + + /** + * Derives a session master key for Alice or Bob. + * + * @param alice true if the session owner is Alice + * @return The secret master key + */ + SecretKey deriveMasterKey(IntroduceeSession s, boolean alice) + throws GeneralSecurityException; + + /** + * Derives a MAC key from the session's master key for Alice or Bob. + * + * @param masterKey The key returned by {@link #deriveMasterKey(IntroduceeSession, boolean)} + * @param alice true for Alice's MAC key, false for Bob's + * @return The MAC key + */ + SecretKey deriveMacKey(SecretKey masterKey, boolean alice); + + /** + * Generates a MAC that covers both introducee's ephemeral public keys and + * transport properties. + */ + byte[] mac(SecretKey macKey, IntroduceeSession s, AuthorId localAuthorId, + boolean alice) throws FormatException; + + /** + * Verifies a received MAC + * + * @param mac The MAC to verify + * as returned by {@link #deriveMasterKey(IntroduceeSession, boolean)} + * @throws GeneralSecurityException if the verification fails + */ + void verifyMac(byte[] mac, IntroduceeSession s, AuthorId localAuthorId) + throws GeneralSecurityException, FormatException; + + /** + * Signs a nonce derived from the macKey + * with the local introducee's identity private key. + * + * @param macKey The corresponding MAC key for the signer's role + * @param privateKey The identity private key + * (from {@link LocalAuthor#getPrivateKey()}) + * @return The signature as a byte array + */ + byte[] sign(SecretKey macKey, byte[] privateKey) + throws GeneralSecurityException; + + /** + * Verifies the signature on a corresponding MAC key. + * + * @throws GeneralSecurityException if the signature is invalid + */ + void verifySignature(byte[] signature, IntroduceeSession s, + AuthorId localAuthorId) throws GeneralSecurityException; + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCryptoImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCryptoImpl.java new file mode 100644 index 0000000000..93ff9b33e3 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionCryptoImpl.java @@ -0,0 +1,216 @@ +package org.briarproject.briar.introduction2; + +import org.briarproject.bramble.api.Bytes; +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.KeyParser; +import org.briarproject.bramble.api.crypto.PrivateKey; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorId; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.briar.api.client.SessionId; + +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Map; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_ALICE_MAC_KEY; +import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_AUTH_MAC; +import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_AUTH_NONCE; +import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_AUTH_SIGN; +import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_BOB_MAC_KEY; +import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_MASTER_KEY; +import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_SESSION_ID; +import static org.briarproject.briar.api.introduction2.IntroductionManager.CLIENT_VERSION; + +@Immutable +@NotNullByDefault +class IntroductionCryptoImpl implements IntroductionCrypto { + + private final CryptoComponent crypto; + private final ClientHelper clientHelper; + + @Inject + IntroductionCryptoImpl( + CryptoComponent crypto, + ClientHelper clientHelper) { + this.crypto = crypto; + this.clientHelper = clientHelper; + } + + @Override + public SessionId getSessionId(Author introducer, Author alice, + Author bob) { + boolean isAlice = isAlice(alice.getId(), bob.getId()); + byte[] hash = crypto.hash( + LABEL_SESSION_ID, + introducer.getId().getBytes(), + isAlice ? alice.getId().getBytes() : bob.getId().getBytes(), + isAlice ? bob.getId().getBytes() : alice.getId().getBytes() + ); + return new SessionId(hash); + } + + @Override + public KeyPair generateKeyPair() { + return crypto.generateAgreementKeyPair(); + } + + @Override + public boolean isAlice(AuthorId alice, AuthorId bob) { + byte[] a = alice.getBytes(); + byte[] b = bob.getBytes(); + return Bytes.COMPARATOR.compare(new Bytes(a), new Bytes(b)) < 0; + } + + @Override + @SuppressWarnings("ConstantConditions") + public SecretKey deriveMasterKey(IntroduceeSession s, boolean alice) + throws GeneralSecurityException { + return deriveMasterKey(s.getEphemeralPublicKey(), + s.getEphemeralPrivateKey(), s.getRemotePublicKey(), alice); + } + + SecretKey deriveMasterKey(byte[] publicKey, byte[] privateKey, + byte[] remotePublicKey, boolean alice) + throws GeneralSecurityException { + KeyParser kp = crypto.getAgreementKeyParser(); + PublicKey remoteEphemeralPublicKey = kp.parsePublicKey(remotePublicKey); + PublicKey ephemeralPublicKey = kp.parsePublicKey(publicKey); + PrivateKey ephemeralPrivateKey = kp.parsePrivateKey(privateKey); + KeyPair keyPair = new KeyPair(ephemeralPublicKey, ephemeralPrivateKey); + return crypto.deriveSharedSecret( + LABEL_MASTER_KEY, + remoteEphemeralPublicKey, + keyPair, + new byte[] {CLIENT_VERSION}, + alice ? publicKey : remotePublicKey, + alice ? remotePublicKey : publicKey + ); + } + + @Override + public SecretKey deriveMacKey(SecretKey masterKey, boolean alice) { + return crypto.deriveKey( + alice ? LABEL_ALICE_MAC_KEY : LABEL_BOB_MAC_KEY, + masterKey + ); + } + + @Override + @SuppressWarnings("ConstantConditions") + public byte[] mac(SecretKey macKey, IntroduceeSession s, + AuthorId localAuthorId, boolean alice) throws FormatException { + return mac(macKey, s.getIntroducer().getId(), localAuthorId, + s.getRemoteAuthor().getId(), s.getAcceptTimestamp(), + s.getRemoteAcceptTimestamp(), s.getEphemeralPublicKey(), + s.getRemotePublicKey(), s.getTransportProperties(), + s.getRemoteTransportProperties(), alice); + } + + byte[] mac(SecretKey macKey, AuthorId introducerId, + AuthorId localAuthorId, AuthorId remoteAuthorId, + long acceptTimestamp, long remoteAcceptTimestamp, + byte[] ephemeralPublicKey, byte[] remoteEphemeralPublicKey, + Map<TransportId, TransportProperties> transportProperties, + Map<TransportId, TransportProperties> remoteTransportProperties, + boolean alice) throws FormatException { + BdfList localInfo = BdfList.of( + localAuthorId, + acceptTimestamp, + ephemeralPublicKey, + clientHelper.toDictionary(transportProperties) + ); + BdfList remoteInfo = BdfList.of( + remoteAuthorId, + remoteAcceptTimestamp, + remoteEphemeralPublicKey, + clientHelper.toDictionary(remoteTransportProperties) + ); + BdfList macList = BdfList.of( + introducerId, + alice ? localInfo : remoteInfo, + alice ? remoteInfo : localInfo + ); + return crypto.mac( + LABEL_AUTH_MAC, + macKey, + clientHelper.toByteArray(macList) + ); + } + + @Override + @SuppressWarnings("ConstantConditions") + public void verifyMac(byte[] mac, IntroduceeSession s, + AuthorId localAuthorId) + throws GeneralSecurityException, FormatException { + boolean alice = isAlice(localAuthorId, s.getRemoteAuthor().getId()); + verifyMac(mac, new SecretKey(s.getMasterKey()), + s.getIntroducer().getId(), localAuthorId, + s.getRemoteAuthor().getId(), s.getAcceptTimestamp(), + s.getRemoteAcceptTimestamp(), s.getEphemeralPublicKey(), + s.getRemotePublicKey(), s.getTransportProperties(), + s.getRemoteTransportProperties(), !alice); + } + + void verifyMac(byte[] mac, SecretKey masterKey, + AuthorId introducerId, AuthorId localAuthorId, + AuthorId remoteAuthorId, long acceptTimestamp, + long remoteAcceptTimestamp, byte[] ephemeralPublicKey, + byte[] remoteEphemeralPublicKey, + Map<TransportId, TransportProperties> transportProperties, + Map<TransportId, TransportProperties> remoteTransportProperties, + boolean alice) throws GeneralSecurityException, FormatException { + SecretKey macKey = deriveMacKey(masterKey, alice); + byte[] calculatedMac = + mac(macKey, introducerId, localAuthorId, remoteAuthorId, + acceptTimestamp, remoteAcceptTimestamp, + ephemeralPublicKey, remoteEphemeralPublicKey, + transportProperties, remoteTransportProperties, !alice); + if (!Arrays.equals(mac, calculatedMac)) { + throw new GeneralSecurityException(); + } + } + + @Override + public byte[] sign(SecretKey macKey, byte[] privateKey) + throws GeneralSecurityException { + return crypto.sign( + LABEL_AUTH_SIGN, + getNonce(macKey), + privateKey + ); + } + + @Override + @SuppressWarnings("ConstantConditions") + public void verifySignature(byte[] signature, IntroduceeSession s, + AuthorId localAuthorId) throws GeneralSecurityException { + boolean alice = isAlice(s.getRemoteAuthor().getId(), localAuthorId); + SecretKey macKey = deriveMacKey(new SecretKey(s.getMasterKey()), alice); + verifySignature(macKey, s.getRemoteAuthor().getPublicKey(), signature); + } + + void verifySignature(SecretKey macKey, byte[] publicKey, + byte[] signature) throws GeneralSecurityException { + byte[] nonce = getNonce(macKey); + if (!crypto.verify(LABEL_AUTH_SIGN, nonce, publicKey, signature)) { + throw new GeneralSecurityException(); + } + } + + private byte[] getNonce(SecretKey macKey) { + return crypto.mac(LABEL_AUTH_NONCE, macKey); + } + +} diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoImplTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoImplTest.java new file mode 100644 index 0000000000..57cadce0e2 --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoImplTest.java @@ -0,0 +1,142 @@ +package org.briarproject.briar.introduction2; + +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorFactory; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.test.BrambleTestCase; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.test.BriarIntegrationTestComponent; +import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent; +import org.junit.Test; + +import java.util.Map; + +import javax.inject.Inject; + +import static org.briarproject.bramble.test.TestUtils.getRandomBytes; +import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap; +import static org.briarproject.bramble.util.StringUtils.fromHexString; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IntroductionCryptoImplTest extends BrambleTestCase { + + @Inject + ClientHelper clientHelper; + @Inject + AuthorFactory authorFactory; + @Inject + CryptoComponent cryptoComponent; + + private final IntroductionCryptoImpl crypto; + + private final Author introducer; + private final LocalAuthor alice, bob; + private final long aliceAcceptTimestamp = 42L; + private final long bobAcceptTimestamp = 1337L; + private final SecretKey masterKey = + new SecretKey(getRandomBytes(SecretKey.LENGTH)); + private final KeyPair aliceEphemeral, bobEphemeral; + private final Map<TransportId, TransportProperties> aliceTransport = + getTransportPropertiesMap(3); + private final Map<TransportId, TransportProperties> bobTransport = + getTransportPropertiesMap(3); + + public IntroductionCryptoImplTest() { + BriarIntegrationTestComponent component = + DaggerBriarIntegrationTestComponent.builder().build(); + component.inject(this); + crypto = new IntroductionCryptoImpl(cryptoComponent, clientHelper); + + // create actual deterministic authors for testing + introducer = authorFactory + .createAuthor("Introducer", new byte[] {0x1, 0x2, 0x3}); + alice = authorFactory.createLocalAuthor("Alice", + fromHexString( + "A626F080C94771698F86B4B4094C4F560904B53398805AE02BA2343F1829187A"), + fromHexString( + "60F010187AF91ACA15141E8C811EC8E79C7CAA6461C21A852BB03066C89B0A70")); + bob = authorFactory.createLocalAuthor("Bob", + fromHexString( + "A0D0FED1CE4674D8B6441AD0A664E41BF60D489F35DA11F52AF923540848546F"), + fromHexString( + "20B25BE7E999F68FE07189449E91984FA79121DBFF28A651669A3CF512D6A758")); + aliceEphemeral = crypto.generateKeyPair(); + bobEphemeral = crypto.generateKeyPair(); + } + + @Test + public void testGetSessionId() { + SessionId s1 = crypto.getSessionId(introducer, alice, bob); + SessionId s2 = crypto.getSessionId(introducer, bob, alice); + assertEquals(s1, s2); + } + + @Test + public void testIsAlice() { + assertTrue(crypto.isAlice(alice.getId(), bob.getId())); + assertFalse(crypto.isAlice(bob.getId(), alice.getId())); + } + + @Test + public void testDeriveMasterKey() throws Exception { + SecretKey aliceMasterKey = crypto.deriveMasterKey(alice.getPublicKey(), + alice.getPrivateKey(), bob.getPublicKey(), true); + SecretKey bobMasterKey = crypto.deriveMasterKey(bob.getPublicKey(), + bob.getPrivateKey(), alice.getPublicKey(), false); + assertArrayEquals(aliceMasterKey.getBytes(), bobMasterKey.getBytes()); + } + + @Test + public void testAliceMac() throws Exception { + SecretKey aliceMacKey = crypto.deriveMacKey(masterKey, true); + byte[] aliceMac = + crypto.mac(aliceMacKey, introducer.getId(), alice.getId(), + bob.getId(), aliceAcceptTimestamp, bobAcceptTimestamp, + aliceEphemeral.getPublic().getEncoded(), + bobEphemeral.getPublic().getEncoded(), aliceTransport, + bobTransport, true); + + crypto.verifyMac(aliceMac, masterKey, introducer.getId(), bob.getId(), + alice.getId(), bobAcceptTimestamp, aliceAcceptTimestamp, + bobEphemeral.getPublic().getEncoded(), + aliceEphemeral.getPublic().getEncoded(), bobTransport, + aliceTransport, true); + } + + @Test + public void testBobMac() throws Exception { + SecretKey bobMacKey = crypto.deriveMacKey(masterKey, false); + byte[] bobMac = + crypto.mac(bobMacKey, introducer.getId(), bob.getId(), + alice.getId(), bobAcceptTimestamp, aliceAcceptTimestamp, + bobEphemeral.getPublic().getEncoded(), + aliceEphemeral.getPublic().getEncoded(), bobTransport, + aliceTransport, false); + + crypto.verifyMac(bobMac, masterKey, introducer.getId(), alice.getId(), + bob.getId(), aliceAcceptTimestamp, bobAcceptTimestamp, + aliceEphemeral.getPublic().getEncoded(), + bobEphemeral.getPublic().getEncoded(), aliceTransport, + bobTransport, false); + } + + @Test + public void testSign() throws Exception { + KeyPair keyPair = cryptoComponent.generateSignatureKeyPair(); + SecretKey macKey = crypto.deriveMacKey(masterKey, true); + byte[] signature = + crypto.sign(macKey, keyPair.getPrivate().getEncoded()); + crypto.verifySignature(macKey, keyPair.getPublic().getEncoded(), + signature); + } + +} diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoTest.java new file mode 100644 index 0000000000..139c4ca408 --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/introduction2/IntroductionCryptoTest.java @@ -0,0 +1,46 @@ +package org.briarproject.briar.introduction2; + +import org.briarproject.bramble.api.UniqueId; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.test.BrambleMockTestCase; +import org.briarproject.briar.api.client.SessionId; +import org.jmock.Expectations; +import org.junit.Test; + +import static org.briarproject.bramble.test.TestUtils.getAuthor; +import static org.briarproject.bramble.test.TestUtils.getRandomBytes; +import static org.briarproject.briar.api.introduction2.IntroductionConstants.LABEL_SESSION_ID; +import static org.junit.Assert.assertEquals; + +public class IntroductionCryptoTest extends BrambleMockTestCase { + + private final CryptoComponent cryptoComponent = + context.mock(CryptoComponent.class); + private final ClientHelper clientHelper = context.mock(ClientHelper.class); + + private final IntroductionCrypto crypto = + new IntroductionCryptoImpl(cryptoComponent, clientHelper); + + private final Author introducer = getAuthor(); + private final Author alice = getAuthor(), bob = getAuthor(); + private final byte[] hash = getRandomBytes(UniqueId.LENGTH); + + @Test + public void testGetSessionId() { + boolean isAlice = crypto.isAlice(alice.getId(), bob.getId()); + context.checking(new Expectations() {{ + oneOf(cryptoComponent).hash( + LABEL_SESSION_ID, + introducer.getId().getBytes(), + isAlice ? alice.getId().getBytes() : bob.getId().getBytes(), + isAlice ? bob.getId().getBytes() : alice.getId().getBytes() + ); + will(returnValue(hash)); + }}); + SessionId sessionId = crypto.getSessionId(introducer, alice, bob); + assertEquals(new SessionId(hash), sessionId); + } + +} diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java index 54709815e4..fe2e745c0d 100644 --- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java +++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java @@ -37,6 +37,7 @@ import org.briarproject.briar.blog.BlogModule; import org.briarproject.briar.client.BriarClientModule; import org.briarproject.briar.forum.ForumModule; import org.briarproject.briar.introduction.IntroductionModule; +import org.briarproject.briar.introduction2.IntroductionCryptoImplTest; import org.briarproject.briar.introduction2.MessageEncoderParserIntegrationTest; import org.briarproject.briar.introduction2.SessionEncoderParserIntegrationTest; import org.briarproject.briar.messaging.MessagingModule; @@ -80,6 +81,7 @@ public interface BriarIntegrationTestComponent { void inject(MessageEncoderParserIntegrationTest init); void inject(SessionEncoderParserIntegrationTest init); + void inject(IntroductionCryptoImplTest init); void inject(BlogModule.EagerSingletons init); -- GitLab