/******************************************************************************* * gMix open source project - https://svs.informatik.uni-hamburg.de/gmix/ * Copyright (C) 2014 SVS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. *******************************************************************************/ package userGeneratedContent.testbedPlugIns.layerPlugIns.layer2recodingScheme.sphinx_v0_001; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.util.Arrays; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import staticContent.framework.AnonNode; import staticContent.framework.message.MixMessage; import staticContent.framework.message.Reply; import staticContent.framework.message.Request; import staticContent.framework.routing.RoutingMode; import staticContent.framework.util.Util; import userGeneratedContent.testbedPlugIns.staticFunctionPlugIns.layer2recodingScheme.curve25519.Curve25519; public class Sphinx { private AnonNode owner; private Sphinx_Config config; private static SecureRandom secureRandom; public static final byte[] ZERO16 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; public static final byte[] ZERO32 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; public Sphinx(AnonNode owner, Sphinx_Config config) { this.owner = owner; this.config = config; if (owner.ROUTING_MODE == RoutingMode.DYNAMIC_ROUTING) throw new RuntimeException("unsupported routing mode: DYNAMIC_ROUTING"); try { Sphinx.secureRandom = SecureRandom.getInstance(config.PRNG_ALGORITHM); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); throw new RuntimeException("could not init random generator " +config.PRNG_ALGORITHM); } } public void initAsClient() { } public void initAsRecoder() { } public synchronized Request applyLayeredEncryption(Request request) { byte[] payload = request.getByteMessage(); if (payload == null) { payload = new byte[0]; System.out.println(owner +" creating dummy"); System.out.println(owner +" config.ROUTE_LENGTH: " +config.ROUTE_LENGTH); } if (payload.length > config.MAX_PAYLOAD) throw new RuntimeException("can't send more than " +config.MAX_PAYLOAD +" bytes in one message"); EncryptedMessage em = new EncryptedMessage(); try { int[] route; if (owner.ROUTING_MODE == RoutingMode.GLOBAL_ROUTING) route = owner.mixList.mixIDs; else route = request.route; MixHeader header = new MixHeader(config, secureRandom, new Route(route, config)); byte[] addressForLastMix = new byte[config.SECURITY_PARAMETER_SIZE]; secureRandom.nextBytes(addressForLastMix); addressForLastMix[0] = MixHeader.SPECIAL_DEST_PREFIX; em.alpBetaGam = header.createHeader(addressForLastMix); byte[] body = Util.concatArrays(Sphinx.ZERO16, payload); body = Sphinx.padBody(body, config); assert body.length == config.DELTA_SIZE; byte[][] deltas = new byte[config.ROUTE_LENGTH][]; // delta v-1 byte[] delta = Sphinx.pi(Sphinx.hashPi(header.getSecret(config.ROUTE_LENGTH-1), config.SECURITY_PARAMETER_SIZE), body, config.SECURITY_PARAMETER_SIZE); deltas[config.ROUTE_LENGTH-1] = delta; // deltas for 0<=i<v-1 for (int i=config.ROUTE_LENGTH-2; i>=0; i--) { delta = Sphinx.pi(Sphinx.hashPi(header.getSecret(i), config.SECURITY_PARAMETER_SIZE), delta, config.SECURITY_PARAMETER_SIZE); deltas[i] = delta; } em.delta = deltas[0]; } catch (Exception e) { e.printStackTrace(); } request.setByteMessage(em.toByteArray(config)); return request; } public int getMaxPayloadForNextMessage() { return config.MAX_PAYLOAD; } public int getMaxPayloadForNextReply() { return config.MAX_PAYLOAD; } private ReplyData createReplyData(byte[] replyId, int[] route) throws Exception { ReplyData replyData = new ReplyData(); replyData.replyId = replyId; Route routeObj = new Route(route, config); MixHeader header = new MixHeader(config, secureRandom, routeObj); replyData.alpBetaGam = header.createHeader(Sphinx.generateReplyAddress(config.id, replyId, config)); replyData.ktilde = new byte[config.SECURITY_PARAMETER_SIZE]; secureRandom.nextBytes(replyData.ktilde); replyData.keytuples = new byte[config.ROUTE_LENGTH][]; for (int i=0; i<config.ROUTE_LENGTH; i++) { try { replyData.keytuples[i] = Sphinx.hashPi(header.getSecret(i), config.SECURITY_PARAMETER_SIZE); } catch (Exception e) { e.printStackTrace(); } } replyData.idMix0 = routeObj.mixIdsSphinx[0]; return replyData; } // "nym" is the pseudonym, the reply data is accessible under on the nymserver public void publishNym(byte[] nym, int[] route) { try { ReplyData replyData = createReplyData(Sphinx.generateReplyID(config), route); config.replyDataTable.put(new String(replyData.replyId, "UTF-8"), replyData); byte[] idMix0 = replyData.idMix0; byte[][] alpBetaGam = replyData.alpBetaGam; byte[] ktilde = replyData.ktilde; owner.getInfoService().postValue(new String(nym, "UTF-8"), Util.concatArrays(new byte[][] {idMix0, alpBetaGam[0], alpBetaGam[1], alpBetaGam[2], ktilde})); // for testing; this should be done via an anonymous channel of course... } catch (Exception e) { e.printStackTrace(); } } public Reply extractPayload(Reply reply) { byte[] replyId = Arrays.copyOfRange(reply.getByteMessage(), 0, 8); byte[] message = Arrays.copyOfRange(reply.getByteMessage(), 8, reply.getByteMessage().length); ReplyData replyData = config.replyDataTable.get(replyId); if (replyData == null) { System.out.println("Unreadable reply message received"); return null; } else { try { for (int i=replyData.keytuples.length-1; i>=0; i--) { message = Sphinx.pi(replyData.keytuples[i], message, config.SECURITY_PARAMETER_SIZE); } message = Sphinx.pii(replyData.ktilde, message, config.SECURITY_PARAMETER_SIZE); message = Sphinx.unpadBody(message, config); byte[] payload = Arrays.copyOfRange(message, config.SECURITY_PARAMETER_SIZE, message.length); if (Arrays.equals(Arrays.copyOf(message, config.SECURITY_PARAMETER_SIZE), Sphinx.ZERO16)) { //System.out.println(new String(payload) + " received by " + Util.md5(clientId)); reply.setByteMessage(payload); return reply; } else { System.err.println("Corrupted message received by"); return null; } } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("could not decrypt reply"); } } } public synchronized Request recodeMessage(Request message) { try { EncryptedMessage msg = new EncryptedMessage(message, config); // System.out.println("processing at " + new String(Hex.encode(_name))); if (msg.alpBetaGam[0].length != 32) { System.out.println("alpha is not an element of ECC group curve25519"); return null; } byte[] sharedSecret = Sphinx.genSharedSecret(msg.alpBetaGam[0], config.privateKey); byte[] tag = Sphinx.hashTau(sharedSecret); if (config.PERFORM_REPLY_DETECTION) { if (config.replayDetection.isReplay(tag)) { System.out.println("the shared secret was already seen"); return null; } } if (!Arrays.equals(msg.alpBetaGam[2], Sphinx.mu(Sphinx.hashMu(sharedSecret, config.SECURITY_PARAMETER_SIZE), msg.alpBetaGam[1], config.SECURITY_PARAMETER_SIZE))) { System.err.println("MAC mismatch " +message +", " +Util.md5(message.getByteMessage()) +", " +message +", " +owner); // TODO: remove return null; } /* * if (anonNode.ROUTING_MODE == RoutingMode.GLOBAL_ROUTING) { outputStrategyLayerMix.addRequest(request); } else if (anonNode.ROUTING_MODE == RoutingMode.SOURCE_ROUTING) { byte[][] splitted = Util.split(getRouteHeaderSize(anonNode), request.getByteMessage()); UnpackedIdArray routeInfo = MixList.unpackIdArrayWithPos(splitted[0]); if (routeInfo.pos >= routeInfo.route.length) { if (anonNode.DISPLAY_ROUTE_INFO) System.out.println(""+anonNode +" setting nextHopAddress to \"LAST HOP\" (pos: " +routeInfo.pos +")"); request.nextHopAddress = MixMessage.NONE; request.setByteMessage(splitted[1]); } else { if (anonNode.DISPLAY_ROUTE_INFO) System.out.println(""+anonNode +" setting nextHopAddress to " +routeInfo.route[routeInfo.pos] +", pos: " +routeInfo.pos); request.nextHopAddress = routeInfo.route[routeInfo.pos]; routeInfo.pos++; System.arraycopy(MixList.packIdArray(routeInfo), 0, request.getByteMessage(), 0, getRouteHeaderSize(anonNode)); } outputStrategyLayerMix.addRequest(request); } else if (anonNode.ROUTING_MODE == RoutingMode.DYNAMIC_ROUTING) { request.nextHopAddress = anonNode.mixList.getRandomMixId(); outputStrategyLayerMix.addRequest(request); } else { throw new RuntimeException("not supported routing mode: " +anonNode.ROUTING_MODE); } */ byte[] B = Sphinx.xor(Util.concatArrays(msg.alpBetaGam[1], Sphinx.ZERO32), Sphinx.rho(Sphinx.hashRho(sharedSecret, config.SECURITY_PARAMETER_SIZE), config.ROUTE_LENGTH, config.SECURITY_PARAMETER_SIZE)); if (B[0] == MixHeader.MIX_PREFIX) { // this mix is not the final hop -> forward message to next hop byte[] nextMixId = Arrays.copyOf(B, config.SECURITY_PARAMETER_SIZE); //byte[] remaining = Arrays.copyOfRange(B, Config._k, B.length); byte[] blindingFactor = Sphinx.hashB(msg.alpBetaGam[0], sharedSecret); msg.alpBetaGam[0] = Sphinx.genSharedSecret(msg.alpBetaGam[0], blindingFactor); msg.alpBetaGam[2] = Arrays.copyOfRange(B, config.SECURITY_PARAMETER_SIZE, 2 * config.SECURITY_PARAMETER_SIZE); msg.alpBetaGam[1] = Arrays.copyOfRange(B, config.SECURITY_PARAMETER_SIZE * 2, B.length); msg.delta = Sphinx.pii(Sphinx.hashPi(sharedSecret, config.SECURITY_PARAMETER_SIZE), msg.delta, config.SECURITY_PARAMETER_SIZE); //System.out.println("this mix is not the final hop -> forward message to next hop"); if (owner.ROUTING_MODE == RoutingMode.SOURCE_ROUTING) { message.nextHopAddress = config.getGlobalMixIdFor(nextMixId); assert message.nextHopAddress != -1; if (owner.DISPLAY_ROUTE_INFO) System.out.println(""+owner +" setting nextHopAddress to " +message.nextHopAddress); } message.setByteMessage(msg.toByteArray(config)); return message; } else if (B[0] == MixHeader.SPECIAL_DEST_PREFIX) { // this mix is the final hop; message is a request //System.out.println("this mix is the final hop; message is a request"); msg.delta = Sphinx.pii(Sphinx.hashPi(sharedSecret, config.SECURITY_PARAMETER_SIZE), msg.delta, config.SECURITY_PARAMETER_SIZE); msg.delta = Sphinx.unpadBody(msg.delta, config); byte[] payload = Arrays.copyOfRange(msg.delta, config.SECURITY_PARAMETER_SIZE, msg.delta.length); //System.out.println("payload received by last mix: " +Util.md5(payload)); message.setByteMessage(payload); if (owner.ROUTING_MODE == RoutingMode.SOURCE_ROUTING) { message.nextHopAddress = MixMessage.NONE; assert message.nextHopAddress != -1; if (owner.DISPLAY_ROUTE_INFO) System.out.println(""+owner +" setting nextHopAddress to \"LAST HOP\""); } return message; } else if (B[0] == MixHeader.CLIENT_PREFIX) { // this mix is the final hop; message is a reply //System.out.println("this mix is the final hop; message is a reply"); byte[] addressData = Arrays.copyOf(B, config.SECURITY_PARAMETER_SIZE); byte[] clientId = Sphinx.extractClientId(addressData, config); byte[] replyId = Sphinx.extractReplyId(addressData, config); msg.delta = Sphinx.pii(Sphinx.hashPi(sharedSecret, config.SECURITY_PARAMETER_SIZE), msg.delta, config.SECURITY_PARAMETER_SIZE); message.nextHopAddress = Util.byteArrayToInt(owner.getInfoService().getValue(new String(clientId, "UTF-8"))); // TODO -> mechanism to forward replies... message.setByteMessage(Util.concatArrays(replyId, msg.delta)); return message; } else { System.err.println("received unknown id"); return null; } } catch (Exception e) { e.printStackTrace(); return null; } } /** * Compute a pseudo-random generator * * @param key * @return the PRG * @throws NoSuchPaddingException * @throws NoSuchProviderException * @throws NoSuchAlgorithmException */ public static byte[] rho(byte[] key, int _r, int _k) throws Exception { byte[] zeros = new byte[(2 * _r + 3) * _k]; // Arrays.fill(zeros, (byte)0); // byte[] prg = new byte[(2 * _r + 3) * _k]; byte[] prg = null; SecretKey secret = new SecretKeySpec(key, "AES"); Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC"); cipher.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(ZERO16)); prg = cipher.doFinal(zeros); return prg; } public static byte[] hashRho(byte[] s, int _k) throws Exception { return Arrays.copyOf(hash(Util.concatArrays("hashRho".getBytes("utf-8"), s)), _k); } /** * The HMAC * * @param key * The hashed shared secret of length _k * @param data * Data of arbitrary length * @return The truncated HMAC of length _k */ public static byte[] mu(byte[] key, byte[] data, int _k) throws Exception { byte[] out = null; SecretKey secret = new SecretKeySpec(key, "Hmac-SHA256"); Mac mac = Mac.getInstance("Hmac-SHA256", "BC"); mac.init(secret); out = mac.doFinal(data); return Arrays.copyOf(out, _k); } public static byte[] hashMu(byte[] s, int _k) throws Exception { return Arrays.copyOf(hash(Util.concatArrays("hashMu:".getBytes("utf-8"), s)), _k); } public static byte[] pi(byte[] key, byte[] data, int _k) throws Exception { return lionessEnc(key, data, _k); } public static byte[] pii(byte[] key, byte[] data, int _k) throws Exception { return lionessDec(key, data, _k); } public static byte[] hashPi(byte[] s, int _k) throws Exception { return Arrays.copyOf(hash(Util.concatArrays("hashPi:".getBytes("utf-8"), s)), _k); } public static byte[] hashTau(byte[] s) throws Exception { return hash(Util.concatArrays("hashTau:".getBytes("utf-8"), s)); } /** * Compute a hash to use as a blinding factor */ public static byte[] hashB(byte[] alpha, byte[] s) throws Exception { return hash(Util.concatArrays(new byte[][] {"hashB".getBytes("utf-8"), alpha, s})); } /** * Compute a SHA-256 hash of the input */ public static byte[] hash(byte[] data) throws Exception { byte[] hash = null; MessageDigest md = MessageDigest.getInstance("SHA-256", "BC"); hash = md.digest(data); return hash; } public static byte[] lionessEnc(byte[] key, byte[] msg, int _k) throws Exception { assert key.length == _k; assert msg.length >= _k * 2; // Round 1 byte[] msgTrunc1 = Arrays.copyOfRange(msg, _k, msg.length); byte[] msgTrunc2 = Arrays.copyOf(msg, _k); byte[] hashR1 = Arrays.copyOf( hash(Util.concatArrays(new byte[][] {msgTrunc1, key, "1".getBytes("utf-8")})), _k); byte[] r1 = Util.concatArrays(xor(hashR1, msgTrunc2), msgTrunc1); // Round 2 byte[] k2 = xor(Arrays.copyOf(r1, _k), key); byte[] r2 = null; { Cipher c = Cipher.getInstance("AES/CTR/NoPadding", "BC"); c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(k2, "AES"), new IvParameterSpec(ZERO16)); r2 = Util.concatArrays(Arrays.copyOf(r1, _k), c.doFinal(Arrays.copyOfRange(r1, _k, r1.length))); } // Round 3 byte[] r2Trunc1 = Arrays.copyOfRange(r2, _k, r2.length); byte[] r2Trunc2 = Arrays.copyOf(r2, _k); byte[] hashR3 = Arrays.copyOf( hash(Util.concatArrays(new byte[][] {r2Trunc1, key, "3".getBytes("utf-8")})), _k); byte[] r3 = Util.concatArrays(xor(hashR3, r2Trunc2), r2Trunc1); // Round 4 byte[] k4 = xor(Arrays.copyOf(r3, _k), key); byte[] r4 = null; { Cipher c = Cipher.getInstance("AES/CTR/NoPadding", "BC"); c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(k4, "AES"), new IvParameterSpec(ZERO16)); r4 = Util.concatArrays(Arrays.copyOf(r3, _k), c.doFinal(Arrays.copyOfRange(r3, _k, r3.length))); } return r4; } public static byte[] lionessDec(byte[] key, byte[] msg, int _k) throws Exception { byte[] r4 = msg; // Round 4 byte[] k4 = xor(Arrays.copyOf(r4, _k), key); byte[] r3 = null; { Cipher c = Cipher.getInstance("AES/CTR/NoPadding", "BC"); c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(k4, "AES"), new IvParameterSpec(ZERO16)); r3 = Util.concatArrays(Arrays.copyOf(r4, _k), c.doFinal(Arrays.copyOfRange(r4, _k, r4.length))); } // Round 3 byte[] r3Trunc1 = Arrays.copyOfRange(r3, _k, r3.length); byte[] r3Trunc2 = Arrays.copyOf(r3, _k); byte[] hashR2 = Arrays.copyOf( hash(Util.concatArrays(new byte[][] {r3Trunc1, key, "3".getBytes("utf-8")})), _k); byte[] r2 = Util.concatArrays(xor(hashR2, r3Trunc2), r3Trunc1); // Round 2 byte[] k2 = xor(Arrays.copyOf(r2, _k), key); byte[] r1 = null; { Cipher c = Cipher.getInstance("AES/CTR/NoPadding", "BC"); c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(k2, "AES"), new IvParameterSpec(ZERO16)); r1 = Util.concatArrays(Arrays.copyOf(r2, _k), c.doFinal(Arrays.copyOfRange(r2, _k, r2.length))); } // Round 1 byte[] r1Trunc1 = Arrays.copyOfRange(r1, _k, r1.length); byte[] r1Trunc2 = Arrays.copyOf(r1, _k); byte[] hashR1 = Arrays.copyOf( hash(Util.concatArrays(new byte[][] {r1Trunc1, key, "1".getBytes("utf-8")})), _k); byte[] r0 = Util.concatArrays(xor(hashR1, r1Trunc2), r1Trunc1); return r0; } /** * Generate the shared secret pubKey^privKey (element of ECC curve22519) * * @param pubKey * public key of participant 1 (Base) * @param privKey * private key of participant 2 (Exponent) * @return the shared secret for both participants */ public static byte[] genSharedSecret(byte[] pubKey, byte[] privKey) { byte[] sharedSecret = new byte[32]; Curve25519.curve(sharedSecret, privKey, pubKey); return sharedSecret; } public static byte[] genSharedSecret(byte[] base, byte[][] exps) { byte[] sharedSecret = new byte[32]; sharedSecret = genSharedSecret(base, exps[0]); for (int exp = 1; exp<exps.length; exp++) if (exps[exp] != null) sharedSecret = genSharedSecret(sharedSecret, exps[exp]); return sharedSecret; } // overhead: 1 byte public static byte[] padBody(byte[] body, Sphinx_Config config) { byte[] paddedBody = Util.concatArrays(body, new byte[] {(byte) 127}); int l = config.DELTA_SIZE - paddedBody.length; if (l > 0) { byte[] padding = new byte[l]; Arrays.fill(padding, (byte) 255); paddedBody = Util.concatArrays(paddedBody, padding); } assert paddedBody.length == config.DELTA_SIZE; return paddedBody; } public static byte[] unpadBody(byte[] body, Sphinx_Config config) { assert body.length == config.DELTA_SIZE; int paddingLength = 0; for (int i = body.length - 1; i >= 0; i--) { assert (body[i] == (byte) 255 | body[i] == (byte) 127); paddingLength++; // number of padded x7f and xff bytes (= Number of xff's +1) if (body[i] == (byte) 127) break; } return Arrays.copyOf(body, body.length - paddingLength); } /** * Xor two byte-arrays */ public static byte[] xor(byte[] x, byte[] y) { assert (x.length == y.length): "x.length != y.length (" +x.length +" != " +y.length +")"; byte[] xored = new byte[x.length]; for (int i = 0; i < xored.length; i++) xored[i] = (byte) (x[i] ^ y[i]); return xored; } // 0: public // 1: private public static byte[][] generateKeyPair(Sphinx_Config sphinx_Config) { byte[] publicKey = new byte[sphinx_Config.ALPHA_SIZE]; byte[] privateKey = new byte[sphinx_Config.ALPHA_SIZE]; try { SecureRandom secureRandom = SecureRandom.getInstance(sphinx_Config.PRNG_ALGORITHM); secureRandom.nextBytes(privateKey); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("could not generate keyPair"); } Curve25519.keygen(publicKey, null, privateKey); return new byte[][] {publicKey, privateKey}; } public static byte[] generateMixId(Sphinx_Config sphinx_Config) { byte[] pseudonym = new byte[sphinx_Config.SECURITY_PARAMETER_SIZE]; SecureRandom secureRandom; try { secureRandom = SecureRandom.getInstance(sphinx_Config.PRNG_ALGORITHM); secureRandom.nextBytes(pseudonym); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); throw new RuntimeException("could not generate mix id"); } pseudonym[0] = MixHeader.MIX_PREFIX; return pseudonym; } public static byte[] generateClientId(Sphinx_Config sphinx_Config) { byte[] id = new byte[sphinx_Config.SECURITY_PARAMETER_SIZE / 2]; SecureRandom secureRandom; try { secureRandom = SecureRandom.getInstance(sphinx_Config.PRNG_ALGORITHM); secureRandom.nextBytes(id); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); throw new RuntimeException("could not generate mix id"); } id[0] = MixHeader.CLIENT_PREFIX; return id; } public static byte[] generateReplyAddress(byte[] clientID, byte[] replyID, Sphinx_Config config) { assert clientID.length == config.SECURITY_PARAMETER_SIZE / 2; assert replyID.length == config.SECURITY_PARAMETER_SIZE / 2; assert clientID[0] == MixHeader.CLIENT_PREFIX;; return Util.concatArrays(clientID, replyID); } // id used to identify the keys needed to derypt a reply public static byte[] generateReplyID(Sphinx_Config config) { byte[] id = new byte[config.SECURITY_PARAMETER_SIZE / 2]; secureRandom.nextBytes(id); return id; } public static byte[] extractClientId(byte[] replyAddress, Sphinx_Config config) { return Arrays.copyOfRange(replyAddress, 0, config.SECURITY_PARAMETER_SIZE / 2); } public static byte[] extractReplyId(byte[] replyAddress, Sphinx_Config config) { return Arrays.copyOfRange(replyAddress, config.SECURITY_PARAMETER_SIZE / 2, replyAddress.length); } }